Show a patch.

GET /api/1.1/patches/20974/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 20974,
    "url": "https://patchwork.libcamera.org/api/1.1/patches/20974/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/20974/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/1.1/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20240820172202.526547-8-chenghaoyang@google.com>",
    "date": "2024-08-20T16:23:38",
    "name": "[v9,7/8] libcamera: pipeline: Load images",
    "commit_ref": null,
    "pull_url": null,
    "state": "superseded",
    "archived": false,
    "hash": "fd37854690437465535a75061b4257b338000121",
    "submitter": {
        "id": 117,
        "url": "https://patchwork.libcamera.org/api/1.1/people/117/?format=api",
        "name": "Cheng-Hao Yang",
        "email": "chenghaoyang@chromium.org"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/20974/mbox/",
    "series": [
        {
            "id": 4529,
            "url": "https://patchwork.libcamera.org/api/1.1/series/4529/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=4529",
            "date": "2024-08-20T16:23:31",
            "name": "Add VirtualPipelineHandler",
            "version": 9,
            "mbox": "https://patchwork.libcamera.org/series/4529/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/20974/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/20974/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<libcamera-devel-bounces@lists.libcamera.org>",
        "X-Original-To": "parsemail@patchwork.libcamera.org",
        "Delivered-To": "parsemail@patchwork.libcamera.org",
        "Received": [
            "from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 4A4BAC32A9\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 20 Aug 2024 17:22:34 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D06A5633DC;\n\tTue, 20 Aug 2024 19:22:33 +0200 (CEST)",
            "from mail-wm1-x332.google.com (mail-wm1-x332.google.com\n\t[IPv6:2a00:1450:4864:20::332])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 78D07633D4\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 20 Aug 2024 19:22:20 +0200 (CEST)",
            "by mail-wm1-x332.google.com with SMTP id\n\t5b1f17b1804b1-4281faefea9so45950895e9.2\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 20 Aug 2024 10:22:20 -0700 (PDT)",
            "from chenghaoyang-germany.c.googlers.com.com\n\t(161.126.77.34.bc.googleusercontent.com. [34.77.126.161])\n\tby smtp.gmail.com with ESMTPSA id\n\t5b1f17b1804b1-42ab6e90db5sm30328575e9.0.2024.08.20.10.22.18\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tTue, 20 Aug 2024 10:22:19 -0700 (PDT)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=chromium.org header.i=@chromium.org\n\theader.b=\"kk5lOgTg\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=chromium.org; s=google; t=1724174540; x=1724779340;\n\tdarn=lists.libcamera.org; \n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=EMjej9H41FgV3cYdc+aKrEQY/wouaLeHJHp1lz774hE=;\n\tb=kk5lOgTgH5Y4nbjthcEICmYdXU1QUAEIMZr04iQINcqNTPBcdH6pjmnoa24og/lBPE\n\tMtijkRAQ/FdPYI+gXI5KKcpjR7kyH3dl7ZCtFsaeZMmt7J3GBw2aY+0qDzJX5BFoRjd6\n\tJsyLwSLjznol+6Tq0kKUjorRbaiIqcGNXpjdQ=",
        "X-Google-DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1724174540; x=1724779340;\n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc\n\t:subject:date:message-id:reply-to;\n\tbh=EMjej9H41FgV3cYdc+aKrEQY/wouaLeHJHp1lz774hE=;\n\tb=PR0/JHY1q5o2obwUx3HoTTFfLISS4j4psVAaH+3do1tggrPnhqRHx99iN7r7kVb5QB\n\ti8w9TAbTk+lOQ8Ekm7apD25260Jwil7yS4ImQguEwVX3MKnkPD4hwcgp8IpyOPQM5jrq\n\tGUkr1yDbp6n96G7hvCqEpRkCt+Y1thY6rTc/k6aIhHQcDB5kvBeHciWttRNQq53Snjfz\n\tnAlB9FLc3J6GsmHwR3v6yLRpMXeZdrddGceb52huqCNuDDfFQJJ8jYMGNxXyMaJd/NbO\n\tzuzAoUC6ZuPvj+jv+U4Jribya+K5hCajaa8ECWtVrmpRmA7pog2Q39j63A/rkn/XPA6H\n\tH05Q==",
        "X-Gm-Message-State": "AOJu0YxwP0g1cZYceJTsofmKGDGppOatjZRvRCc5Z7LJ+3JBcLhHf1/x\n\tS5uZnp26dYEK/UA8Q00qsimgsyYT5uCcvIaHCLTDvMjnFBgMLmr4WLQLcXwSp5bQoMbRvXhzAh4\n\tv0hQh8ZY=",
        "X-Google-Smtp-Source": "AGHT+IFAJfm+86+s83SNFXJ9sqSU9dVneVH3zHjryC0V4u1zPGpdpxQIev6IwKnNZ0m6rIkzjmz5Dw==",
        "X-Received": "by 2002:a05:600c:198c:b0:425:69b7:3361 with SMTP id\n\t5b1f17b1804b1-42abd21ef23mr587585e9.18.1724174539551; \n\tTue, 20 Aug 2024 10:22:19 -0700 (PDT)",
        "From": "Harvey Yang <chenghaoyang@chromium.org>",
        "X-Google-Original-From": "Harvey Yang <chenghaoyang@google.com>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Cc": "Konami Shu <konamiz@google.com>, Harvey Yang <chenghaoyang@chromium.org>,\n\tYunke Cao <yunkec@chromium.org>, Tomasz Figa <tfiga@chromium.org>",
        "Subject": "[PATCH v9 7/8] libcamera: pipeline: Load images",
        "Date": "Tue, 20 Aug 2024 16:23:38 +0000",
        "Message-ID": "<20240820172202.526547-8-chenghaoyang@google.com>",
        "X-Mailer": "git-send-email 2.46.0.184.g6999bdac58-goog",
        "In-Reply-To": "<20240820172202.526547-1-chenghaoyang@google.com>",
        "References": "<20240820172202.526547-1-chenghaoyang@google.com>",
        "MIME-Version": "1.0",
        "Content-Transfer-Encoding": "8bit",
        "X-BeenThere": "libcamera-devel@lists.libcamera.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "<libcamera-devel.lists.libcamera.org>",
        "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>",
        "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>",
        "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>",
        "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>",
        "Errors-To": "libcamera-devel-bounces@lists.libcamera.org",
        "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"
    },
    "content": "From: Konami Shu <konamiz@google.com>\n\n- Refactor ImageFrameGenerator and TestPatternGenerator\n- Extend the config file to have section to choose to use images or test\n  pattern\n- Extend Parser to parse the configuration\n- Add ImageFrameGenerator which uses images to provide frames\n\nPatchset1->2\n- Extend the parser to accept files with \".jpeg\"\n- Heap allocate some buffers in ImageFrameGenerator::generateFrame so\n  the buffers won't cause stack over flow\n\nPatchset5->6\n- Move static factory function from the interface class to the derived\n  classes\n\nPatchset6->8\n- create FrameGenerator before starting the camera\n\nSigned-off-by: Konami Shu <konamiz@google.com>\nCo-developed-by: Harvey Yang <chenghaoyang@chromium.org>\nCo-developed-by: Yunke Cao <yunkec@chromium.org>\nCo-developed-by: Tomasz Figa <tfiga@chromium.org>\n---\n src/libcamera/pipeline/virtual/README.md      |  18 +-\n .../pipeline/virtual/common_functions.cpp     |  27 +++\n .../pipeline/virtual/common_functions.h       |  18 ++\n .../pipeline/virtual/frame_generator.h        |   2 +-\n .../virtual/image_frame_generator.cpp         | 154 ++++++++++++++++++\n .../pipeline/virtual/image_frame_generator.h  |  65 ++++++++\n src/libcamera/pipeline/virtual/meson.build    |   6 +-\n src/libcamera/pipeline/virtual/parser.cpp     |  77 +++++++--\n src/libcamera/pipeline/virtual/parser.h       |   5 +-\n .../virtual/test_pattern_generator.cpp        |   4 +-\n .../pipeline/virtual/test_pattern_generator.h |   2 +-\n src/libcamera/pipeline/virtual/virtual.cpp    |  37 +++--\n src/libcamera/pipeline/virtual/virtual.h      |  20 ++-\n 13 files changed, 390 insertions(+), 45 deletions(-)\n create mode 100644 src/libcamera/pipeline/virtual/common_functions.cpp\n create mode 100644 src/libcamera/pipeline/virtual/common_functions.h\n create mode 100644 src/libcamera/pipeline/virtual/image_frame_generator.cpp\n create mode 100644 src/libcamera/pipeline/virtual/image_frame_generator.h",
    "diff": "diff --git a/src/libcamera/pipeline/virtual/README.md b/src/libcamera/pipeline/virtual/README.md\nindex 27d6283df..5e21ce74a 100644\n--- a/src/libcamera/pipeline/virtual/README.md\n+++ b/src/libcamera/pipeline/virtual/README.md\n@@ -15,8 +15,13 @@ Each camera block is a dictionary, containing the following keys:\n - `supported_formats` (list of `VirtualCameraData::Resolution`, optional) : List of supported resolution and frame rates of the emulated camera\n     - `width` (`unsigned int`, default=1920): Width of the window resolution. This needs to be even.\n     - `height` (`unsigned int`, default=1080): Height of the window resolution.\n-    - `frame_rates` (list of `int`, default=`[30,60]` ): Range of the frame rate. The list has to be two values of the lower bound and the upper bound of the frame rate.\n-- `test_pattern` (`string`, default=\"bars\"): Which test pattern to use as frames. The options are \"bars\", \"lines\".\n+    - `frame_rates` (list of `int`, default=`[30,60]` ): Range of the frame rate. The list has to be two values of the lower bound and the upper bound of the frame rate. This does not affect the frame rate for now.\n+- `frames` (dictionary):\n+  - `path` (`string`, default=\"bars\"): Name of a test pattern, path to an image, or path to a directory of a series of images.\n+    - The test patterns are \"bars\" which means color bars, and \"lines\" which means diagonal lines.\n+    - The path to an image has \".jpg\" extension.\n+    - 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.\n+  - `scale_mode`(`string`, default=\"fill\"): Scale mode when the frames are images. The scale modes are \"fill\", \"contain\", and \"cover\". This does not matter when frames is a test pattern. This does not affect the scale mode for now.\n - `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.\n - `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.\n \n@@ -35,13 +40,16 @@ A sample config file:\n     frame_rates:\n     - 70\n     - 80\n-  test_pattern: \"bars\"\n+  frames:\n+    path: \"lines\"\n   location: \"front\"\n   model: \"Virtual Video Device\"\n \"Virtual1\":\n   supported_formats:\n   - width: 800\n-  test_pattern: \"lines\"\n+  frames:\n+    path: \"path/to/directory_of_images/\"\n+    scale_mode: \"contain\"\n   location: \"back\"\n   model: \"Virtual Video Device1\"\n \"Virtual2\":\n@@ -61,7 +69,7 @@ This is the procedure of the Parser class:\n     - If the config file contains invalid configuration, this method returns nullptr. The camera will be skipped.\n 3. Parse each property and register the data.\n     - `parseSupportedFormats()`: Parses `supported_formats` in the config, which contains resolutions and frame rates.\n-    - `parseTestPattern()`: Parses `test_pattern` in the config.\n+    - `parseFrame()`: Parses `frames` in the config.\n     - `parseLocation()`: Parses `location` in the config.\n     - `parseModel()`: Parses `model` in the config.\n 4. Back to `parseConfigFile()` and append the camera configuration.\ndiff --git a/src/libcamera/pipeline/virtual/common_functions.cpp b/src/libcamera/pipeline/virtual/common_functions.cpp\nnew file mode 100644\nindex 000000000..207827ee0\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/common_functions.cpp\n@@ -0,0 +1,27 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2023, Google Inc.\n+ *\n+ * common_functions.cpp - Helper that do not depend on any class\n+ */\n+\n+#include \"common_functions.h\"\n+\n+namespace libcamera {\n+\n+std::string getExtension(const std::string &path)\n+{\n+\tsize_t i = path.find(\".\");\n+\tif (i != std::string::npos) {\n+\t\treturn path.substr(i);\n+\t}\n+\treturn \"\";\n+}\n+\n+std::size_t numberOfFilesInDirectory(std::filesystem::path path)\n+{\n+\tusing std::filesystem::directory_iterator;\n+\treturn std::distance(directory_iterator(path), directory_iterator{});\n+}\n+\n+} // namespace libcamera\ndiff --git a/src/libcamera/pipeline/virtual/common_functions.h b/src/libcamera/pipeline/virtual/common_functions.h\nnew file mode 100644\nindex 000000000..4203f9505\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/common_functions.h\n@@ -0,0 +1,18 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2023, Google Inc.\n+ *\n+ * common_functions.h - Helper that do not depend on any class\n+ */\n+\n+#pragma once\n+\n+#include <filesystem>\n+\n+namespace libcamera {\n+\n+std::string getExtension(const std::string &path);\n+\n+std::size_t numberOfFilesInDirectory(std::filesystem::path path);\n+\n+} // namespace libcamera\ndiff --git a/src/libcamera/pipeline/virtual/frame_generator.h b/src/libcamera/pipeline/virtual/frame_generator.h\nindex 9699af7a4..f69576b36 100644\n--- a/src/libcamera/pipeline/virtual/frame_generator.h\n+++ b/src/libcamera/pipeline/virtual/frame_generator.h\n@@ -23,7 +23,7 @@ public:\n \t/** Fill the output frame buffer.\n \t * Use the frame at the frameCount of image frames\n \t */\n-\tvirtual void generateFrame(const Size &size,\n+\tvirtual void generateFrame(unsigned int &frameCount, const Size &size,\n \t\t\t\t   const FrameBuffer *buffer) = 0;\n \n protected:\ndiff --git a/src/libcamera/pipeline/virtual/image_frame_generator.cpp b/src/libcamera/pipeline/virtual/image_frame_generator.cpp\nnew file mode 100644\nindex 000000000..d374877ff\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/image_frame_generator.cpp\n@@ -0,0 +1,154 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2023, Google Inc.\n+ *\n+ * image_frame_generator.cpp - Derived class of FrameGenerator for\n+ * generating frames from images\n+ */\n+\n+#include \"image_frame_generator.h\"\n+\n+#include <memory>\n+#include <optional>\n+#include <string>\n+\n+#include <libcamera/base/file.h>\n+#include <libcamera/base/log.h>\n+\n+#include <libcamera/framebuffer.h>\n+\n+#include \"libcamera/internal/mapped_framebuffer.h\"\n+\n+#include \"libyuv/convert.h\"\n+#include \"libyuv/scale.h\"\n+namespace libcamera {\n+\n+LOG_DECLARE_CATEGORY(Virtual)\n+\n+std::unique_ptr<ImageFrameGenerator> ImageFrameGenerator::create(\n+\tImageFrames &imageFrames)\n+{\n+\tstd::unique_ptr<ImageFrameGenerator> imageFrameGenerator =\n+\t\tstd::make_unique<ImageFrameGenerator>();\n+\timageFrameGenerator->imageFrames_ = &imageFrames;\n+\n+\t/** For each file in the directory\n+\t *  load the image, convert it to NV12, and store the pointer\n+\t */\n+\tfor (unsigned int i = 0; i < imageFrames.number.value_or(1); i++) {\n+\t\tstd::string path;\n+\t\tif (!imageFrames.number.has_value()) {\n+\t\t\t/* If the path is to an image */\n+\t\t\tpath = imageFrames.path;\n+\t\t} else {\n+\t\t\t/* If the path is to a directory */\n+\t\t\tpath = constructPath(imageFrames.path, i);\n+\t\t}\n+\n+\t\tFile file(path);\n+\t\tbool isOpen = file.open(File::OpenModeFlag::ReadOnly);\n+\t\tif (!isOpen) {\n+\t\t\tLOG(Virtual, Error) << \"Failed to open image file: \" << file.fileName();\n+\t\t\treturn nullptr;\n+\t\t}\n+\n+\t\t/* Read the image file to data */\n+\t\tuint8_t buffer[file.size()];\n+\t\tSpan<unsigned char> data{ buffer, (unsigned long)file.size() };\n+\t\tlong dataSize = file.read(data);\n+\n+\t\t/* Get the width and height of the image */\n+\t\tint width, height;\n+\t\tif (libyuv::MJPGSize(data.data(), dataSize, &width, &height)) {\n+\t\t\tLOG(Virtual, Error) << \"Failed to get the size of the image file: \"\n+\t\t\t\t\t    << file.fileName();\n+\t\t\treturn nullptr;\n+\t\t}\n+\n+\t\t/* Convert to NV12 and write the data to tmpY and tmpUV */\n+\t\tint halfWidth = (width + 1) / 2;\n+\t\tint halfHeight = (height + 1) / 2;\n+\t\tstd::unique_ptr<uint8_t[]> dstY =\n+\t\t\tstd::make_unique<uint8_t[]>(width * height);\n+\t\tstd::unique_ptr<uint8_t[]> dstUV =\n+\t\t\tstd::make_unique<uint8_t[]>(halfWidth * halfHeight * 2);\n+\t\tint ret = libyuv::MJPGToNV12(data.data(), dataSize,\n+\t\t\t\t\t     dstY.get(), width, dstUV.get(),\n+\t\t\t\t\t     width, width, height, width, height);\n+\t\tif (ret != 0) {\n+\t\t\tLOG(Virtual, Error) << \"MJPGToNV12() failed with \" << ret;\n+\t\t}\n+\n+\t\timageFrameGenerator->imageFrameDatas_.emplace_back(\n+\t\t\tImageFrameData{ std::move(dstY), std::move(dstUV),\n+\t\t\t\t\tSize(width, height) });\n+\t}\n+\treturn imageFrameGenerator;\n+}\n+\n+std::string ImageFrameGenerator::constructPath(std::string &name, unsigned int &i)\n+{\n+\treturn name + std::to_string(i) + \".jpg\";\n+}\n+\n+void ImageFrameGenerator::configure(const Size &size)\n+{\n+\tfor (unsigned int i = 0; i < imageFrames_->number.value_or(1); i++) {\n+\t\t/* Scale the imageFrameDatas_ to scaledY and scaledUV */\n+\t\tint halfSizeWidth = (size.width + 1) / 2;\n+\t\tint halfSizeHeight = (size.height + 1) / 2;\n+\t\tstd::unique_ptr<uint8_t[]> scaledY =\n+\t\t\tstd::make_unique<uint8_t[]>(size.width * size.height);\n+\t\tstd::unique_ptr<uint8_t[]> scaledUV =\n+\t\t\tstd::make_unique<uint8_t[]>(halfSizeWidth * halfSizeHeight * 2);\n+\t\tauto &src = imageFrameDatas_[i];\n+\n+\t\t/*\n+\t\t * \\todo Implement \"contain\" & \"cover\", based on\n+\t\t * |imageFrames_[i].scaleMode|.\n+\t\t */\n+\n+\t\t/*\n+\t\t * \\todo Some platforms might enforce stride due to GPU, like\n+                 * ChromeOS ciri (64). The weight needs to be a multiple of\n+                 * the stride to work properly for now.\n+\t\t */\n+\t\tlibyuv::NV12Scale(src.Y.get(), src.size.width,\n+\t\t\t\t  src.UV.get(), src.size.width,\n+\t\t\t\t  src.size.width, src.size.height,\n+\t\t\t\t  scaledY.get(), size.width, scaledUV.get(), size.width,\n+\t\t\t\t  size.width, size.height, libyuv::FilterMode::kFilterBilinear);\n+\n+\t\t/* Store the pointers to member variable */\n+\t\tscaledFrameDatas_.emplace_back(\n+\t\t\tImageFrameData{ std::move(scaledY), std::move(scaledUV), size });\n+\t}\n+}\n+\n+void ImageFrameGenerator::generateFrame(unsigned int &frameCount, const Size &size, const FrameBuffer *buffer)\n+{\n+\t/* Don't do anything when the list of buffers is empty*/\n+\tif (scaledFrameDatas_.size() == 0)\n+\t\treturn;\n+\n+\tMappedFrameBuffer mappedFrameBuffer(buffer, MappedFrameBuffer::MapFlag::Write);\n+\n+\tauto planes = mappedFrameBuffer.planes();\n+\n+\t/* Make sure the frameCount does not over the number of images */\n+\tframeCount %= imageFrames_->number.value_or(1);\n+\n+\t/* Write the scaledY and scaledUV to the mapped frame buffer */\n+\tlibyuv::NV12Copy(scaledFrameDatas_[frameCount].Y.get(), size.width,\n+\t\t\t scaledFrameDatas_[frameCount].UV.get(), size.width, planes[0].begin(),\n+\t\t\t size.width, planes[1].begin(), size.width,\n+\t\t\t size.width, size.height);\n+\n+\t/* proceed an image every 4 frames */\n+\t/* \\todo read the parameter_ from the configuration file? */\n+\tparameter_++;\n+\tif (parameter_ % 4 == 0)\n+\t\tframeCount++;\n+}\n+\n+} // namespace libcamera\ndiff --git a/src/libcamera/pipeline/virtual/image_frame_generator.h b/src/libcamera/pipeline/virtual/image_frame_generator.h\nnew file mode 100644\nindex 000000000..74468e075\n--- /dev/null\n+++ b/src/libcamera/pipeline/virtual/image_frame_generator.h\n@@ -0,0 +1,65 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2023, Google Inc.\n+ *\n+ * image_frame_generator.h - Derived class of FrameGenerator for\n+ * generating frames from images\n+ */\n+\n+#pragma once\n+\n+#include <memory>\n+#include <optional>\n+#include <stdint.h>\n+#include <sys/types.h>\n+\n+#include \"frame_generator.h\"\n+\n+namespace libcamera {\n+\n+enum class ScaleMode : char {\n+\tFill = 0,\n+\tContain = 1,\n+\tCover = 2,\n+};\n+\n+/* Frame configuration provided by the config file */\n+struct ImageFrames {\n+\tstd::string path;\n+\tScaleMode scaleMode;\n+\tstd::optional<unsigned int> number;\n+};\n+\n+class ImageFrameGenerator : public FrameGenerator\n+{\n+public:\n+\t/** Factory function to create an ImageFrameGenerator object.\n+\t *  Read the images and convert them to buffers in NV12 format.\n+\t *  Store the pointers to the buffers to a list (imageFrameDatas)\n+\t */\n+\tstatic std::unique_ptr<ImageFrameGenerator> create(ImageFrames &imageFrames);\n+\n+private:\n+\tstruct ImageFrameData {\n+\t\tstd::unique_ptr<uint8_t[]> Y;\n+\t\tstd::unique_ptr<uint8_t[]> UV;\n+\t\tSize size;\n+\t};\n+\n+\t/* Scale the buffers for image frames. */\n+\tvoid configure(const Size &size) override;\n+\tvoid generateFrame(unsigned int &frameCount, const Size &size, const FrameBuffer *buffer) override;\n+\n+\tstatic std::string constructPath(std::string &name, unsigned int &i);\n+\n+\t/* List of pointers to the not scaled image buffers */\n+\tstd::vector<ImageFrameData> imageFrameDatas_;\n+\t/* List of pointers to the scaled image buffers */\n+\tstd::vector<ImageFrameData> scaledFrameDatas_;\n+\t/* Pointer to the imageFrames_ in VirtualCameraData */\n+\tImageFrames *imageFrames_;\n+\t/* Speed parameter. Change to the next image every parameter_ frames. */\n+\tint parameter_;\n+};\n+\n+} /* namespace libcamera */\ndiff --git a/src/libcamera/pipeline/virtual/meson.build b/src/libcamera/pipeline/virtual/meson.build\nindex 2e82e64cb..d56aedec4 100644\n--- a/src/libcamera/pipeline/virtual/meson.build\n+++ b/src/libcamera/pipeline/virtual/meson.build\n@@ -2,11 +2,14 @@\n \n libcamera_sources += files([\n     'virtual.cpp',\n-    'test_pattern_generator.cpp',\n     'parser.cpp',\n+    'test_pattern_generator.cpp',\n+    'image_frame_generator.cpp',\n+    'common_functions.cpp',\n ])\n \n libyuv_dep = dependency('libyuv', required : false)\n+libjpeg = dependency('libjpeg', required : false)\n \n # Fallback to a subproject if libyuv isn't found, as it's typically not\n # provided by distributions.\n@@ -26,3 +29,4 @@ if not libyuv_dep.found()\n endif\n \n libcamera_deps += [libyuv_dep]\n+libcamera_deps += [libjpeg]\ndiff --git a/src/libcamera/pipeline/virtual/parser.cpp b/src/libcamera/pipeline/virtual/parser.cpp\nindex 032c0cd9d..46d1ab181 100644\n--- a/src/libcamera/pipeline/virtual/parser.cpp\n+++ b/src/libcamera/pipeline/virtual/parser.cpp\n@@ -18,6 +18,7 @@\n #include \"libcamera/internal/pipeline_handler.h\"\n #include \"libcamera/internal/yaml_parser.h\"\n \n+#include \"common_functions.h\"\n #include \"virtual.h\"\n \n namespace libcamera {\n@@ -52,12 +53,12 @@ std::vector<std::unique_ptr<VirtualCameraData>> Parser::parseConfigFile(\n \t\t\tcontinue;\n \t\t}\n \n-\t\tdata->id_ = cameraId;\n+\t\tdata->config_.id = cameraId;\n \t\tControlInfoMap::Map controls;\n \t\t/* todo: Check which resolution's frame rate to be reported */\n \t\tcontrols[&controls::FrameDurationLimits] =\n-\t\t\tControlInfo(int64_t(1000 / data->supportedResolutions_[0].frameRates[1]),\n-\t\t\t\t    int64_t(1000 / data->supportedResolutions_[0].frameRates[0]));\n+\t\t\tControlInfo(int64_t(1000 / data->config_.resolutions[0].frameRates[1]),\n+\t\t\t\t    int64_t(1000 / data->config_.resolutions[0].frameRates[0]));\n \t\tdata->controlInfo_ = ControlInfoMap(std::move(controls), controls::controls);\n \t\tconfigurations.push_back(std::move(data));\n \t}\n@@ -72,7 +73,7 @@ std::unique_ptr<VirtualCameraData> Parser::parseCameraConfigData(\n \tif (parseSupportedFormats(cameraConfigData, data.get()))\n \t\treturn nullptr;\n \n-\tif (parseTestPattern(cameraConfigData, data.get()))\n+\tif (parseFrame(cameraConfigData, data.get()))\n \t\treturn nullptr;\n \n \tif (parseLocation(cameraConfigData, data.get()))\n@@ -122,14 +123,14 @@ int Parser::parseSupportedFormats(\n \t\t\t\tframeRates.push_back(60);\n \t\t\t}\n \n-\t\t\tdata->supportedResolutions_.emplace_back(\n+\t\t\tdata->config_.resolutions.emplace_back(\n \t\t\t\tVirtualCameraData::Resolution{ Size{ width, height },\n \t\t\t\t\t\t\t       frameRates });\n \n \t\t\tactiveResolution = std::max(activeResolution, Size{ width, height });\n \t\t}\n \t} else {\n-\t\tdata->supportedResolutions_.emplace_back(\n+\t\tdata->config_.resolutions.emplace_back(\n \t\t\tVirtualCameraData::Resolution{ Size{ 1920, 1080 },\n \t\t\t\t\t\t       { 30, 60 } });\n \t\tactiveResolution = Size(1920, 1080);\n@@ -141,21 +142,65 @@ int Parser::parseSupportedFormats(\n \treturn 0;\n }\n \n-int Parser::parseTestPattern(\n+int Parser::parseFrame(\n \tconst YamlObject &cameraConfigData, VirtualCameraData *data)\n {\n-\tstd::string testPattern = cameraConfigData[\"test_pattern\"].get<std::string>().value();\n+\tconst YamlObject &frames = cameraConfigData[\"frames\"];\n+\t/* When there is no frames provided in the config file, use color bar test pattern */\n+\tif (frames.size() == 0) {\n+\t\tdata->config_.frame = TestPattern::ColorBars;\n+\t\treturn 0;\n+\t}\n+\n+\tif (!frames.isDictionary()) {\n+\t\tLOG(Virtual, Error) << \"'frames' is not a dictionary.\";\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tstd::string path = frames[\"path\"].get<std::string>().value();\n+\n+\tif (auto ext = getExtension(path); ext == \".jpg\" || ext == \".jpeg\") {\n+\t\tScaleMode scaleMode;\n+\t\tif (parseScaleMode(frames, &scaleMode))\n+\t\t\treturn -EINVAL;\n+\t\tdata->config_.frame = ImageFrames{ path, scaleMode, std::nullopt };\n+\t} else if (path.back() == '/') {\n+\t\tScaleMode scaleMode;\n+\t\tif (parseScaleMode(frames, &scaleMode))\n+\t\t\treturn -EINVAL;\n+\t\tdata->config_.frame = ImageFrames{ path, scaleMode,\n+\t\t\t\t\t\t   numberOfFilesInDirectory(path) };\n+\t} else if (path == \"bars\" || path == \"\") {\n+\t\t/* Default value is \"bars\" */\n+\t\tdata->config_.frame = TestPattern::ColorBars;\n+\t} else if (path == \"lines\") {\n+\t\tdata->config_.frame = TestPattern::DiagonalLines;\n+\t} else {\n+\t\tLOG(Virtual, Error) << \"Frame: \" << path\n+\t\t\t\t    << \" is not supported\";\n+\t\treturn -EINVAL;\n+\t}\n+\treturn 0;\n+}\n \n-\t/* Default value is \"bars\" */\n-\tif (testPattern == \"bars\" || testPattern == \"\") {\n-\t\tdata->testPattern_ = TestPattern::ColorBars;\n-\t} else if (testPattern == \"lines\") {\n-\t\tdata->testPattern_ = TestPattern::DiagonalLines;\n+int Parser::parseScaleMode(\n+\tconst YamlObject &framesConfigData, ScaleMode *scaleMode)\n+{\n+\tstd::string mode = framesConfigData[\"scale_mode\"].get<std::string>().value();\n+\n+\t/* Default value is fill */\n+\tif (mode == \"fill\" || mode == \"\") {\n+\t\t*scaleMode = ScaleMode::Fill;\n+\t} else if (mode == \"contain\") {\n+\t\t*scaleMode = ScaleMode::Contain;\n+\t} else if (mode == \"cover\") {\n+\t\t*scaleMode = ScaleMode::Cover;\n \t} else {\n-\t\tLOG(Virtual, Error) << \"Test pattern: \" << testPattern\n-\t\t\t\t    << \"is not supported\";\n+\t\tLOG(Virtual, Error) << \"scaleMode: \" << mode\n+\t\t\t\t    << \" is not supported\";\n \t\treturn -EINVAL;\n \t}\n+\n \treturn 0;\n }\n \n@@ -195,4 +240,4 @@ int Parser::parseModel(\n \treturn 0;\n }\n \n-} /* namespace libcamera */\n+} // namespace libcamera\ndiff --git a/src/libcamera/pipeline/virtual/parser.h b/src/libcamera/pipeline/virtual/parser.h\nindex a377d8aa1..38ea460d5 100644\n--- a/src/libcamera/pipeline/virtual/parser.h\n+++ b/src/libcamera/pipeline/virtual/parser.h\n@@ -34,12 +34,15 @@ private:\n \n \tint parseSupportedFormats(\n \t\tconst YamlObject &cameraConfigData, VirtualCameraData *data);\n-\tint parseTestPattern(\n+\tint parseFrame(\n \t\tconst YamlObject &cameraConfigData, VirtualCameraData *data);\n \tint parseLocation(\n \t\tconst YamlObject &cameraConfigData, VirtualCameraData *data);\n \tint parseModel(\n \t\tconst YamlObject &cameraConfigData, VirtualCameraData *data);\n+\n+\tint parseScaleMode(\n+\t\tconst YamlObject &framesConfigData, ScaleMode *scaleMode);\n };\n \n } // namespace libcamera\ndiff --git a/src/libcamera/pipeline/virtual/test_pattern_generator.cpp b/src/libcamera/pipeline/virtual/test_pattern_generator.cpp\nindex 6df9b31e9..844dffd49 100644\n--- a/src/libcamera/pipeline/virtual/test_pattern_generator.cpp\n+++ b/src/libcamera/pipeline/virtual/test_pattern_generator.cpp\n@@ -20,7 +20,7 @@ LOG_DECLARE_CATEGORY(Virtual)\n static const unsigned int kARGBSize = 4;\n \n void TestPatternGenerator::generateFrame(\n-\tconst Size &size,\n+\t[[maybe_unused]] unsigned int &frameCount, const Size &size,\n \tconst FrameBuffer *buffer)\n {\n \tMappedFrameBuffer mappedFrameBuffer(buffer,\n@@ -29,7 +29,7 @@ void TestPatternGenerator::generateFrame(\n \tauto planes = mappedFrameBuffer.planes();\n \n \t/* TODO: select whether to do shifting or not */\n-\tshiftLeft(size);\n+\t// shiftLeft(size);\n \n \t/* Convert the template_ to the frame buffer */\n \tint ret = libyuv::ARGBToNV12(\ndiff --git a/src/libcamera/pipeline/virtual/test_pattern_generator.h b/src/libcamera/pipeline/virtual/test_pattern_generator.h\nindex ed8d4e43b..cebdd0141 100644\n--- a/src/libcamera/pipeline/virtual/test_pattern_generator.h\n+++ b/src/libcamera/pipeline/virtual/test_pattern_generator.h\n@@ -25,7 +25,7 @@ enum class TestPattern : char {\n class TestPatternGenerator : public FrameGenerator\n {\n private:\n-\tvoid generateFrame(const Size &size,\n+\tvoid generateFrame(unsigned int &frameCount, const Size &size,\n \t\t\t   const FrameBuffer *buffer) override;\n \n protected:\ndiff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp\nindex 0fe471f00..2412af703 100644\n--- a/src/libcamera/pipeline/virtual/virtual.cpp\n+++ b/src/libcamera/pipeline/virtual/virtual.cpp\n@@ -20,9 +20,13 @@\n #include \"libcamera/internal/pipeline_handler.h\"\n #include \"libcamera/internal/yaml_parser.h\"\n \n-#include \"frame_generator.h\"\n #include \"parser.h\"\n \n+#define ifTestPattern(v) std::holds_alternative<TestPattern>(v)\n+#define getTestPattern(v) std::get<TestPattern>(v)\n+#define ifImageFrames(v) std::holds_alternative<ImageFrames>(v)\n+#define getImageFrames(v) std::get<ImageFrames>(v)\n+\n namespace libcamera {\n \n LOG_DEFINE_CATEGORY(Virtual)\n@@ -63,12 +67,12 @@ CameraConfiguration::Status VirtualCameraConfiguration::validate()\n \t}\n \n \tSize maxSize;\n-\tfor (const auto &resolution : data_->supportedResolutions_)\n+\tfor (const auto &resolution : data_->config_.resolutions)\n \t\tmaxSize = std::max(maxSize, resolution.size);\n \n \tfor (StreamConfiguration &cfg : config_) {\n \t\tbool found = false;\n-\t\tfor (const auto &resolution : data_->supportedResolutions_) {\n+\t\tfor (const auto &resolution : data_->config_.resolutions) {\n \t\t\tif (resolution.size.width == cfg.size.width &&\n \t\t\t    resolution.size.height == cfg.size.height) {\n \t\t\t\tfound = true;\n@@ -110,7 +114,7 @@ PipelineHandlerVirtual::generateConfiguration(Camera *camera,\n \t\treturn config;\n \n \tSize minSize, sensorResolution;\n-\tfor (const auto &resolution : data->supportedResolutions_) {\n+\tfor (const auto &resolution : data->config_.resolutions) {\n \t\tif (minSize.isNull() || minSize > resolution.size)\n \t\t\tminSize = resolution.size;\n \n@@ -199,7 +203,7 @@ int PipelineHandlerVirtual::exportFrameBuffers(\n int PipelineHandlerVirtual::start(Camera *camera,\n \t\t\t\t  [[maybe_unused]] const ControlList *controls)\n {\n-\t/* \\todo Start reading the virtual video if any. */\n+\t/* Start reading the images/generating test patterns */\n \tVirtualCameraData *data = cameraData(camera);\n \n \tdata->frameGenerator_->configure(data->stream_.configuration().size);\n@@ -219,8 +223,8 @@ int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera *camera,\n \n \t/* \\todo Read from the virtual video if any. */\n \tfor (auto const &[stream, buffer] : request->buffers()) {\n-\t\t/* map buffer and fill test patterns */\n-\t\tdata->frameGenerator_->generateFrame(stream->configuration().size, buffer);\n+\t\t/* Map buffer. Fill test patterns or images */\n+\t\tdata->frameGenerator_->generateFrame(data->frameCount_, stream->configuration().size, buffer);\n \t\tcompleteBuffer(request, buffer);\n \t}\n \n@@ -250,9 +254,10 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator\n \t/* Configure and register cameras with configData */\n \tfor (auto &data : configData) {\n \t\tstd::set<Stream *> streams{ &data->stream_ };\n-\t\tstd::string id = data->id_;\n+\t\tstd::string id = data->config_.id;\n \t\tstd::shared_ptr<Camera> camera = Camera::create(std::move(data), id, streams);\n \n+\t\t/* Initialize FrameGenerator*/\n \t\tinitFrameGenerator(camera.get());\n \n \t\tregisterCamera(std::move(camera));\n@@ -264,13 +269,19 @@ bool PipelineHandlerVirtual::match([[maybe_unused]] DeviceEnumerator *enumerator\n void PipelineHandlerVirtual::initFrameGenerator(Camera *camera)\n {\n \tauto data = cameraData(camera);\n-\tif (data->testPattern_ == TestPattern::DiagonalLines) {\n-\t\tdata->frameGenerator_ = DiagonalLinesGenerator::create();\n-\t} else {\n-\t\tdata->frameGenerator_ = ColorBarsGenerator::create();\n+\tauto &frame = data->config_.frame;\n+\tif (ifTestPattern(frame)) {\n+\t\tTestPattern &testPattern = getTestPattern(frame);\n+\t\tif (testPattern == TestPattern::DiagonalLines) {\n+\t\t\tdata->frameGenerator_ = DiagonalLinesGenerator::create();\n+\t\t} else {\n+\t\t\tdata->frameGenerator_ = ColorBarsGenerator::create();\n+\t\t}\n+\t} else if (ifImageFrames(frame)) {\n+\t\tdata->frameGenerator_ = ImageFrameGenerator::create(getImageFrames(frame));\n \t}\n }\n \n REGISTER_PIPELINE_HANDLER(PipelineHandlerVirtual, \"virtual\")\n \n-} /* namespace libcamera */\n+} // namespace libcamera\ndiff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h\nindex c1ac4eb90..f41a4a906 100644\n--- a/src/libcamera/pipeline/virtual/virtual.h\n+++ b/src/libcamera/pipeline/virtual/virtual.h\n@@ -7,16 +7,22 @@\n \n #pragma once\n \n+#include <variant>\n+\n #include <libcamera/base/file.h>\n \n #include \"libcamera/internal/camera.h\"\n #include \"libcamera/internal/dma_buf_allocator.h\"\n #include \"libcamera/internal/pipeline_handler.h\"\n \n+#include \"frame_generator.h\"\n+#include \"image_frame_generator.h\"\n #include \"test_pattern_generator.h\"\n \n namespace libcamera {\n \n+using VirtualFrame = std::variant<TestPattern, ImageFrames>;\n+\n class VirtualCameraData : public Camera::Private\n {\n public:\n@@ -24,6 +30,13 @@ public:\n \t\tSize size;\n \t\tstd::vector<int> frameRates;\n \t};\n+\t/* The config file is parsed to the Configuration struct */\n+\tstruct Configuration {\n+\t\tstd::string id;\n+\t\tstd::vector<Resolution> resolutions;\n+\t\tVirtualFrame frame;\n+\t};\n+\n \tVirtualCameraData(PipelineHandler *pipe)\n \t\t: Camera::Private(pipe)\n \t{\n@@ -31,12 +44,9 @@ public:\n \n \t~VirtualCameraData() = default;\n \n-\tstd::string id_;\n-\tstd::vector<Resolution> supportedResolutions_;\n-\tTestPattern testPattern_;\n-\n+\tunsigned int frameCount_ = 0;\n+\tConfiguration config_;\n \tStream stream_;\n-\n \tstd::unique_ptr<FrameGenerator> frameGenerator_;\n };\n \n",
    "prefixes": [
        "v9",
        "7/8"
    ]
}