diff options
-rw-r--r-- | include/libcamera/internal/v4l2_device.h | 1 | ||||
-rw-r--r-- | meson.build | 2 | ||||
-rw-r--r-- | src/ipa/libipa/histogram.cpp | 42 | ||||
-rw-r--r-- | src/libcamera/pipeline/rpi/pisp/pisp.cpp | 4 | ||||
-rw-r--r-- | src/libcamera/pipeline/rpi/vc4/vc4.cpp | 4 | ||||
-rw-r--r-- | src/libcamera/pipeline/simple/simple.cpp | 66 | ||||
-rw-r--r-- | src/libcamera/v4l2_device.cpp | 22 | ||||
-rw-r--r-- | test/ipa/libipa/histogram.cpp | 66 | ||||
-rw-r--r-- | test/ipa/libipa/meson.build | 4 |
9 files changed, 177 insertions, 34 deletions
diff --git a/include/libcamera/internal/v4l2_device.h b/include/libcamera/internal/v4l2_device.h index affe52c2..a647c96a 100644 --- a/include/libcamera/internal/v4l2_device.h +++ b/include/libcamera/internal/v4l2_device.h @@ -45,6 +45,7 @@ public: const std::string &deviceNode() const { return deviceNode_; } std::string devicePath() const; + bool supportsFrameStartEvent(); int setFrameStartEnabled(bool enable); Signal<uint32_t> frameStart; diff --git a/meson.build b/meson.build index 668ec396..9ba5e2ca 100644 --- a/meson.build +++ b/meson.build @@ -2,7 +2,7 @@ project('libcamera', 'c', 'cpp', meson_version : '>= 0.63', - version : '0.4.0', + version : '0.5.0', default_options : [ 'werror=true', 'warning_level=2', diff --git a/src/ipa/libipa/histogram.cpp b/src/ipa/libipa/histogram.cpp index 10e44b54..bcf26390 100644 --- a/src/ipa/libipa/histogram.cpp +++ b/src/ipa/libipa/histogram.cpp @@ -130,7 +130,8 @@ double Histogram::quantile(double q, uint32_t first, uint32_t last) const if (cumulative_[first + 1] == cumulative_[first]) frac = 0; else - frac = (item - cumulative_[first]) / (cumulative_[first + 1] - cumulative_[first]); + frac = (q * total() - cumulative_[first]) + / (cumulative_[first + 1] - cumulative_[first]); return first + frac; } @@ -148,26 +149,37 @@ double Histogram::quantile(double q, uint32_t first, uint32_t last) const double Histogram::interQuantileMean(double lowQuantile, double highQuantile) const { ASSERT(highQuantile > lowQuantile); - /* Proportion of pixels which lies below lowQuantile */ - double lowPoint = quantile(lowQuantile); - /* Proportion of pixels which lies below highQuantile */ - double highPoint = quantile(highQuantile, static_cast<uint32_t>(lowPoint)); - double sumBinFreq = 0, cumulFreq = 0; - - for (double p_next = floor(lowPoint) + 1.0; - p_next <= ceil(highPoint); - lowPoint = p_next, p_next += 1.0) { - int bin = floor(lowPoint); + + /* Proportion of pixels which lies below lowQuantile and highQuantile. */ + const double lowPoint = quantile(lowQuantile); + const double highPoint = quantile(highQuantile, static_cast<uint32_t>(lowPoint)); + + double sumBinFreq = 0; + double cumulFreq = 0; + + /* + * Calculate the mean pixel value between the low and high points by + * summing all the pixels between the two points, and dividing the sum + * by the number of pixels. Given the discrete nature of the histogram + * data, the sum of the pixels is approximated by accumulating the + * product of the bin values (calculated as the mid point of the bin) by + * the number of pixels they contain, for each bin in the internal. + */ + for (unsigned bin = std::floor(lowPoint); bin < std::ceil(highPoint); bin++) { + const double lowBound = std::max<double>(bin, lowPoint); + const double highBound = std::min<double>(bin + 1, highPoint); + double freq = (cumulative_[bin + 1] - cumulative_[bin]) - * (std::min(p_next, highPoint) - lowPoint); + * (highBound - lowBound); /* Accumulate weighted bin */ - sumBinFreq += bin * freq; + sumBinFreq += (highBound + lowBound) / 2 * freq; + /* Accumulate weights */ cumulFreq += freq; } - /* add 0.5 to give an average for bin mid-points */ - return sumBinFreq / cumulFreq + 0.5; + + return sumBinFreq / cumulFreq; } } /* namespace ipa */ diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp index 42ca7c80..91e7f4c9 100644 --- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp +++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp @@ -1350,9 +1350,9 @@ int PiSPCameraData::platformPipelineConfigure(const std::unique_ptr<YamlObject> } std::optional<std::string> target = (*root)["target"].get<std::string>(); - if (!target || *target != "pisp") { + if (target != "pisp") { LOG(RPI, Error) << "Unexpected target reported: expected \"pisp\", got " - << *target; + << (target ? target->c_str() : "(unknown)"); return -EINVAL; } diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp index fd8d84b1..fe910bdf 100644 --- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp +++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp @@ -510,9 +510,9 @@ int Vc4CameraData::platformPipelineConfigure(const std::unique_ptr<YamlObject> & } std::optional<std::string> target = (*root)["target"].get<std::string>(); - if (!target || *target != "bcm2835") { + if (target != "bcm2835") { LOG(RPI, Error) << "Unexpected target reported: expected \"bcm2835\", got " - << *target; + << (target ? target->c_str() : "(unknown)"); return -EINVAL; } diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp index fd0ccdca..efb07051 100644 --- a/src/libcamera/pipeline/simple/simple.cpp +++ b/src/libcamera/pipeline/simple/simple.cpp @@ -327,6 +327,7 @@ public: std::list<Entity> entities_; std::unique_ptr<CameraSensor> sensor_; V4L2VideoDevice *video_; + V4L2Subdevice *frameStartEmitter_; std::vector<Configuration> configs_; std::map<PixelFormat, std::vector<const Configuration *>> formats_; @@ -541,6 +542,13 @@ SimpleCameraData::SimpleCameraData(SimplePipelineHandler *pipe, if (!sensor_) return; + const CameraSensorProperties::SensorDelays &delays = sensor_->sensorDelays(); + std::unordered_map<uint32_t, DelayedControls::ControlParams> params = { + { V4L2_CID_ANALOGUE_GAIN, { delays.gainDelay, false } }, + { V4L2_CID_EXPOSURE, { delays.exposureDelay, false } }, + }; + delayedCtrls_ = std::make_unique<DelayedControls>(sensor_->device(), params); + LOG(SimplePipeline, Debug) << "Found pipeline: " << utils::join(entities_, " -> ", @@ -633,6 +641,20 @@ int SimpleCameraData::init() properties_ = sensor_->properties(); + /* Find the first subdev that can generate a frame start signal, if any. */ + frameStartEmitter_ = nullptr; + for (const Entity &entity : entities_) { + V4L2Subdevice *sd = pipe->subdev(entity.entity); + if (!sd || !sd->supportsFrameStartEvent()) + continue; + + LOG(SimplePipeline, Debug) + << "Using frameStart signal from '" + << entity.entity->name() << "'"; + frameStartEmitter_ = sd; + break; + } + return 0; } @@ -983,8 +1005,18 @@ void SimpleCameraData::metadataReady(uint32_t frame, const ControlList &metadata void SimpleCameraData::setSensorControls(const ControlList &sensorControls) { delayedCtrls_->push(sensorControls); - ControlList ctrls(sensorControls); - sensor_->setControls(&ctrls); + /* + * Directly apply controls now if there is no frameStart signal. + * + * \todo Applying controls directly not only increases the risk of + * applying them to the wrong frame (or across a frame boundary), + * but it also bypasses delayedCtrls_, creating AGC regulation issues. + * Both problems should be fixed. + */ + if (!frameStartEmitter_) { + ControlList ctrls(sensorControls); + sensor_->setControls(&ctrls); + } } /* Retrieve all source pads connected to a sink pad through active routes. */ @@ -1363,17 +1395,6 @@ int SimplePipelineHandler::configure(Camera *camera, CameraConfiguration *c) if (outputCfgs.empty()) return 0; - const CameraSensorProperties::SensorDelays &delays = data->sensor_->sensorDelays(); - std::unordered_map<uint32_t, DelayedControls::ControlParams> params = { - { V4L2_CID_ANALOGUE_GAIN, { delays.gainDelay, false } }, - { V4L2_CID_EXPOSURE, { delays.exposureDelay, false } }, - }; - data->delayedCtrls_ = - std::make_unique<DelayedControls>(data->sensor_->device(), - params); - data->video_->frameStart.connect(data->delayedCtrls_.get(), - &DelayedControls::applyControls); - StreamConfiguration inputCfg; inputCfg.pixelFormat = pipeConfig->captureFormat; inputCfg.size = pipeConfig->captureSize; @@ -1411,6 +1432,7 @@ int SimplePipelineHandler::start(Camera *camera, [[maybe_unused]] const ControlL { SimpleCameraData *data = cameraData(camera); V4L2VideoDevice *video = data->video_; + V4L2Subdevice *frameStartEmitter = data->frameStartEmitter_; int ret; const MediaPad *pad = acquirePipeline(data); @@ -1440,6 +1462,17 @@ int SimplePipelineHandler::start(Camera *camera, [[maybe_unused]] const ControlL video->bufferReady.connect(data, &SimpleCameraData::imageBufferReady); + data->delayedCtrls_->reset(); + if (frameStartEmitter) { + ret = frameStartEmitter->setFrameStartEnabled(true); + if (ret) { + stop(camera); + return ret; + } + frameStartEmitter->frameStart.connect(data->delayedCtrls_.get(), + &DelayedControls::applyControls); + } + ret = video->streamOn(); if (ret < 0) { stop(camera); @@ -1471,6 +1504,13 @@ void SimplePipelineHandler::stopDevice(Camera *camera) { SimpleCameraData *data = cameraData(camera); V4L2VideoDevice *video = data->video_; + V4L2Subdevice *frameStartEmitter = data->frameStartEmitter_; + + if (frameStartEmitter) { + frameStartEmitter->setFrameStartEnabled(false); + frameStartEmitter->frameStart.disconnect(data->delayedCtrls_.get(), + &DelayedControls::applyControls); + } if (data->useConversion_) { if (data->converter_) diff --git a/src/libcamera/v4l2_device.cpp b/src/libcamera/v4l2_device.cpp index 2f65a43a..0db92c19 100644 --- a/src/libcamera/v4l2_device.cpp +++ b/src/libcamera/v4l2_device.cpp @@ -450,6 +450,28 @@ std::string V4L2Device::devicePath() const } /** + * \brief Check if frame start event is supported + * + * Due to limitations in the kernel API, this function may disable the frame + * start event as a side effect. It should only be called during initialization, + * before enabling the frame start event with setFrameStartEnabled(). + * + * \return True if frame start event is supported, false otherwise + */ +bool V4L2Device::supportsFrameStartEvent() +{ + struct v4l2_event_subscription event{}; + event.type = V4L2_EVENT_FRAME_SYNC; + + int ret = ioctl(VIDIOC_SUBSCRIBE_EVENT, &event); + if (ret) + return false; + + ioctl(VIDIOC_UNSUBSCRIBE_EVENT, &event); + return true; +} + +/** * \brief Enable or disable frame start event notification * \param[in] enable True to enable frame start events, false to disable them * diff --git a/test/ipa/libipa/histogram.cpp b/test/ipa/libipa/histogram.cpp new file mode 100644 index 00000000..77ff31a6 --- /dev/null +++ b/test/ipa/libipa/histogram.cpp @@ -0,0 +1,66 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2024, Ideas on Board Oy + * + * Histogram tests + */ + +#include "../src/ipa/libipa/histogram.h" + +#include <cmath> +#include <iostream> +#include <map> +#include <stdint.h> + +#include "test.h" + +using namespace std; +using namespace libcamera; +using namespace ipa; + +#define ASSERT_EQ(a, b) \ + if (!((a) == (b))) { \ + std::cout << #a " != " #b << std::endl; \ + return TestFail; \ + } + +class HistogramTest : public Test +{ +protected: + int run() + { + auto hist = Histogram({ { 50, 50 } }); + + ASSERT_EQ(hist.bins(), 2); + ASSERT_EQ(hist.total(), 100); + + ASSERT_EQ(hist.cumulativeFrequency(1.0), 50); + ASSERT_EQ(hist.cumulativeFrequency(1.5), 75); + ASSERT_EQ(hist.cumulativeFrequency(2.0), 100); + + ASSERT_EQ(hist.quantile(0.0), 0.0); + ASSERT_EQ(hist.quantile(1.0), 2.0); + ASSERT_EQ(hist.quantile(0.5), 1.0); + + /* Test quantile in the middle of a bin. */ + ASSERT_EQ(hist.quantile(0.75), 1.5); + + /* Test quantile smaller than the smallest histogram step. */ + ASSERT_EQ(hist.quantile(0.001), 0.002); + + ASSERT_EQ(hist.interQuantileMean(0.0, 1.0), 1.0); + ASSERT_EQ(hist.interQuantileMean(0.0, 0.5), 0.5); + ASSERT_EQ(hist.interQuantileMean(0.5, 1.0), 1.5); + + /* Test interquantile mean that starts and ends in the middle of a bin. */ + ASSERT_EQ(hist.interQuantileMean(0.25, 0.75), 1.0); + + /* Test small ranges at the borders of the histogram. */ + ASSERT_EQ(hist.interQuantileMean(0.0, 0.1), 0.1); + ASSERT_EQ(hist.interQuantileMean(0.9, 1.0), 1.9); + + return TestPass; + } +}; + +TEST_REGISTER(HistogramTest) diff --git a/test/ipa/libipa/meson.build b/test/ipa/libipa/meson.build index eaf4b49a..8c63ebd8 100644 --- a/test/ipa/libipa/meson.build +++ b/test/ipa/libipa/meson.build @@ -2,6 +2,7 @@ libipa_test = [ {'name': 'fixedpoint', 'sources': ['fixedpoint.cpp']}, + {'name': 'histogram', 'sources': ['histogram.cpp']}, {'name': 'interpolator', 'sources': ['interpolator.cpp']}, ] @@ -13,5 +14,6 @@ foreach test : libipa_test include_directories : [test_includes_internal, '../../../src/ipa/libipa/']) - test(test['name'], exe, suite : 'ipa') + test(test['name'], exe, suite : 'ipa', + should_fail : test.get('should_fail', false)) endforeach |