[v10,6/7] libcamera: virtual: Add ImageFrameGenerator
diff mbox series

Message ID 20240829195703.1456614-7-chenghaoyang@chromium.org
State Superseded
Headers show
Series
  • Add VirtualPipelineHandler
Related show

Commit Message

Harvey Yang Aug. 29, 2024, 7:47 p.m. UTC
From: Konami Shu <konamiz@google.com>

Besides TestPatternGenerator, this patch adds ImageFrameGenerator that
loads real images (jpg / jpeg for now) as the source and generates
scaled frames.

Signed-off-by: Konami Shu <konamiz@google.com>
Co-developed-by: Harvey Yang <chenghaoyang@chromium.org>
Co-developed-by: Yunke Cao <yunkec@chromium.org>
Co-developed-by: Tomasz Figa <tfiga@chromium.org>
---
 src/libcamera/pipeline/virtual/README.md      |   9 +-
 .../virtual/image_frame_generator.cpp         | 179 ++++++++++++++++++
 .../pipeline/virtual/image_frame_generator.h  |  54 ++++++
 src/libcamera/pipeline/virtual/meson.build    |   4 +
 src/libcamera/pipeline/virtual/parser.cpp     |  76 +++++++-
 src/libcamera/pipeline/virtual/parser.h       |   2 +
 src/libcamera/pipeline/virtual/utils.h        |  17 ++
 src/libcamera/pipeline/virtual/virtual.cpp    |  60 ++++--
 src/libcamera/pipeline/virtual/virtual.h      |  24 ++-
 9 files changed, 390 insertions(+), 35 deletions(-)
 create mode 100644 src/libcamera/pipeline/virtual/image_frame_generator.cpp
 create mode 100644 src/libcamera/pipeline/virtual/image_frame_generator.h
 create mode 100644 src/libcamera/pipeline/virtual/utils.h

Patch
diff mbox series

diff --git a/src/libcamera/pipeline/virtual/README.md b/src/libcamera/pipeline/virtual/README.md
index ff1e8a5f9..847b8eb87 100644
--- a/src/libcamera/pipeline/virtual/README.md
+++ b/src/libcamera/pipeline/virtual/README.md
@@ -16,7 +16,13 @@  Each camera block is a dictionary, containing the following keys:
     - `width` (`unsigned int`, default=1920): Width of the window resolution. This needs to be even.
     - `height` (`unsigned int`, default=1080): Height of the window resolution.
     - `frame_rates` (list of `int`, default=`[30,60]` ): Range of the frame rate (per second). The list has to be two values of the lower bound and the upper bound of the frame rate.
-- `test_pattern` (`string`): Which test pattern to use as frames. The options are "bars", "lines".
+- `test_pattern` (`string`): Which test pattern to use as frames. The options are "bars", "lines". Cannot be set with `frames`.
+- `frames` (dictionary):
+  - `path` (`string`): Path to an image, or path to a directory of a series of images. Cannot be set with `test_pattern`.
+    - The test patterns are "bars" which means color bars, and "lines" which means diagonal lines.
+    - The path to an image has ".jpg" extension.
+    - The path to a directory ends with "/". The name of the images in the directory are "{n}.jpg" with {n} is the sequence of images starting with 0.
+  - `scale_mode`(`string`, default="fill"): Scale mode when the frames are images. The only scale mode supported now is "fill". This does not affect the scale mode for now.
 - `location` (`string`, default="front"): The location of the camera. Support "front" and "back". This is displayed in qcam camera selection window but this does not change the output.
 - `model` (`string`, default="Unknown"): The model name of the camera. This is displayed in qcam camera selection window but this does not change the output.
 
@@ -37,6 +43,7 @@  This is the procedure of the Parser class:
 3. Parse each property and register the data.
     - `parseSupportedFormats()`: Parses `supported_formats` in the config, which contains resolutions and frame rates.
     - `parseTestPattern()`: Parses `test_pattern` in the config.
