{"id":19570,"url":"https://patchwork.libcamera.org/api/patches/19570/?format=json","web_url":"https://patchwork.libcamera.org/patch/19570/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20240229170115.159450-1-stefan.klug@ideasonboard.com>","date":"2024-02-29T17:01:15","name":"[RFC] libcamera: lc-compliance: Add initial set of per frame control tests","commit_ref":null,"pull_url":null,"state":"superseded","archived":false,"hash":"552016f8e4635580d28052c4a3975470c1a70c98","submitter":{"id":184,"url":"https://patchwork.libcamera.org/api/people/184/?format=json","name":"Stefan Klug","email":"stefan.klug@ideasonboard.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/19570/mbox/","series":[{"id":4194,"url":"https://patchwork.libcamera.org/api/series/4194/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=4194","date":"2024-02-29T17:01:15","name":"[RFC] libcamera: lc-compliance: Add initial set of per frame control tests","version":1,"mbox":"https://patchwork.libcamera.org/series/4194/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/19570/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/19570/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 4F587BE080\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 29 Feb 2024 17:01:22 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id ABE3B62867;\n\tThu, 29 Feb 2024 18:01:21 +0100 (CET)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 370B161C96\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 29 Feb 2024 18:01:20 +0100 (CET)","from jasper.fritz.box (unknown\n\t[IPv6:2a00:6020:448c:6c00:69c9:7315:683e:a8b])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 91283C67;\n\tThu, 29 Feb 2024 18:01:06 +0100 (CET)"],"Authentication-Results":"lancelot.ideasonboard.com;\n\tdkim=fail reason=\"signature verification failed\" (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"uhM/eH7F\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1709226066;\n\tbh=JIxSa+R1rH1J0MeEaCBkwHypBwQpIkywAnA+kexo1g8=;\n\th=From:To:Cc:Subject:Date:From;\n\tb=uhM/eH7Fxws2yC2N01heojf4LF/4eOls8X2iZI5LB0Vqce5sUR5C2KOmWa6LwwDzS\n\tzwgiPVGsP7ELMeVvx+U/HEsb/pifvhw+KWM+6iOVE9tZwjrAnBtN0W5k1p136KsiHV\n\tW/dwKIYyPUKeBrZLnmwYRFgBR3rfAA3KMaXJLd+s=","From":"Stefan Klug <stefan.klug@ideasonboard.com>","To":"libcamera-devel@lists.libcamera.org","Subject":"[RFC] libcamera: lc-compliance: Add initial set of per frame control\n\ttests","Date":"Thu, 29 Feb 2024 18:01:15 +0100","Message-Id":"<20240229170115.159450-1-stefan.klug@ideasonboard.com>","X-Mailer":"git-send-email 2.40.1","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":"These tests check if controls (only exposure time and analogue gain at\nthe moment) get applied on the frame they were requested for.\n\nThis is tested by looking at the metadata and additionally by\ncalculating a mean brightness on a centered rect of 20x20 pixels.\n\nUntil today, these tests where only run on a project specific branch\nwith a modified simple pipeline. In theory they should pass on a\ncurrent master :-)\n\nCurrent test setup: imx219 with simple pipeline on an imx8mp.\nModifications of either the exposure delay or the gain delay in\nthe camera_sensor class resulted in test failures.\nWhich is exactly what this test shall proove.\n\nSigned-off-by: Stefan Klug <stefan.klug@ideasonboard.com>\n---\n src/apps/lc-compliance/capture_test.cpp       |  39 ++++\n src/apps/lc-compliance/meson.build            |   2 +\n src/apps/lc-compliance/per_frame_controls.cpp | 214 ++++++++++++++++++\n src/apps/lc-compliance/per_frame_controls.h   |  41 ++++\n src/apps/lc-compliance/simple_capture.cpp     |   4 +-\n src/apps/lc-compliance/simple_capture.h       |   2 +-\n src/apps/lc-compliance/time_sheet.cpp         | 135 +++++++++++\n src/apps/lc-compliance/time_sheet.h           |  53 +++++\n 8 files changed, 487 insertions(+), 3 deletions(-)\n create mode 100644 src/apps/lc-compliance/per_frame_controls.cpp\n create mode 100644 src/apps/lc-compliance/per_frame_controls.h\n create mode 100644 src/apps/lc-compliance/time_sheet.cpp\n create mode 100644 src/apps/lc-compliance/time_sheet.h","diff":"diff --git a/src/apps/lc-compliance/capture_test.cpp b/src/apps/lc-compliance/capture_test.cpp\nindex 1dcfcf92..43fe59f3 100644\n--- a/src/apps/lc-compliance/capture_test.cpp\n+++ b/src/apps/lc-compliance/capture_test.cpp\n@@ -11,6 +11,7 @@\n #include <gtest/gtest.h>\n \n #include \"environment.h\"\n+#include \"per_frame_controls.h\"\n #include \"simple_capture.h\"\n \n using namespace libcamera;\n@@ -133,3 +134,41 @@ INSTANTIATE_TEST_SUITE_P(CaptureTests,\n \t\t\t testing::Combine(testing::ValuesIn(ROLES),\n \t\t\t\t\t  testing::ValuesIn(NUMREQUESTS)),\n \t\t\t SingleStream::nameParameters);\n+\n+/*\n+ * Test Per frame controls\n+ */\n+TEST_F(SingleStream, testFramePreciseExposureChange)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::Viewfinder);\n+\tcapture.testFramePreciseExposureChange();\n+}\n+\n+TEST_F(SingleStream, testFramePreciseGainChange)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::Viewfinder);\n+\tcapture.testFramePreciseGainChange();\n+}\n+\n+TEST_F(SingleStream, testExposureGainIsAppliedOnFirstFrame)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::Viewfinder);\n+\tcapture.testExposureGainIsAppliedOnFirstFrame();\n+}\n+\n+TEST_F(SingleStream, testExposureGainFromFirstRequestGetsApplied)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::Viewfinder);\n+\tcapture.testExposureGainFromFirstRequestGetsApplied();\n+}\n+\n+TEST_F(SingleStream, testExposureGainFromFirstAndSecondRequestGetsApplied)\n+{\n+\tPerFrameControls capture(camera_);\n+\tcapture.configure(StreamRole::Viewfinder);\n+\tcapture.testExposureGainFromFirstAndSecondRequestGetsApplied();\n+}\ndiff --git a/src/apps/lc-compliance/meson.build b/src/apps/lc-compliance/meson.build\nindex c792f072..2a6f52af 100644\n--- a/src/apps/lc-compliance/meson.build\n+++ b/src/apps/lc-compliance/meson.build\n@@ -15,7 +15,9 @@ lc_compliance_sources = files([\n     'capture_test.cpp',\n     'environment.cpp',\n     'main.cpp',\n+    'per_frame_controls.cpp',\n     'simple_capture.cpp',\n+    'time_sheet.cpp',\n ])\n \n lc_compliance  = executable('lc-compliance', lc_compliance_sources,\ndiff --git a/src/apps/lc-compliance/per_frame_controls.cpp b/src/apps/lc-compliance/per_frame_controls.cpp\nnew file mode 100644\nindex 00000000..70fc44ac\n--- /dev/null\n+++ b/src/apps/lc-compliance/per_frame_controls.cpp\n@@ -0,0 +1,214 @@\n+/* SPDX-License-Identifier: GPL-2.0-or-later */\n+/*\n+ * Copyright (C) 2024, Ideas on Board Oy\n+ *\n+ * per_frame_controls.cpp - Tests for per frame controls\n+ */\n+#include \"per_frame_controls.h\"\n+\n+#include <gtest/gtest.h>\n+\n+#include \"time_sheet.h\"\n+\n+using namespace libcamera;\n+\n+PerFrameControls::PerFrameControls(std::shared_ptr<Camera> camera)\n+\t: SimpleCapture(camera)\n+{\n+}\n+\n+std::shared_ptr<TimeSheet> PerFrameControls::startCaptureWithTimeSheet(unsigned int framesToCapture, const ControlList *controls)\n+{\n+\tControlList ctrls(camera_->controls().idmap());\n+\t/* Ensure defined default values */\n+\tctrls.set(controls::AeEnable, false);\n+\tctrls.set(controls::AeExposureMode, controls::ExposureCustom);\n+\tctrls.set(controls::ExposureTime, 10000);\n+\tctrls.set(controls::AnalogueGain, 1.0);\n+\n+\tif (controls) {\n+\t\tctrls.merge(*controls, true);\n+\t}\n+\n+\tstart(&ctrls);\n+\n+\tqueueCount_ = 0;\n+\tcaptureCount_ = 0;\n+\tcaptureLimit_ = framesToCapture;\n+\n+\tauto timeSheet = std::make_shared<TimeSheet>(captureLimit_, camera_->controls().idmap());\n+\ttimeSheet_ = timeSheet;\n+\treturn timeSheet;\n+}\n+\n+int PerFrameControls::queueRequest(Request *request)\n+{\n+\tqueueCount_++;\n+\tif (queueCount_ > captureLimit_)\n+\t\treturn 0;\n+\n+\tauto ts = timeSheet_.lock();\n+\tif (ts) {\n+\t\tts->prepareForQueue(request, queueCount_ - 1);\n+\t}\n+\n+\treturn camera_->queueRequest(request);\n+}\n+\n+void PerFrameControls::requestComplete(Request *request)\n+{\n+\tauto ts = timeSheet_.lock();\n+\tif (ts) {\n+\t\tts->handleCompleteRequest(request);\n+\t}\n+\n+\tcaptureCount_++;\n+\tif (captureCount_ >= captureLimit_) {\n+\t\tloop_->exit(0);\n+\t\treturn;\n+\t}\n+\n+\trequest->reuse(Request::ReuseBuffers);\n+\tif (queueRequest(request))\n+\t\tloop_->exit(-EINVAL);\n+}\n+\n+void PerFrameControls::runCaptureSession()\n+{\n+\tStream *stream = config_->at(0).stream();\n+\tconst std::vector<std::unique_ptr<FrameBuffer>> &buffers = allocator_->buffers(stream);\n+\n+\t/* Queue the recommended number of reqeuests. */\n+\tfor (const std::unique_ptr<FrameBuffer> &buffer : buffers) {\n+\t\tstd::unique_ptr<Request> request = camera_->createRequest();\n+\t\trequest->addBuffer(stream, buffer.get());\n+\t\tqueueRequest(request.get());\n+\t\trequests_.push_back(std::move(request));\n+\t}\n+\n+\t/* Run capture session. */\n+\tloop_ = new EventLoop();\n+\tloop_->exec();\n+\tstop();\n+\tdelete loop_;\n+}\n+\n+void PerFrameControls::testFramePreciseExposureChange()\n+{\n+\tauto timeSheet = startCaptureWithTimeSheet(10);\n+\tauto &ts = *timeSheet;\n+\n+\n+\tts[3].controls().set(controls::ExposureTime, 5000);\n+\t//wait a few frames to settle\n+\tts[6].controls().set(controls::ExposureTime, 20000);\n+\tts.printAllInfos();\n+\n+\trunCaptureSession();\n+\n+\tEXPECT_NEAR(ts[5].metadata().get(controls::ExposureTime).value(), 5000, 20);\n+\tEXPECT_NEAR(ts[6].metadata().get(controls::ExposureTime).value(), 20000, 20);\n+\n+\t/* No increase just before setting exposure */\n+\tEXPECT_NEAR(ts[5].getBrightnessChange(), 1.0, 0.05) << \"Brightness changed too much before the expected time of change (control delay too high?).\";\n+\t/*\n+     * Todo: The change is brightness was a bit low (Exposure time increase by 4x resulted in a brightness increase of < 2).\n+     * This should be investigated.\n+    */\n+\tEXPECT_GT(ts[6].getBrightnessChange(), 1.3) << \"Brightness in frame \" << 6 << \" did not increase as expected (reference: \"\n+\t\t\t\t\t\t    << ts[3].getSpotBrightness() << \" current: \" << ts[6].getSpotBrightness() << \" )\" << std::endl;\n+\n+\t/* No increase just after setting exposure */\n+\tEXPECT_NEAR(ts[7].getBrightnessChange(), 1.0, 0.05) << \"Brightness changed too much after the expected time of change (control delay too low?).\";\n+\n+\t/* No increase just after setting exposure */\n+\tEXPECT_NEAR(ts[8].getBrightnessChange(), 1.0, 0.05) << \"Brightness changed too much2 frames after the expected time of change (control delay too low?).\";\n+}\n+\n+void PerFrameControls::testFramePreciseGainChange()\n+{\n+\tauto timeSheet = startCaptureWithTimeSheet(10);\n+\tauto &ts = *timeSheet;\n+\n+\tts[3].controls().set(controls::AnalogueGain, 1.0);\n+\t//wait a few frames to settle\n+\tts[6].controls().set(controls::AnalogueGain, 4.0);\n+\n+\trunCaptureSession();\n+\n+\tEXPECT_NEAR(ts[5].metadata().get(controls::AnalogueGain).value(), 1.0, 0.1);\n+\tEXPECT_NEAR(ts[6].metadata().get(controls::AnalogueGain).value(), 4.0, 0.1);\n+\n+\t/* No increase just before setting gain */\n+\tEXPECT_NEAR(ts[5].getBrightnessChange(), 1.0, 0.05) << \"Brightness changed too much before the expected time of change (control delay too high?).\";\n+\t/*\n+     * Todo: I see a brightness change of roughly half the expected one. This is not yet understood and needs investigation\n+    */\n+\tEXPECT_GT(ts[6].getBrightnessChange(), 1.7) << \"Brightness in frame \" << 6 << \" did not increase as expected (reference: \"\n+\t\t\t\t\t\t    << ts[5].getSpotBrightness() << \" current: \" << ts[6].getSpotBrightness() << \" )\" << std::endl;\n+\n+\t/* No increase just after setting gain */\n+\tEXPECT_NEAR(ts[7].getBrightnessChange(), 1.0, 0.05) << \"Brightness changed too much after the expected time of change (control delay too low?).\";\n+}\n+\n+void PerFrameControls::testExposureGainFromFirstRequestGetsApplied()\n+{\n+\tauto timeSheet = startCaptureWithTimeSheet(5);\n+\tauto &ts = *timeSheet;\n+\n+\tts[0].controls().set(controls::ExposureTime, 10000);\n+\tts[0].controls().set(controls::AnalogueGain, 4.0);\n+\n+\trunCaptureSession();\n+\n+\t/* We expect it to be applied after 3 frames, the latest*/\n+\tEXPECT_NEAR(ts[4].metadata().get(controls::ExposureTime).value(), 10000, 20);\n+\tEXPECT_NEAR(ts[4].metadata().get(controls::AnalogueGain).value(), 4.0, 0.1);\n+}\n+\n+void PerFrameControls::testExposureGainFromFirstAndSecondRequestGetsApplied()\n+{\n+\tauto timeSheet = startCaptureWithTimeSheet(5);\n+\tauto &ts = *timeSheet;\n+\n+\tts[0].controls().set(controls::ExposureTime, 8000);\n+\tts[0].controls().set(controls::AnalogueGain, 2.0);\n+\tts[1].controls().set(controls::ExposureTime, 10000);\n+\tts[1].controls().set(controls::AnalogueGain, 4.0);\n+\n+\trunCaptureSession();\n+\n+\t/* We expect it to be applied after 3 frames, the latest*/\n+\tEXPECT_NEAR(ts[4].metadata().get(controls::ExposureTime).value(), 10000, 20);\n+\tEXPECT_NEAR(ts[4].metadata().get(controls::AnalogueGain).value(), 4.0, 0.1);\n+}\n+\n+void PerFrameControls::testExposureGainIsAppliedOnFirstFrame()\n+{\n+\tControlList startValues;\n+\tstartValues.set(controls::ExposureTime, 5000);\n+\tstartValues.set(controls::AnalogueGain, 1.0);\n+\n+\tauto ts1 = startCaptureWithTimeSheet(3, &startValues);\n+\n+\trunCaptureSession();\n+\n+\tEXPECT_NEAR((*ts1)[0].metadata().get(controls::ExposureTime).value(), 5000, 20);\n+\tEXPECT_NEAR((*ts1)[0].metadata().get(controls::AnalogueGain).value(), 1.0, 0.01);\n+\n+\t/* Second capture with different values to ensure we don't hit default/old values */\n+\n+\tstartValues.set(controls::ExposureTime, 15000);\n+\tstartValues.set(controls::AnalogueGain, 4.0);\n+\n+\tauto ts2 = startCaptureWithTimeSheet(3, &startValues);\n+\n+\trunCaptureSession();\n+\n+\tEXPECT_NEAR((*ts2)[0].metadata().get(controls::ExposureTime).value(), 15000, 20);\n+\tEXPECT_NEAR((*ts2)[0].metadata().get(controls::AnalogueGain).value(), 4.0, 0.01);\n+\n+\t/* with 3x exposure and 4x gain we could expect a brightness increase of 2x */\n+\tdouble brightnessChange = ts2->get(1).getSpotBrightness() / ts1->get(1).getSpotBrightness();\n+\tEXPECT_GT(brightnessChange, 2.0);\n+}\ndiff --git a/src/apps/lc-compliance/per_frame_controls.h b/src/apps/lc-compliance/per_frame_controls.h\nnew file mode 100644\nindex 00000000..e783f024\n--- /dev/null\n+++ b/src/apps/lc-compliance/per_frame_controls.h\n@@ -0,0 +1,41 @@\n+/* SPDX-License-Identifier: GPL-2.0-or-later */\n+/*\n+ * Copyright (C) 2024, Ideas on Board Oy\n+ *\n+ * per_frame_controls.h - Tests for per frame controls\n+ */\n+\n+#pragma once\n+\n+#include <memory>\n+\n+#include <libcamera/libcamera.h>\n+\n+#include \"../common/event_loop.h\"\n+\n+#include \"simple_capture.h\"\n+#include \"time_sheet.h\"\n+\n+class PerFrameControls : public SimpleCapture\n+{\n+public:\n+\tPerFrameControls(std::shared_ptr<libcamera::Camera> camera);\n+\n+\tstd::shared_ptr<TimeSheet> startCaptureWithTimeSheet(unsigned int framesToCapture, const libcamera::ControlList *controls = nullptr);\n+\tvoid runCaptureSession();\n+\n+\tvoid testFramePreciseExposureChange();\n+\tvoid testFramePreciseGainChange();\n+\tvoid testExposureGainIsAppliedOnFirstFrame();\n+\tvoid testExposureGainFromFirstRequestGetsApplied();\n+\tvoid testExposureGainFromFirstAndSecondRequestGetsApplied();\n+\n+\tint queueRequest(libcamera::Request *request);\n+\tvoid requestComplete(libcamera::Request *request) override;\n+\n+\tunsigned int queueCount_;\n+\tunsigned int captureCount_;\n+\tunsigned int captureLimit_;\n+\n+\tstd::weak_ptr<TimeSheet> timeSheet_;\n+};\ndiff --git a/src/apps/lc-compliance/simple_capture.cpp b/src/apps/lc-compliance/simple_capture.cpp\nindex cf4d7cf3..56680a83 100644\n--- a/src/apps/lc-compliance/simple_capture.cpp\n+++ b/src/apps/lc-compliance/simple_capture.cpp\n@@ -42,7 +42,7 @@ void SimpleCapture::configure(StreamRole role)\n \t}\n }\n \n-void SimpleCapture::start()\n+void SimpleCapture::start(const ControlList *controls)\n {\n \tStream *stream = config_->at(0).stream();\n \tint count = allocator_->allocate(stream);\n@@ -52,7 +52,7 @@ void SimpleCapture::start()\n \n \tcamera_->requestCompleted.connect(this, &SimpleCapture::requestComplete);\n \n-\tASSERT_EQ(camera_->start(), 0) << \"Failed to start camera\";\n+\tASSERT_EQ(camera_->start(controls), 0) << \"Failed to start camera\";\n }\n \n void SimpleCapture::stop()\ndiff --git a/src/apps/lc-compliance/simple_capture.h b/src/apps/lc-compliance/simple_capture.h\nindex 2911d601..54b1d54b 100644\n--- a/src/apps/lc-compliance/simple_capture.h\n+++ b/src/apps/lc-compliance/simple_capture.h\n@@ -22,7 +22,7 @@ protected:\n \tSimpleCapture(std::shared_ptr<libcamera::Camera> camera);\n \tvirtual ~SimpleCapture();\n \n-\tvoid start();\n+\tvoid start(const libcamera::ControlList *controls = nullptr);\n \tvoid stop();\n \n \tvirtual void requestComplete(libcamera::Request *request) = 0;\ndiff --git a/src/apps/lc-compliance/time_sheet.cpp b/src/apps/lc-compliance/time_sheet.cpp\nnew file mode 100644\nindex 00000000..9a0e6544\n--- /dev/null\n+++ b/src/apps/lc-compliance/time_sheet.cpp\n@@ -0,0 +1,135 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Ideas on Board Oy\n+ *\n+ * time_sheet.cpp\n+ */\n+#include \"time_sheet.h\"\n+\n+#include <sstream>\n+#include <libcamera/libcamera.h>\n+\n+#include \"libcamera/internal/formats.h\"\n+#include \"libcamera/internal/mapped_framebuffer.h\"\n+\n+using namespace libcamera;\n+\n+double calcPixelMeanNV12(const uint8_t *data)\n+{\n+\treturn (double)*data;\n+}\n+\n+double calcPixelMeanRAW10(const uint8_t *data)\n+{\n+\treturn (double)*((const uint16_t *)data);\n+}\n+\n+double calculateMeanBrightnessFromCenterSpot(libcamera::Request *request)\n+{\n+\tconst Request::BufferMap &buffers = request->buffers();\n+\tfor (const auto &[stream, buffer] : buffers) {\n+\t\tMappedFrameBuffer in(buffer, MappedFrameBuffer::MapFlag::Read);\n+\t\tif (in.isValid()) {\n+\t\t\tauto data = in.planes()[0].data();\n+\t\t\tauto streamConfig = stream->configuration();\n+\t\t\tauto formatInfo = PixelFormatInfo::info(streamConfig.pixelFormat);\n+\n+\t\t\tstd::function<double(const uint8_t *data)> calcPixelMean;\n+\t\t\tint pixelStride;\n+\n+\t\t\tswitch (streamConfig.pixelFormat) {\n+\t\t\tcase formats::NV12:\n+\t\t\t\tcalcPixelMean = calcPixelMeanNV12;\n+\t\t\t\tpixelStride = 1;\n+\t\t\t\tbreak;\n+\t\t\tcase formats::SRGGB10:\n+\t\t\t\tcalcPixelMean = calcPixelMeanRAW10;\n+\t\t\t\tpixelStride = 2;\n+\t\t\t\tbreak;\n+\t\t\tdefault:\n+\t\t\t\tstd::stringstream s;\n+\t\t\t\ts << \"Unsupported Pixelformat \" << formatInfo.name;\n+\t\t\t\tthrow std::invalid_argument(s.str());\n+\t\t\t}\n+\n+\t\t\tdouble sum = 0;\n+\t\t\tint w = 20;\n+\t\t\tint xs = streamConfig.size.width / 2 - w / 2;\n+\t\t\tint ys = streamConfig.size.height / 2 - w / 2;\n+\n+\t\t\tfor (auto y = ys; y < ys + w; y++) {\n+\t\t\t\tauto line = data + y * streamConfig.stride;\n+\t\t\t\tfor (auto x = xs; x < xs + w; x++) {\n+\t\t\t\t\tsum += calcPixelMean(line + x * pixelStride);\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tsum = sum / (w * w);\n+\t\t\treturn sum;\n+\t\t}\n+\t}\n+\treturn 0;\n+}\n+\n+TimeSheetEntry::TimeSheetEntry(const ControlIdMap &idmap)\n+\t: controls_(idmap)\n+{\n+}\n+\n+void TimeSheetEntry::handleCompleteRequest(libcamera::Request *request, const TimeSheetEntry *previous)\n+{\n+\tmetadata_ = request->metadata();\n+\n+\tspotBrightness_ = calculateMeanBrightnessFromCenterSpot(request);\n+\tif (previous) {\n+\t\tbrightnessChange_ = spotBrightness_ / previous->getSpotBrightness();\n+\t}\n+\tsequence_ = request->sequence();\n+}\n+\n+void TimeSheetEntry::printInfo()\n+{\n+\tstd::cout << \"=== Frame \" << sequence_ << std::endl;\n+\tstd::cout << \"Brightness: \" << spotBrightness_ << std::endl;\n+\n+\tif (!metadata_.empty()) {\n+\t\tstd::cout << \"Metadata:\" << std::endl;\n+\t\tauto idMap = metadata_.idMap();\n+\t\tassert(idMap);\n+\t\tfor (const auto &[id, value] : metadata_) {\n+\t\t\tstd::cout << \"  \" << idMap->at(id)->name() << \" : \" << value.toString() << std::endl;\n+\t\t}\n+\t}\n+}\n+\n+TimeSheetEntry &TimeSheet::get(size_t pos)\n+{\n+\tauto &entry = entries_[pos];\n+\tif (!entry)\n+\t\tentry = std::make_shared<TimeSheetEntry>(idmap_);\n+\treturn *entry;\n+}\n+\n+void TimeSheet::prepareForQueue(libcamera::Request *request, uint32_t sequence)\n+{\n+\trequest->controls() = get(sequence).controls();\n+}\n+\n+void TimeSheet::handleCompleteRequest(libcamera::Request *request)\n+{\n+\tuint32_t sequence = request->sequence();\n+\tauto &entry = get(sequence);\n+\tTimeSheetEntry *previous = nullptr;\n+\tif (sequence >= 1) {\n+\t\tprevious = entries_[sequence - 1].get();\n+\t}\n+\n+\tentry.handleCompleteRequest(request, previous);\n+}\n+\n+void TimeSheet::printAllInfos()\n+{\n+\tfor (auto entry : entries_) {\n+\t\tif (entry)\n+\t\t\tentry->printInfo();\n+\t}\n+}\ndiff --git a/src/apps/lc-compliance/time_sheet.h b/src/apps/lc-compliance/time_sheet.h\nnew file mode 100644\nindex 00000000..c155763c\n--- /dev/null\n+++ b/src/apps/lc-compliance/time_sheet.h\n@@ -0,0 +1,53 @@\n+/* SPDX-License-Identifier: LGPL-2.1-or-later */\n+/*\n+ * Copyright (C) 2024, Ideas on Board Oy\n+ *\n+ * time_sheet.h\n+ */\n+\n+#pragma once\n+\n+#include <future>\n+#include <vector>\n+\n+#include <libcamera/libcamera.h>\n+\n+class TimeSheetEntry\n+{\n+public:\n+\tTimeSheetEntry(const libcamera::ControlIdMap &idmap);\n+\tTimeSheetEntry(TimeSheetEntry &&other) noexcept = default;\n+\tTimeSheetEntry(const TimeSheetEntry &) = delete;\n+\n+\tlibcamera::ControlList &controls() { return controls_; };\n+\tlibcamera::ControlList &metadata() { return metadata_; };\n+\tvoid handleCompleteRequest(libcamera::Request *request, const TimeSheetEntry *previous);\n+\tvoid printInfo();\n+\tdouble getSpotBrightness() const { return spotBrightness_; };\n+\tdouble getBrightnessChange() const { return brightnessChange_; };\n+\n+private:\n+\tdouble spotBrightness_ = 0.0;\n+\tdouble brightnessChange_ = 0.0;\n+\tlibcamera::ControlList controls_;\n+\tlibcamera::ControlList metadata_;\n+\tuint32_t sequence_ = 0;\n+};\n+\n+class TimeSheet\n+{\n+public:\n+\tTimeSheet(int count, const libcamera::ControlIdMap &idmap)\n+\t\t: idmap_(idmap), entries_(count){};\n+\n+\tvoid prepareForQueue(libcamera::Request *request, uint32_t sequence);\n+\tvoid handleCompleteRequest(libcamera::Request *request);\n+\tvoid printAllInfos();\n+\n+\tTimeSheetEntry &operator[](size_t pos) { return get(pos); };\n+\tTimeSheetEntry &get(size_t pos);\n+\n+private:\n+\tconst libcamera::ControlIdMap &idmap_;\n+\tstd::vector<std::shared_ptr<TimeSheetEntry>> entries_;\n+};\n","prefixes":["RFC"]}