libcamera: backport contrast autofocus for the main camera (r4)
Some checks failed
image / image (push) Has been cancelled

The DW9784 VCM works and libcamera discovers it through the sensor's
ancillary link, but nothing in the software ISP ever moves it. Backport
the out-of-tree autofocus work onto v0.7.2:

- 0007-0009 are Vasiliy Doylov's focus control, contrast autofocus and
  focus-loss detection from the softisp-playground branch;
- 0010 is Pavel Machek's Librem5-tested robustness work (centre-window,
  brightness-normalised sharpness, two-phase sweep, settle skip),
  squashed and adapted;
- 0011-0013 are ours: the lens write no longer sits inside the
  no-frame-start-emitter branch, the lensless-camera paths are guarded,
  and AfMode is advertised with a continuous default so that stock
  applications get autofocus without sending AfTrigger.

Provenance and the adaptations made to each patch are recorded in the
patch commit messages.
This commit is contained in:
Jorijn van der Graaf 2026-08-17 02:02:45 +02:00
commit 40ccdfeed6
8 changed files with 1605 additions and 1 deletions

View file

@ -0,0 +1,379 @@
From d3ee3b91af950deae8edd4ce4148ac11664c48e9 Mon Sep 17 00:00:00 2001
From: Vasiliy Doylov <nekocwd@mainlining.org>
Date: Mon, 17 Mar 2025 04:24:56 +0300
Subject: [PATCH] libcamera: software_isp: Add focus control
Signed-off-by: Vasiliy Doylov <nekocwd@mainlining.org>
---
.../internal/software_isp/software_isp.h | 4 +-
include/libcamera/ipa/soft.mojom | 3 +-
src/ipa/simple/algorithms/af.cpp | 71 +++++++++++++++++++
src/ipa/simple/algorithms/af.h | 40 +++++++++++
src/ipa/simple/algorithms/meson.build | 1 +
src/ipa/simple/data/uncalibrated.yaml | 1 +
src/ipa/simple/ipa_context.h | 9 +++
src/ipa/simple/soft_simple.cpp | 18 ++++-
src/libcamera/pipeline/simple/simple.cpp | 30 ++++++--
src/libcamera/software_isp/software_isp.cpp | 4 +-
10 files changed, 169 insertions(+), 12 deletions(-)
create mode 100644 src/ipa/simple/algorithms/af.cpp
create mode 100644 src/ipa/simple/algorithms/af.h
diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
index 4f72dce9b..bf2cfea43 100644
--- a/include/libcamera/internal/software_isp/software_isp.h
+++ b/include/libcamera/internal/software_isp/software_isp.h
@@ -86,11 +86,11 @@ public:
Signal<FrameBuffer *> outputBufferReady;
Signal<uint32_t, uint32_t> ispStatsReady;
Signal<uint32_t, const ControlList &> metadataReady;
- Signal<const ControlList &> setSensorControls;
+ Signal<const ControlList &, const ControlList &> setSensorControls;
private:
void saveIspParams();
- void setSensorCtrls(const ControlList &sensorControls);
+ void setSensorCtrls(const ControlList &sensorControls, const ControlList &lensControls);
void statsReady(uint32_t frame, uint32_t bufferId);
void inputReady(FrameBuffer *input);
void outputReady(FrameBuffer *output);
diff --git a/include/libcamera/ipa/soft.mojom b/include/libcamera/ipa/soft.mojom
index 77328c5fd..e5767532c 100644
--- a/include/libcamera/ipa/soft.mojom
+++ b/include/libcamera/ipa/soft.mojom
@@ -10,6 +10,7 @@ import "include/libcamera/ipa/core.mojom";
struct IPAConfigInfo {
libcamera.ControlInfoMap sensorControls;
+ libcamera.ControlInfoMap lensControls;
};
interface IPASoftInterface {
@@ -32,7 +33,7 @@ interface IPASoftInterface {
};
interface IPASoftEventInterface {
- setSensorControls(libcamera.ControlList sensorControls);
+ setSensorControls(libcamera.ControlList sensorControls, libcamera.ControlList lensControls);
setIspParams();
metadataReady(uint32 frame, libcamera.ControlList metadata);
};
diff --git a/src/ipa/simple/algorithms/af.cpp b/src/ipa/simple/algorithms/af.cpp
new file mode 100644
index 000000000..6197f3271
--- /dev/null
+++ b/src/ipa/simple/algorithms/af.cpp
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025 Vasiliy Doylov <nekodevelopper@gmail.com>
+ *
+ * Auto focus
+ */
+
+#include "af.h"
+
+#include <stdint.h>
+
+#include <libcamera/base/log.h>
+
+#include "control_ids.h"
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(IPASoftAutoFocus)
+
+namespace ipa::soft::algorithms {
+
+Af::Af()
+{
+}
+
+int Af::init(IPAContext &context,
+ [[maybe_unused]] const ValueNode &tuningData)
+{
+ context.ctrlMap[&controls::LensPosition] = ControlInfo(0.0f, 100.0f, 50.0f);
+ return 0;
+}
+
+int Af::configure(IPAContext &context,
+ [[maybe_unused]] const IPAConfigInfo &configInfo)
+{
+ context.activeState.knobs.focus_pos = std::optional<double>();
+
+ return 0;
+}
+
+void Af::queueRequest([[maybe_unused]] typename Module::Context &context,
+ [[maybe_unused]] const uint32_t frame,
+ [[maybe_unused]] typename Module::FrameContext &frameContext,
+ const ControlList &controls)
+{
+ const auto &focus_pos = controls.get(controls::LensPosition);
+ if (focus_pos.has_value()) {
+ context.activeState.knobs.focus_pos = focus_pos;
+ LOG(IPASoftAutoFocus, Debug) << "Setting focus position to " << focus_pos.value();
+ }
+}
+
+void Af::updateFocus([[maybe_unused]] IPAContext &context, [[maybe_unused]] IPAFrameContext &frameContext, [[maybe_unused]] double exposureMSV)
+{
+ frameContext.lens.focus_pos = context.activeState.knobs.focus_pos.value_or(50.0) / 100.0 * (context.configuration.focus.focus_max - context.configuration.focus.focus_min);
+}
+
+void Af::process([[maybe_unused]] IPAContext &context,
+ [[maybe_unused]] const uint32_t frame,
+ [[maybe_unused]] IPAFrameContext &frameContext,
+ [[maybe_unused]] const SwIspStats *stats,
+ [[maybe_unused]] ControlList &metadata)
+{
+ updateFocus(context, frameContext, 0);
+}
+
+REGISTER_IPA_ALGORITHM(Af, "Af")
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/af.h b/src/ipa/simple/algorithms/af.h
new file mode 100644
index 000000000..b138b4d63
--- /dev/null
+++ b/src/ipa/simple/algorithms/af.h
@@ -0,0 +1,40 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2025 Vasiliy Doylov <nekodevelopper@gmail.com>
+ *
+ * Auto focus
+ */
+
+#pragma once
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+class Af : public Algorithm
+{
+public:
+ Af();
+ ~Af() = default;
+
+ int init(IPAContext &context, const ValueNode &tuningData) override;
+ int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
+ void queueRequest(typename Module::Context &context,
+ const uint32_t frame,
+ typename Module::FrameContext &frameContext,
+ const ControlList &controls)
+ override;
+ void process(IPAContext &context, const uint32_t frame,
+ IPAFrameContext &frameContext,
+ const SwIspStats *stats,
+ ControlList &metadata) override;
+
+private:
+ void updateFocus(IPAContext &context, IPAFrameContext &frameContext, double focus);
+};
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/meson.build b/src/ipa/simple/algorithms/meson.build
index 73c637220..8950b008f 100644
--- a/src/ipa/simple/algorithms/meson.build
+++ b/src/ipa/simple/algorithms/meson.build
@@ -6,4 +6,5 @@ soft_simple_ipa_algorithms = files([
'agc.cpp',
'blc.cpp',
'ccm.cpp',
+ 'af.cpp',
])
diff --git a/src/ipa/simple/data/uncalibrated.yaml b/src/ipa/simple/data/uncalibrated.yaml
index fc90ca526..ede277d1d 100644
--- a/src/ipa/simple/data/uncalibrated.yaml
+++ b/src/ipa/simple/data/uncalibrated.yaml
@@ -16,4 +16,5 @@ algorithms:
# 0, 0, 1]
- Adjust:
- Agc:
+ - Af:
...
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index 8ccfacb46..2b6bd4b3a 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -33,6 +33,9 @@ struct IPASessionConfiguration {
struct {
std::optional<uint8_t> level;
} black;
+ struct {
+ int32_t focus_min, focus_max;
+ } focus;
};
struct IPAActiveState {
@@ -60,6 +63,8 @@ struct IPAActiveState {
/* 0..2 range, 1.0 = normal */
std::optional<float> contrast;
std::optional<float> saturation;
+ /* 0..100 range, 50.0 = normal */
+ std::optional<double> focus_pos;
} knobs;
};
@@ -76,6 +81,10 @@ struct IPAFrameContext : public FrameContext {
float gamma;
std::optional<float> contrast;
std::optional<float> saturation;
+
+ struct {
+ int32_t focus_pos;
+ } lens;
};
struct IPAContext {
diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
index 629e1a32d..dcf746cb2 100644
--- a/src/ipa/simple/soft_simple.cpp
+++ b/src/ipa/simple/soft_simple.cpp
@@ -78,6 +78,7 @@ private:
SwIspStats *stats_;
std::unique_ptr<CameraSensorHelper> camHelper_;
ControlInfoMap sensorInfoMap_;
+ ControlInfoMap lensInfoMap_;
/* Local parameter storage */
struct IPAContext context_;
@@ -202,6 +203,7 @@ int IPASoftSimple::init(const IPASettings &settings,
int IPASoftSimple::configure(const IPAConfigInfo &configInfo)
{
sensorInfoMap_ = configInfo.sensorControls;
+ lensInfoMap_ = configInfo.lensControls;
const ControlInfo &exposureInfo = sensorInfoMap_.find(V4L2_CID_EXPOSURE)->second;
const ControlInfo &gainInfo = sensorInfoMap_.find(V4L2_CID_ANALOGUE_GAIN)->second;
@@ -211,6 +213,17 @@ int IPASoftSimple::configure(const IPAConfigInfo &configInfo)
context_.activeState = {};
context_.frameContexts.clear();
+ if (lensInfoMap_.empty()) {
+ LOG(IPASoft, Warning) << "No camera leans found! Focus control disabled.";
+ context_.configuration.focus.focus_min = 0;
+ context_.configuration.focus.focus_max = 0;
+ } else {
+ const ControlInfo &lensInfo = lensInfoMap_.find(V4L2_CID_FOCUS_ABSOLUTE)->second;
+ context_.configuration.focus.focus_min = lensInfo.min().get<int32_t>();
+ context_.configuration.focus.focus_max = lensInfo.max().get<int32_t>();
+ LOG(IPASoft, Warning) << "Camera leans found! Focus: " << context_.configuration.focus.focus_min << "-" << context_.configuration.focus.focus_max;
+ }
+
context_.configuration.agc.lineDuration =
context_.sensorInfo.minLineLength * 1.0s / context_.sensorInfo.pixelRate;
context_.configuration.agc.exposureMin = exposureInfo.min().get<int32_t>();
@@ -325,7 +338,10 @@ void IPASoftSimple::processStats(const uint32_t frame,
ctrls.set(V4L2_CID_ANALOGUE_GAIN,
static_cast<int32_t>(camHelper_ ? camHelper_->gainCode(againNew) : againNew));
- setSensorControls.emit(ctrls);
+ ControlList lens_ctrls(lensInfoMap_);
+ lens_ctrls.set(V4L2_CID_FOCUS_ABSOLUTE, frameContext.lens.focus_pos);
+
+ setSensorControls.emit(ctrls, lens_ctrls);
}
std::string IPASoftSimple::logPrefix() const
diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
index b96b8b529..047800b59 100644
--- a/src/libcamera/pipeline/simple/simple.cpp
+++ b/src/libcamera/pipeline/simple/simple.cpp
@@ -33,6 +33,7 @@
#include <libcamera/stream.h>
#include "libcamera/internal/camera.h"
+#include "libcamera/internal/camera_lens.h"
#include "libcamera/internal/camera_manager.h"
#include "libcamera/internal/camera_sensor.h"
#include "libcamera/internal/camera_sensor_properties.h"
@@ -48,6 +49,8 @@
#include "libcamera/internal/v4l2_subdevice.h"
#include "libcamera/internal/v4l2_videodevice.h"
+#include "libcamera/controls.h"
+
namespace libcamera {
LOG_DEFINE_CATEGORY(SimplePipeline)
@@ -371,7 +374,7 @@ private:
void ispStatsReady(uint32_t frame, uint32_t bufferId);
void metadataReady(uint32_t frame, const ControlList &metadata);
- void setSensorControls(const ControlList &sensorControls);
+ void setSensorControls(const ControlList &sensorControls, const ControlList &lensControls);
};
class SimpleCameraConfiguration : public CameraConfiguration
@@ -1039,7 +1042,7 @@ void SimpleCameraData::metadataReady(uint32_t frame, const ControlList &metadata
tryCompleteRequest(info->request);
}
-void SimpleCameraData::setSensorControls(const ControlList &sensorControls)
+void SimpleCameraData::setSensorControls(const ControlList &sensorControls, const ControlList &lensControls)
{
delayedCtrls_->push(sensorControls);
/*
@@ -1050,10 +1053,21 @@ void SimpleCameraData::setSensorControls(const ControlList &sensorControls)
* but it also bypasses delayedCtrls_, creating AGC regulation issues.
* Both problems should be fixed.
*/
- if (!frameStartEmitter_) {
- ControlList ctrls(sensorControls);
- sensor_->setControls(&ctrls);
- }
+ if (frameStartEmitter_)
+ return;
+
+ ControlList ctrls(sensorControls);
+ sensor_->setControls(&ctrls);
+
+ CameraLens *focusLens = sensor_->focusLens();
+ if (!focusLens)
+ return;
+
+ if (!lensControls.contains(V4L2_CID_FOCUS_ABSOLUTE))
+ return;
+
+ const ControlValue &focusValue = lensControls.get(V4L2_CID_FOCUS_ABSOLUTE);
+ focusLens->setFocusPosition(focusValue.get<int32_t>());
}
/* Retrieve all source pads connected to a sink pad through active routes. */
@@ -1603,6 +1617,10 @@ int SimplePipelineHandler::configure(Camera *camera, CameraConfiguration *c)
} else {
ipa::soft::IPAConfigInfo configInfo;
configInfo.sensorControls = data->sensor_->controls();
+ if (data->sensor_->focusLens() != nullptr)
+ configInfo.lensControls = data->sensor_->focusLens()->controls();
+ else
+ configInfo.lensControls = ControlInfoMap();
return data->swIsp_->configure(inputCfg, outputCfgs, configInfo);
}
}
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
index c73a16ce0..2e6fc3d98 100644
--- a/src/libcamera/software_isp/software_isp.cpp
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -436,9 +436,9 @@ void SoftwareIsp::saveIspParams()
debayerParams_ = *sharedParams_;
}
-void SoftwareIsp::setSensorCtrls(const ControlList &sensorControls)
+void SoftwareIsp::setSensorCtrls(const ControlList &sensorControls, const ControlList &lensControls)
{
- setSensorControls.emit(sensorControls);
+ setSensorControls.emit(sensorControls, lensControls);
}
void SoftwareIsp::statsReady(uint32_t frame, uint32_t bufferId)

View file

@ -0,0 +1,224 @@
From 832c016ad79e1280d1a2e840cc47f434cba4e382 Mon Sep 17 00:00:00 2001
From: Vasiliy Doylov <nekocwd@mainlining.org>
Date: Wed, 9 Jul 2025 16:07:14 +0300
Subject: [PATCH] libcamera: software_isp: Add autofocus
Signed-off-by: Vasiliy Doylov <nekocwd@mainlining.org>
---
.../internal/software_isp/swisp_stats.h | 4 ++
src/ipa/simple/algorithms/af.cpp | 59 ++++++++++++++++++-
src/ipa/simple/algorithms/af.h | 1 +
src/ipa/simple/ipa_context.h | 5 ++
src/libcamera/software_isp/swstats_cpu.cpp | 22 +++++--
5 files changed, 86 insertions(+), 5 deletions(-)
diff --git a/include/libcamera/internal/software_isp/swisp_stats.h b/include/libcamera/internal/software_isp/swisp_stats.h
index d9d0d9be8..c2ec20e42 100644
--- a/include/libcamera/internal/software_isp/swisp_stats.h
+++ b/include/libcamera/internal/software_isp/swisp_stats.h
@@ -43,6 +43,10 @@ struct SwIspStats {
* \brief A histogram of luminance values of all the sampled pixels
*/
Histogram yHistogram;
+ /**
+ * \brief Holds the sharpness of an image
+ */
+ uint64_t sharpness;
};
} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/af.cpp b/src/ipa/simple/algorithms/af.cpp
index 6197f3271..3f0f98f93 100644
--- a/src/ipa/simple/algorithms/af.cpp
+++ b/src/ipa/simple/algorithms/af.cpp
@@ -27,14 +27,17 @@ int Af::init(IPAContext &context,
[[maybe_unused]] const ValueNode &tuningData)
{
context.ctrlMap[&controls::LensPosition] = ControlInfo(0.0f, 100.0f, 50.0f);
+ context.ctrlMap[&controls::AfTrigger] = ControlInfo(0, 1, 0);
return 0;
}
int Af::configure(IPAContext &context,
[[maybe_unused]] const IPAConfigInfo &configInfo)
{
+ context.activeState.knobs.focus_sweep = std::optional<bool>();
context.activeState.knobs.focus_pos = std::optional<double>();
-
+ context.activeState.knobs.focus_sweep = false;
+ context.activeState.knobs.focus_pos = 0;
return 0;
}
@@ -44,10 +47,24 @@ void Af::queueRequest([[maybe_unused]] typename Module::Context &context,
const ControlList &controls)
{
const auto &focus_pos = controls.get(controls::LensPosition);
+ const auto &af_trigger = controls.get(controls::AfTrigger);
if (focus_pos.has_value()) {
context.activeState.knobs.focus_pos = focus_pos;
LOG(IPASoftAutoFocus, Debug) << "Setting focus position to " << focus_pos.value();
}
+ if (af_trigger.has_value()) {
+ context.activeState.knobs.focus_sweep = af_trigger.value() == 1;
+ if(context.activeState.knobs.focus_sweep){
+ context.activeState.knobs.focus_pos = 0;
+ context.configuration.focus.focus_max_pos = 0;
+ context.configuration.focus.sharpness_max = 0;
+ context.configuration.focus.start = 0;
+ context.configuration.focus.stop = 100;
+ context.configuration.focus.step = 25;
+ LOG(IPASoftAutoFocus, Info) << "Starting focus sweep";
+ }
+ }
+
}
void Af::updateFocus([[maybe_unused]] IPAContext &context, [[maybe_unused]] IPAFrameContext &frameContext, [[maybe_unused]] double exposureMSV)
@@ -55,12 +72,52 @@ void Af::updateFocus([[maybe_unused]] IPAContext &context, [[maybe_unused]] IPAF
frameContext.lens.focus_pos = context.activeState.knobs.focus_pos.value_or(50.0) / 100.0 * (context.configuration.focus.focus_max - context.configuration.focus.focus_min);
}
+void Af::step(double& start, double& stop, double& step, double& focus_pos, double& max_pos, uint64_t& max_sharp, uint64_t sharp, bool& sweep){
+ if(!sweep)
+ return;
+ if(focus_pos < start) {
+ focus_pos = start;
+ return;
+ }
+ if(sharp > max_sharp) {
+ max_sharp = sharp;
+ max_pos = focus_pos;
+ }
+ if(focus_pos >= stop) {
+ LOG(IPASoftAutoFocus, Info) << "Best focus on step " <<step << ": " << focus_pos;
+ start = std::clamp(max_pos - step, 0.0, 100.0);
+ stop = std::clamp(max_pos + step, 0.0, 100.0);
+ focus_pos = start;
+ max_sharp = 0;
+ step /= 2;
+ if(step <= 0.2){
+ sweep = false;
+ LOG(IPASoftAutoFocus, Info) << "Sweep end. Best focus: " << max_pos;
+ focus_pos = max_pos;
+ }
+ return;
+ }
+
+ focus_pos += step;
+}
+
void Af::process([[maybe_unused]] IPAContext &context,
[[maybe_unused]] const uint32_t frame,
[[maybe_unused]] IPAFrameContext &frameContext,
[[maybe_unused]] const SwIspStats *stats,
[[maybe_unused]] ControlList &metadata)
{
+ if (stats->valid) {
+ step(context.configuration.focus.start,
+ context.configuration.focus.stop,
+ context.configuration.focus.step,
+ context.activeState.knobs.focus_pos.value(),
+ context.configuration.focus.focus_max_pos,
+ context.configuration.focus.sharpness_max,
+ stats->sharpness,
+ context.activeState.knobs.focus_sweep.value());
+ }
+
updateFocus(context, frameContext, 0);
}
diff --git a/src/ipa/simple/algorithms/af.h b/src/ipa/simple/algorithms/af.h
index b138b4d63..4d0ea58b3 100644
--- a/src/ipa/simple/algorithms/af.h
+++ b/src/ipa/simple/algorithms/af.h
@@ -33,6 +33,7 @@ public:
private:
void updateFocus(IPAContext &context, IPAFrameContext &frameContext, double focus);
+ void step(double& start, double& stop, double& step, double& focus_pos, double& max_pos, uint64_t& max_sharp, uint64_t sharp, bool& sweep);
};
} /* namespace ipa::soft::algorithms */
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index 2b6bd4b3a..f869d1a67 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -35,6 +35,9 @@ struct IPASessionConfiguration {
} black;
struct {
int32_t focus_min, focus_max;
+ double focus_max_pos;
+ uint64_t sharpness_max;
+ double start, stop, step;
} focus;
};
@@ -65,6 +68,8 @@ struct IPAActiveState {
std::optional<float> saturation;
/* 0..100 range, 50.0 = normal */
std::optional<double> focus_pos;
+ /* 0..1 range, 0 = normal */
+ std::optional<bool> focus_sweep;
} knobs;
};
diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
index 7fb77ce7d..f243992fb 100644
--- a/src/libcamera/software_isp/swstats_cpu.cpp
+++ b/src/libcamera/software_isp/swstats_cpu.cpp
@@ -177,7 +177,10 @@ static constexpr unsigned int kBlueYMul = 29; /* 0.114 * 256 */
\
uint64_t sumR = 0; \
uint64_t sumG = 0; \
- uint64_t sumB = 0;
+ uint64_t sumB = 0; \
+ pixel_t r0 = 0, r1 = 0, b0 = 0, \
+ b1 = 0, g0 = 0, g1 = 0; \
+ uint64_t sharpness = 0;
#define SWSTATS_ACCUMULATE_LINE_STATS(div) \
sumR += r; \
@@ -187,12 +190,20 @@ static constexpr unsigned int kBlueYMul = 29; /* 0.114 * 256 */
yVal = r * kRedYMul; \
yVal += g * kGreenYMul; \
yVal += b * kBlueYMul; \
- stats.yHistogram[yVal * SwIspStats::kYHistogramSize / (256 * 256 * (div))]++;
-
+ stats.yHistogram[yVal * SwIspStats::kYHistogramSize / (256 * 256 * (div))]++; \
+ if (r0 != 0) \
+ sharpness += abs(r - 2 * r1 + r0) * kRedYMul + abs(g - 2 * g1 + g0) * kGreenYMul + abs(b - 2 * b1 + b0) * kBlueYMul; \
+ r0 = r1; \
+ g0 = g1; \
+ b0 = b1; \
+ r1 = r; \
+ g1 = g; \
+ b1 = b;
#define SWSTATS_FINISH_LINE_STATS() \
stats.sum_.r() += sumR; \
stats.sum_.g() += sumG; \
- stats.sum_.b() += sumB;
+ stats.sum_.b() += sumB; \
+ stats.sharpness += sharpness;
void SwStatsCpu::statsBGGR8Line0(const uint8_t *src[], SwIspStats &stats)
{
@@ -392,6 +403,7 @@ void SwStatsCpu::startFrame(uint32_t frame)
for (auto &s : stats_) {
s.sum_ = RGB<uint64_t>({ 0, 0, 0 });
s.yHistogram.fill(0);
+ s.sharpness = 0;
}
}
@@ -408,8 +420,10 @@ void SwStatsCpu::finishFrame(uint32_t frame, uint32_t bufferId)
if (valid) {
sharedStats_->sum_ = RGB<uint64_t>({ 0, 0, 0 });
+ sharedStats_->sharpness = 0;
sharedStats_->yHistogram.fill(0);
for (const auto &s : stats_) {
+ sharedStats_->sharpness += s.sharpness;
sharedStats_->sum_ += s.sum_;
for (unsigned int j = 0; j < SwIspStats::kYHistogramSize; j++)
sharedStats_->yHistogram[j] += s.yHistogram[j];

View file

@ -0,0 +1,64 @@
From 5f34df040cda47cfb08d1cb02f4a288de2b8ab8c Mon Sep 17 00:00:00 2001
From: Vasiliy Doylov <nekocwd@mainlining.org>
Date: Mon, 17 Aug 2026 01:51:48 +0200
Subject: [PATCH] AF: detect focus loss
Re-trigger the focus sweep when the sharpness of the settled scene
departs from the sharpness the sweep converged on by more than 30%,
which is what happens when the scene or the subject distance changes.
Keeping the converged maximum around (instead of clearing it at every
phase transition) is what makes that comparison possible.
[Backported from 4da3fec35a15 on gitlab.com/tui/libcamera
millicam_af_6. That tree already carried the settle-skip counter, so
the skip handling of the original hunk is dropped here; it arrives
with the next patch.]
Signed-off-by: Vasiliy Doylov <nekocwd@mainlining.org>
---
src/ipa/simple/algorithms/af.cpp | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/src/ipa/simple/algorithms/af.cpp b/src/ipa/simple/algorithms/af.cpp
index 3f0f98f93..f321f361e 100644
--- a/src/ipa/simple/algorithms/af.cpp
+++ b/src/ipa/simple/algorithms/af.cpp
@@ -10,6 +10,7 @@
#include <stdint.h>
#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
#include "control_ids.h"
@@ -73,8 +74,21 @@ void Af::updateFocus([[maybe_unused]] IPAContext &context, [[maybe_unused]] IPAF
}
void Af::step(double& start, double& stop, double& step, double& focus_pos, double& max_pos, uint64_t& max_sharp, uint64_t sharp, bool& sweep){
- if(!sweep)
- return;
+ if(!sweep){
+ if(utils::abs_diff(sharp, max_sharp) > max_sharp*0.3){
+ LOG(IPASoftAutoFocus, Info) << "Focus lost :(";
+ sweep = true;
+ max_sharp = 0;
+ max_pos = 0;
+ focus_pos = 0;
+ start = 0;
+ stop = 100;
+ step = 25;
+ }
+ else {
+ return;
+ }
+ }
if(focus_pos < start) {
focus_pos = start;
return;
@@ -88,7 +102,6 @@ void Af::step(double& start, double& stop, double& step, double& focus_pos, doub
start = std::clamp(max_pos - step, 0.0, 100.0);
stop = std::clamp(max_pos + step, 0.0, 100.0);
focus_pos = start;
- max_sharp = 0;
step /= 2;
if(step <= 0.2){
sweep = false;

View file

@ -0,0 +1,529 @@
From eeaef1aff4f95ca78f6005b13cf82a9bccee5844 Mon Sep 17 00:00:00 2001
From: Pavel Machek <pavel@ucw.cz>
Date: Mon, 17 Aug 2026 01:57:53 +0200
Subject: [PATCH] af: two-phase sweep over a brightness-normalised
centre-window metric
Three changes that together turn the sweep from "moves the lens" into
"finds the focus":
- Measure sharpness over the centre of the frame only, so that a
cluttered background does not outvote the subject.
- Compare green samples two sampling steps apart instead of taking a
full RGB Laplacian, which keeps the metric away from the spatial
frequencies where sensor noise lives, and normalise the result by the
brightness of the same pixels: the AGC keeps moving while a sweep
runs, and an unnormalised metric simply follows the exposure.
- Sweep in two phases (coarse, then fine around the coarse maximum) and
skip a few frames after every large lens movement so that the lens has
settled before the next sample is taken.
[Squashed from da42da564afa, 3ac05187459d and 4312a90e32ea on
gitlab.com/tui/libcamera millicam_af_6, and adapted to libcamera
v0.7.2:
- the statistics line functions take a SwIspStats reference for
multi-threaded stats, so the y coordinate is threaded through as an
extra parameter rather than replacing one;
- the centre window is band 2 of 5 on both axes (the original used band
3, which is off-centre), and the x window is derived from each
format's own loop bound because the packed formats count bytes, not
pixels;
- the normalisation divides by the square of the mean green level of
the sampled pixels, which makes the metric independent of exposure
rather than merely less dependent on it, and is guarded against an
all-black window;
- the sweep constants are named and tuned for a 0..4095 VCM sampled
once every four frames: coarse 10%, fine 2%, ending below 1%;
- the mcam test application, the forced-focus button and the
AeState/step debug output are dropped.]
Signed-off-by: Pavel Machek <pavel@ucw.cz>
---
.../internal/software_isp/swstats_cpu.h | 20 +--
src/ipa/simple/algorithms/af.cpp | 138 +++++++++++-------
src/ipa/simple/algorithms/af.h | 5 +-
src/ipa/simple/ipa_context.h | 2 +
src/libcamera/software_isp/swstats_cpu.cpp | 84 +++++++----
5 files changed, 160 insertions(+), 89 deletions(-)
diff --git a/include/libcamera/internal/software_isp/swstats_cpu.h b/include/libcamera/internal/software_isp/swstats_cpu.h
index 551870921..df399d71c 100644
--- a/include/libcamera/internal/software_isp/swstats_cpu.h
+++ b/include/libcamera/internal/software_isp/swstats_cpu.h
@@ -67,7 +67,7 @@ public:
y >= (window_.y + window_.height))
return;
- (this->*stats0_)(src, stats_[statsBufferIndex]);
+ (this->*stats0_)(src, y, stats_[statsBufferIndex]);
}
void processLine2(uint32_t frame, unsigned int y, const uint8_t *src[], unsigned int statsBufferIndex = 0)
@@ -79,28 +79,28 @@ public:
y >= (window_.y + window_.height))
return;
- (this->*stats2_)(src, stats_[statsBufferIndex]);
+ (this->*stats2_)(src, y, stats_[statsBufferIndex]);
}
Signal<uint32_t, uint32_t> statsReady;
private:
- using statsProcessFn = void (SwStatsCpu::*)(const uint8_t *src[], SwIspStats &stats);
+ using statsProcessFn = void (SwStatsCpu::*)(const uint8_t *src[], unsigned int y, SwIspStats &stats);
using processFrameFn = void (SwStatsCpu::*)(MappedFrameBuffer &in);
int setupStandardBayerOrder(BayerFormat::Order order);
/* Bayer 8 bpp unpacked */
- void statsBGGR8Line0(const uint8_t *src[], SwIspStats &stats);
+ void statsBGGR8Line0(const uint8_t *src[], unsigned int y, SwIspStats &stats);
/* Bayer 10 bpp unpacked */
- void statsBGGR10Line0(const uint8_t *src[], SwIspStats &stats);
+ void statsBGGR10Line0(const uint8_t *src[], unsigned int y, SwIspStats &stats);
/* Bayer 12 bpp unpacked */
- void statsBGGR12Line0(const uint8_t *src[], SwIspStats &stats);
+ void statsBGGR12Line0(const uint8_t *src[], unsigned int y, SwIspStats &stats);
/* Bayer 10 bpp packed */
- void statsBGGR10PLine0(const uint8_t *src[], SwIspStats &stats);
- void statsGBRG10PLine0(const uint8_t *src[], SwIspStats &stats);
+ void statsBGGR10PLine0(const uint8_t *src[], unsigned int y, SwIspStats &stats);
+ void statsGBRG10PLine0(const uint8_t *src[], unsigned int y, SwIspStats &stats);
/* Bayer 12 bpp packed */
- void statsBGGR12PLine0(const uint8_t *src[], SwIspStats &stats);
- void statsGBRG12PLine0(const uint8_t *src[], SwIspStats &stats);
+ void statsBGGR12PLine0(const uint8_t *src[], unsigned int y, SwIspStats &stats);
+ void statsGBRG12PLine0(const uint8_t *src[], unsigned int y, SwIspStats &stats);
void processBayerFrame2(MappedFrameBuffer &in);
diff --git a/src/ipa/simple/algorithms/af.cpp b/src/ipa/simple/algorithms/af.cpp
index f321f361e..19235a567 100644
--- a/src/ipa/simple/algorithms/af.cpp
+++ b/src/ipa/simple/algorithms/af.cpp
@@ -7,6 +7,7 @@
#include "af.h"
+#include <algorithm>
#include <stdint.h>
#include <libcamera/base/log.h>
@@ -20,7 +21,23 @@ LOG_DEFINE_CATEGORY(IPASoftAutoFocus)
namespace ipa::soft::algorithms {
+namespace {
+
+/* Percentage of the lens travel between two samples of the coarse sweep. */
+constexpr double kCoarseStep = 10.0;
+/* The step is divided by this at every phase of the sweep. */
+constexpr double kStepDivisor = 5.0;
+/* The sweep ends once the step would become finer than this. */
+constexpr double kFineStepMin = 1.0;
+/* Stats frames to ignore after a large lens movement. */
+constexpr uint32_t kSettleSkipLong = 3;
+/* Relative sharpness change that makes a settled scene worth re-focusing. */
+constexpr double kFocusLossThreshold = 0.3;
+
+} /* namespace */
+
Af::Af()
+ : steps_(0)
{
}
@@ -39,6 +56,7 @@ int Af::configure(IPAContext &context,
context.activeState.knobs.focus_pos = std::optional<double>();
context.activeState.knobs.focus_sweep = false;
context.activeState.knobs.focus_pos = 0;
+ context.configuration.focus.skip = kSettleSkipLong;
return 0;
}
@@ -55,17 +73,9 @@ void Af::queueRequest([[maybe_unused]] typename Module::Context &context,
}
if (af_trigger.has_value()) {
context.activeState.knobs.focus_sweep = af_trigger.value() == 1;
- if(context.activeState.knobs.focus_sweep){
- context.activeState.knobs.focus_pos = 0;
- context.configuration.focus.focus_max_pos = 0;
- context.configuration.focus.sharpness_max = 0;
- context.configuration.focus.start = 0;
- context.configuration.focus.stop = 100;
- context.configuration.focus.step = 25;
- LOG(IPASoftAutoFocus, Info) << "Starting focus sweep";
- }
+ if (context.activeState.knobs.focus_sweep)
+ restart(context);
}
-
}
void Af::updateFocus([[maybe_unused]] IPAContext &context, [[maybe_unused]] IPAFrameContext &frameContext, [[maybe_unused]] double exposureMSV)
@@ -73,45 +83,78 @@ void Af::updateFocus([[maybe_unused]] IPAContext &context, [[maybe_unused]] IPAF
frameContext.lens.focus_pos = context.activeState.knobs.focus_pos.value_or(50.0) / 100.0 * (context.configuration.focus.focus_max - context.configuration.focus.focus_min);
}
-void Af::step(double& start, double& stop, double& step, double& focus_pos, double& max_pos, uint64_t& max_sharp, uint64_t sharp, bool& sweep){
- if(!sweep){
- if(utils::abs_diff(sharp, max_sharp) > max_sharp*0.3){
- LOG(IPASoftAutoFocus, Info) << "Focus lost :(";
- sweep = true;
- max_sharp = 0;
- max_pos = 0;
- focus_pos = 0;
- start = 0;
- stop = 100;
- step = 25;
- }
- else {
- return;
- }
+void Af::restart(IPAContext &context)
+{
+ auto &focus = context.configuration.focus;
+
+ steps_ = 0;
+ focus.focus_max_pos = 0;
+ focus.sharpness_max = 0;
+ focus.start = 0;
+ focus.stop = 100;
+ focus.step = kCoarseStep;
+ focus.skip = kSettleSkipLong;
+ context.activeState.knobs.focus_pos = 0;
+ context.activeState.knobs.focus_sweep = true;
+ LOG(IPASoftAutoFocus, Info) << "Starting focus sweep";
+}
+
+void Af::step(IPAContext &context, double &focus_pos, uint64_t sharp, bool &sweep)
+{
+ auto &focus = context.configuration.focus;
+
+ steps_++;
+
+ /* Even during a sweep the lens needs time to reach the new position. */
+ if (focus.skip != 0) {
+ focus.skip--;
+ return;
}
- if(focus_pos < start) {
- focus_pos = start;
+
+ if (!sweep) {
+ if (utils::abs_diff(sharp, focus.sharpness_max) >
+ focus.sharpness_max * kFocusLossThreshold) {
+ LOG(IPASoftAutoFocus, Info)
+ << "Focus lost: " << sharp << " vs "
+ << focus.sharpness_max;
+ restart(context);
+ }
return;
}
- if(sharp > max_sharp) {
- max_sharp = sharp;
- max_pos = focus_pos;
+
+ if (sharp > focus.sharpness_max) {
+ focus.sharpness_max = sharp;
+ focus.focus_max_pos = focus_pos;
}
- if(focus_pos >= stop) {
- LOG(IPASoftAutoFocus, Info) << "Best focus on step " <<step << ": " << focus_pos;
- start = std::clamp(max_pos - step, 0.0, 100.0);
- stop = std::clamp(max_pos + step, 0.0, 100.0);
- focus_pos = start;
- step /= 2;
- if(step <= 0.2){
- sweep = false;
- LOG(IPASoftAutoFocus, Info) << "Sweep end. Best focus: " << max_pos;
- focus_pos = max_pos;
- }
+
+ focus_pos += focus.step;
+ if (focus_pos > focus.start && focus_pos < focus.stop)
+ return;
+
+ /* Sweep phase over, narrow the range around the best position. */
+ focus.start = std::clamp(focus.focus_max_pos - focus.step, 0.0, 100.0);
+ focus.stop = std::clamp(focus.focus_max_pos + focus.step, 0.0, 100.0);
+ LOG(IPASoftAutoFocus, Info)
+ << "Best focus with step " << focus.step << ": "
+ << focus.focus_max_pos << " (sharpness "
+ << focus.sharpness_max << "), next range " << focus.start
+ << ".." << focus.stop;
+ focus_pos = focus.start;
+ focus.step /= kStepDivisor;
+ focus.skip = kSettleSkipLong;
+
+ if (focus.step >= kFineStepMin) {
+ /* Look for the maximum again at the finer step. */
+ focus.sharpness_max = 0;
return;
}
- focus_pos += step;
+ sweep = false;
+ focus_pos = focus.focus_max_pos;
+ LOG(IPASoftAutoFocus, Info)
+ << "Sweep end. Best focus: " << focus.focus_max_pos
+ << " after " << steps_ << " frames";
+ /* sharpness_max is kept to detect the scene changing later on. */
}
void Af::process([[maybe_unused]] IPAContext &context,
@@ -121,14 +164,9 @@ void Af::process([[maybe_unused]] IPAContext &context,
[[maybe_unused]] ControlList &metadata)
{
if (stats->valid) {
- step(context.configuration.focus.start,
- context.configuration.focus.stop,
- context.configuration.focus.step,
- context.activeState.knobs.focus_pos.value(),
- context.configuration.focus.focus_max_pos,
- context.configuration.focus.sharpness_max,
- stats->sharpness,
- context.activeState.knobs.focus_sweep.value());
+ step(context, context.activeState.knobs.focus_pos.value(),
+ stats->sharpness,
+ context.activeState.knobs.focus_sweep.value());
}
updateFocus(context, frameContext, 0);
diff --git a/src/ipa/simple/algorithms/af.h b/src/ipa/simple/algorithms/af.h
index 4d0ea58b3..0e0f19423 100644
--- a/src/ipa/simple/algorithms/af.h
+++ b/src/ipa/simple/algorithms/af.h
@@ -33,7 +33,10 @@ public:
private:
void updateFocus(IPAContext &context, IPAFrameContext &frameContext, double focus);
- void step(double& start, double& stop, double& step, double& focus_pos, double& max_pos, uint64_t& max_sharp, uint64_t sharp, bool& sweep);
+ void step(IPAContext &context, double &focus_pos, uint64_t sharp, bool &sweep);
+ void restart(IPAContext &context);
+
+ unsigned int steps_;
};
} /* namespace ipa::soft::algorithms */
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index f869d1a67..4fc9c7cdf 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -38,6 +38,8 @@ struct IPASessionConfiguration {
double focus_max_pos;
uint64_t sharpness_max;
double start, stop, step;
+ /* Stats frames to ignore while the lens settles */
+ uint32_t skip;
} focus;
};
diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
index f243992fb..2cbdbefa3 100644
--- a/src/libcamera/software_isp/swstats_cpu.cpp
+++ b/src/libcamera/software_isp/swstats_cpu.cpp
@@ -130,6 +130,7 @@ namespace libcamera {
* \typedef SwStatsCpu::statsProcessFn
* \brief Called when there is data to get statistics from
* \param[in] src The input data
+ * \param[in] y The y coordinate of the line, relative to the window
*
* These functions take an array of (patternSize_.height + 1) src
* pointers each pointing to a line in the source image. The middle
@@ -171,17 +172,34 @@ static constexpr unsigned int kRedYMul = 77; /* 0.299 * 256 */
static constexpr unsigned int kGreenYMul = 150; /* 0.587 * 256 */
static constexpr unsigned int kBlueYMul = 29; /* 0.114 * 256 */
-#define SWSTATS_START_LINE_STATS(pixel_t) \
+/*
+ * The sharpness metric is only gathered over the centre fifth of the frame,
+ * which is where the subject an autofocus run should focus on normally is.
+ * \a lineLength is the upper bound of the sampling loop of the caller, in the
+ * units that loop counts in (pixels or bytes depending on the format).
+ */
+#define SWSTATS_START_LINE_STATS(pixel_t, lineLength) \
pixel_t r, g, g2, b; \
uint64_t yVal; \
\
uint64_t sumR = 0; \
uint64_t sumG = 0; \
uint64_t sumB = 0; \
- pixel_t r0 = 0, r1 = 0, b0 = 0, \
- b1 = 0, g0 = 0, g1 = 0; \
- uint64_t sharpness = 0;
+ pixel_t gPrev = 0, gPrev2 = 0; \
+ uint64_t sharpness = 0; \
+ uint64_t sharpSumG = 0; \
+ unsigned int sharpCount = 0; \
+ const unsigned int sharpXBegin = (lineLength) * 2 / 5; \
+ const unsigned int sharpXEnd = (lineLength) * 3 / 5; \
+ const bool sharpRow = y >= window_.height * 2 / 5 && \
+ y < window_.height * 3 / 5;
+/*
+ * Sharpness is the sum of the squared differences between green samples two
+ * sampling steps apart; skipping one sample keeps the metric away from the
+ * highest spatial frequencies, where sensor noise dominates. gPrev/gPrev2 are
+ * updated for every sample so that the window always has valid history.
+ */
#define SWSTATS_ACCUMULATE_LINE_STATS(div) \
sumR += r; \
sumG += g; \
@@ -191,26 +209,36 @@ static constexpr unsigned int kBlueYMul = 29; /* 0.114 * 256 */
yVal += g * kGreenYMul; \
yVal += b * kBlueYMul; \
stats.yHistogram[yVal * SwIspStats::kYHistogramSize / (256 * 256 * (div))]++; \
- if (r0 != 0) \
- sharpness += abs(r - 2 * r1 + r0) * kRedYMul + abs(g - 2 * g1 + g0) * kGreenYMul + abs(b - 2 * b1 + b0) * kBlueYMul; \
- r0 = r1; \
- g0 = g1; \
- b0 = b1; \
- r1 = r; \
- g1 = g; \
- b1 = b;
+ if (sharpRow && x >= sharpXBegin && x < sharpXEnd) { \
+ const int64_t gDiff = static_cast<int64_t>(g) - gPrev2; \
+ sharpness += gDiff * gDiff; \
+ sharpSumG += g; \
+ sharpCount++; \
+ } \
+ gPrev2 = gPrev; \
+ gPrev = g;
+
+/*
+ * Normalise the sharpness by the square of the mean green level of the same
+ * pixels, so that the metric tracks contrast rather than exposure: the AGC
+ * keeps moving while an autofocus sweep runs.
+ */
#define SWSTATS_FINISH_LINE_STATS() \
stats.sum_.r() += sumR; \
stats.sum_.g() += sumG; \
stats.sum_.b() += sumB; \
- stats.sharpness += sharpness;
+ if (sharpCount) { \
+ const uint64_t meanG = sharpSumG / sharpCount; \
+ if (meanG) \
+ stats.sharpness += sharpness * 1024 / (meanG * meanG); \
+ }
-void SwStatsCpu::statsBGGR8Line0(const uint8_t *src[], SwIspStats &stats)
+void SwStatsCpu::statsBGGR8Line0(const uint8_t *src[], unsigned int y, SwIspStats &stats)
{
const uint8_t *src0 = src[1] + window_.x;
const uint8_t *src1 = src[2] + window_.x;
- SWSTATS_START_LINE_STATS(uint8_t)
+ SWSTATS_START_LINE_STATS(uint8_t, window_.width)
if (swapLines_)
std::swap(src0, src1);
@@ -230,12 +258,12 @@ void SwStatsCpu::statsBGGR8Line0(const uint8_t *src[], SwIspStats &stats)
SWSTATS_FINISH_LINE_STATS()
}
-void SwStatsCpu::statsBGGR10Line0(const uint8_t *src[], SwIspStats &stats)
+void SwStatsCpu::statsBGGR10Line0(const uint8_t *src[], unsigned int y, SwIspStats &stats)
{
const uint16_t *src0 = (const uint16_t *)src[1] + window_.x;
const uint16_t *src1 = (const uint16_t *)src[2] + window_.x;
- SWSTATS_START_LINE_STATS(uint16_t)
+ SWSTATS_START_LINE_STATS(uint16_t, window_.width)
if (swapLines_)
std::swap(src0, src1);
@@ -256,12 +284,12 @@ void SwStatsCpu::statsBGGR10Line0(const uint8_t *src[], SwIspStats &stats)
SWSTATS_FINISH_LINE_STATS()
}
-void SwStatsCpu::statsBGGR12Line0(const uint8_t *src[], SwIspStats &stats)
+void SwStatsCpu::statsBGGR12Line0(const uint8_t *src[], unsigned int y, SwIspStats &stats)
{
const uint16_t *src0 = (const uint16_t *)src[1] + window_.x;
const uint16_t *src1 = (const uint16_t *)src[2] + window_.x;
- SWSTATS_START_LINE_STATS(uint16_t)
+ SWSTATS_START_LINE_STATS(uint16_t, window_.width)
if (swapLines_)
std::swap(src0, src1);
@@ -282,7 +310,7 @@ void SwStatsCpu::statsBGGR12Line0(const uint8_t *src[], SwIspStats &stats)
SWSTATS_FINISH_LINE_STATS()
}
-void SwStatsCpu::statsBGGR10PLine0(const uint8_t *src[], SwIspStats &stats)
+void SwStatsCpu::statsBGGR10PLine0(const uint8_t *src[], unsigned int y, SwIspStats &stats)
{
const uint8_t *src0 = src[1] + window_.x * 5 / 4;
const uint8_t *src1 = src[2] + window_.x * 5 / 4;
@@ -291,7 +319,7 @@ void SwStatsCpu::statsBGGR10PLine0(const uint8_t *src[], SwIspStats &stats)
if (swapLines_)
std::swap(src0, src1);
- SWSTATS_START_LINE_STATS(uint8_t)
+ SWSTATS_START_LINE_STATS(uint8_t, widthInBytes)
/* x += 5 sample every other 2x2 block */
for (unsigned int x = 0; x < widthInBytes; x += 5) {
@@ -308,7 +336,7 @@ void SwStatsCpu::statsBGGR10PLine0(const uint8_t *src[], SwIspStats &stats)
SWSTATS_FINISH_LINE_STATS()
}
-void SwStatsCpu::statsGBRG10PLine0(const uint8_t *src[], SwIspStats &stats)
+void SwStatsCpu::statsGBRG10PLine0(const uint8_t *src[], unsigned int y, SwIspStats &stats)
{
const uint8_t *src0 = src[1] + window_.x * 5 / 4;
const uint8_t *src1 = src[2] + window_.x * 5 / 4;
@@ -317,7 +345,7 @@ void SwStatsCpu::statsGBRG10PLine0(const uint8_t *src[], SwIspStats &stats)
if (swapLines_)
std::swap(src0, src1);
- SWSTATS_START_LINE_STATS(uint8_t)
+ SWSTATS_START_LINE_STATS(uint8_t, widthInBytes)
/* x += 5 sample every other 2x2 block */
for (unsigned int x = 0; x < widthInBytes; x += 5) {
@@ -334,13 +362,13 @@ void SwStatsCpu::statsGBRG10PLine0(const uint8_t *src[], SwIspStats &stats)
SWSTATS_FINISH_LINE_STATS()
}
-void SwStatsCpu::statsBGGR12PLine0(const uint8_t *src[], SwIspStats &stats)
+void SwStatsCpu::statsBGGR12PLine0(const uint8_t *src[], unsigned int y, SwIspStats &stats)
{
const uint8_t *src0 = src[1] + window_.x * 3 / 2;
const uint8_t *src1 = src[2] + window_.x * 3 / 2;
const unsigned int widthInBytes = window_.width * 3 / 2;
- SWSTATS_START_LINE_STATS(uint8_t)
+ SWSTATS_START_LINE_STATS(uint8_t, widthInBytes)
if (swapLines_)
std::swap(src0, src1);
@@ -360,13 +388,13 @@ void SwStatsCpu::statsBGGR12PLine0(const uint8_t *src[], SwIspStats &stats)
SWSTATS_FINISH_LINE_STATS()
}
-void SwStatsCpu::statsGBRG12PLine0(const uint8_t *src[], SwIspStats &stats)
+void SwStatsCpu::statsGBRG12PLine0(const uint8_t *src[], unsigned int y, SwIspStats &stats)
{
const uint8_t *src0 = src[1] + window_.x * 3 / 2;
const uint8_t *src1 = src[2] + window_.x * 3 / 2;
const unsigned int widthInBytes = window_.width * 3 / 2;
- SWSTATS_START_LINE_STATS(uint8_t)
+ SWSTATS_START_LINE_STATS(uint8_t, widthInBytes)
if (swapLines_)
std::swap(src0, src1);
@@ -603,7 +631,7 @@ void SwStatsCpu::processBayerFrame2(MappedFrameBuffer &in)
/* linePointers[0] is not used by any stats0_ functions */
linePointers[1] = src;
linePointers[2] = src + stride_;
- (this->*stats0_)(linePointers, stats_[0]);
+ (this->*stats0_)(linePointers, y, stats_[0]);
src += stride_ * 2;
}
}

View file

@ -0,0 +1,74 @@
From 8ee13c9f2a3a8084d2360e78da32ed9793bfe5e9 Mon Sep 17 00:00:00 2001
From: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
Date: Mon, 17 Aug 2026 01:58:13 +0200
Subject: [PATCH] libcamera: pipeline: simple: Apply the lens position
unconditionally
The focus control patch applies the lens position inside the branch that
only runs when the pipeline has no frame-start emitter, because that is
where the sensor controls are applied directly. The lens is a different
kind of device: it is a separate subdevice, it is not pushed through
delayedCtrls_, and it has no frame-latched controls, so there is nothing
to synchronise it to. On a platform whose pipeline does register a
frame-start emitter the lens would simply never move.
Hoist the lens write above the early return. The FP6's CAMSS pipeline
registers no frame-start emitter (no subdevice in the graph supports
V4L2_EVENT_FRAME_SYNC), so this makes no difference there, but it is a
prerequisite for the feature working anywhere else.
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
---
src/libcamera/pipeline/simple/simple.cpp | 32 +++++++++++++-----------
1 file changed, 17 insertions(+), 15 deletions(-)
diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
index 047800b59..0095ea7a5 100644
--- a/src/libcamera/pipeline/simple/simple.cpp
+++ b/src/libcamera/pipeline/simple/simple.cpp
@@ -1045,6 +1045,19 @@ void SimpleCameraData::metadataReady(uint32_t frame, const ControlList &metadata
void SimpleCameraData::setSensorControls(const ControlList &sensorControls, const ControlList &lensControls)
{
delayedCtrls_->push(sensorControls);
+
+ /*
+ * The lens is a separate subdevice with no frame-latched controls, so
+ * its position is applied straight away and independently of the
+ * sensor controls below.
+ */
+ CameraLens *focusLens = sensor_->focusLens();
+ if (focusLens && lensControls.contains(V4L2_CID_FOCUS_ABSOLUTE)) {
+ const ControlValue &focusValue =
+ lensControls.get(V4L2_CID_FOCUS_ABSOLUTE);
+ focusLens->setFocusPosition(focusValue.get<int32_t>());
+ }
+
/*
* Directly apply controls now if there is no frameStart signal.
*
@@ -1053,21 +1066,10 @@ void SimpleCameraData::setSensorControls(const ControlList &sensorControls, cons
* but it also bypasses delayedCtrls_, creating AGC regulation issues.
* Both problems should be fixed.
*/
- if (frameStartEmitter_)
- return;
-
- ControlList ctrls(sensorControls);
- sensor_->setControls(&ctrls);
-
- CameraLens *focusLens = sensor_->focusLens();
- if (!focusLens)
- return;
-
- if (!lensControls.contains(V4L2_CID_FOCUS_ABSOLUTE))
- return;
-
- const ControlValue &focusValue = lensControls.get(V4L2_CID_FOCUS_ABSOLUTE);
- focusLens->setFocusPosition(focusValue.get<int32_t>());
+ if (!frameStartEmitter_) {
+ ControlList ctrls(sensorControls);
+ sensor_->setControls(&ctrls);
+ }
}
/* Retrieve all source pads connected to a sink pad through active routes. */

View file

@ -0,0 +1,113 @@
From 0e454d335ba23c306e7c8f011ef7d5234dd3aa9a Mon Sep 17 00:00:00 2001
From: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
Date: Mon, 17 Aug 2026 01:58:59 +0200
Subject: [PATCH] libcamera: software_isp: af: Harden the lens control handling
Three defects in the focus control patch that show up on a camera
without a focus lens, which on the FP6 is the ultra-wide:
- configure() dereferences the result of ControlInfoMap::find() for
V4L2_CID_FOCUS_ABSOLUTE whenever the lens control map is non-empty,
without checking that the control is actually there.
- processStats() unconditionally sets V4L2_CID_FOCUS_ABSOLUTE on a
ControlList built from an empty ControlInfoMap, which logs a control
validation error for every frame of every capture.
- the lens position is computed as a fraction of the travel but the
minimum is never added back, so a lens whose range does not start at
zero is driven past its near end.
Also demote the "found a lens" message to Info and fix its spelling.
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
---
src/ipa/simple/algorithms/af.cpp | 8 +++++++-
src/ipa/simple/ipa_context.h | 1 +
src/ipa/simple/soft_simple.cpp | 24 +++++++++++++++---------
3 files changed, 23 insertions(+), 10 deletions(-)
diff --git a/src/ipa/simple/algorithms/af.cpp b/src/ipa/simple/algorithms/af.cpp
index 19235a567..43d4a41c4 100644
--- a/src/ipa/simple/algorithms/af.cpp
+++ b/src/ipa/simple/algorithms/af.cpp
@@ -8,6 +8,7 @@
#include "af.h"
#include <algorithm>
+#include <cmath>
#include <stdint.h>
#include <libcamera/base/log.h>
@@ -80,7 +81,12 @@ void Af::queueRequest([[maybe_unused]] typename Module::Context &context,
void Af::updateFocus([[maybe_unused]] IPAContext &context, [[maybe_unused]] IPAFrameContext &frameContext, [[maybe_unused]] double exposureMSV)
{
- frameContext.lens.focus_pos = context.activeState.knobs.focus_pos.value_or(50.0) / 100.0 * (context.configuration.focus.focus_max - context.configuration.focus.focus_min);
+ const auto &focus = context.configuration.focus;
+ const double pos = std::clamp(context.activeState.knobs.focus_pos.value_or(50.0),
+ 0.0, 100.0);
+
+ frameContext.lens.focus_pos = focus.focus_min +
+ std::lround(pos / 100.0 * (focus.focus_max - focus.focus_min));
}
void Af::restart(IPAContext &context)
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index 4fc9c7cdf..ccc938ce9 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -34,6 +34,7 @@ struct IPASessionConfiguration {
std::optional<uint8_t> level;
} black;
struct {
+ bool available;
int32_t focus_min, focus_max;
double focus_max_pos;
uint64_t sharpness_max;
diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
index dcf746cb2..51a73d2dd 100644
--- a/src/ipa/simple/soft_simple.cpp
+++ b/src/ipa/simple/soft_simple.cpp
@@ -213,15 +213,20 @@ int IPASoftSimple::configure(const IPAConfigInfo &configInfo)
context_.activeState = {};
context_.frameContexts.clear();
- if (lensInfoMap_.empty()) {
- LOG(IPASoft, Warning) << "No camera leans found! Focus control disabled.";
+ const auto lensFocus = lensInfoMap_.find(V4L2_CID_FOCUS_ABSOLUTE);
+ if (lensFocus == lensInfoMap_.end()) {
+ LOG(IPASoft, Info) << "No focus lens, focus control disabled";
+ context_.configuration.focus.available = false;
context_.configuration.focus.focus_min = 0;
context_.configuration.focus.focus_max = 0;
} else {
- const ControlInfo &lensInfo = lensInfoMap_.find(V4L2_CID_FOCUS_ABSOLUTE)->second;
- context_.configuration.focus.focus_min = lensInfo.min().get<int32_t>();
- context_.configuration.focus.focus_max = lensInfo.max().get<int32_t>();
- LOG(IPASoft, Warning) << "Camera leans found! Focus: " << context_.configuration.focus.focus_min << "-" << context_.configuration.focus.focus_max;
+ context_.configuration.focus.available = true;
+ context_.configuration.focus.focus_min = lensFocus->second.min().get<int32_t>();
+ context_.configuration.focus.focus_max = lensFocus->second.max().get<int32_t>();
+ LOG(IPASoft, Info)
+ << "Focus lens range "
+ << context_.configuration.focus.focus_min << "-"
+ << context_.configuration.focus.focus_max;
}
context_.configuration.agc.lineDuration =
@@ -338,10 +343,11 @@ void IPASoftSimple::processStats(const uint32_t frame,
ctrls.set(V4L2_CID_ANALOGUE_GAIN,
static_cast<int32_t>(camHelper_ ? camHelper_->gainCode(againNew) : againNew));
- ControlList lens_ctrls(lensInfoMap_);
- lens_ctrls.set(V4L2_CID_FOCUS_ABSOLUTE, frameContext.lens.focus_pos);
+ ControlList lensCtrls(lensInfoMap_);
+ if (context_.configuration.focus.available)
+ lensCtrls.set(V4L2_CID_FOCUS_ABSOLUTE, frameContext.lens.focus_pos);
- setSensorControls.emit(ctrls, lens_ctrls);
+ setSensorControls.emit(ctrls, lensCtrls);
}
std::string IPASoftSimple::logPrefix() const

View file

@ -0,0 +1,207 @@
From ce00095430294bdb8d99e07e39d94b1cd3c85c88 Mon Sep 17 00:00:00 2001
From: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
Date: Mon, 17 Aug 2026 02:00:43 +0200
Subject: [PATCH] libcamera: software_isp: af: Add AfMode and focus without
being asked
The WIP exposes AfTrigger and LensPosition only, so the lens moves for
an application that knows about libcamera's AF controls and never moves
for one that does not. Every stock camera application on the phone is
the second kind: they open the camera, stream, and expect focus to
happen.
Advertise AfMode and default it to AfModeContinuous, start a sweep from
configure() so that focus happens as soon as the camera streams, and let
the existing focus-loss detection re-run the sweep when the scene
changes. AfModeAuto keeps the triggered behaviour (a sweep per
AfTriggerStart, no chasing afterwards) and AfModeManual hands the lens
to LensPosition, which per its documentation is ignored in the other
modes.
Report AfState and LensPosition in the metadata so applications can see
what the algorithm is doing. This replaces the WIP's debug abuse of
AeState.
Note that LensPosition is a percentage of the lens travel here, not the
dioptres its documentation asks for; converting requires lens
calibration data we do not have for the DW9784. It is inherited from the
WIP and kept for now.
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
---
src/ipa/simple/algorithms/af.cpp | 97 ++++++++++++++++++++++++++------
src/ipa/simple/ipa_context.h | 2 +
2 files changed, 81 insertions(+), 18 deletions(-)
diff --git a/src/ipa/simple/algorithms/af.cpp b/src/ipa/simple/algorithms/af.cpp
index 43d4a41c4..00148ce1b 100644
--- a/src/ipa/simple/algorithms/af.cpp
+++ b/src/ipa/simple/algorithms/af.cpp
@@ -34,6 +34,11 @@ constexpr double kFineStepMin = 1.0;
constexpr uint32_t kSettleSkipLong = 3;
/* Relative sharpness change that makes a settled scene worth re-focusing. */
constexpr double kFocusLossThreshold = 0.3;
+/*
+ * Focus continuously by default: applications that know nothing about AF never
+ * send an AfMode or an AfTrigger, and they are the ones this exists for.
+ */
+constexpr int32_t kDefaultAfMode = controls::AfModeContinuous;
} /* namespace */
@@ -45,41 +50,76 @@ Af::Af()
int Af::init(IPAContext &context,
[[maybe_unused]] const ValueNode &tuningData)
{
+ context.ctrlMap[&controls::AfMode] =
+ ControlInfo(controls::AfModeValues,
+ ControlValue(static_cast<int32_t>(kDefaultAfMode)));
+ context.ctrlMap[&controls::AfTrigger] =
+ ControlInfo(controls::AfTriggerValues,
+ ControlValue(static_cast<int32_t>(controls::AfTriggerStart)));
context.ctrlMap[&controls::LensPosition] = ControlInfo(0.0f, 100.0f, 50.0f);
- context.ctrlMap[&controls::AfTrigger] = ControlInfo(0, 1, 0);
return 0;
}
int Af::configure(IPAContext &context,
[[maybe_unused]] const IPAConfigInfo &configInfo)
{
- context.activeState.knobs.focus_sweep = std::optional<bool>();
- context.activeState.knobs.focus_pos = std::optional<double>();
+ context.activeState.knobs.af_mode = kDefaultAfMode;
context.activeState.knobs.focus_sweep = false;
context.activeState.knobs.focus_pos = 0;
context.configuration.focus.skip = kSettleSkipLong;
+
+ /*
+ * Start focusing as soon as the camera streams. Applications that do
+ * not know about AF at all never send an AfTrigger, so waiting for one
+ * would leave the lens parked wherever it was.
+ */
+ if (context.configuration.focus.available &&
+ context.activeState.knobs.af_mode != controls::AfModeManual)
+ restart(context);
+
return 0;
}
-void Af::queueRequest([[maybe_unused]] typename Module::Context &context,
+void Af::queueRequest(typename Module::Context &context,
[[maybe_unused]] const uint32_t frame,
[[maybe_unused]] typename Module::FrameContext &frameContext,
const ControlList &controls)
{
- const auto &focus_pos = controls.get(controls::LensPosition);
- const auto &af_trigger = controls.get(controls::AfTrigger);
- if (focus_pos.has_value()) {
- context.activeState.knobs.focus_pos = focus_pos;
- LOG(IPASoftAutoFocus, Debug) << "Setting focus position to " << focus_pos.value();
+ if (!context.configuration.focus.available)
+ return;
+
+ const auto &afMode = controls.get(controls::AfMode);
+ if (afMode.has_value() && afMode.value() != context.activeState.knobs.af_mode) {
+ context.activeState.knobs.af_mode = afMode.value();
+ LOG(IPASoftAutoFocus, Debug) << "Setting AF mode to " << afMode.value();
+
+ if (afMode.value() == controls::AfModeContinuous)
+ restart(context);
+ else
+ context.activeState.knobs.focus_sweep = false;
}
- if (af_trigger.has_value()) {
- context.activeState.knobs.focus_sweep = af_trigger.value() == 1;
- if (context.activeState.knobs.focus_sweep)
+
+ const auto &lensPosition = controls.get(controls::LensPosition);
+ if (lensPosition.has_value() &&
+ context.activeState.knobs.af_mode == controls::AfModeManual) {
+ context.activeState.knobs.focus_pos = lensPosition.value();
+ context.configuration.focus.skip = kSettleSkipLong;
+ LOG(IPASoftAutoFocus, Debug)
+ << "Setting focus position to " << lensPosition.value();
+ }
+
+ const auto &afTrigger = controls.get(controls::AfTrigger);
+ if (afTrigger.has_value() &&
+ context.activeState.knobs.af_mode == controls::AfModeAuto) {
+ if (afTrigger.value() == controls::AfTriggerStart)
restart(context);
+ else
+ context.activeState.knobs.focus_sweep = false;
}
}
-void Af::updateFocus([[maybe_unused]] IPAContext &context, [[maybe_unused]] IPAFrameContext &frameContext, [[maybe_unused]] double exposureMSV)
+void Af::updateFocus(IPAContext &context, IPAFrameContext &frameContext,
+ [[maybe_unused]] double exposureMSV)
{
const auto &focus = context.configuration.focus;
const double pos = std::clamp(context.activeState.knobs.focus_pos.value_or(50.0),
@@ -118,6 +158,13 @@ void Af::step(IPAContext &context, double &focus_pos, uint64_t sharp, bool &swee
}
if (!sweep) {
+ /*
+ * Only continuous AF chases the scene; in auto mode the lens
+ * stays where the last triggered sweep left it.
+ */
+ if (context.activeState.knobs.af_mode != controls::AfModeContinuous)
+ return;
+
if (utils::abs_diff(sharp, focus.sharpness_max) >
focus.sharpness_max * kFocusLossThreshold) {
LOG(IPASoftAutoFocus, Info)
@@ -163,19 +210,33 @@ void Af::step(IPAContext &context, double &focus_pos, uint64_t sharp, bool &swee
/* sharpness_max is kept to detect the scene changing later on. */
}
-void Af::process([[maybe_unused]] IPAContext &context,
+void Af::process(IPAContext &context,
[[maybe_unused]] const uint32_t frame,
- [[maybe_unused]] IPAFrameContext &frameContext,
- [[maybe_unused]] const SwIspStats *stats,
- [[maybe_unused]] ControlList &metadata)
+ IPAFrameContext &frameContext,
+ const SwIspStats *stats,
+ ControlList &metadata)
{
- if (stats->valid) {
+ if (!context.configuration.focus.available)
+ return;
+
+ if (stats->valid &&
+ context.activeState.knobs.af_mode != controls::AfModeManual) {
step(context, context.activeState.knobs.focus_pos.value(),
stats->sharpness,
context.activeState.knobs.focus_sweep.value());
}
updateFocus(context, frameContext, 0);
+
+ int32_t afState = controls::AfStateIdle;
+ if (context.activeState.knobs.af_mode != controls::AfModeManual)
+ afState = context.activeState.knobs.focus_sweep.value()
+ ? controls::AfStateScanning
+ : controls::AfStateFocused;
+
+ metadata.set(controls::AfState, afState);
+ metadata.set(controls::LensPosition,
+ static_cast<float>(context.activeState.knobs.focus_pos.value()));
}
REGISTER_IPA_ALGORITHM(Af, "Af")
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index ccc938ce9..4c594cd29 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -73,6 +73,8 @@ struct IPAActiveState {
std::optional<double> focus_pos;
/* 0..1 range, 0 = normal */
std::optional<bool> focus_sweep;
+ /* One of the controls::AfModeEnum values */
+ int32_t af_mode;
} knobs;
};

View file

@ -3,7 +3,7 @@ maintainer="Robert Mader <robert.mader@collabora.com>"
pkgname=libcamera
_pkgver=0.7.2
pkgver=9999$_pkgver
pkgrel=3
pkgrel=4
pkgdesc="Linux camera framework"
url="https://libcamera.org/"
arch="all"
@ -63,6 +63,13 @@ source="https://gitlab.freedesktop.org/camera/libcamera/-/archive/v$_pkgver/libc
0004-libcamera-sensor-Add-OV13B10-sensor-properties.patch
0005-libipa-Add-camera-sensor-helper-for-IMX896.patch
0006-libcamera-sensor-Add-IMX896-sensor-properties.patch
0007-libcamera-software_isp-Add-focus-control.patch
0008-libcamera-software_isp-Add-autofocus.patch
0009-AF-detect-focus-loss.patch
0010-af-two-phase-sweep-over-a-brightness-normalised-cent.patch
0011-libcamera-pipeline-simple-Apply-the-lens-position-un.patch
0012-libcamera-software_isp-af-Harden-the-lens-control-ha.patch
0013-libcamera-software_isp-af-Add-AfMode-and-focus-witho.patch
qcam.desktop
$_tuning_files
"
@ -182,6 +189,13 @@ eef5e38efc874f2049d17f08b04d9f21c3e4616f66873358cd2c446e651f78d6952ae25ee13a6ef9
89e0fc4a12aa4d5fbdc77b50e28b0a6d2cf8dfeb884b536507b6c24aa2ae5c616a32a517deb086e3284c72a82e6d6ddcf6eaa88f6c1c021adc6a0167194b7711 0004-libcamera-sensor-Add-OV13B10-sensor-properties.patch
f746cff46f78a8e7bd4cadd72c4ab05044971e36e412a56d4a14a420aeb1c9168b1f4ef411f4cc81a596159cab3d8a3713214a4ff0b2a4daf839b1a380c09b6e 0005-libipa-Add-camera-sensor-helper-for-IMX896.patch
1ac9747c941787c7ae76ed6520585b0d26494eb0f697bd74a9155ed04736a7b61f838dfb1bad01cf13b9cdd36f644a95a6ec1cbfe4f5c5a42cb23d4b6d9aa166 0006-libcamera-sensor-Add-IMX896-sensor-properties.patch
09fcc064f5263fbf5f759d74a798ac0a667af1b24729b7e3e7ff9806b7e4156fb68812f495454563e9a96d38d0f383f5b341adfadf31ebb3a54067bffdc2de74 0007-libcamera-software_isp-Add-focus-control.patch
4d4ed6462c6ba334fc0c568d93a15f0a4afc330d5e548c14eb2846855e6a08c5fb2627a1eb24f323f9c1861f22b57315d2d29e9c5875fe926d65658ebc53d1db 0008-libcamera-software_isp-Add-autofocus.patch
3797467e1a6a3370549fc3516ecbc41e02e01f347173146c569d7ba1ca239c5273316a532ba205550ce99ae2a4fd5e8a32aa5f021237096093319138694fea7f 0009-AF-detect-focus-loss.patch
c8ddc64ab943d9b215f4c997f2629e4403744c0ddb5de88cd42d384506a27c0762191eddd4d13559e7cc15e1a4d4542a55e5de985c7ea98f938ce796b43cc9e2 0010-af-two-phase-sweep-over-a-brightness-normalised-cent.patch
3965405be757917030c89db5da60aa226d0f0591e213d055d8312dbddee0c3d81fbcaa5928d6de4d3cb636572a27261d8686d87ae39b9634d70b33cf8388dc73 0011-libcamera-pipeline-simple-Apply-the-lens-position-un.patch
8734e27966bd1bc5a4b0324a3d8a3ee4ab371f6d1536500b2bb44d49bde874c3fd6f244312332226c352e4716ba12fb72bf51e64b9e97cf6fa646aa6dfef3277 0012-libcamera-software_isp-af-Harden-the-lens-control-ha.patch
dce81c2863ff6b9374115325f4af14ac5d396a6e2aee12f87d55a47d1d0bda860ae2af5a9d6885b126e20a12b7f3f40e49a2b9d738f94c04177bff6149da64e1 0013-libcamera-software_isp-af-Add-AfMode-and-focus-witho.patch
22167a4eceb6d1b40b0b7c45fdf116c71684f5340de7f767535cb8e160ad9d2ae0f00cb3d461f73a344520a48a4641cf46226841d78bee06bfbfd2a91337f754 qcam.desktop
2ee566653b17d565d2c0de67cbe6ebad25df5aac6051cad78e6bae52016fa2ca0247e964d735ac776b3e0bd447ead9c4fcb26cd7629228cdba3ceb91b46f378f hi846.yaml
55dab9dcb9b1982143b9d63e4e51138c5bffd7c8f4a8e4e122750b3ad9d475446956bc297d9f564819ade4be75a575d06633a064e14f8ba3ef77986783c5f067 imx355.yaml