From 1f9690fa76aff86837c84562fa4db0ee1da2a5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20H=C3=B6rl?= Date: Thu, 4 Nov 2021 13:40:57 +0100 Subject: [PATCH] Add humble library to produce video --- README.md | 5 + pom.xml | 5 + .../matsim/renderer/config/RenderConfig.java | 12 ++- .../matsim/renderer/main/RenderFrame.java | 101 ++++++++++++++++-- .../presets/RunNantesVisualization.java | 67 ++++++++++++ 5 files changed, 181 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ch/ethzm/matsim/renderer/presets/RunNantesVisualization.java diff --git a/README.md b/README.md index 2682ab7..054fe60 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,8 @@ ffmpeg -framerate 25 -i video_%d.png -c:v libx264 -profile:v high -crf 20 -pix_f An example output can be seen in `example/output.mp4`. ![Example](example/output.png) + +## Video output + +As of November 2021, the renderer can also produce videos directly. For that, add the `outputFormat` option to the configuration file and set it to `Video`. The output path will then be interpreted as a path to a video file and the video will be created at this place. + diff --git a/pom.xml b/pom.xml index 30c3a34..5cdb911 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,11 @@ matsim ${matsim.version} + + io.humble + humble-video-all + 0.3.0 + diff --git a/src/main/java/ch/ethzm/matsim/renderer/config/RenderConfig.java b/src/main/java/ch/ethzm/matsim/renderer/config/RenderConfig.java index ddc0a9c..812787a 100644 --- a/src/main/java/ch/ethzm/matsim/renderer/config/RenderConfig.java +++ b/src/main/java/ch/ethzm/matsim/renderer/config/RenderConfig.java @@ -10,6 +10,12 @@ public class RenderConfig { public String eventsPath; public String networkPath; + public enum OutputFormat { + None, Images, Video + } + + public OutputFormat outputFormat = OutputFormat.Video; + public double startTime = 0.0; public double endTime = 24 * 3600.0; public double secondsPerFrame = 120.0; @@ -44,9 +50,13 @@ public void validate() { File outputFile = new File(outputPath); - if (outputFile.exists() && !outputFile.isDirectory()) { + if (outputFile.exists() && !outputFile.isDirectory() && outputFormat.equals(OutputFormat.Images)) { throw new IllegalStateException("Output path is not a directory"); } + + if (outputFile.exists() && !outputFile.isFile() && outputFormat.equals(OutputFormat.Video)) { + throw new IllegalStateException("Output path is not a file"); + } networks.forEach(NetworkConfig::validate); vehicles.forEach(VehicleConfig::validate); diff --git a/src/main/java/ch/ethzm/matsim/renderer/main/RenderFrame.java b/src/main/java/ch/ethzm/matsim/renderer/main/RenderFrame.java index 0c1393c..d45dbb3 100644 --- a/src/main/java/ch/ethzm/matsim/renderer/main/RenderFrame.java +++ b/src/main/java/ch/ethzm/matsim/renderer/main/RenderFrame.java @@ -27,10 +27,21 @@ import ch.ethzm.matsim.renderer.config.ActivityConfig; import ch.ethzm.matsim.renderer.config.NetworkConfig; import ch.ethzm.matsim.renderer.config.RenderConfig; +import ch.ethzm.matsim.renderer.config.RenderConfig.OutputFormat; import ch.ethzm.matsim.renderer.config.VehicleConfig; import ch.ethzm.matsim.renderer.network.LinkDatabase; import ch.ethzm.matsim.renderer.traversal.TraversalDatabase; import ch.ethzm.matsim.renderer.traversal.VehicleDatabase; +import io.humble.video.Codec; +import io.humble.video.Encoder; +import io.humble.video.MediaPacket; +import io.humble.video.MediaPicture; +import io.humble.video.Muxer; +import io.humble.video.MuxerFormat; +import io.humble.video.PixelFormat; +import io.humble.video.Rational; +import io.humble.video.awt.MediaPictureConverter; +import io.humble.video.awt.MediaPictureConverterFactory; public class RenderFrame extends JPanel { final private TraversalDatabase traversalDatabase; @@ -63,6 +74,9 @@ public RenderFrame(TraversalDatabase traversalDatabase, LinkDatabase linkDatabas this.activityTypeMapper = activityTypeMapper; this.vehicleDatabase = vehicleDatabase; + this.makeVideo = renderConfig.outputFormat.equals(OutputFormat.Images) + || renderConfig.outputFormat.equals(OutputFormat.Video); + this.renderConfig = renderConfig; this.time = renderConfig.startTime; windowWidth = renderConfig.width; @@ -126,7 +140,7 @@ public RenderFrame(TraversalDatabase traversalDatabase, LinkDatabase linkDatabas private double time; private long previousRenderTime = -1; - private boolean makeVideo = true; + private final boolean makeVideo; private double timeStepPerSecond; // 600; // 120.0; double framesPerSecond = 25.0; @@ -163,6 +177,18 @@ public void paintComponent(Graphics windowGraphics) { if (time > renderConfig.endTime) { if (makeVideo) { + if (renderConfig.outputFormat.equals(OutputFormat.Video)) { + if (muxer != null) { + do { + encoder.encode(packet, null); + if (packet.isComplete()) + muxer.write(packet, false); + } while (packet.isComplete()); + + muxer.close(); + } + } + System.exit(1); } else { time = renderConfig.startTime; @@ -251,19 +277,78 @@ public void paintComponent(Graphics windowGraphics) { } if (makeVideo) { - try { - synchronized (imageLock) { - ImageIO.write(surface, "png", - new File(String.format(renderConfig.outputPath + "/video_%d.png", frameIndex))); + if (renderConfig.outputFormat.equals(OutputFormat.Images)) { + try { + synchronized (imageLock) { + ImageIO.write(surface, "png", + new File(String.format(renderConfig.outputPath + "/video_%d.png", frameIndex))); + + System.out.println(Time.writeTime(time)); + } + } catch (IOException e) { + e.printStackTrace(); + } + } else { + try { + if (muxer == null) { + muxer = Muxer.make(renderConfig.outputPath, null, null); + + MuxerFormat format = muxer.getFormat(); + Codec codec = Codec.findEncodingCodec(format.getDefaultVideoCodecId()); + + encoder = Encoder.make(codec); + encoder.setWidth(renderConfig.width); + encoder.setHeight(renderConfig.height); + // We are going to use 420P as the format because that's what most video formats + // these days use + final PixelFormat.Type pixelformat = PixelFormat.Type.PIX_FMT_YUV420P; + encoder.setPixelFormat(pixelformat); + encoder.setTimeBase(Rational.make(1.0 / 25.0)); - System.out.println(Time.writeTime(time)); + if (format.getFlag(MuxerFormat.Flag.GLOBAL_HEADER)) { + encoder.setFlag(Encoder.Flag.FLAG_GLOBAL_HEADER, true); + } + + encoder.open(null, null); + muxer.addNewStream(encoder); + + muxer.open(null, null); + + picture = MediaPicture.make(encoder.getWidth(), encoder.getHeight(), pixelformat); + picture.setTimeBase(Rational.make(1.0 / 25.0)); + + packet = MediaPacket.make(); + } + + do { + BufferedImage newImage = new BufferedImage(surface.getWidth(), surface.getHeight(), + BufferedImage.TYPE_3BYTE_BGR); + newImage.getGraphics().drawImage(surface, 0, 0, null); + + if (converter == null) { + converter = MediaPictureConverterFactory.createConverter(newImage, picture); + } + + converter.toPicture(picture, newImage, frameIndex); + + encoder.encode(packet, picture); + if (packet.isComplete()) { + muxer.write(packet, false); + } + } while (packet.isComplete()); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); } - } catch (IOException e) { - e.printStackTrace(); } } super.paintComponent(windowGraphics); windowGraphics.drawImage(surface, 0, 0, this); } + + private Muxer muxer = null; + private Encoder encoder = null; + private MediaPicture picture = null; + private MediaPacket packet = null; + private MediaPictureConverter converter = null; } diff --git a/src/main/java/ch/ethzm/matsim/renderer/presets/RunNantesVisualization.java b/src/main/java/ch/ethzm/matsim/renderer/presets/RunNantesVisualization.java new file mode 100644 index 0000000..c5aaade --- /dev/null +++ b/src/main/java/ch/ethzm/matsim/renderer/presets/RunNantesVisualization.java @@ -0,0 +1,67 @@ +package ch.ethzm.matsim.renderer.presets; + +import java.util.Arrays; + +import ch.ethzm.matsim.renderer.config.ActivityConfig; +import ch.ethzm.matsim.renderer.config.NetworkConfig; +import ch.ethzm.matsim.renderer.config.RenderConfig; +import ch.ethzm.matsim.renderer.config.RenderConfig.OutputFormat; +import ch.ethzm.matsim.renderer.config.VehicleConfig; +import ch.ethzm.matsim.renderer.main.RunRenderer; + +public class RunNantesVisualization { + static public void main(String[] args) { + // START CONFIGURATION + + RenderConfig renderConfig = new RenderConfig(); + + renderConfig.width = 1280; + renderConfig.height = 720; + + renderConfig.networkPath = "output_network.xml.gz"; + renderConfig.eventsPath = "output_events.xml.gz"; + renderConfig.outputPath = "video.mp4"; + renderConfig.outputFormat = OutputFormat.Video; + + renderConfig.startTime = 8.0 * 3600.0; + renderConfig.endTime = 10.0 * 3600.0; + renderConfig.secondsPerFrame = 120.0; + + renderConfig.showTime = false; + + renderConfig.center = Arrays.asList(355424.0, 6689212.0); + renderConfig.zoom = 20000.0; + + NetworkConfig roadNetwork = new NetworkConfig(); + renderConfig.networks.add(roadNetwork); + roadNetwork.modes = Arrays.asList("car"); + roadNetwork.color = Arrays.asList(240, 240, 240); + + NetworkConfig subwayNetwork = new NetworkConfig(); + renderConfig.networks.add(subwayNetwork); + subwayNetwork.modes = Arrays.asList("subway", "rail"); + subwayNetwork.color = Arrays.asList(200, 200, 200); + + VehicleConfig otherVehicle = new VehicleConfig(); + renderConfig.vehicles.add(otherVehicle); + otherVehicle.color = Arrays.asList(160, 160, 160); + otherVehicle.size = 2; + + VehicleConfig ptVehicle = new VehicleConfig(); + renderConfig.vehicles.add(ptVehicle); + ptVehicle.contains = Arrays.asList("subway", "rail"); + ptVehicle.color = Arrays.asList(0, 0, 0); // .asList(7, 145, 222); + ptVehicle.size = 4; + + ActivityConfig workActivity = new ActivityConfig(); + renderConfig.activities.add(workActivity); + workActivity.types.add("work"); + workActivity.maximumLifetime = 300.0; + workActivity.size = 12; + workActivity.color = Arrays.asList(7, 145, 222); + + // END CONFIGURATION + + RunRenderer.run(renderConfig); + } +}