From patchwork Tue Mar 19 12:05:04 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stefan Klug X-Patchwork-Id: 19743 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id C9A62BD160 for ; Tue, 19 Mar 2024 12:05:37 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id EC06662CAB; Tue, 19 Mar 2024 13:05:31 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="k4Xvmih+"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 20A6962973 for ; Tue, 19 Mar 2024 13:05:28 +0100 (CET) Received: from jasper.fritz.box (unknown [IPv6:2a00:6020:448c:6c00:1478:344b:8fcb:baf5]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 6F44B480; Tue, 19 Mar 2024 13:05:01 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1710849901; bh=YWcx60Xes0BHpk2TegmXEa7+lGhzQhQJfS3TcDAV8iI=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=k4Xvmih+w1jxLZBhr7oQwpNK1S3UfkuD236IJMWPnuBQrerhUebHDnpKfVMyCAjIK 5ZBrfZXxDXu7qkrtMaXNw67tNY0qirT5gRv4f314Uq0m47A08rKSUhm0tDRQ6e6VPj /GJNhURw5PneCB/1ptk+ZSXGpmm+iNCEZva/X2Mg= From: Stefan Klug To: libcamera-devel@lists.libcamera.org Cc: Stefan Klug Subject: [PATCH v3 03/16] libcamera: lc-compliance: Add initial set of per-frame-control tests Date: Tue, 19 Mar 2024 13:05:04 +0100 Message-Id: <20240319120517.362082-4-stefan.klug@ideasonboard.com> X-Mailer: git-send-email 2.40.1 In-Reply-To: <20240319120517.362082-1-stefan.klug@ideasonboard.com> References: <20240319120517.362082-1-stefan.klug@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Add tests that check if controls (only exposure time and analogue gain at the moment) get applied on the frame they were requested for. This is tested by looking at the metadata and the mean brightness of the image center. At the moment these tests fail. Fixes for the pipelines will be delivered in later patches (rkisp1 for now). To run the pfc tests only: lc-compliance -c -f "PerFrameControlTests.*" Note that the current implementation is a bit picky on what the camera actually sees. If it is too dark (or too bright), the tests will fail. Looking at a white wall in a normally lit office usually works. These tests are known to pass using a imx219 (RPi cam v2) with a imx8mp (debix-som) using the rkisp1 pipeline Signed-off-by: Stefan Klug --- src/apps/lc-compliance/meson.build | 1 + .../lc-compliance/per_frame_controls_test.cpp | 398 ++++++++++++++++++ 2 files changed, 399 insertions(+) create mode 100644 src/apps/lc-compliance/per_frame_controls_test.cpp diff --git a/src/apps/lc-compliance/meson.build b/src/apps/lc-compliance/meson.build index eb7b2d71..8f4eec55 100644 --- a/src/apps/lc-compliance/meson.build +++ b/src/apps/lc-compliance/meson.build @@ -15,6 +15,7 @@ lc_compliance_sources = files([ 'capture_test.cpp', 'environment.cpp', 'main.cpp', + 'per_frame_controls_test.cpp', 'simple_capture.cpp', 'time_sheet.cpp', ]) diff --git a/src/apps/lc-compliance/per_frame_controls_test.cpp b/src/apps/lc-compliance/per_frame_controls_test.cpp new file mode 100644 index 00000000..017e8d60 --- /dev/null +++ b/src/apps/lc-compliance/per_frame_controls_test.cpp @@ -0,0 +1,398 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2024, Ideas on Board Oy + * + * per_frame_controls.cpp - Tests for per frame controls + */ +#include + +#include "environment.h" +#include "simple_capture.h" +#include "time_sheet.h" + +using namespace libcamera; + +class PerFrameControlTests : public testing::Test +{ +protected: + void SetUp() override; + void TearDown() override; + + std::shared_ptr camera_; +}; + +/* + * We use gtest's SetUp() and TearDown() instead of constructor and destructor + * in order to be able to assert on them. + */ +void PerFrameControlTests::SetUp() +{ + Environment *env = Environment::get(); + + camera_ = env->cm()->get(env->cameraId()); + + ASSERT_EQ(camera_->acquire(), 0); +} + +void PerFrameControlTests::TearDown() +{ + if (!camera_) + return; + + camera_->release(); + camera_.reset(); +} + +class PerFrameControlsCapture : public SimpleCapture +{ +public: + PerFrameControlsCapture(std::shared_ptr camera); + + std::shared_ptr + startCaptureWithTimeSheet(unsigned int framesToCapture, + const libcamera::ControlList *controls = nullptr); + + void runCaptureSession(); + int queueRequest(libcamera::Request *request); + void requestComplete(libcamera::Request *request) override; + + unsigned int queueCount_; + unsigned int captureCount_; + unsigned int captureLimit_; + + std::weak_ptr timeSheet_; +}; + +static const bool doImageTests = true; + +PerFrameControlsCapture::PerFrameControlsCapture(std::shared_ptr camera) + : SimpleCapture(camera) +{ +} + +std::shared_ptr +PerFrameControlsCapture::startCaptureWithTimeSheet(unsigned int framesToCapture, + const ControlList *controls) +{ + ControlList ctrls(camera_->controls().idmap()); + + /* Ensure defined default values */ + ctrls.set(controls::AeEnable, false); + ctrls.set(controls::AeExposureMode, controls::ExposureCustom); + ctrls.set(controls::ExposureTime, 10000); + ctrls.set(controls::AnalogueGain, 1.0); + + if (controls) + ctrls.merge(*controls, ControlList::MergePolicy::OverwriteExisting); + + start(&ctrls); + + queueCount_ = 0; + captureCount_ = 0; + captureLimit_ = framesToCapture; + + auto timeSheet = std::make_shared(captureLimit_, + camera_->controls().idmap()); + timeSheet_ = timeSheet; + return timeSheet; +} + +int PerFrameControlsCapture::queueRequest(Request *request) +{ + queueCount_++; + if (queueCount_ > captureLimit_) + return 0; + + auto ts = timeSheet_.lock(); + if (ts) + ts->prepareForQueue(request, queueCount_ - 1); + + return camera_->queueRequest(request); +} + +void PerFrameControlsCapture::requestComplete(Request *request) +{ + auto ts = timeSheet_.lock(); + if (ts) + ts->handleCompleteRequest(request); + + captureCount_++; + if (captureCount_ >= captureLimit_) { + loop_->exit(0); + return; + } + + request->reuse(Request::ReuseBuffers); + if (queueRequest(request)) + loop_->exit(-EINVAL); +} + +void PerFrameControlsCapture::runCaptureSession() +{ + Stream *stream = config_->at(0).stream(); + const std::vector> &buffers = allocator_->buffers(stream); + + /* Queue the recommended number of requests. */ + for (const std::unique_ptr &buffer : buffers) { + std::unique_ptr request = camera_->createRequest(); + request->addBuffer(stream, buffer.get()); + queueRequest(request.get()); + requests_.push_back(std::move(request)); + } + + /* Run capture session. */ + loop_ = new EventLoop(); + loop_->exec(); + stop(); + delete loop_; +} + +TEST_F(PerFrameControlTests, testExposureGainChangeOnSameFrame) +{ + PerFrameControlsCapture capture(camera_); + capture.configure(StreamRole::VideoRecording); + + ControlList startValues; + startValues.set(controls::ExposureTime, 5000); + startValues.set(controls::AnalogueGain, 1.0); + + auto timeSheet = capture.startCaptureWithTimeSheet(10, &startValues); + auto &ts = *timeSheet; + + /* wait a few frames to settle */ + ts[7].controls().set(controls::ExposureTime, 10000); + ts[7].controls().set(controls::AnalogueGain, 4.0); + + capture.runCaptureSession(); + + ASSERT_TRUE(ts[5].metadata().contains(controls::ExposureTime.id())) + << "Required metadata entry is missing"; + ASSERT_TRUE(ts[5].metadata().contains(controls::AnalogueGain.id())) + << "Required metadata entry is missing"; + + EXPECT_NEAR(ts[3].metadata().get(controls::ExposureTime).value(), 5000, 20); + EXPECT_NEAR(ts[3].metadata().get(controls::AnalogueGain).value(), 1.0, 0.05); + + /* find the frame with the changes */ + int exposureChangeIndex = 0; + for (unsigned i = 3; i < ts.size(); i++) { + if (ts[i].metadata().get(controls::ExposureTime).value() > 7500) { + exposureChangeIndex = i; + break; + } + } + + int gainChangeIndex = 0; + for (unsigned i = 3; i < ts.size(); i++) { + if (ts[i].metadata().get(controls::AnalogueGain).value() > 2.0) { + gainChangeIndex = i; + break; + } + } + + EXPECT_NE(exposureChangeIndex, 0) << "Exposure change not found in metadata"; + EXPECT_NE(gainChangeIndex, 0) << "Gain change not found in metadata"; + EXPECT_EQ(exposureChangeIndex, gainChangeIndex) + << "Metadata contained gain and exposure changes on different frames"; + + if (doImageTests) { + int brightnessChangeIndex = 0; + for (unsigned i = 3; i < ts.size(); i++) { + if (ts[i].brightnessChange() > 1.3) { + EXPECT_EQ(brightnessChangeIndex, 0) + << "Detected multiple frames with brightness increase" + << " (Wrong control delays?)"; + + if (!brightnessChangeIndex) + brightnessChangeIndex = i; + } + } + + EXPECT_EQ(exposureChangeIndex, brightnessChangeIndex) + << "Exposure change and measured brightness change were not on same" + << " frame. (Wrong control delay?, Start frame event too late?)"; + EXPECT_EQ(exposureChangeIndex, gainChangeIndex) + << "Gain change and measured brightness change were not on same " + << " frame. (Wrong control delay?, Start frame event too late?)"; + } +} + +TEST_F(PerFrameControlTests, testFramePreciseExposureChange) +{ + PerFrameControlsCapture capture(camera_); + capture.configure(StreamRole::VideoRecording); + + auto timeSheet = capture.startCaptureWithTimeSheet(10); + auto &ts = *timeSheet; + + ts[3].controls().set(controls::ExposureTime, 5000); + /* wait a few frames to settle */ + ts[6].controls().set(controls::ExposureTime, 20000); + + capture.runCaptureSession(); + + ASSERT_TRUE(ts[5].metadata().contains(controls::ExposureTime.id())) + << "Required metadata entry is missing"; + + EXPECT_NEAR(ts[5].metadata().get(controls::ExposureTime).value(), 5000, 20); + EXPECT_NEAR(ts[6].metadata().get(controls::ExposureTime).value(), 20000, 20); + + if (doImageTests) { + /* No increase just before setting exposure */ + EXPECT_NEAR(ts[5].brightnessChange(), 1.0, 0.05) + << "Brightness changed too much before the expected time of change" + << " (control delay too high?)."; + /* + * \todo The change is brightness was a bit low + * (Exposure time increase by 4x resulted in a brightness increase of < 2). + * This should be investigated. + */ + EXPECT_GT(ts[6].brightnessChange(), 1.3) + << "Brightness in frame " << 6 << " did not increase as expected" + << " (reference: " << ts[3].spotBrightness() << " current: " + << ts[6].spotBrightness() << " )" << std::endl; + + /* No increase just after setting exposure */ + EXPECT_NEAR(ts[7].brightnessChange(), 1.0, 0.05) + << "Brightness changed too much after the expected time of change" + << " (control delay too low?)."; + + /* No increase just after setting exposure */ + EXPECT_NEAR(ts[8].brightnessChange(), 1.0, 0.05) + << "Brightness changed too much 2 frames after the expected time" + << " of change (control delay too low?)."; + } +} + +TEST_F(PerFrameControlTests, testFramePreciseGainChange) +{ + PerFrameControlsCapture capture(camera_); + capture.configure(StreamRole::VideoRecording); + + auto timeSheet = capture.startCaptureWithTimeSheet(10); + auto &ts = *timeSheet; + + ts[3].controls().set(controls::AnalogueGain, 1.0); + /* wait a few frames to settle */ + ts[6].controls().set(controls::AnalogueGain, 4.0); + + capture.runCaptureSession(); + + ASSERT_TRUE(ts[5].metadata().contains(controls::AnalogueGain.id())) + << "Required metadata entry is missing"; + + EXPECT_NEAR(ts[5].metadata().get(controls::AnalogueGain).value(), 1.0, 0.1); + EXPECT_NEAR(ts[6].metadata().get(controls::AnalogueGain).value(), 4.0, 0.1); + + if (doImageTests) { + /* No increase just before setting gain */ + EXPECT_NEAR(ts[5].brightnessChange(), 1.0, 0.05) + << "Brightness changed too much before the expected time of change" + << " (control delay too high?)."; + /* + * \todo I see a brightness change of roughly half the expected one. + * This is not yet understood and needs investigation + */ + EXPECT_GT(ts[6].brightnessChange(), 1.7) + << "Brightness in frame " << 6 << " did not increase as expected" + << " (reference: " << ts[5].spotBrightness() + << " current: " << ts[6].spotBrightness() << ")" << std::endl; + + /* No increase just after setting gain */ + EXPECT_NEAR(ts[7].brightnessChange(), 1.0, 0.05) + << "Brightness changed too much after the expected time of change" + << " (control delay too low?)."; + + /* No increase just after setting gain */ + EXPECT_NEAR(ts[8].brightnessChange(), 1.0, 0.05) + << "Brightness changed too much after the expected time of change" + << " (control delay too low?)."; + } +} + +TEST_F(PerFrameControlTests, testExposureGainFromFirstRequestGetsApplied) +{ + PerFrameControlsCapture capture(camera_); + capture.configure(StreamRole::VideoRecording); + + auto timeSheet = capture.startCaptureWithTimeSheet(5); + auto &ts = *timeSheet; + + ts[0].controls().set(controls::ExposureTime, 10000); + ts[0].controls().set(controls::AnalogueGain, 4.0); + + capture.runCaptureSession(); + + ASSERT_TRUE(ts[4].metadata().contains(controls::ExposureTime.id())) + << "Required metadata entry is missing"; + ASSERT_TRUE(ts[4].metadata().contains(controls::AnalogueGain.id())) + << "Required metadata entry is missing"; + + /* We expect it to be applied after 3 frames, the latest*/ + EXPECT_NEAR(ts[4].metadata().get(controls::ExposureTime).value(), 10000, 20); + EXPECT_NEAR(ts[4].metadata().get(controls::AnalogueGain).value(), 4.0, 0.1); +} + +TEST_F(PerFrameControlTests, testExposureGainFromFirstAndSecondRequestGetsApplied) +{ + PerFrameControlsCapture capture(camera_); + capture.configure(StreamRole::VideoRecording); + + auto timeSheet = capture.startCaptureWithTimeSheet(5); + auto &ts = *timeSheet; + + ts[0].controls().set(controls::ExposureTime, 8000); + ts[0].controls().set(controls::AnalogueGain, 2.0); + ts[1].controls().set(controls::ExposureTime, 10000); + ts[1].controls().set(controls::AnalogueGain, 4.0); + + capture.runCaptureSession(); + + ASSERT_TRUE(ts[4].metadata().contains(controls::ExposureTime.id())) + << "Required metadata entry is missing"; + ASSERT_TRUE(ts[4].metadata().contains(controls::AnalogueGain.id())) + << "Required metadata entry is missing"; + + /* We expect it to be applied after 3 frames, the latest */ + EXPECT_NEAR(ts[4].metadata().get(controls::ExposureTime).value(), 10000, 20); + EXPECT_NEAR(ts[4].metadata().get(controls::AnalogueGain).value(), 4.0, 0.1); +} + +TEST_F(PerFrameControlTests, testExposureGainIsAppliedOnFirstFrame) +{ + PerFrameControlsCapture capture(camera_); + capture.configure(StreamRole::VideoRecording); + + ControlList startValues; + startValues.set(controls::ExposureTime, 5000); + startValues.set(controls::AnalogueGain, 1.0); + + auto ts1 = capture.startCaptureWithTimeSheet(3, &startValues); + + capture.runCaptureSession(); + + ASSERT_TRUE((*ts1)[0].metadata().contains(controls::ExposureTime.id())) + << "Required metadata entry is missing"; + ASSERT_TRUE((*ts1)[0].metadata().contains(controls::AnalogueGain.id())) + << "Required metadata entry is missing"; + + EXPECT_NEAR((*ts1)[0].metadata().get(controls::ExposureTime).value(), 5000, 20); + EXPECT_NEAR((*ts1)[0].metadata().get(controls::AnalogueGain).value(), 1.0, 0.02); + + /* Second capture with different values to ensure we don't hit default/old values */ + startValues.set(controls::ExposureTime, 15000); + startValues.set(controls::AnalogueGain, 4.0); + + auto ts2 = capture.startCaptureWithTimeSheet(3, &startValues); + + capture.runCaptureSession(); + + EXPECT_NEAR((*ts2)[0].metadata().get(controls::ExposureTime).value(), 15000, 20); + EXPECT_NEAR((*ts2)[0].metadata().get(controls::AnalogueGain).value(), 4.0, 0.02); + + if (doImageTests) { + /* With 3x exposure and 4x gain we could expect a brightness increase of 2x */ + double brightnessChange = ts2->get(1).spotBrightness() / ts1->get(1).spotBrightness(); + EXPECT_GT(brightnessChange, 2.0); + } +}