+    - `parseFrame()`: Parses `frames` in the config.
     - `parseLocation()`: Parses `location` in the config.
     - `parseModel()`: Parses `model` in the config.
 4. Back to `parseConfigFile()` and append the camera configuration.
diff --git a/src/libcamera/pipeline/virtual/image_frame_generator.cpp b/src/libcamera/pipeline/virtual/image_frame_generator.cpp
new file mode 100644
index 000000000..072e55620
--- /dev/null
+++ b/src/libcamera/pipeline/virtual/image_frame_generator.cpp
@@ -0,0 +1,179 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Google Inc.
+ *
+ * image_frame_generator.cpp - Derived class of FrameGenerator for
+ * generating frames from images
+ */
+
+#include "image_frame_generator.h"
+
+#include <filesystem>
+#include <memory>
+#include <string>
+
+#include <libcamera/base/file.h>
+#include <libcamera/base/log.h>
+
+#include <libcamera/framebuffer.h>
+
+#include "libcamera/internal/mapped_framebuffer.h"
+
+#include "libyuv/convert.h"
+#include "libyuv/scale.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(Virtual)
+
+/*
+ * Factory function to create an ImageFrameGenerator object.
+ * Read the images and convert them to buffers in NV12 format.
+ * Store the pointers to the buffers to a list (imageFrameDatas)
+ */
+std::unique_ptr<ImageFrameGenerator>
+ImageFrameGenerator::create(ImageFrames &imageFrames)
+{
+	std::unique_ptr<ImageFrameGenerator> imageFrameGenerator =
+		std::make_unique<ImageFrameGenerator>();
+	imageFrameGenerator->imageFrames_ = &imageFrames;
+
+	/*
+         * For each file in the directory, load the image,
+         * convert it to NV12, and store the pointer.
+	 */
+	for (unsigned int i = 0; i < imageFrames.number.value_or(1); i++) {
+		std::filesystem::path path;
+		if (!imageFrames.number) {
+			/* If the path is to an image */
+			path = imageFrames.path;
+		} else {
+			/* If the path is to a directory */
+			path = imageFrames.path / (std::to_string(i) + ".jpg");
+		}
+
+		File file(path);
+		if (!file.open(File::OpenModeFlag::ReadOnly)) {
+			LOG(Virtual, Error) << "Failed to open image file " << file.fileName()
+					    << ": " << strerror(file.error());
+			return nullptr;
+		}
+
+		/* Read the image file to data */
+		auto fileSize = file.size();
+		auto buffer = std::make_unique<uint8_t[]>(file.size());
+		if (file.read({ buffer.get(), static_cast<size_t>(fileSize) }) != fileSize) {
+			LOG(Virtual, Error) << "Failed to read file " << file.fileName()
+					    << ": " << strerror(file.error());
+			return nullptr;
+		}
+
+		/* Get the width and height of the image */
+		int width, height;
+		if (libyuv::MJPGSize(buffer.get(), fileSize, &width, &height)) {
+			LOG(Virtual, Error) << "Failed to get the size of the image file: "
+					    << file.fileName();
+			return nullptr;
+		}
+
+		/* Convert to NV12 and write the data to tmpY and tmpUV */
+		unsigned int halfWidth = (width + 1) / 2;
+		unsigned int halfHeight = (height + 1) / 2;
+		std::unique_ptr<uint8_t[]> dstY =
+			std::make_unique<uint8_t[]>(width * height);
+		std::unique_ptr<uint8_t[]> dstUV =
+			std::make_unique<uint8_t[]>(halfWidth * halfHeight * 2);
+		int ret = libyuv::MJPGToNV12(buffer.get(), fileSize,
+					     dstY.get(), width, dstUV.get(),
+					     width, width, height, width, height);
+		if (ret != 0)
+			LOG(Virtual, Error) << "MJPGToNV12() failed with " << ret;
+
+		imageFrameGenerator->imageFrameDatas_.emplace_back(
+			ImageFrameData{ std::move(dstY), std::move(dstUV),
+					Size(width, height) });
+	}
+
+	return imageFrameGenerator;
+}
+
+/* Scale the buffers for image frames. */
+void ImageFrameGenerator::configure(const Size &size)
+{
+	/* Reset the source images to prevent multiple configuration calls */
+	scaledFrameDatas_.clear();
+	frameCount_ = 0;
+	parameter_ = 0;
+
+	for (unsigned int i = 0; i < imageFrames_->number.value_or(1); i++) {
+		/* Scale the imageFrameDatas_ to scaledY and scaledUV */
+		unsigned int halfSizeWidth = (size.width + 1) / 2;
+		unsigned int halfSizeHeight = (size.height + 1) / 2;
+		std::unique_ptr<uint8_t[]> scaledY =
+			std::make_unique<uint8_t[]>(size.width * size.height);
+		std::unique_ptr<uint8_t[]> scaledUV =
+			std::make_unique<uint8_t[]>(halfSizeWidth * halfSizeHeight * 2);
+		auto &src = imageFrameDatas_[i];
+
+		/*
+		 * \todo Some platforms might enforce stride due to GPU, like
+		 * ChromeOS ciri (64). The weight needs to be a multiple of
+		 * the stride to work properly for now.
+		 */
+		libyuv::NV12Scale(src.Y.get(), src.size.width,
+				  src.UV.get(), src.size.width,
+				  src.size.width, src.size.height,
+				  scaledY.get(), size.width, scaledUV.get(), size.width,
+				  size.width, size.height, libyuv::FilterMode::kFilterBilinear);
+
+		scaledFrameDatas_.emplace_back(
+			ImageFrameData{ std::move(scaledY), std::move(scaledUV), size });
+	}
+}
+
+void ImageFrameGenerator::generateFrame(const Size &size, const FrameBuffer *buffer)
+{
+	/* Don't do anything when the list of buffers is empty*/
+	ASSERT(!scaledFrameDatas_.empty());
+
+	MappedFrameBuffer mappedFrameBuffer(buffer, MappedFrameBuffer::MapFlag::Write);
+
+	auto planes = mappedFrameBuffer.planes();
+
+	/* Make sure the frameCount does not over the number of images */
+	frameCount_ %= imageFrames_->number.value_or(1);
+
+	/* Write the scaledY and scaledUV to the mapped frame buffer */
+	libyuv::NV12Copy(scaledFrameDatas_[frameCount_].Y.get(), size.width,
+			 scaledFrameDatas_[frameCount_].UV.get(), size.width, planes[0].begin(),
+			 size.width, planes[1].begin(), size.width,
+			 size.width, size.height);
+
+	/* proceed an image every 4 frames */
+	/* \todo read the parameter_ from the configuration file? */
+	parameter_++;
+	if (parameter_ % 4 == 0)
+		frameCount_++;
+}
+
+/**
+ * \var ImageFrameGenerator::imageFrameDatas_
+ * \brief List of pointers to the not scaled image buffers
+ */
+
+/**
+ * \var ImageFrameGenerator::scaledFrameDatas_
+ * \brief List of pointers to the scaled image buffers
+ */
+
+/**
+ * \var ImageFrameGenerator::imageFrames_
+ * \brief Pointer to the imageFrames_ in VirtualCameraData
+ */
+
+/**
+ * \var ImageFrameGenerator::parameter_
+ * \brief Speed parameter. Change to the next image every parameter_ frames
+ */
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/virtual/image_frame_generator.h b/src/libcamera/pipeline/virtual/image_frame_generator.h
new file mode 100644
index 000000000..4ad8aad24
--- /dev/null
+++ b/src/libcamera/pipeline/virtual/image_frame_generator.h
@@ -0,0 +1,54 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Google Inc.
+ *
+ * image_frame_generator.h - Derived class of FrameGenerator for
+ * generating frames from images
+ */
+
+#pragma once
+
+#include <filesystem>
+#include <memory>
+#include <optional>
+#include <stdint.h>
+#include <sys/types.h>
+
+#include "frame_generator.h"
+
+namespace libcamera {
+
+enum class ScaleMode : char {
+	Fill = 0,
+};
+
+/* Frame configuration provided by the config file */
+struct ImageFrames {
+	std::filesystem::path path;
+	ScaleMode scaleMode;
+	std::optional<unsigned int> number;
+};
+
+class ImageFrameGenerator : public FrameGenerator
+{
+public:
+	static std::unique_ptr<ImageFrameGenerator> create(ImageFrames &imageFrames);
+
+private:
+	struct ImageFrameData {
+		std::unique_ptr<uint8_t[]> Y;
+		std::unique_ptr<uint8_t[]> UV;
+		Size size;
+	};
+
+	void configure(const Size &size) override;
+	void generateFrame(const Size &size, const FrameBuffer *buffer) override;
+
+	std::vector<ImageFrameData> imageFrameDatas_;
+	std::vector<ImageFrameData> scaledFrameDatas_;
+	ImageFrames *imageFrames_;
+	unsigned int frameCount_;
+	unsigned int parameter_;
+};
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build
index d72ac5be7..395919b39 100644
--- a/src/libcamera/pipeline/virtual/meson.build
+++ b/src/libcamera/pipeline/virtual/meson.build
@@ -1,9 +1,13 @@ 
 # SPDX-License-Identifier: CC0-1.0
 
 libcamera_internal_sources += files([
+    'image_frame_generator.cpp',
     'parser.cpp',
     'test_pattern_generator.cpp',
     'virtual.cpp',
 ])
 
+libjpeg = dependency('libjpeg', required : false)
+
 libcamera_deps += [libyuv_dep]
+libcamera_deps += [libjpeg]
diff --git a/src/libcamera/pipeline/virtual/parser.cpp b/src/libcamera/pipeline/virtual/parser.cpp
index e579a3f7c..f0797fc7e 100644
--- a/src/libcamera/pipeline/virtual/parser.cpp
+++ b/src/libcamera/pipeline/virtual/parser.cpp
@@ -52,12 +52,12 @@  Parser::parseConfigFile(File &file, PipelineHandler *pipe)
 			continue;
 		}
 
