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);
+ }
+}