-		data->id_ = cameraId;
+		data->config_.id = cameraId;
 		ControlInfoMap::Map controls;
 		/* todo: Check which resolution's frame rate to be reported */
 		controls[&controls::FrameDurationLimits] =
-			ControlInfo(int64_t(1000000 / data->supportedResolutions_[0].frameRates[1]),
-				    int64_t(1000000 / data->supportedResolutions_[0].frameRates[0]));
+			ControlInfo(int64_t(1000000 / data->config_.resolutions[0].frameRates[1]),
+				    int64_t(1000000 / data->config_.resolutions[0].frameRates[0]));
 		data->controlInfo_ = ControlInfoMap(std::move(controls), controls::controls);
 		configurations.push_back(std::move(data));
 	}
@@ -75,7 +75,8 @@  Parser::parseCameraConfigData(const YamlObject &cameraConfigData,
 	std::unique_ptr<VirtualCameraData> data =
 		std::make_unique<VirtualCameraData>(pipe, resolutions);
 
-	if (parseTestPattern(cameraConfigData, data.get()))
+	if (parseTestPattern(cameraConfigData, data.get()) &&
+	    parseFrame(cameraConfigData, data.get()))
 		return nullptr;
 
 	if (parseLocation(cameraConfigData, data.get()))
@@ -142,16 +143,75 @@  int Parser::parseTestPattern(const YamlObject &cameraConfigData, VirtualCameraDa
 {
 	std::string testPattern = cameraConfigData["test_pattern"].get<std::string>("");
 
-	/* Default value is "bars" */
 	if (testPattern == "bars") {
-		data->testPattern_ = TestPattern::ColorBars;
+		data->config_.frame = TestPattern::ColorBars;
 	} else if (testPattern == "lines") {
-		data->testPattern_ = TestPattern::DiagonalLines;
+		data->config_.frame = TestPattern::DiagonalLines;
 	} else {
-		LOG(Virtual, Error) << "Test pattern: " << testPattern
+		LOG(Virtual, Debug) << "Test pattern: " << testPattern
 				    << "is not supported";
 		return -EINVAL;
 	}
+
+	return 0;
+}
+
+int Parser::parseFrame(const YamlObject &cameraConfigData, VirtualCameraData *data)
+{
+	const YamlObject &frames = cameraConfigData["frames"];
+
+	/* When there is no frames provided in the config file, use color bar test pattern */
+	if (frames.size() == 0) {
+		data->config_.frame = TestPattern::ColorBars;
+		return 0;
+	}
+
+	if (!frames.isDictionary()) {
+		LOG(Virtual, Error) << "'frames' is not a dictionary.";
+		return -EINVAL;
+	}
+
+	std::string path = frames["path"].get<std::string>("");
+
+	ScaleMode scaleMode;
+	if (auto ext = std::filesystem::path(path).extension();
+	    ext == ".jpg" || ext == ".jpeg") {
+		if (parseScaleMode(frames, &scaleMode))
+			return -EINVAL;
+		data->config_.frame = ImageFrames{ path, scaleMode, std::nullopt };
+	} else if (std::filesystem::is_directory(std::filesystem::symlink_status(path))) {
+		if (parseScaleMode(frames, &scaleMode))
+			return -EINVAL;
+
+		using std::filesystem::directory_iterator;
+		unsigned int numOfFiles = std::distance(directory_iterator(path), directory_iterator{});
+		if (numOfFiles == 0) {
+			LOG(Virtual, Error) << "Empty directory";
+			return -EINVAL;
+		}
+		data->config_.frame = ImageFrames{ path, scaleMode, numOfFiles };
+	} else {
+		LOG(Virtual, Error) << "Frame: " << path << " is not supported";
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+int Parser::parseScaleMode(
+	const YamlObject &framesConfigData, ScaleMode *scaleMode)
+{
+	std::string mode = framesConfigData["scale_mode"].get<std::string>("");
+
+	/* Default value is fill */
+	if (mode == "fill" || mode == "") {
+		*scaleMode = ScaleMode::Fill;
+	} else {
+		LOG(Virtual, Error) << "scaleMode: " << mode
+				    << " is not supported";
+		return -EINVAL;
+	}
+
 	return 0;
 }
 
diff --git a/src/libcamera/pipeline/virtual/parser.h b/src/libcamera/pipeline/virtual/parser.h
index 09c3c56b8..f65616e33 100644
--- a/src/libcamera/pipeline/virtual/parser.h
+++ b/src/libcamera/pipeline/virtual/parser.h
@@ -35,8 +35,10 @@  private:
 	int parseSupportedFormats(const YamlObject &cameraConfigData,
 				  std::vector<VirtualCameraData::Resolution> *resolutions);
 	int parseTestPattern(const YamlObject &cameraConfigData, VirtualCameraData *data);
+	int parseFrame(const YamlObject &cameraConfigData, VirtualCameraData *data);
 	int parseLocation(const YamlObject &cameraConfigData, VirtualCameraData *data);
 	int parseModel(const YamlObject &cameraConfigData, VirtualCameraData *data);
+	int parseScaleMode(const YamlObject &framesConfigData, ScaleMode *scaleMode);
 };
 
 } /* namespace libcamera */
diff --git a/src/libcamera/pipeline/virtual/utils.h b/src/libcamera/pipeline/virtual/utils.h
new file mode 100644
index 000000000..43a14d4b5
--- /dev/null
+++ b/src/libcamera/pipeline/virtual/utils.h
@@ -0,0 +1,17 @@ 
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Google Inc.
+ *
+ * utils.h - Utility types for Virtual Pipeline Handler
+ */
+
+namespace libcamera {
+
+template<class... Ts>
+struct overloaded : Ts... {
+	using Ts::operator()...;
+};
+template<class... Ts>
+overloaded(Ts...) -> overloaded<Ts...>;
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp
index c196a56aa..e79e6c095 100644
--- a/src/libcamera/pipeline/virtual/virtual.cpp
+++ b/src/libcamera/pipeline/virtual/virtual.cpp
@@ -27,6 +27,7 @@ 
 #include "libcamera/internal/yaml_parser.h"
 
 #include "parser.h"
+#include "utils.h"
 
 namespace libcamera {
 
@@ -49,17 +50,18 @@  uint64_t currentTimestamp()
 
 VirtualCameraData::VirtualCameraData(PipelineHandler *pipe,
 				     std::vector<Resolution> supportedResolutions)
-	: Camera::Private(pipe), supportedResolutions_(std::move(supportedResolutions))
+	: Camera::Private(pipe)
 {
-	for (const auto &resolution : supportedResolutions_) {
-		if (minResolutionSize_.isNull() || minResolutionSize_ > resolution.size)
-			minResolutionSize_ = resolution.size;
+	config_.resolutions = std::move(supportedResolutions);
+	for (const auto &resolution : config_.resolutions) {
+		if (config_.minResolutionSize.isNull() || config_.minResolutionSize > resolution.size)
+			config_.minResolutionSize = resolution.size;
 
-		maxResolutionSize_ = std::max(maxResolutionSize_, resolution.size);
+		config_.maxResolutionSize = std::max(config_.maxResolutionSize, resolution.size);
 	}
 
 	properties_.set(properties::PixelArrayActiveAreas,
-			{ Rectangle(maxResolutionSize_) });
+			{ Rectangle(config_.maxResolutionSize) });
 
 	/* \todo Support multiple streams and pass multi_stream_test */
 	streamConfigs_.resize(kMaxStream);
@@ -87,7 +89,7 @@  CameraConfiguration::Status VirtualCameraConfiguration::validate()
 
 	for (StreamConfiguration &cfg : config_) {
 		bool found = false;
-		for (const auto &resolution : data_->supportedResolutions_) {
+		for (const auto &resolution : data_->config_.resolutions) {
 			if (resolution.size.width == cfg.size.width &&
 			    resolution.size.height == cfg.size.height) {
 				found = true;
@@ -96,7 +98,7 @@  CameraConfiguration::Status VirtualCameraConfiguration::validate()
 		}
 
 		if (!found) {
-			cfg.size = data_->maxResolutionSize_;
+			cfg.size = data_->config_.maxResolutionSize;
 			status = Adjusted;
 		}
 
@@ -139,11 +141,11 @@  PipelineHandlerVirtual::generateConfiguration(Camera *camera,
 	for (const StreamRole role : roles) {
 		std::map<PixelFormat, std::vector<SizeRange>> streamFormats;
 		PixelFormat pixelFormat = formats::NV12;
-		streamFormats[pixelFormat] = { { data->minResolutionSize_, data->maxResolutionSize_ } };
+		streamFormats[pixelFormat] = { { data->config_.minResolutionSize, data->config_.maxResolutionSize } };
 		StreamFormats formats(streamFormats);
 		StreamConfiguration cfg(formats);
 		cfg.pixelFormat = pixelFormat;
-		cfg.size = data->maxResolutionSize_;
+		cfg.size = data->config_.maxResolutionSize;
 		cfg.bufferCount = VirtualCameraConfiguration::kBufferCount;
 
 		switch (role) {
@@ -175,6 +177,7 @@  int PipelineHandlerVirtual::configure(Camera *camera,
 	VirtualCameraData *data = cameraData(camera);
 	for (size_t i = 0; i < config->size(); ++i) {
 		config->at(i).setStream(&data->streamConfigs_[i].stream);
+		/* Start reading the images/generating test patterns */
 		data->streamConfigs_[i].frameGenerator->configure(
 			data->streamConfigs_[i].stream.configuration().size);
 	}
@@ -268,10 +271,14 @@  bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator
 		std::set<Stream *> streams;
 		for (auto &streamConfig : data->streamConfigs_)
 			streams.insert(&streamConfig.stream);
-		std::string id = data->id_;
+		std::string id = data->config_.id;
 		std::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);
 
-		initFrameGenerator(camera.get());
+		if (!initFrameGenerator(camera.get())) {
+			LOG(Virtual, Error) << "Failed to initialize frame "
+					    << "generator for camera: " << id;
+			continue;
+		}
 
 		registerCamera(std::move(camera));
 	}
@@ -279,15 +286,30 @@  bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator
 	return true;
 }
 
-void PipelineHandlerVirtual::initFrameGenerator(Camera *camera)
+bool PipelineHandlerVirtual::initFrameGenerator(Camera *camera)
 {
 	auto data = cameraData(camera);
-	for (auto &streamConfig : data->streamConfigs_) {
-		if (data->testPattern_ == TestPattern::DiagonalLines)
-			streamConfig.frameGenerator = std::make_unique<DiagonalLinesGenerator>();
-		else
-			streamConfig.frameGenerator = std::make_unique<ColorBarsGenerator>();
-	}
+	auto &frame = data->config_.frame;
+	std::visit(overloaded{
+			   [&](TestPattern &testPattern) {
+				   for (auto &streamConfig : data->streamConfigs_) {
+					   if (testPattern == TestPattern::DiagonalLines)
+						   streamConfig.frameGenerator = std::make_unique<DiagonalLinesGenerator>();
+					   else
+						   streamConfig.frameGenerator = std::make_unique<ColorBarsGenerator>();
+				   }
+			   },
+			   [&](ImageFrames &imageFrames) {
+				   for (auto &streamConfig : data->streamConfigs_)
+					   streamConfig.frameGenerator = ImageFrameGenerator::create(imageFrames);
+			   } },
+		   frame);
+
+	for (auto &streamConfig : data->streamConfigs_)
+		if (!streamConfig.frameGenerator)
+			return false;
+
+	return true;
 }
 
 REGISTER_PIPELINE_HANDLER(PipelineHandlerVirtual, "virtual")
diff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h
index 5daa960b1..efa97e889 100644
--- a/src/libcamera/pipeline/virtual/virtual.h
+++ b/src/libcamera/pipeline/virtual/virtual.h
@@ -8,6 +8,8 @@ 
 #pragma once
 
 #include <memory>
+#include <string>
+#include <variant>
 #include <vector>
 
 #include <libcamera/base/file.h>
@@ -16,10 +18,14 @@ 
 #include "libcamera/internal/dma_buf_allocator.h"
 #include "libcamera/internal/pipeline_handler.h"
 
+#include "frame_generator.h"
+#include "image_frame_generator.h"
 #include "test_pattern_generator.h"
 
 namespace libcamera {
 
+using VirtualFrame = std::variant<TestPattern, ImageFrames>;
+
 class VirtualCameraData : public Camera::Private
 {
 public:
@@ -33,18 +39,22 @@  public:
 		Stream stream;
 		std::unique_ptr<FrameGenerator> frameGenerator;
 	};
+	/* The config file is parsed to the Configuration struct */
+	struct Configuration {
+		std::string id;
+		std::vector<Resolution> resolutions;
+		VirtualFrame frame;
+
+		Size maxResolutionSize;
+		Size minResolutionSize;
+	};
 
 	VirtualCameraData(PipelineHandler *pipe,
 			  std::vector<Resolution> supportedResolutions);
 
 	~VirtualCameraData() = default;
 
-	std::string id_;
-	TestPattern testPattern_;
-
-	const std::vector<Resolution> supportedResolutions_;
-	Size maxResolutionSize_;
-	Size minResolutionSize_;
+	Configuration config_;
 
 	std::vector<StreamConfig> streamConfigs_;
 };
@@ -89,7 +99,7 @@  private:
 		return static_cast<VirtualCameraData *>(camera->_d());
 	}
 
-	void initFrameGenerator(Camera *camera);
+	bool initFrameGenerator(Camera *camera);
 
 	DmaBufAllocator dmaBufAllocator_;
 };