Commit ccd8de6b authored by 尹洪福's avatar 尹洪福

初始提交:S3/S5 新对焦架构设计文档

包含:
- 主架构文档(s3 s5 新对焦架构.md)
- 会议简版(新对焦架构-会议简版.md)
- 关键信息记录、业界调研、旧架构参考
- libcamera 参考资料
- 参考图片
Co-Authored-By: 's avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
parents
# Raspberry Pi 自动对焦(AF)算法类总结
## 核心功能
这是一个**自动对焦控制算法实现**,支持两种对焦方式:
- **PDAF**(相位对焦) - 快速、精确
- **CDAF**(对比度对焦) - 备用方案
---
## 主要类结构
### 配置参数类
```
CfgParams
├── RangeDependentParams (3个范围)
│ ├── AfRangeNormal - 正常范围
│ ├── AfRangeMacro - 微距范围
│ └── AfRangeFull - 全范围
└── SpeedDependentParams (2个速度)
├── AfSpeedNormal
└── AfSpeedFast
```
---
## 核心算法流程
### 1️⃣ **对焦模式** (mode_)
| 模式 | 行为 |
|------|------|
| **Manual** | 手动设置镜头位置,不自动对焦 |
| **Auto** | 触发一次扫描完成对焦 |
| **Continuous** | 持续监测场景变化并自动调整 |
### 2️⃣ **扫描状态机** (scanState_)
```
Idle ──触发──> Trigger ──PDAF启动──> Pdaf
掉帧或场景变化
Coarse1 ↔ Coarse2 ──> Fine ──> Settle ──> Idle
(粗扫) (粗扫) (细扫) (稳定)
```
### 3️⃣ **核心处理流程**
```cpp
prepare() ──PDAF数据处理──> doAF() ──┬─> doPDAF() (闭环控制)
└─> doScan() (扫描)
process() ──获取CDAF统计──> updateLensPosition()
```
---
## 关键算法
### **PDAF处理** `doPDAF()`
1. 应用环路增益
2. 在连续模式下抑制抖动(低置信度时)
3. 在触发模式下逐步缩放(确保稳定)
4. 施加最大滑动率限制
5. 更新目标镜头位置 `ftarget_`
### **CDAF扫描** `doScan()`
1. 记录扫描数据(位置、对比度、相位、置信度)
2. **粗扫**(`Coarse1/2`):
- 如果首次扫描未括住峰值,反向扫描
- 对比度下降时结束
3. **细扫**(`Fine`):
- 在粗扫峰值附近细化搜索
- 用抛物线插值找到最优位置
4. **稳定**(`Settle`):
- 验证对焦质量
- 成功→`AfState::Focused` / 失败→`AfState::Failed`
### **早期终止** `earlyTerminationByPhase()`
- 利用两个PDAF样本直接计算零相位位置
- 避免完整扫描,加速对焦
---
## 权重系统 `computeWeights()`
支持**窗口化对焦**:
- 计算AF窗口内各统计区域的权重
- 融合多个窗口(最多10个)
- 默认使用中央1/2宽、中央1/3高的区域
---
## 场景变化检测
### 连续模式(CAF)
```cpp
if (对比度变化 > 25% || 颜色变化 > 25%) {
sceneChangeCount_++;
if (sceneChangeCount_ >= retriggerDelay) {
startProgrammedScan();
}
}
```
---
## 红外检测 `getAverageAndTestIr()`
- 检查RGB是否在4:5比例范围内
- 若>50%区域都是"灰色"(RGB相等) → 红外补光
- IR模式下禁用PDAF置信度(避免干扰)
---
## 镜头控制
| 方法 | 功能 |
|------|------|
| `updateLensPosition()` | 应用斜率限制,从未知位置初始化 |
| `setLensPosition()` | 手动设置镜头位置(手动模式) |
| `getLensPosition()` | 获取当前镜头位置(单位:屈光度) |
| `getLensLimits()` | 获取硬件限制范围 |
---
## 关键状态变量
| 变量 | 含义 |
|------|------|
| `ftarget_` | 目标焦距(屈光度) |
| `fsmooth_` | 实际镜头位置(应用斜率限制后) |
| `prevContrast_` | 上一帧对比度 |
| `phase`, `conf` | PDAF相位和置信度 |
| `scanData_` | 扫描过程中的历史数据 |
---
## 特点总结
✅ **混合对焦**: PDAF优先,失败时自动回退到CDAF
✅ **自适应**: 可配置速度、范围、参数
✅ **鲁棒性**: 场景变化检测、IR补光识别、掉帧恢复
✅ **灵活控制**: 支持手动/自动/连续三种模式
✅ **高效**: 早期终止、抛物线插值加速收敛
\ No newline at end of file
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* Copyright (C) 2022-2023, Raspberry Pi Ltd
*
* Autofocus control algorithm
*/
#include "af.h"
#include <cmath>
#include <iomanip>
#include <stdlib.h>
#include <libcamera/base/log.h>
#include <libcamera/control_ids.h>
using namespace RPiController;
using namespace libcamera;
LOG_DEFINE_CATEGORY(RPiAf)
#define NAME "rpi.af"
/*
* Default values for parameters. All may be overridden in the tuning file.
* Many of these values are sensor- or module-dependent; the defaults here
* assume IMX708 in a Raspberry Pi V3 camera with the standard lens.
*
* Here all focus values are in dioptres (1/m). They are converted to hardware
* units when written to status.lensSetting or returned from setLensPosition().
*
* Gain and delay values are relative to the update rate, since much (not all)
* of the delay is in the sensor and (for CDAF) ISP, not the lens mechanism;
* but note that algorithms are updated at no more than 30 Hz.
*/
Af::RangeDependentParams::RangeDependentParams()
: focusMin(0.0),
focusMax(12.0),
focusDefault(1.0)
{
}
Af::SpeedDependentParams::SpeedDependentParams()
: stepCoarse(1.0),
stepFine(0.25),
contrastRatio(0.75),
retriggerRatio(0.75),
retriggerDelay(10),
pdafGain(-0.02),
pdafSquelch(0.125),
maxSlew(2.0),
pdafFrames(20),
dropoutFrames(6),
stepFrames(4)
{
}
Af::CfgParams::CfgParams()
: confEpsilon(8),
confThresh(16),
confClip(512),
skipFrames(5),
checkForIR(false),
map()
{
}
template<typename T>
static void readNumber(T &dest, const libcamera::YamlObject &params, char const *name)
{
auto value = params[name].get<T>();
if (value)
dest = *value;
else
LOG(RPiAf, Warning) << "Missing parameter \"" << name << "\"";
}
void Af::RangeDependentParams::read(const libcamera::YamlObject &params)
{
readNumber<double>(focusMin, params, "min");
readNumber<double>(focusMax, params, "max");
readNumber<double>(focusDefault, params, "default");
}
void Af::SpeedDependentParams::read(const libcamera::YamlObject &params)
{
readNumber<double>(stepCoarse, params, "step_coarse");
readNumber<double>(stepFine, params, "step_fine");
readNumber<double>(contrastRatio, params, "contrast_ratio");
readNumber<double>(retriggerRatio, params, "retrigger_ratio");
readNumber<uint32_t>(retriggerDelay, params, "retrigger_delay");
readNumber<double>(pdafGain, params, "pdaf_gain");
readNumber<double>(pdafSquelch, params, "pdaf_squelch");
readNumber<double>(maxSlew, params, "max_slew");
readNumber<uint32_t>(pdafFrames, params, "pdaf_frames");
readNumber<uint32_t>(dropoutFrames, params, "dropout_frames");
readNumber<uint32_t>(stepFrames, params, "step_frames");
}
int Af::CfgParams::read(const libcamera::YamlObject &params)
{
if (params.contains("ranges")) {
auto &rr = params["ranges"];
if (rr.contains("normal"))
ranges[AfRangeNormal].read(rr["normal"]);
else
LOG(RPiAf, Warning) << "Missing range \"normal\"";
ranges[AfRangeMacro] = ranges[AfRangeNormal];
if (rr.contains("macro"))
ranges[AfRangeMacro].read(rr["macro"]);
ranges[AfRangeFull].focusMin = std::min(ranges[AfRangeNormal].focusMin,
ranges[AfRangeMacro].focusMin);
ranges[AfRangeFull].focusMax = std::max(ranges[AfRangeNormal].focusMax,
ranges[AfRangeMacro].focusMax);
ranges[AfRangeFull].focusDefault = ranges[AfRangeNormal].focusDefault;
if (rr.contains("full"))
ranges[AfRangeFull].read(rr["full"]);
} else
LOG(RPiAf, Warning) << "No ranges defined";
if (params.contains("speeds")) {
auto &ss = params["speeds"];
if (ss.contains("normal"))
speeds[AfSpeedNormal].read(ss["normal"]);
else
LOG(RPiAf, Warning) << "Missing speed \"normal\"";
speeds[AfSpeedFast] = speeds[AfSpeedNormal];
if (ss.contains("fast"))
speeds[AfSpeedFast].read(ss["fast"]);
} else
LOG(RPiAf, Warning) << "No speeds defined";
readNumber<uint32_t>(confEpsilon, params, "conf_epsilon");
readNumber<uint32_t>(confThresh, params, "conf_thresh");
readNumber<uint32_t>(confClip, params, "conf_clip");
readNumber<uint32_t>(skipFrames, params, "skip_frames");
readNumber<bool>(checkForIR, params, "check_for_ir");
if (params.contains("map"))
map = params["map"].get<ipa::Pwl>(ipa::Pwl{});
else
LOG(RPiAf, Warning) << "No map defined";
return 0;
}
void Af::CfgParams::initialise()
{
if (map.empty()) {
/* Default mapping from dioptres to hardware setting */
static constexpr double DefaultMapX0 = 0.0;
static constexpr double DefaultMapY0 = 445.0;
static constexpr double DefaultMapX1 = 15.0;
static constexpr double DefaultMapY1 = 925.0;
map.append(DefaultMapX0, DefaultMapY0);
map.append(DefaultMapX1, DefaultMapY1);
}
}
/* Af Algorithm class */
static constexpr unsigned MaxWindows = 10;
Af::Af(Controller *controller)
: AfAlgorithm(controller),
cfg_(),
range_(AfRangeNormal),
speed_(AfSpeedNormal),
mode_(AfAlgorithm::AfModeManual),
pauseFlag_(false),
statsRegion_(0, 0, 0, 0),
windows_(),
useWindows_(false),
phaseWeights_(),
contrastWeights_(),
awbWeights_(),
scanState_(ScanState::Idle),
initted_(false),
irFlag_(false),
ftarget_(-1.0),
fsmooth_(-1.0),
prevContrast_(0.0),
oldSceneContrast_(0.0),
prevAverage_{ 0.0, 0.0, 0.0 },
oldSceneAverage_{ 0.0, 0.0, 0.0 },
prevPhase_(0.0),
skipCount_(0),
stepCount_(0),
dropCount_(0),
sameSignCount_(0),
sceneChangeCount_(0),
scanMaxContrast_(0.0),
scanMinContrast_(1.0e9),
scanStep_(0.0),
scanData_(),
reportState_(AfState::Idle)
{
/*
* Reserve space for data structures, to reduce memory fragmentation.
* It's too early to query the size of the PDAF sensor data, so guess.
*/
windows_.reserve(1);
phaseWeights_.w.reserve(16 * 12);
contrastWeights_.w.reserve(getHardwareConfig().focusRegions.width *
getHardwareConfig().focusRegions.height);
contrastWeights_.w.reserve(getHardwareConfig().awbRegions.width *
getHardwareConfig().awbRegions.height);
scanData_.reserve(32);
}
Af::~Af()
{
}
char const *Af::name() const
{
return NAME;
}
int Af::read(const libcamera::YamlObject &params)
{
return cfg_.read(params);
}
void Af::initialise()
{
cfg_.initialise();
}
void Af::switchMode(CameraMode const &cameraMode, [[maybe_unused]] Metadata *metadata)
{
(void)metadata;
/* Assume that PDAF and Focus stats grids cover the visible area */
statsRegion_.x = (int)cameraMode.cropX;
statsRegion_.y = (int)cameraMode.cropY;
statsRegion_.width = (unsigned)(cameraMode.width * cameraMode.scaleX);
statsRegion_.height = (unsigned)(cameraMode.height * cameraMode.scaleY);
LOG(RPiAf, Debug) << "switchMode: statsRegion: "
<< statsRegion_.x << ','
<< statsRegion_.y << ','
<< statsRegion_.width << ','
<< statsRegion_.height;
invalidateWeights();
if (scanState_ >= ScanState::Coarse1 && scanState_ < ScanState::Settle) {
/*
* If a scan was in progress, re-start it, as CDAF statistics
* may have changed. Though if the application is just about
* to take a still picture, this will not help...
*/
startProgrammedScan();
updateLensPosition();
}
skipCount_ = cfg_.skipFrames;
}
void Af::computeWeights(RegionWeights *wgts, unsigned rows, unsigned cols)
{
wgts->rows = rows;
wgts->cols = cols;
wgts->sum = 0;
wgts->w.resize(rows * cols);
std::fill(wgts->w.begin(), wgts->w.end(), 0);
if (rows > 0 && cols > 0 && useWindows_ &&
statsRegion_.height >= rows && statsRegion_.width >= cols) {
/*
* Here we just merge all of the given windows, weighted by area.
* \todo Perhaps a better approach might be to find the phase in each
* window and choose either the closest or the highest-confidence one?
* Ensure weights sum to less than (1<<16). 46080 is a "round number"
* below 65536, for better rounding when window size is a simple
* fraction of image dimensions.
*/
const unsigned maxCellWeight = 46080u / (MaxWindows * rows * cols);
const unsigned cellH = statsRegion_.height / rows;
const unsigned cellW = statsRegion_.width / cols;
const unsigned cellA = cellH * cellW;
for (auto &w : windows_) {
for (unsigned r = 0; r < rows; ++r) {
int y0 = std::max(statsRegion_.y + (int)(cellH * r), w.y);
int y1 = std::min(statsRegion_.y + (int)(cellH * (r + 1)),
w.y + (int)(w.height));
if (y0 >= y1)
continue;
y1 -= y0;
for (unsigned c = 0; c < cols; ++c) {
int x0 = std::max(statsRegion_.x + (int)(cellW * c), w.x);
int x1 = std::min(statsRegion_.x + (int)(cellW * (c + 1)),
w.x + (int)(w.width));
if (x0 >= x1)
continue;
unsigned a = y1 * (x1 - x0);
a = (maxCellWeight * a + cellA - 1) / cellA;
wgts->w[r * cols + c] += a;
wgts->sum += a;
}
}
}
}
if (wgts->sum == 0) {
/* Default AF window is the middle 1/2 width of the middle 1/3 height */
for (unsigned r = rows / 3; r < rows - rows / 3; ++r) {
for (unsigned c = cols / 4; c < cols - cols / 4; ++c) {
wgts->w[r * cols + c] = 1;
wgts->sum += 1;
}
}
}
}
void Af::invalidateWeights()
{
phaseWeights_.sum = 0;
contrastWeights_.sum = 0;
awbWeights_.sum = 0;
}
bool Af::getPhase(PdafRegions const &regions, double &phase, double &conf)
{
libcamera::Size size = regions.size();
if (size.height != phaseWeights_.rows || size.width != phaseWeights_.cols ||
phaseWeights_.sum == 0) {
LOG(RPiAf, Debug) << "Recompute Phase weights " << size.width << 'x' << size.height;
computeWeights(&phaseWeights_, size.height, size.width);
}
uint32_t sumWc = 0;
int64_t sumWcp = 0;
for (unsigned i = 0; i < regions.numRegions(); ++i) {
unsigned w = phaseWeights_.w[i];
if (w) {
const PdafData &data = regions.get(i).val;
unsigned c = data.conf;
if (c >= cfg_.confThresh) {
if (c > cfg_.confClip)
c = cfg_.confClip;
c -= (cfg_.confThresh >> 1);
sumWc += w * c;
sumWcp += (int64_t)(w * c) * (int64_t)data.phase;
}
}
}
if (0 < phaseWeights_.sum && phaseWeights_.sum <= sumWc) {
phase = (double)sumWcp / (double)sumWc;
conf = (double)sumWc / (double)phaseWeights_.sum;
return true;
} else {
phase = 0.0;
conf = 0.0;
return false;
}
}
double Af::getContrast(const FocusRegions &focusStats)
{
libcamera::Size size = focusStats.size();
if (size.height != contrastWeights_.rows ||
size.width != contrastWeights_.cols || contrastWeights_.sum == 0) {
LOG(RPiAf, Debug) << "Recompute Contrast weights "
<< size.width << 'x' << size.height;
computeWeights(&contrastWeights_, size.height, size.width);
}
uint64_t sumWc = 0;
for (unsigned i = 0; i < focusStats.numRegions(); ++i)
sumWc += contrastWeights_.w[i] * focusStats.get(i).val;
return (contrastWeights_.sum > 0) ? ((double)sumWc / (double)contrastWeights_.sum) : 0.0;
}
/*
* Get the average R, G, B values in AF window[s] (from AWB statistics).
* Optionally, check if all of {R,G,B} are within 4:5 of each other
* across more than 50% of the counted area and within the AF window:
* for an RGB sensor this strongly suggests that IR lighting is in use.
*/
bool Af::getAverageAndTestIr(const RgbyRegions &awbStats, double rgb[3])
{
libcamera::Size size = awbStats.size();
if (size.height != awbWeights_.rows ||
size.width != awbWeights_.cols || awbWeights_.sum == 0) {
LOG(RPiAf, Debug) << "Recompute RGB weights " << size.width << 'x' << size.height;
computeWeights(&awbWeights_, size.height, size.width);
}
uint64_t sr = 0, sg = 0, sb = 0, sw = 1;
uint64_t greyCount = 0, allCount = 0;
for (unsigned i = 0; i < awbStats.numRegions(); ++i) {
uint64_t r = awbStats.get(i).val.rSum;
uint64_t g = awbStats.get(i).val.gSum;
uint64_t b = awbStats.get(i).val.bSum;
uint64_t w = awbWeights_.w[i];
if (w) {
sw += w;
sr += w * r;
sg += w * g;
sb += w * b;
}
if (cfg_.checkForIR) {
if (4 * r < 5 * b && 4 * b < 5 * r &&
4 * r < 5 * g && 4 * g < 5 * r &&
4 * b < 5 * g && 4 * g < 5 * b)
greyCount += awbStats.get(i).counted;
allCount += awbStats.get(i).counted;
}
}
rgb[0] = sr / (double)sw;
rgb[1] = sg / (double)sw;
rgb[2] = sb / (double)sw;
return (cfg_.checkForIR && 2 * greyCount > allCount &&
4 * sr < 5 * sb && 4 * sb < 5 * sr &&
4 * sr < 5 * sg && 4 * sg < 5 * sr &&
4 * sb < 5 * sg && 4 * sg < 5 * sb);
}
void Af::doPDAF(double phase, double conf)
{
/* Apply loop gain */
phase *= cfg_.speeds[speed_].pdafGain;
if (mode_ == AfModeContinuous) {
/*
* PDAF in Continuous mode. Scale down lens movement when
* delta is small or confidence is low, to suppress wobble.
*/
phase *= conf / (conf + cfg_.confEpsilon);
if (std::abs(phase) < cfg_.speeds[speed_].pdafSquelch) {
double a = phase / cfg_.speeds[speed_].pdafSquelch;
phase *= a * a;
}
} else {
/*
* PDAF in triggered-auto mode. Allow early termination when
* phase delta is small; scale down lens movements towards
* the end of the sequence, to ensure a stable image.
*/
if (stepCount_ >= cfg_.speeds[speed_].stepFrames) {
if (std::abs(phase) < cfg_.speeds[speed_].pdafSquelch)
stepCount_ = cfg_.speeds[speed_].stepFrames;
} else
phase *= stepCount_ / cfg_.speeds[speed_].stepFrames;
}
/* Apply slew rate limit. Report failure if out of bounds. */
if (phase < -cfg_.speeds[speed_].maxSlew) {
phase = -cfg_.speeds[speed_].maxSlew;
reportState_ = (ftarget_ <= cfg_.ranges[range_].focusMin) ? AfState::Failed
: AfState::Scanning;
} else if (phase > cfg_.speeds[speed_].maxSlew) {
phase = cfg_.speeds[speed_].maxSlew;
reportState_ = (ftarget_ >= cfg_.ranges[range_].focusMax) ? AfState::Failed
: AfState::Scanning;
} else
reportState_ = AfState::Focused;
ftarget_ = fsmooth_ + phase;
}
bool Af::earlyTerminationByPhase(double phase)
{
if (scanData_.size() > 0 &&
scanData_[scanData_.size() - 1].conf >= cfg_.confThresh) {
double oldFocus = scanData_[scanData_.size() - 1].focus;
double oldPhase = scanData_[scanData_.size() - 1].phase;
/*
* Check that the gradient is finite and has the expected sign;
* Interpolate/extrapolate the lens position for zero phase.
* Check that the extrapolation is well-conditioned.
*/
if ((ftarget_ - oldFocus) * (phase - oldPhase) * cfg_.speeds[speed_].pdafGain < 0.0) {
double param = phase / (phase - oldPhase);
if ((-2.5 <= param || mode_ == AfModeContinuous) && param <= 3.0) {
LOG(RPiAf, Debug) << "ETBP: param=" << param;
param = std::max(param, -2.5);
ftarget_ += param * (oldFocus - ftarget_);
return true;
}
}
}
return false;
}
double Af::findPeak(unsigned i) const
{
double f = scanData_[i].focus;
if (scanData_.size() >= 3) {
/*
* Given the sample with the highest contrast score and its two
* neighbours either side (or same side if at the end of a scan),
* solve for the best lens position by fitting a parabola.
* Adapted from awb.cpp: interpolateQaudaratic()
*/
if (i == 0)
i++;
else if (i + 1 >= scanData_.size())
i--;
double abx = scanData_[i - 1].focus - scanData_[i].focus;
double aby = scanData_[i - 1].contrast - scanData_[i].contrast;
double cbx = scanData_[i + 1].focus - scanData_[i].focus;
double cby = scanData_[i + 1].contrast - scanData_[i].contrast;
double denom = 2.0 * (aby * cbx - cby * abx);
if (std::abs(denom) >= (1.0 / 64.0) && denom * abx > 0.0) {
f = (aby * cbx * cbx - cby * abx * abx) / denom;
f = std::clamp(f, std::min(abx, cbx), std::max(abx, cbx));
f += scanData_[i].focus;
}
}
LOG(RPiAf, Debug) << "FindPeak: " << f;
return f;
}
void Af::doScan(double contrast, double phase, double conf)
{
/* Record lens position, contrast and phase values for the current scan */
if (scanData_.empty() || contrast > scanMaxContrast_) {
scanMaxContrast_ = contrast;
scanMaxIndex_ = scanData_.size();
if (scanState_ != ScanState::Fine)
std::copy(prevAverage_, prevAverage_ + 3, oldSceneAverage_);
}
if (contrast < scanMinContrast_)
scanMinContrast_ = contrast;
scanData_.emplace_back(ScanRecord{ ftarget_, contrast, phase, conf });
if ((scanStep_ >= 0.0 && ftarget_ >= cfg_.ranges[range_].focusMax) ||
(scanStep_ <= 0.0 && ftarget_ <= cfg_.ranges[range_].focusMin) ||
(scanState_ == ScanState::Fine && scanData_.size() >= 3) ||
contrast < cfg_.speeds[speed_].contrastRatio * scanMaxContrast_) {
double pk = findPeak(scanMaxIndex_);
/*
* Finished a scan, by hitting a limit or due to constrast dropping off.
* If this is a first coarse scan and we didn't bracket the peak, reverse!
* If this is a fine scan, or no fine step was defined, we've finished.
* Otherwise, start fine scan in opposite direction.
*/
if (scanState_ == ScanState::Coarse1 &&
scanData_[0].contrast >= cfg_.speeds[speed_].contrastRatio * scanMaxContrast_) {
scanStep_ = -scanStep_;
scanState_ = ScanState::Coarse2;
} else if (scanState_ == ScanState::Fine || cfg_.speeds[speed_].stepFine <= 0.0) {
ftarget_ = pk;
scanState_ = ScanState::Settle;
} else if (scanState_ == ScanState::Coarse1 &&
scanData_[0].contrast >= cfg_.speeds[speed_].contrastRatio * scanMaxContrast_) {
scanStep_ = -scanStep_;
scanState_ = ScanState::Coarse2;
} else if (scanStep_ >= 0.0) {
ftarget_ = std::min(pk + cfg_.speeds[speed_].stepFine,
cfg_.ranges[range_].focusMax);
scanStep_ = -cfg_.speeds[speed_].stepFine;
scanState_ = ScanState::Fine;
} else {
ftarget_ = std::max(pk - cfg_.speeds[speed_].stepFine,
cfg_.ranges[range_].focusMin);
scanStep_ = cfg_.speeds[speed_].stepFine;
scanState_ = ScanState::Fine;
}
scanData_.clear();
} else
ftarget_ += scanStep_;
stepCount_ = (ftarget_ == fsmooth_) ? 0 : cfg_.speeds[speed_].stepFrames;
}
void Af::doAF(double contrast, double phase, double conf)
{
/* Skip frames at startup and after sensor mode change */
if (skipCount_ > 0) {
LOG(RPiAf, Debug) << "SKIP";
skipCount_--;
return;
}
/* Count frames for which PDAF phase has had same sign */
if (phase * prevPhase_ <= 0.0)
sameSignCount_ = 0;
else
sameSignCount_++;
prevPhase_ = phase;
if (mode_ == AfModeManual)
return; /* nothing to do */
if (scanState_ == ScanState::Pdaf) {
/*
* Use PDAF closed-loop control whenever available, in both CAF
* mode and (for a limited number of iterations) when triggered.
* If PDAF fails (due to poor contrast, noise or large defocus)
* for at least dropoutFrames, fall back to a CDAF-based scan
* immediately (in triggered-auto) or on scene change (in CAF).
*/
if (conf >= cfg_.confEpsilon) {
if (mode_ == AfModeAuto || sameSignCount_ >= 3)
doPDAF(phase, conf);
if (stepCount_ > 0)
stepCount_--;
else if (mode_ != AfModeContinuous)
scanState_ = ScanState::Idle;
oldSceneContrast_ = contrast;
std::copy(prevAverage_, prevAverage_ + 3, oldSceneAverage_);
sceneChangeCount_ = 0;
dropCount_ = 0;
return;
} else {
dropCount_++;
if (dropCount_ < cfg_.speeds[speed_].dropoutFrames)
return;
if (mode_ != AfModeContinuous) {
startProgrammedScan();
return;
}
/* else fall through to waiting for a scene change */
}
}
if (scanState_ < ScanState::Coarse1 && mode_ == AfModeContinuous) {
/*
* In CAF mode, not in a scan, and PDAF is unavailable.
* Wait for a scene change, followed by stability.
*/
if (contrast + 1.0 < cfg_.speeds[speed_].retriggerRatio * oldSceneContrast_ ||
oldSceneContrast_ + 1.0 < cfg_.speeds[speed_].retriggerRatio * contrast ||
prevAverage_[0] + 1.0 < cfg_.speeds[speed_].retriggerRatio * oldSceneAverage_[0] ||
oldSceneAverage_[0] + 1.0 < cfg_.speeds[speed_].retriggerRatio * prevAverage_[0] ||
prevAverage_[1] + 1.0 < cfg_.speeds[speed_].retriggerRatio * oldSceneAverage_[1] ||
oldSceneAverage_[1] + 1.0 < cfg_.speeds[speed_].retriggerRatio * prevAverage_[1] ||
prevAverage_[2] + 1.0 < cfg_.speeds[speed_].retriggerRatio * oldSceneAverage_[2] ||
oldSceneAverage_[2] + 1.0 < cfg_.speeds[speed_].retriggerRatio * prevAverage_[2]) {
oldSceneContrast_ = contrast;
std::copy(prevAverage_, prevAverage_ + 3, oldSceneAverage_);
sceneChangeCount_ = 1;
} else if (sceneChangeCount_)
sceneChangeCount_++;
if (sceneChangeCount_ >= cfg_.speeds[speed_].retriggerDelay)
startProgrammedScan();
} else if (scanState_ >= ScanState::Coarse1 && fsmooth_ == ftarget_) {
/*
* CDAF-based scanning sequence.
* Allow a delay between steps for CDAF FoM statistics to be
* updated, and a "settling time" at the end of the sequence.
* [A coarse or fine scan can be abandoned if two PDAF samples
* allow direct interpolation of the zero-phase lens position.]
*/
if (stepCount_ > 0)
stepCount_--;
else if (scanState_ == ScanState::Settle) {
if (prevContrast_ >= cfg_.speeds[speed_].contrastRatio * scanMaxContrast_ &&
scanMinContrast_ <= cfg_.speeds[speed_].contrastRatio * scanMaxContrast_)
reportState_ = AfState::Focused;
else
reportState_ = AfState::Failed;
if (mode_ == AfModeContinuous && !pauseFlag_ &&
cfg_.speeds[speed_].dropoutFrames > 0)
scanState_ = ScanState::Pdaf;
else
scanState_ = ScanState::Idle;
dropCount_ = 0;
sceneChangeCount_ = 0;
oldSceneContrast_ = std::max(scanMaxContrast_, prevContrast_);
scanData_.clear();
} else if (conf >= cfg_.confThresh && earlyTerminationByPhase(phase)) {
std::copy(prevAverage_, prevAverage_ + 3, oldSceneAverage_);
scanState_ = ScanState::Settle;
stepCount_ = (mode_ == AfModeContinuous) ? 0 : cfg_.speeds[speed_].stepFrames;
} else
doScan(contrast, phase, conf);
}
}
void Af::updateLensPosition()
{
if (scanState_ >= ScanState::Pdaf) {
ftarget_ = std::clamp(ftarget_,
cfg_.ranges[range_].focusMin,
cfg_.ranges[range_].focusMax);
}
if (initted_) {
/* from a known lens position: apply slew rate limit */
fsmooth_ = std::clamp(ftarget_,
fsmooth_ - cfg_.speeds[speed_].maxSlew,
fsmooth_ + cfg_.speeds[speed_].maxSlew);
} else {
/* from an unknown position: go straight to target, but add delay */
fsmooth_ = ftarget_;
initted_ = true;
skipCount_ = cfg_.skipFrames;
}
}
void Af::startAF()
{
/* Use PDAF if the tuning file allows it; else CDAF. */
if (cfg_.speeds[speed_].pdafGain != 0.0 &&
cfg_.speeds[speed_].dropoutFrames > 0 &&
(mode_ == AfModeContinuous || cfg_.speeds[speed_].pdafFrames > 0)) {
if (!initted_) {
ftarget_ = cfg_.ranges[range_].focusDefault;
updateLensPosition();
}
stepCount_ = (mode_ == AfModeContinuous) ? 0 : cfg_.speeds[speed_].pdafFrames;
scanState_ = ScanState::Pdaf;
scanData_.clear();
dropCount_ = 0;
oldSceneContrast_ = 0.0;
sceneChangeCount_ = 0;
reportState_ = AfState::Scanning;
} else {
startProgrammedScan();
updateLensPosition();
}
}
void Af::startProgrammedScan()
{
if (!initted_ || mode_ != AfModeContinuous ||
fsmooth_ <= cfg_.ranges[range_].focusMin + 2.0 * cfg_.speeds[speed_].stepCoarse) {
ftarget_ = cfg_.ranges[range_].focusMin;
scanStep_ = cfg_.speeds[speed_].stepCoarse;
scanState_ = ScanState::Coarse2;
} else if (fsmooth_ >= cfg_.ranges[range_].focusMax - 2.0 * cfg_.speeds[speed_].stepCoarse) {
ftarget_ = cfg_.ranges[range_].focusMax;
scanStep_ = -cfg_.speeds[speed_].stepCoarse;
scanState_ = ScanState::Coarse2;
} else {
scanStep_ = -cfg_.speeds[speed_].stepCoarse;
scanState_ = ScanState::Coarse1;
}
scanMaxContrast_ = 0.0;
scanMinContrast_ = 1.0e9;
scanMaxIndex_ = 0;
scanData_.clear();
stepCount_ = cfg_.speeds[speed_].stepFrames;
reportState_ = AfState::Scanning;
}
void Af::goIdle()
{
scanState_ = ScanState::Idle;
reportState_ = AfState::Idle;
scanData_.clear();
}
/*
* PDAF phase data are available in prepare(), but CDAF statistics are not
* available until process(). We are gambling on the availability of PDAF.
* To expedite feedback control using PDAF, issue the V4L2 lens control from
* prepare(). Conversely, during scans, we must allow an extra frame delay
* between steps, to retrieve CDAF statistics from the previous process()
* so we can terminate the scan early without having to change our minds.
*/
void Af::prepare(Metadata *imageMetadata)
{
/* Initialize for triggered scan or start of CAF mode */
if (scanState_ == ScanState::Trigger)
startAF();
if (initted_) {
/* Get PDAF from the embedded metadata, and run AF algorithm core */
PdafRegions regions;
double phase = 0.0, conf = 0.0;
double oldFt = ftarget_;
double oldFs = fsmooth_;
ScanState oldSs = scanState_;
uint32_t oldSt = stepCount_;
if (imageMetadata->get("pdaf.regions", regions) == 0)
getPhase(regions, phase, conf);
doAF(prevContrast_, phase, irFlag_ ? 0 : conf);
updateLensPosition();
LOG(RPiAf, Debug) << std::fixed << std::setprecision(2)
<< static_cast<unsigned int>(reportState_)
<< " sst" << static_cast<unsigned int>(oldSs)
<< "->" << static_cast<unsigned int>(scanState_)
<< " stp" << oldSt << "->" << stepCount_
<< " ft" << oldFt << "->" << ftarget_
<< " fs" << oldFs << "->" << fsmooth_
<< " cont=" << (int)prevContrast_
<< " phase=" << (int)phase << " conf=" << (int)conf
<< (irFlag_ ? " IR" : "");
}
/* Report status and produce new lens setting */
AfStatus status;
if (pauseFlag_)
status.pauseState = (scanState_ == ScanState::Idle) ? AfPauseState::Paused
: AfPauseState::Pausing;
else
status.pauseState = AfPauseState::Running;
if (mode_ == AfModeAuto && scanState_ != ScanState::Idle)
status.state = AfState::Scanning;
else if (mode_ == AfModeManual)
status.state = AfState::Idle;
else
status.state = reportState_;
status.lensSetting = initted_ ? std::optional<int>(cfg_.map.eval(fsmooth_))
: std::nullopt;
imageMetadata->set("af.status", status);
}
void Af::process(StatisticsPtr &stats, [[maybe_unused]] Metadata *imageMetadata)
{
(void)imageMetadata;
prevContrast_ = getContrast(stats->focusRegions);
irFlag_ = getAverageAndTestIr(stats->awbRegions, prevAverage_);
}
/* Controls */
void Af::setRange(AfRange r)
{
LOG(RPiAf, Debug) << "setRange: " << (unsigned)r;
if (r < AfAlgorithm::AfRangeMax)
range_ = r;
}
void Af::setSpeed(AfSpeed s)
{
LOG(RPiAf, Debug) << "setSpeed: " << (unsigned)s;
if (s < AfAlgorithm::AfSpeedMax) {
if (scanState_ == ScanState::Pdaf &&
cfg_.speeds[s].pdafFrames > cfg_.speeds[speed_].pdafFrames)
stepCount_ += cfg_.speeds[s].pdafFrames - cfg_.speeds[speed_].pdafFrames;
speed_ = s;
}
}
void Af::setMetering(bool mode)
{
if (useWindows_ != mode) {
useWindows_ = mode;
invalidateWeights();
}
}
void Af::setWindows(libcamera::Span<libcamera::Rectangle const> const &wins)
{
windows_.clear();
for (auto &w : wins) {
LOG(RPiAf, Debug) << "Window: "
<< w.x << ", "
<< w.y << ", "
<< w.width << ", "
<< w.height;
windows_.push_back(w);
if (windows_.size() >= MaxWindows)
break;
}
if (useWindows_)
invalidateWeights();
}
double Af::getDefaultLensPosition() const
{
return cfg_.ranges[AfRangeNormal].focusDefault;
}
void Af::getLensLimits(double &min, double &max) const
{
/* Limits for manual focus are set by map, not by ranges */
min = cfg_.map.domain().start;
max = cfg_.map.domain().end;
}
bool Af::setLensPosition(double dioptres, int *hwpos, bool force)
{
bool changed = false;
if (mode_ == AfModeManual || force) {
LOG(RPiAf, Debug) << "setLensPosition: " << dioptres;
ftarget_ = cfg_.map.domain().clamp(dioptres);
changed = !(initted_ && fsmooth_ == ftarget_);
updateLensPosition();
}
if (hwpos)
*hwpos = cfg_.map.eval(fsmooth_);
return changed;
}
std::optional<double> Af::getLensPosition() const
{
/*
* \todo We ought to perform some precise timing here to determine
* the current lens position.
*/
return initted_ ? std::optional<double>(fsmooth_) : std::nullopt;
}
void Af::cancelScan()
{
LOG(RPiAf, Debug) << "cancelScan";
if (mode_ == AfModeAuto)
goIdle();
}
void Af::triggerScan()
{
LOG(RPiAf, Debug) << "triggerScan";
if (mode_ == AfModeAuto && scanState_ == ScanState::Idle)
scanState_ = ScanState::Trigger;
}
void Af::setMode(AfAlgorithm::AfMode mode)
{
LOG(RPiAf, Debug) << "setMode: " << (unsigned)mode;
if (mode_ != mode) {
mode_ = mode;
pauseFlag_ = false;
if (mode == AfModeContinuous)
scanState_ = ScanState::Trigger;
else if (mode != AfModeAuto || scanState_ < ScanState::Coarse1)
goIdle();
}
}
AfAlgorithm::AfMode Af::getMode() const
{
return mode_;
}
void Af::pause(AfAlgorithm::AfPause pause)
{
LOG(RPiAf, Debug) << "pause: " << (unsigned)pause;
if (mode_ == AfModeContinuous) {
if (pause == AfPauseResume && pauseFlag_) {
pauseFlag_ = false;
if (scanState_ < ScanState::Coarse1)
scanState_ = ScanState::Trigger;
} else if (pause != AfPauseResume && !pauseFlag_) {
pauseFlag_ = true;
if (pause == AfPauseImmediate || scanState_ < ScanState::Coarse1) {
scanState_ = ScanState::Idle;
scanData_.clear();
}
}
}
}
// Register algorithm with the system.
static Algorithm *create(Controller *controller)
{
return (Algorithm *)new Af(controller);
}
static RegisterAlgorithm reg(NAME, &create);
# S3/S5 新对焦架构设计
> **pdaf lib 定义**:接受 PD 和 FV 数据流,根据对焦模式和对焦框配置,决策并执行推镜头操作。
>
> 核心三段式:**输入 → 决策 → 执行**
## 一、模块总览
```
AFManager (总调度)
┌─────────────────────────┼──────────────────────────────┐
│ 输入 │ 决策 执行 │
│ │ │
│ ProcessModule Window Policy StateMachine│
│ ┌──────────────┐ Manager Module Module │
│ │ PdProcessor │ (目标选择) (触发决策) (状态管理) │
│ │ FvProcessor │ │ │ │ │
│ │ TofProcessor │ │ └──────┬─────┘ │
│ │ (预留) │ │ ▼ │
│ └──────┬───────┘ │ ActuateModule │
│ ▼ ─────┘ ┌───────┴────────┐ │
│ 统一 12×6 zone PdafActuator CdafActuator│
│ PD + FV 对齐 (主路径) (降级路径) │
│ │ │
│ DccCalculator → LensMotor │
└────────────────────────────────────────────────────────┘
```
## 二、各模块职责
### 1. AFManager — 总调度
- 初始化/销毁所有子模块
- 模式管理:CAF / AFS / MF / FACE_TRACKING
- 转发数据:PD 帧 → PdProcessor,视频帧 → FvProcessor
- 对外接口:startFocus / setMode / setZoom 等
### 2. ProcessModule — 数据采集与处理
统一的数据处理模块,包含多个 Processor:
```
ProcessModule
├── PdProcessor — PD 数据处理
├── FvProcessor — FV 数据处理
└── TofProcessor — ToF 深度数据(预留)
```
所有 Processor 统一输出 12×6 zone 数据,下游不感知数据来源差异。
#### PdProcessor — PD 数据处理
- 接收 PD raw 帧(原始分辨率约 1920×540)
- **双轨算法,统一输出**
```
ZnccProcessor (传统PD) ──┐
├──→ PdZoneMap (12×6)
AipdProcessor (AI PD) ──┘ 每个 zone (~160×90): { pd, conf }
```
- 两个算法实现同一接口,下游不感知差异
- AIPD 不成熟阶段用 zncc 兜底,成熟后无缝替换
- 滤波(柯西、时域)在统一输出之后做
- **待定**:zncc 和 aipd 是否长期保留两套
#### FvProcessor — FV 数据处理
- ISP AF stats 配置为 **12×6 zone,与 PdZoneMap 完全对齐**
- ISP 接口:设置 zone 的 w/h(单 zone 尺寸)和 col/row(列行数)
- w/h 有对齐限制(偶数或 8 的倍数),col/row 无限制
- 3840×2160 时:w=320, h=360, col=12, row=6 ✅
- zoom 裁剪后需重新计算 w/h 并确保对齐
- 配置入口在 videomodule
- 同一 zone index 同时取 PD 和 FV,**零坐标映射**
- 预留 **SharpnessAssessor** 接口(Focus Peaking,与 ContrastAssessor 并列)
#### TofProcessor — ToF 深度数据(预留)
- 未来接入 ToF 传感器时,输出统一的 zone 深度数据
#### 坐标系与 zoom 处理
三个坐标系:
```
┌─────────────────────────────────┐
│ Sensor 全图坐标系 │ ← PD raw 始终基于此(不随 zoom 变化)
│ 3840 × 2160 │
│ │
│ ┌───────────────────┐ │
│ │ 可视区域坐标系 │ │ ← zoom 裁剪后的区域
│ │ (VPSS 输出) │ │ 人脸框、用户触摸点等基于此坐标系
│ │ │ │
│ │ AF stats 12×6 │ │ ← ISP 对可视区域重新配置 zone(暂定)
│ │ zone 覆盖此区域 │ │
│ └───────────────────┘ │
│ │
└─────────────────────────────────┘
```
**坐标转换链路**
```
VPSS 坐标(用户看到的画面)
↕ 偏移 + 缩放(zoom crop 参数)
Sensor 全图坐标
↕ zone 网格映射
Zone index (row, col)
→ 取 PD(需从全图 PD 中定位对应区域)
→ 取 FV(直接按 zone index,AF stats 已对齐可视区域)
```
**zoom 时的处理**
| 数据 | zoom 影响 | 处理方式 |
|------|----------|---------|
| AF stats (FV) | 重新配置到可视区域,zone 数量不变(12×6),单 zone 覆盖面积变小 | ISP 重配 w/h,保持 col=12, row=6 |
| PD raw | 始终全图,不随 zoom 变化 | 需要将可视区域映射到全图,裁剪出对应 PD 区域计算(备选:直接裁剪 PD raw) |
| VPSS 坐标(人脸框等) | 基于可视区域 | WindowManager 负责转换到 zone index |
> **注意**:zoom 后单 zone 内容变少,PD 计算精度可能受影响,需实测验证。
### 3. WindowManager — zone 管理 & 对焦目标选择
- 统一 zone 体系:PD 和 FV 共用 12×6 网格
- zone ↔ 图像坐标转换(处理 zoom/EPTZ)
- **按模式选择对焦目标**,最终输出选中的 zone index 列表:
| 模式 | 选择策略 |
|------|---------|
| 人脸模式 | AI 人脸框 → 映射到 zone |
| 跟踪模式 | 跟踪目标框 → 映射到 zone |
| 手动选区 | 用户指定区域 → 映射到 zone |
| 中心点对焦 | 直接取中心 zone |
| 大区域自动 | 组合加权(conf × 距离 × 中心权重)选 zone |
- 下游模块只看选中 zone 的 PD/FV,不感知选择策略
### 4. PolicyModule — 触发决策(四层门控)
30Hz 轮询,分层判定:
```
第一层:场景变化检测(粗筛,大部分帧在此过滤)
│ FV 大幅下降?
│ PD 偏移变大?
│ → 任一满足 → sceneChangeCount++
│ → 都不满足 → 不触发
│ 对焦抑制信号(暂停判定,等稳定后再继续):
│ AE 曝光剧烈调整中?
│ AWB 颜色剧烈变化中?
│ SAD 帧差过大(画面剧烈运动)?
第二层:稳定性确认(防抖)
│ sceneChangeCount >= N?
│ → 没稳 → 继续等
│ → 稳了 → 进入第三层
第三层:PD 分析(精确判定)
│ PD conf > 阈值?PD 偏移 > DOF 阈值?方向连续一致?
│ → 是 → 触发 PDAF
│ → PD 无效 → 触发 CDAF
│ → near 端卡死 + PD 无效 → 强制往 far 方向
第四层:合焦门控(防重复触发)— 动态 dead zone
上次对焦成功 && FV 在基准附近 && |PD| < dead_zone
→ 满足 → 不触发
```
**动态 dead zone**(合焦容忍区,非固定值):
```
dead_zone = base_zone / (confidence × texture_strength)
高置信 + 强纹理 → dead zone 小 → 精度要求高,小偏差也触发
低置信 + 弱纹理 → dead zone 大 → 容忍更大偏差,避免在不确定时反复触发
```
> 参考:手机 PDAF 通用做法,dead zone 与 confidence / texture 动态关联。
**开发原则:每个信号(PD/FV/AWB/AE 等)先单独测试验证,确认可靠后再组合。**
### 5. StateMachineModule — 状态管理
**状态转换**
```
IDLE → FOCUSING → TRACKING → IDLE
↑ │
└────────┘ (PD 大幅变化 → 重新对焦)
```
| 状态 | 含义 | 进入条件 | 退出条件 |
|------|------|----------|----------|
| IDLE | 静默监测 | TRACKING 稳定 N 帧 / 初始 | PolicyModule 触发 |
| FOCUSING | 完整对焦流程 (Phase 1/2/3) | PolicyModule 触发 / TRACKING 中 PD 大幅变化 | 对焦完成 |
| TRACKING | 小步跟随,持续监测 PD | FOCUSING 完成后(CAF 模式) | PD 稳定 → IDLE / PD 大变 → FOCUSING |
**TRACKING 状态详细设计**(针对移动目标场景):
- 对焦完成后不回 IDLE,进入 TRACKING 持续监测 PD
- **PD 小幅变化**(|PD| < 阈值1)→ 小步跟随:`deltaPulse = pd × dcc`,直接推镜头,不走完整 Phase 1/2/3
- **PD 大幅变化**(|PD| > 阈值2)→ 重新进入 FOCUSING,走完整对焦流程
- **PD 稳定 N 帧**(连续 N 帧 |PD| 很小)→ 目标静止,退出 TRACKING 回 IDLE
- 跟随频率:每帧或每 2 帧判定一次(不需要等多帧确认,响应要快)
- AFS 模式下对焦完成直接回 IDLE,不进 TRACKING
**其他**
- 新触发可打断当前 FOCUSING
- 管理对焦结果回调
### 6. ActuateModule — 对焦执行
- **PdafActuator**:PD 粗跳 + FV 精调(主路径)
- **CdafActuator**:PD 失效时 FV 双向爬峰(降级路径)
- **DccCalculator**:PD × DCC 分段系数 → deltaPulse,含温度/重力补偿
- **LensMotor**:电机驱动,pulse 控制
---
## 三、可用输入数据
### 第一层:直接可用
| 数据 | 来源 | 用途 |
|------|------|------|
| PdZoneMap (12×6) | ProcessModule | 方向+距离估计,粗跳 |
| FV (12×6 对齐) | FvProcessor | 爬峰、精调、合焦验证 |
| 当前 pulse | LensMotor | 步长规划、DCC 分段 |
| zoom/EPTZ | videomodule | 坐标映射、ROI 调整 |
### 第二层:ISP 已有,当前未用
| 数据 | 用途 |
|------|------|
| AE 统计(亮度/曝光) | 对焦抑制信号(?):曝光剧变时暂停触发;低光→PD 降权 |
| AWB 统计(颜色) | 对焦抑制信号(?):颜色剧变时暂停触发;IR 补光检测 |
| SAD(帧差) | 对焦抑制信号(?):画面剧烈运动时暂停触发 |
| 直方图 | 辅助判断对比度可靠性 |
| 增益/曝光时间 | gain 高时 FV 噪声大,调整阈值 |
| PD/FV 历史序列 | 方向一致性、波动趋势、缓慢变化检测 |
| 上次对焦结果 | 基准值;DCC 自适应修正数据源 |
| 帧时间戳 | PD/FV 帧对齐、延迟计算 |
### 第三层:需额外开发
| 数据 | 优先级 | 说明 |
|------|--------|------|
| **Focus Peaking** | 较高 | 梯度阈值化+计数,准焦/失焦二值判定(小米实拍验证效果很好) |
| 人脸/目标检测 | — | AI 模块已有,需对接 |
| AIPD 深度图 | — | 全局深度分布(取决于 AIPD 能力) |
| 运动检测 | — | 帧差法/颜色变化→场景变化触发 |
| PD 置信度 | — | AIPD 扩展输出(当前暂无) |
**Focus Peaking 细节**
- 原理:Sobel/Laplacian 梯度 → 高阈值过滤 → 统计高亮像素数
- 有纹理场景:准焦→高亮密集,失焦→高亮**完全消失**(不是渐变,是有/无)
- 无纹理场景:始终为零,此信号不可用,依赖 PD/FV
- 架构:FvProcessor 预留 SharpnessAssessor 接口
### 第四层:远期预留
- 陀螺仪/加速度计:摇镜头抑制触发(S3 硬件待确认)
- 温度传感器:温漂补偿
- DCC 自适应修正:每次对焦完记录 (预期, 实际) → 运行时修正
---
## 四、数据流
### PDAF 决策链路
```
PD measure — PD 采集(PdProcessor 从传感器读取原始 PD)
temporal filter — 时域滤波(多帧平滑,去噪声去抖动)
confidence check — 置信度评估(方差、方向一致性、多 zone 投票)
dead zone check — 动态合焦容忍区(依赖 confidence + texture)
trigger decision — 触发决策(PolicyModule 四层门控)
lens move — 推镜头(ActuateModule 粗跳 + 精调)
```
### 模块级数据流
```
ProcessModule
ISP PD raw ──→ PdProcessor ──→ PdZoneMap (12×6) ──┐
├→ PolicyModule → StateMachine → ActuateModule
ISP AF stats ──→ FvProcessor ──→ FvZoneMap (12×6) ─┘ │
LensMotor (pulse)
```
### PD/FV 数据分离设计
PD 和 FV **分开存储、独立更新,用 zone index 关联**
```
PdZoneMap (12×6) FvZoneMap (12×6)
zone[i].pd zone[i].fv
zone[i].conf zone[i].timestamp
zone[i].timestamp
各自按自己的帧率更新(PD ~25fps, FV ~30/60fps)
下游按 zone index 取对应的 pd 和 fv
```
> **对比旧版 BlockInfo**:旧版把 pd/fv/pulse/sad/face/trend 全塞进一个 struct,
> PD 和 FV 帧率不同步时 FV 可能是旧值,且 block 粒度被 PD 绑死(5个)。
旧 BlockInfo 字段拆解到新架构:
| 旧字段 | 新归属 |
|--------|--------|
| pd, confidence | PdZoneMap |
| totalScore (fv) | FvZoneMap |
| pulse, distance | LensMotor 状态 |
| sadValue | 独立的对焦抑制信号 |
| pdTrend | PolicyModule 内部状态 |
| face | WindowManager 目标选择 |
| rect, idx | 统一 zone index 替代 |
---
## 五、PdafActuator 详细算法
### 总体流程
```
Phase 1 (PD 粗跳) → Phase 2 (PD 修正, 可选) → Phase 3 (FV 精调)
```
### 为什么选「粗跳 + 精调」而不是「每步微调」
每步微调(每帧读 PD → 小步推镜头 → 等信号更新 → 再推)的问题:
- 每次推镜头后需等 2-4 帧 PD 信号才稳定
- DCC 精度不够时需多次迭代才能收敛
- 3-5 次迭代 × 每次 2-3 帧延迟 = 9-15 帧(300-500ms)
粗跳 + 精调的思路:
- **一次算好目标位置,直接大步跳过去**,只吃一次延迟
- 到位后用 FV 精调确认峰值,不依赖 PD 收敛
- 总共 1-2 次跳转 + 1 次精调,耗时大幅缩短
这也是业界通用方案(Sony Hybrid AF、libcamera PDAF+CDAF)。
### DCC 补偿:温度 & 重力
当前 DCC 的 offset 仅与 zoom 关联,但实际部署中:
- **重力影响(较大)**:VCM 电机磁力抵抗重力,镜头朝上/朝下/水平时同样 pulse 对应不同实际位置
- **温度影响**:VCM 磁力随温度变化,高低温下 pulse-位置映射偏移
补偿公式:
```
compensatedPulse = basePulse + gravityOffset + tempOffset
gravityOffset = sin(angle) × K_gravity // K_gravity 需标定,预估 ~5 pulse
tempOffset = (T - T_ref) × K_temp // K_temp 需标定,预估 ~1 pulse/°C
```
- 重力角度:通过加速度计获取(S3 硬件待确认),或固定安装角度配置
- 温度:通过温度传感器获取(ISP/sensor 内置或外部)
- 两个系数需实机标定
### PD 稳定性判定 → 步长调节
粗跳前评估连续 N 帧 PD 的稳定性,决定步长激进程度:
```
示例:方向一致 + 幅度稳定 → 可信,正常步长
frame1 +3
frame2 +3
frame3 +3 → stable defocus,放心大步推
示例:方向一致 + 幅度波动 → PD noisy,降低步长
frame1 +3
frame2 +1
frame3 +4 → PD noisy,降低 move step
```
- **stable**:连续 N 帧 PD 方向一致且幅度方差小 → 正常步长
- **noisy**:方向一致但幅度波动大 → 降低步长(比如 ×0.5),避免过冲
- **inconsistent**:方向都不一致 → 不触发(已在 PolicyModule 门控中处理)
### Phase 1: PD 粗跳
```
deltaPulse = pd × dcc(curPulse) + gravityOffset + tempOffset
if (pd_noisy) deltaPulse *= 0.5 // 噪声大时保守
targetPulse = curPulse + deltaPulse
如果 |deltaPulse| 较大 → 分 2~3 步匀速推到 targetPulse(画面过渡,不等信号更新)
如果 |deltaPulse| 较小 → 直接一步到位
到达后等待 3 帧(镜头到位 + PD 更新)
读取 newPd
|newPd| > 0.15 → Phase 2
|newPd| <= 0.15 → 跳过 Phase 2,直接 Phase 3
```
> **分步 vs 旧版区别**:旧版分步是因为不信任 PD,每步都等确认;
> 新版分步只是画面过渡,中间不等 PD/FV 更新,到达目标后才读信号。
> 分步策略(步数、阈值)待后续讨论。
### Phase 2: PD 修正(最多 1 次)
```
deltaPulse2 = newPd × dcc(curPulse)
moveTo(curPulse + deltaPulse2) // 同样可分步过渡
等待 3 帧 → 无论结果如何 → Phase 3
```
- 不无限迭代,DCC 有系统偏差时多跳也不收敛
- 2 次跳转已吃 6 帧 (~200ms)
### Phase 3: FV 精调 — 三点抛物线拟合【暂定,待实测】
> 参考:libcamera af.cpp `findPeak()` — 业界验证方案
> **精调阶段每步必须等 PD+FV 信号更新**,这个等待是必要的(和粗跳分步不等信号不同)。
**step 大小(按镜头位置动态调整)**
| pulse 区间 | step | 原因 |
|-----------|------|------|
| 600~900 (近端) | 15~20 | 景深浅,FV 峰窄 |
| 400~600 (中间) | 10~15 | 中等 |
| 200~400 (远端) | 5~10 | 景深深,FV 峰宽 |
**算法**
```
1. fv_center = getFV() at curPos
2. fv_near = getFV() at curPos + step (等 N 帧)
3. fv_far = getFV() at curPos - step (等 N 帧)
4. 抛物线插值(libcamera findPeak 公式):
abx = -step, aby = fv_far - fv_center
cbx = +step, cby = fv_near - fv_center
denom = 2 × (aby × cbx - cby × abx)
peakPos = curPos + (aby × cbx² - cby × abx²) / denom
5. moveTo(peakPos) → 完成
```
**异常处理**
- denom ≈ 0(FV 变化极小)→ 停在当前位置,认为已合焦
- peakPos 超出 ±2×step → clamp(防跑飞)
- 三点 FV 都很低 → 无纹理,放弃精调
**备选**:抛物线拟合效果不佳时,改为逐步爬峰或混合方案
### 对焦过程数据记录
每次对焦执行过程中,记录完整的 PD/FV 变化序列(独立于历史数据)。
- 粗跳分步过程中虽不等信号同步,但**被动记录**每帧的 PD/FV
- 推镜头过程中数据不稳(运动模糊、PD 延迟),**记录但不依赖**
- 价值:
- DCC 校准:对比"PD 预测位置" vs "FV 实际峰值位置",累积修正 DCC
- 方向验证:分步中 FV 趋势至少可判断方向对不对
- 调试诊断:dump 完整对焦曲线,排查问题
- 异常检测:PD 突然反向或 FV 骤降时可提前中止
### 镜头到位等待
VCM 马达无位置反馈,发 pulse 后不知道何时真正到位。
方案:实测不同移动量下的到位耗时,按移动量分档固定等待。VCM 响应特性稳定,同样移动量到位时间基本一致。
| 移动量 | 等待时间 | 备注 |
|--------|---------|------|
| 小步(<30 pulse) | 待测 | 精调阶段 |
| 中步(30~100) | 待测 | 修正阶段 |
| 大步(>100) | 待测 | 粗跳阶段 |
### 时间估算
```
Phase 1: 3 帧等待 ≈ 100ms
Phase 2: 0~3 帧(可跳过) ≈ 0~100ms
Phase 3: 3 次采样 × 1~2 帧 + 1 跳转 ≈ 100~200ms
─────────────────────────────────────────────────
总计: 200~400ms
典型(DCC 准,跳过 Phase 2): 200~300ms
```
---
## 六、CdafActuator(待补充)
PD 持续无效时的降级路径,核心为 FV 双向爬峰。
待设计:
- [ ] 初始探测方向选择
- [ ] 步长规划(按 pulse 位置分段)
- [ ] 反转判定 & 终止条件
- [ ] 扫描中 PD 恢复时提前终止(参考 libcamera earlyTerminationByPhase)
---
## 七、待实测参数
- [ ] FV 延迟:推镜头后几帧 FV 稳定?(预期 1~2 帧)
- [ ] FV 峰值宽度:不同 pulse 位置下半宽多少?(决定 step)
- [ ] Phase 1→3 的 PD 阈值 0.15 是否合适
- [ ] step 分段表具体数值
- [ ] DCC 实际精度:粗跳后偏差多大
- [ ] 重力补偿系数 K_gravity:不同安装角度下 pulse 偏差
- [ ] 温度补偿系数 K_temp:不同温度下 pulse 偏差
- [ ] VCM 到位耗时:不同移动量下的稳定时间
---
## 八、设计原则 — S3/S5 统一架构
### 1. 一套 lib 兼容 S3 和 S5
pdaf lib 作为纯算法层,不直接操作硬件,S3/S5 差异在硬件层而非算法层。
**统一部分**(pdaf lib 内):数据流处理、PD 算法、FV 算法、触发模块、执行模块、对焦模式、对焦框模式
**平台差异**(配置化):pulse 范围、DCC 参数表、zone 配置、阈值表、帧率等
```
pdaf lib(统一算法)
├── ProcessModule / PolicyModule / ActuateModule / WindowManager / ...
└── 平台配置:
├── S3: pulse 0~1024, DCC 表, 12×6 zone, AIPD ~25fps ...
└── S5: pulse 范围, DCC 表, zone 配置, ...
```
### 2. lib 职责边界
> 接受 PD 和 FV 数据流,根据对焦模式和对焦框设置等配置,执行推镜头操作。
| 归 pdaf lib | 不归 pdaf lib |
|-------------|--------------|
| PD/FV 数据处理 | PD raw 采集(ISP/sensor 驱动) |
| 触发判定 | ISP AF stats 配置 |
| 对焦执行算法 | 电机物理驱动 |
| 模式/对焦框管理 | 人脸/目标检测 AI |
对外接口:
- `feedPdData()` / `feedFvData()` — 喂数据
- `setMode()` / `setWindow()` — 配置
- 电机控制指令输出(回调或轮询)
### 3. PD 算法兼容性
禁止硬编码分辨率和 block 数量(旧版 384×144 / 5 block 的教训)。
- zone 行列数、尺寸通过配置传入
- ProcessModule 输入 `PdRawFrame`(分辨率、格式由平台配置),输出统一 `PdZoneMap(rows, cols)`
- 不同 PD 源(zncc / aipd / 其他)统一输出格式
### 4. 调试模式 — 在 S3 上模拟 S5 开发
S5 硬件未就绪前,在 S3 上通过命令行调试通道模拟 S5 操作,提前开发验证。
**命令通道**:pdaf lib 内开轻量通道(FIFO 或 Unix socket),监听指令,配置开关启用。
**支持的指令类型**
| 类型 | 示例指令 | 用途 |
|------|---------|------|
| 电机控制 | `motor goto 500` / `motor step +20` | 直接控制镜头位置,测 DCC、FV 曲线 |
| 模式切换 | `mode caf` / `mode afs` / `mode mf` | 模拟 S5 对焦模式切换 |
| 按键模拟 | `key half_press` / `key full_press` / `key release` | 模拟 S5 快门半按触发 AFS |
| 对焦框 | `window center` / `window 3,2` / `window face` | 模拟对焦区域选择 |
| 数据查询 | `dump pd` / `dump fv` / `dump state` | 实时查看 PD/FV/状态机 |
| 参数调整 | `set dcc 85` / `set threshold 0.15` | 运行时调参,免重编译 |
| 录制回放 | `record start` / `replay file.log` | 记录完整对焦过程数据,离线分析 |
**使用方式**
```bash
# 相机上直接发指令
echo "mode afs" > /tmp/pdaf_cmd
echo "key half_press" > /tmp/pdaf_cmd
echo "dump pd" > /tmp/pdaf_cmd
```
**额外价值**:不只开发期用,量产后现场工程师也可通过调试通道远程诊断对焦问题。
### 5. 日志与数据上报
- **调试日志分级开关**:可运行时开关,不同级别(关键事件 / 详细数据 / 全量 dump)
- **debug 绘制开关**:OSD 叠加 zone 网格、PD 方向箭头、FV 热力图等,开发调试用
- **对焦流程记录**:每次对焦完整记录一条结构化数据,包含:
- 触发原因、触发时的 PD/FV 快照
- 各 Phase 耗时、步数、PD/FV 变化序列
- 最终结果(成功/失败)、最终 pulse 位置
- **数据上报**:对焦流程记录用于远程数据分析,测试和用户反馈问题时直接拉数据定位,不再依赖远程桌面
### 6. 开发原则:模块独立验证,再组合集成
每个模块先单独测试确认可靠,再组合。避免混到一起后定位不了问题出在哪。
| 模块 | 独立验证内容 |
|------|-------------|
| PD 计算 | 固定镜头位置,采集各 zone 的 PD 值,验证准度和一致性 |
| FV 计算 | 固定镜头位置,验证各 zone 的 FV 值是否反映真实清晰度 |
| DCC | 已知 PD → `deltaPulse = pd × dcc` → 推镜头 → 验证到位精度 |
| 触发模块 | 制造场景变化(遮挡/移物),验证触发是否及时、是否误触发 |
| 执行模块 | 手动触发对焦,验证 Phase 1/2/3 各阶段行为和耗时 |
| 坐标映射 | 不同 zoom 倍数下验证 zone 对齐、VPSS→zone 转换正确性 |
---
## 九、开发周期
### 总览
| 阶段 | 内容 | AI 编码 | 人工测试 | 备注 |
|------|------|--------|---------|------|
| Phase 1 | 基础搭建 + 数据层 | 2~3 天 | 3~5 天 | 框架 + 调试通道 + PD/FV zone 对齐验证 |
| Phase 2 | DCC + 实测标定 | 1~2 天 | 5~7 天 | DCC 简化快,标定和 VCM 测试耗时 |
| Phase 3 | 触发模块 | 2~3 天 | 5~7 天 | 编码快,信号逐个实测调参耗时 |
| Phase 4 | 执行模块 | 2~3 天 | 5~7 天 | Phase 1/2/3 各阶段需反复上机调 |
| Phase 5 | 集成调优 | — | 1~2 周 | 全链路场景测试 + 参数调优 |
| **合计** | | **~2 周** | **4~6 周** | **总计 1~2 个月** |
### Phase 1:基础搭建 + 数据层(2~3 周)
- [ ] pdaf lib 框架搭建(AFManager、模块初始化/销毁)
- [ ] FIFO 调试命令通道
- [ ] 调试日志分级开关 + debug 绘制开关
- [ ] PdProcessor:接入 PD raw,输出 PdZoneMap (12×6)
- [ ] FvProcessor:ISP AF stats 配置 12×6,输出 FvZoneMap
- [ ] 坐标系处理:zoom 时 AF stats 重配 + PD 区域映射
- [ ] 独立验证:PD 各 zone 准度、FV 各 zone 准度
### Phase 2:DCC + 实测标定(1~2 周)
- [ ] DccCalculator:分段常量 `deltaPulse = pd × dcc`
- [ ] 温度/重力补偿标定
- [ ] VCM 到位耗时实测(小步/中步/大步分档)
- [ ] AIPD 精度测试:固定位置采集 PD-pulse 曲线
- [ ] 独立验证:DCC 粗跳到位精度
### Phase 3:触发模块(2~3 周)
- [ ] WindowManager:按模式选择目标 zone
- [ ] PolicyModule 四层门控逐层实现
- [ ] 动态 dead zone(confidence × texture)
- [ ] 对焦抑制信号(AE/AWB/SAD)逐个测试
- [ ] PD 时域滤波 + 稳定性判定(stable/noisy)
- [ ] 独立验证:场景变化触发、误触发、重复触发
### Phase 4:执行模块(2~3 周)
- [ ] StateMachineModule:IDLE → FOCUSING → TRACKING → IDLE
- [ ] PdafActuator:Phase 1 粗跳 + Phase 2 修正 + Phase 3 FV 精调
- [ ] 对焦过程数据记录
- [ ] CdafActuator:FV 双向爬峰降级路径
- [ ] TRACKING 状态:小步跟随
- [ ] 独立验证:各 Phase 行为和耗时
### Phase 5:集成调优(2~3 周)
- [ ] 全链路联调
- [ ] 场景测试:静止/近远切换/移动目标/低光/前景遮挡/zoom
- [ ] 数据上报对接
- [ ] 参数调优(阈值、step 表、dead zone 基准值等)
- [ ] S5 配置适配(如 S5 硬件到位)
> 代码开发全程使用 AI 辅助,人工负责实机测试、标定和调参。
> 实测标定和调参阶段不确定性较大,可能需要额外时间。
---
## 十、待细化
- [ ] ProcessModule:AIPD 接入方式、PD 滤波参数
- [ ] DccCalculator:分段表设计、自适应修正机制
- [ ] PolicyModule:触发阈值、时间窗口、FV 门控细节
- [ ] 模式差异:CAF vs AFS vs FACE_TRACKING 各模块行为区别
- [ ] S5 平台适配:确认 S5 硬件参数(pulse 范围、PD 源、FV 配置)
对焦流程:
数据源:
pd -> pd 计算模块 -> 算出目标位置的 pd 值。之前是分成 5 block,算 5 个值。
由于性能限制 很多 pd raw 处理放在了 stream 中。并且只下采样并裁剪出来 一个大致 384x144 区域。
人脸模式、AFS 模式的 pd 计算就只算一个区域。并且使用的是另外的 pd raw
af stats -> 获取 fv 模块 -> 计算 block 对应 fv 值。
对焦触发检测:
根据 block lsit,选择一个 最近的 block,然后根据 pd 值的方差,fv 的变化等信息。决策是否触发对焦。
如果 检测到 pd 不可信。则还有个 caf 的逻辑。
不过 最新版本是准备替换 pd 算法的。 使用 ai计算 pd。所以置信度这个指标暂时没有了。
对焦执行:
也分为 2 个,一个就是 pd 可信的时候 pdaf
另外一个是 caf 模糊搜索。
对焦执行完成之后 会将结果发送给 对焦触发检测模块,更新这次对焦的结果,包括最终的 pd 值, fv 值。
镜头控制:
控制值 pulse 范围 0~1024 near 端应该再 800~900 far 端在 300~400
已知问题:
1 首先就是 dcc 模块,之前搞错了 用错了。引入了物距的概念。其实应该是 分段 dcc 常量。只用 pulse 值控制就好了。这样也好控制分步。
2 pd 计算结果准度不够,还有波动,以及 pd 0 并不是准焦点这个奇怪的现象。可能是 pd 取值问题或者其他我没想到的吧。
3 不管是传统 pd 算法还是 aipd 都有一定延迟 aipd 计算耗时更长。镜头推动之后的 pd 值相应是有延迟的。
4 fv 的计算逻辑写的比较乱,坐标的转换和映射难理解。
5 模糊搜索的处理也是填坑的处理,这样处理感觉并不好。并且只能向远处搜。其实就是规避了一下镜头在 near 处时 画面模糊 pd 失效,卡死在 near
以及可以在 pd 完全失效的时候 执行一次 caf 对焦。而且还要避免反复执行。
6 之前的对焦执行模块很多逻辑也不够完善。比如步长规划还依赖物距、caf 阶段的 fv 上升下降判定逻辑、pd 延迟的处理等等。不过好在多次调试,对焦效果还算 OK 了。
7 对焦触发检测的逻辑也不够好。经常会有不应该有的触发。虽然实际效果看着还行。
想到再补充吧
其他:
1 是否考虑,这次重新设计的架构,同时兼容 s3 s5(拍照相机)。整体的 数据流处理、PD算法、FV算法、触发模块、执行模块、对焦模式、对焦框模式、统一起来。
这样收益比较高,同时优化了 s3 的对焦效果,也开发推进了 s5 项目的对焦模块。同时统一的 pdaf 代码后续维护更友好。
而且 2 个项目的对焦,本质上差异其实不大。pdaf 作为一个专门处理对焦的 lib,适当的做好接口兼容问题应该不大。
2 这个 lib 的作用简化描述本质上就是:接受 pd fv 数据流,根据对焦模式和对焦框设置等配置执行推镜头操作。输入数据 -> 执行控制镜头。
3 pdaipd 算法的计算逻辑要做的兼容性强一点。不能出现之前那种 384x144 5 block 这种逻辑。
#开发计划:
# 1 首先这个对焦算法其实不算复杂。 我建议保持 大致架构不变,主要是:
# AFManager WindowManager ProcessModule PolicyModule ActuateModule StateMachineModule 这些定义应该是比较合理的。
# 当然你有好方案也可以改。
# 2 然后我们从头开始,一个一个模块完成,尽量做到每个模块完成后独立验证,单元测试。
# 3 初步计划是:
# 1 先修改 dcc 相关的。移除物距概念。这个比较独立。
# 2 测试 aipd 算法准度。优化 pd 值的处理。
# 3 窗口的管理,fv 获取优化下。更好理解点。
# 4 开发测试对焦触发模块。这里整体的触发检测算法都可以重新设计开发。这里本身也不算复杂。对焦效果想要好,肯定触发的要准,不能乱触发、不触发。
# 5 模糊搜索模块我没想好...
# 6 对焦执行模块的重新设计和开发,如何推镜头、步长规划和限制、如何通过 fv 以及 pd 信息将镜头推到最清晰为止。
开发计划:
架构:保持大致架构不变(AFManager / WindowManager / ProcessModule / PolicyModule / ActuateModule / StateMachineModule
方式:从头开始,逐模块完成,每个模块独立验证。
Step 1: DCC 简化
移除物距概念,改为分段 DCC 常量:deltaPulse = pd × dcc
dcc 大部分区间 80,近焦端更大(80~150)
最独立,改完立刻可验证。
Step 2: FV 获取优化
理清坐标映射,简化计算逻辑。
后面触发和执行都依赖 FV,先搞干净。
Step 3: AIPD 准度测试
写测试工具,固定不同位置采集 PD 值,画 PD-pulse 曲线。
摸清 PD=0 偏移规律。优化 PD 值处理。
Step 4: 触发模块重写
三种触发场景:
- 正常:PD 偏移 > 阈值,连续稳定 触发 PDAF
- PD 失效:连续 N 帧无有效 PD 触发 FV 爬峰(双向)
- near 端卡死:pulse near + PD 失效 强制往 far 方向爬峰
后续优化方向:focus peaking(单张图判断清晰/模糊)辅助触发。
Step 5: 对焦执行模块重写
PD 粗跳 + FV 验证精调。
步长规划基于 pulse(不依赖物距)。
PD 失效时降级为 FV 双向爬峰(替代之前单独的模糊搜索模块)。
Step 6: 集成测试 & 调优
批量场景测试(近→远、远→近、小幅移动、near端、低光等)。
af_summary.py 对比历史基线。
对焦架构:
┌─────────────────────────────────────────────────────────┐
│ ISP 硬件层 │
│ onPdFrameAvailable() onVideoFrameAvailable() │
└──────────┬───────────────────────────────┬──────────────┘
↓ ↓
┌──────────────────────┐ ┌─────────────────────┐
│ ProcessModule │ │ IQAModule │
│ (HPdProcessor) │ │ (ContrastAssessor) │
│ │ │ │
│ 模式分发: │ │ ISP AF统计 → FV值 │
│ ├─ STABILIZE (单点) │ │ H1/V1 低频 + H2/V2 │
│ ├─ FACE_TRACKING │ │ 高频 → 17×15 zone │
│ ├─ CONTINUE (5块) │ └─────────┬───────────┘
│ └─ AIPD (AI深度) │ │
│ │ │
│ PD计算 → 柯西滤波 │ │
│ → 时域滤波 → DCC转换 │ │
│ → 输出 PdData │ │
└──────────┬───────────┘ │
↓ │
┌──────────────────────────────────────────┴──────────┐
│ PolicyModule │
│ (SamplePolicy) │
│ │
│ 30Hz 轮询线程 │
│ ┌─────────────────────────────────────┐ │
│ │ TriggerComponent │ │
│ │ findBestBlock() → 选最优块 │ │
│ │ PdDetectUnit.detect(): │ │
│ │ ├─ PD 历史 (5帧) │ │
│ │ ├─ 方差 < 100 (稳定性) │ │
│ │ ├─ 方向一致性 │ │
│ │ ├─ 距离相关 DOF 阈值 │ │
│ │ └─ 累计帧数 ≥ triggerThreshold │ │
│ └──────────────┬──────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────┐ │
│ │ 触发决策 │ │
│ │ 有效PD + 偏移够大 → TYPE_CLEAR_PD │ │
│ │ 无有效PD(10帧) → TYPE_BLUR_CAF │ │
│ │ 否则 → TYPE_NONE │ │
│ └──────────────┬──────────────────────┘ │
│ ↓ │
│ FV 门控: shouldTriggerAgain() │
│ ├─ maxFv>2000 且 偏差<1% → 不触发 │
│ └─ |PD|<0.08 → 不触发 │
└──────────────────────┬──────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ StateMachineModule │
│ │
│ WAIT_FOR_TRIGGER ──→ STATUS_FOCUSING │
│ ↑ │ │
│ └────────────────────┘ (onFocusFinish) │
│ │
│ 新触发到达时 mTriggerSeq++ 可打断当前对焦 │
└──────────────────────┬───────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ ActuateModule │
│ │
│ TYPE_BLUR_CAF ──→ CafActuator │
│ │ │
│ ├─ COARSE: 扫描路径, 记录FV │
│ ├─ FINE: 峰值附近精搜 │
│ ├─ PD_FOCUS: 可选PD微调 │
│ │ │
│ └─ 如果扫到有效PD → 转PDAF ──┐ │
│ ↓ │
│ TYPE_CLEAR_PD ──→ V2PdAfActuator │
│ │ │
│ ├─ APPROACH: PD大跳 │
│ │ 收集PD → 计算目标 → 跳转 │
│ │ |PD|<0.25 → 进CONVERGE │
│ │ │
│ ├─ CONVERGE: EMA自适应小步 │
│ │ speed=|pd|×3, clamp[0.3,0.75]│
│ │ |PD|<0.10 连续2帧 → 收敛 │
│ │ 超20步 → 放弃 │
│ │ │
│ └─ returnToBest() 兜底 │
└──────────────────────┬───────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ LensMotor + DccCalculator │
│ │
│ distance → DCC曲线(OTP/文件) → pulse → 电机驱动 │
└──────────────────────────────────────────────────────┘
\ No newline at end of file
⏺ 好的,信息很关键。我整理一下你提到的要点,确认我理解对了:
核心纠正
1. DCC 应该简化 — 不要物距概念,直接用分段 DCC 常量,pulse 值控制。这样步长规划也更直接。
2. PD=0 ≠ 准焦 — 这是个大问题,意味着不能简单用 |PD|<阈值 判收敛,需要其他手段(FV)确认。
3. AIPD 会替代传统 PD — 但延迟更大,且暂时没有置信度指标。
架构层面
4. PD 计算受限 — 只有 384×144 下采样区域,人脸/AFS 模式用另外的 PD raw,只算一个区域。
5. FV 坐标映射混乱 — 需要重写清晰化。
6. 模糊搜索是打补丁 — 本质问题是 near 端 PD 失效 + 画面模糊,需要更好的方案而不是单方向扫描。
几个问题想确认:
1. 分段 DCC 常量 — 你的意思是,PD 值直接乘以一个分段系数得到 pulse 偏移量?比如 PD=0.2 在某个 pulse 区间对应移动 N 个 pulse?
2. PD=0 不是准焦 — 偏差大概多少?是固定偏移还是随位置变化?
3. 帧率 — PD 帧和视频帧都是多少 fps?AIPD 计算延迟大概几帧?
4. 电机响应 — 发 pulse 指令后,大约多久到位?移动过程中 PD/FV 数据可用吗?
这些会直接影响新算法的设计。
1 你可以看 DccCalculator::mGlodenParamMap 的定义 dcc 应该是这样算的 deltaPulse = pd * dcc。我这个镜头 Pulse 范围只 0~1024 全程。 dcc 大致值应该是 80 左右。 你理解的应该对吧。
2 偏的到时不多。可能在 0.03~0.08 不过这里可能是由于 pd 计算和取值 导致的。不确定了。
3 视频 是可以 30 60 帧切换。 aipd 最快 25 帧左右吧,是刚优化的我还没测试。 之前的 20帧不到。
4 多久到位我不太确定 这个是 vcm 马达应该算快的 不过计算镜头到位 那 pd 帧的延迟也存在。比如推完镜头后 获取的 pd 值 可能要 2~4 帧才准确。
看完了。你的计划很清晰,我有几点想法:
整体架构
同意保持大框架不变(AFManager / ProcessModule / PolicyModule / ActuateModule / StateMachineModule),分工合理,不需要推翻。
对开发顺序的建议
你的顺序基本是对的,我微调一下:
┌──────┬───────────────┬────────────────────────────────────────────────────────────────────────────┐
│ 步骤 │ 模块 │ 说明 │
├──────┼───────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 1 │ DCC 简化 │ 移除物距,改为 deltaPulse = pd × dcc常量。最独立,改完立刻可验证 │
├──────┼───────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 2 │ FV 获取优化 │ 理清坐标映射,简化计算逻辑。后面触发和执行都依赖 FV,先搞干净 │
├──────┼───────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 3 │ AIPD 准度测试 │ 写个测试工具,固定不同位置采集 PD 值,画 PD-pulse 曲线,摸清 PD=0 偏移规律 │
├──────┼───────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 4 │ 触发模块重写 │ 基于清晰的 PD+FV 数据重新设计触发条件 │
├──────┼───────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 5 │ 对焦执行重写 │ PD 粗跳 + FV 验证精调 │
├──────┼───────────────┼────────────────────────────────────────────────────────────────────────────┤
│ 6 │ 模糊搜索 │ 最后再想 │
└──────┴───────────────┴────────────────────────────────────────────────────────────────────────────┘
调整理由:FV 提到 AIPD 前面,因为 FV 是后面所有模块的基础(触发要用、执行要用、模糊搜索也要用),先把它搞清楚。AIPD 测试可以和 FV 并行,但 FV 优先级更高。
关于模糊搜索(第5点)
我的想法:不需要单独的"模糊搜索模块"。可以这样处理:
- PD 有效时:正常 PDAF 执行
- PD 失效时:对焦执行模块内部切换到 FV hill-climbing 模式(双向探测,不限于只往远处搜)
- 本质上是对焦执行模块的一个子策略,而不是独立模块
这样就不用纠结"什么时候触发模糊搜索"和"怎么避免反复执行"的问题了 — 它就是对焦执行的降级路径。
---
你觉得这个顺序和模糊搜索的思路可以吗?如果可以我们就从 DCC 简化 开始。
===========================================
业界 PDAF 方案调研 & 与 S3 系统的差距分析
===========================================
一、业界主流方案
1. Canon — Dual Pixel CMOS AF
- 每个像素分左右两个光电二极管,全像素参与 AF(百万级对焦点)
- 对比左右信号 → 算方向 + 偏移量 → 一步到位驱动镜头,不需要 hunting
- 二代加入深度学习主体识别,软件可升级
2. Sony — Fast Hybrid AF
- PDAF + CDAF 混合:693 PDAF 点 + 425 CDAF 点(A9)
- PDAF 粗定位 → CDAF 精调(确认 FV 峰值)
- AF 计算频率 60Hz,光圈 ≥F13 时 PDAF 失效自动降级为纯 CDAF
3. Qualcomm(手机 ISP)— DCC 分区标定
- 核心公式:Defocus = K × PD(K = DCC 系数)
- 传感器分 9×7 = 63 个 block,每个 block 独立 K 值(因镜头场曲和 shading)
- 标定流程:已知距离拍图 → 对比 PDAF/CDAF 马达位置差 → 生成 K 值表
- K 可运行时动态更新(根据对焦结果微调)
4. 学术界通用模式 — Hybrid Coarse-Fine
Phase 1: PDAF 粗跳 → PD×DCC → 目标 pulse → 大步跳转
Phase 2: CDAF 精调 → 到达附近后小步 FV 爬峰 → 锁定最清晰位置
大厂也有延迟,但处理方式不同
物理延迟是不可避免的,大厂也一样存在:
- 推镜头 → 镜头到位 → 新帧曝光 → 读出 → PD 计算
- 这个链路至少 2-3 帧,谁都跑不掉
但大厂和你的关键区别不是"有没有延迟",而是"需不需要多次迭代":
大厂做法:开环单次跳转 + 闭环验证
帧 0: 读 PD → 算出 deltaPulse → 一次性跳到目标位置
帧 1-3: 等镜头到位 + PD 更新(这个延迟大家都有)
帧 3: 读新 PD → 确认是否合焦
└─ 如果偏差小 → 完成(总共 ~3-4 帧,100-130ms @30fps)
└─ 如果还有偏差 → 再修正一次
关键:他们的 DCC 标定够准,第一次跳就能到 90%+ 的位置,所以通常只需要 1 次跳转 + 1 次确认。
你的 S3 现状
帧 0: 读 PD → DCC 不准 → 跳到大致位置(可能只有 60-70% 精度)
帧 3: 读新 PD → 偏差还挺大 → 再跳
帧 6: 读新 PD → 还差一点 → 再调
帧 9: 读新 PD → 差不多了
...可能要 3-5 次迭代
每次迭代都要吃 2-3 帧延迟,3 次迭代就是 9 帧 = 300ms,5 次就是 500ms。
所以真正的差距是:
┌──────────┬─────────────────┬──────────────────────┐
│ │ 大厂 │ S3 │
├──────────┼─────────────────┼──────────────────────┤
│ 单次延迟 │ 2-3 帧 │ 2-3 帧(一样) │
├──────────┼─────────────────┼──────────────────────┤
│ 迭代次数 │ 1-2 次 │ 3-5 次 │
├──────────┼─────────────────┼──────────────────────┤
│ DCC 精度 │ 90%+ │ 60-70% │
├──────────┼─────────────────┼──────────────────────┤
│ 总耗时 │ 3-4 帧 (~130ms) │ 9-15 帧 (~300-500ms) │
└──────────┴─────────────────┴──────────────────────┘
结论:延迟本身不是问题,迭代次数才是。而迭代次数取决于 DCC 标定精度。
三、对重构计划的启示
- Step 1 DCC:分段常量是合理折中,但要注意 center vs edge 差异。
后续可考虑引入运行时自适应修正(每次对焦完对比预期 vs 实际位置微调 DCC)。
- Step 2 FV:必须先修好 zoom 坐标 bug,否则后续所有依赖 FV 的模块都不可靠。
- Step 5 执行模块:PDAF 粗跳 + FV 精调是业界验证方案,方向正确。
关键是精调阶段要做真正的 FV 爬峰,不能只靠 PD 收敛。
- 触发模块:短期用数值阈值,长期可考虑引入简单的图像特征辅助判断。
四、参考实现
libcamera (Raspberry Pi) af.cpp — 目前公开最完整的 PDAF+CDAF 混合对焦实现
源码:https://github.com/raspberrypi/libcamera/blob/main/src/ipa/rpi/controller/rpi/af.cpp
关键借鉴:
- FV 精调用三点抛物线拟合(findPeak),3 个采样点插值出精确峰值,不需要逐步爬峰
- CDAF 扫描中如果 PD 恢复有效,可提前终止(earlyTerminationByPhase)
- PDAF 闭环是每帧微调(类 PID),但 S3 因 AIPD 延迟大不适合,改用大跳 + 抛物线精调
- 场景变化检测:对比度或颜色变化 >25% 触发重对焦
五、待实测项
□ FV 延迟:推镜头后几帧 ISP AF stats (FV) 才稳定?预期 1~2 帧,需实测确认
□ FV 峰值宽度:不同 pulse 位置下,FV 峰的半宽是多少 pulse?决定 Phase 3 step 大小
□ DCC 实际精度:Phase 1 粗跳后偏差多大?决定 Phase 2 是否必要
各大厂 PDAF 对焦方案总结
1. Canon — Dual Pixel CMOS AF
核心思路: 每个像素分成左右两个光电二极管(Dual Pixel),独立读出产生视差图像。
- 对比左右信号 → 信号一致 = 合焦,有偏差 → 算出方向 + 偏移量 → 一步到位驱动镜头
- 全像素参与 AF(百万级对焦点),不是只有少量专用像素
- 第二代(Dual Pixel CMOS AF II)加入了深度学习主体识别,软件可升级
- 关键特点:不需要 hunting(搜索),直接计算目标位置
2. Sony — Fast Hybrid AF
核心思路: PDAF + CDAF(对比度检测)混合。
- 693 个 PDAF 点 + 425 个 CDAF 点(A9 系列)
- PDAF 做粗定位(快速跳到大致位置),CDAF 做精调(确认峰值)
- AF 计算频率可达 60Hz(A9)
- 光圈 ≥ F13 时 PDAF 失效,自动降级为纯 CDAF
- 关键特点:两阶段策略 — PDAF 粗跳 + CDAF 精确收敛
3. Qualcomm(手机 ISP 平台)— DCC 分区标定
核心公式: Defocus = K × PD
- K 就是 DCC(Defocus Conversion Coefficient),不是全局常量
- 传感器区域被分成 9×7 = 63 个 block,每个 block 有独立的 K 值
- 原因:镜头场曲和 shading 导致中心和边缘的 DCC 不一样
- 标定流程:在已知距离拍图 → 对比 PDAF 和 CDAF 的马达位置差 → 生成 K 值表
- 标定系数 K 可以在运行时动态更新(根据实际对焦结果微调)
- 关键特点:DCC 是空间分区的,需要标定,且可以自适应修正
4. 学术界/通用方案 — Hybrid Coarse-Fine
多篇论文描述的共同模式:
Phase 1: PDAF 粗跳
PD → DCC 转换 → 计算目标 pulse → 大步跳转
Phase 2: CDAF 精调
到达附近后 → 小步搜索 FV 峰值 → 锁定最清晰位置
- PDAF 单次就能到 90% 的位置,但最后 10% 靠 FV 更可靠
- PD=0 不代表完美合焦(标定误差、场曲等),需要 FV 验证
- 有论文用贝叶斯估计做 PD→Defocus 映射,更鲁棒
---
对你 S3 项目的参考价值
┌─────────────────────────────────────────┬───────────┬─────────────────────────────────────────────────────────────────────────┐
│ 启示 │ 来源 │ 对应你的问题 │
├─────────────────────────────────────────┼───────────┼─────────────────────────────────────────────────────────────────────────┤
│ PDAF 粗跳 + FV 精调 是业界通用方案 │ Sony/学术 │ 你的 Step 5 方向完全正确 │
├─────────────────────────────────────────┼───────────┼─────────────────────────────────────────────────────────────────────────┤
│ DCC 不应该是全局常量,应该按区域标定 │ Qualcomm │ 你的 DCC 简化方案(分段常量)是合理的折中,但要注意 center vs edge 差异 │
├─────────────────────────────────────────┼───────────┼─────────────────────────────────────────────────────────────────────────┤
│ DCC 可以运行时自适应修正 │ Qualcomm │ 每次对焦完成后,对比预期 vs 实际位置,微调 DCC 系数 │
├─────────────────────────────────────────┼───────────┼─────────────────────────────────────────────────────────────────────────┤
│ PD=0 ≠ 合焦是已知问题,不是你代码的 bug │ 通用 │ 所有系统都需要 FV 验证最终合焦,不能只信 PD │
├─────────────────────────────────────────┼───────────┼─────────────────────────────────────────────────────────────────────────┤
│ 光线不足 / 光圈太小时 PDAF 会失效 │ Sony │ 你的触发模块需要有 PDAF→FV 的降级路径(Step 4 已规划) │
├─────────────────────────────────────────┼───────────┼─────────────────────────────────────────────────────────────────────────┤
│ Canon 强调"一步到位" │ Canon │ 说明 DCC 标定准的话,单次跳转就够了,不需要多步迭代 │
└─────────────────────────────────────────┴───────────┴─────────────────────────────────────────────────────────────────────────┘
最核心的参考:Sony 的 Hybrid AF 策略和你的 Step 5 设计几乎一致 — PDAF 粗跳到目标附近,然后 FV 微调收敛。这是业界验证过的方案。
---
想深入讨论哪个方面?比如:
- Qualcomm 的 DCC 分区标定思路要不要借鉴到你的 DCC 设计里?
- Sony 的双阶段收敛策略具体怎么映射到你的 ActuateModule?
- DCC 运行时自适应修正要不要加入计划?
Sources:
- https://www.usa.canon.com/learning/training-articles/training-articles-list/canon-autofocus-series-dual-pixel-cmos-af-explained
- https://www.abelcine.com/articles/blog-and-knowledge/tech-news/the-core-technology-behind-canons-dual-pixel-af
- https://www.sony-semicon.com/en/technology/camera/index.html
- https://www.topteks.com/blog/sonys-fast-hybrid-af-merging-precision-and-swiftness-in-autofocus/
- https://patents.google.com/patent/US10387477B2/en
- https://patents.google.com/patent/US10313579B2/en
- https://www.researchgate.net/publication/305421139_Hybrid_Auto-Focusing_System_Using_Dual_Pixel-Type_CMOS_Sensor_With_Contrast_Det
ection_Algorithm
- https://blog.reikanfocal.com/2023/05/how-it-works-on-sensor-phase-detect-autofocus/
- https://openaccess.thecvf.com/content_ECCV_2018/papers/Abdullah_Abuolaim_Revisiting_Autofocus_for_ECCV_2018_paper.pdf
- https://patents.google.com/patent/CN106556960B/en
✻ Cogitated for 1m 49s
\ No newline at end of file
# S3/S5 新对焦架构 — 会议简版
## 一、目标
- 对焦速度:200-300ms(当前 400-700ms)
- 稳定收敛,无来回震荡
- S3/S5 统一一套 pdaf lib,降低维护成本
- 可调试:运行时调参,不依赖重编译
---
## 二、pdaf lib 定义
> **接受 PD 和 FV 数据流,根据对焦模式和对焦框配置,决策并执行推镜头操作。**
核心三段式:**输入 → 决策 → 执行**
```
┌─────────────────────────────────────────────────────────────────┐
│ pdaf lib │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ 输入 │ │ 决策 │ │ 执行 │ │
│ │ │ │ │ │ │ │
│ │ ProcessModule│──→│ PolicyModule │──→│ ActuateModule │ │
│ │ ├ PdProcessor│ │ (触发判定) │ │ (镜头控制) │ │
│ │ ├ FvProcessor│ │ │ │ │ │
│ │ └ TofProc...│──→│ WindowManager│ │ PdafActuator │ │
│ │ (预留) │ │ (目标选择) │ │ CdafActuator │ │
│ └─────────────┘ └──────────────┘ └────────┬────────┘ │
│ │ │
│ 配置输入: StateMachine ──┘ │
│ 对焦模式 / 对焦框 / 平台参数 │
└───────────────────────────────────────────────────┬─────────────┘
电机控制指令输出
```
| 阶段 | 模块 | 职责 |
|------|------|------|
| **输入** | ProcessModule(PdProcessor + FvProcessor + TofProcessor预留) | 接收 PD/FV 数据流,统一输出 12×6 zone 数据 |
| **决策** | PolicyModule + WindowManager | 选择对焦目标,判定是否触发对焦、用哪种策略 |
| **执行** | ActuateModule + StateMachine | PD 粗跳 + FV 精调,驱动镜头到位 |
### PDAF 决策链路
```
PD measure — PD 采集
temporal filter — 时域滤波(多帧平滑去噪)
confidence check — 置信度评估(方差、方向一致性、多 zone 投票)
dead zone check — 动态合焦容忍区(依赖 confidence + texture)
trigger decision — 触发决策(四层门控)
lens move — 推镜头(粗跳 + 精调)
```
---
## 三、输入层设计
### PD/FV 统一 12×6 zone,零坐标映射
PD 和 FV 共用 12×6 网格,独立存储、独立更新,用 zone index 关联。
```
PdZoneMap (12×6) FvZoneMap (12×6)
zone[i].pd zone[i].fv
zone[i].conf zone[i].timestamp
zone[i].timestamp
各自按自己帧率更新(PD ~25fps, FV ~30/60fps)
同一 zone index 直接取 PD 和 FV,零坐标映射
```
- PD 算法双轨:ZnccProcessor(传统)+ AipdProcessor(AI),统一输出 PdZoneMap
- zone 行列数配置化,不硬编码分辨率和 block 数量
### 坐标系与 zoom 处理
三个坐标系:
```
┌─────────────────────────────────┐
│ Sensor 全图坐标系 │ ← PD raw 始终基于此(不随 zoom 变化)
│ 3840 × 2160 │
│ │
│ ┌───────────────────┐ │
│ │ 可视区域坐标系 │ │ ← zoom 裁剪后的区域
│ │ (VPSS 输出) │ │ 人脸框、用户触摸点等基于此坐标系
│ │ │ │
│ │ AF stats 12×6 │ │ ← ISP 对可视区域重新配置 zone(暂定)
│ │ zone 覆盖此区域 │ │
│ └───────────────────┘ │
│ │
└─────────────────────────────────┘
```
**坐标转换链路**
```
VPSS 坐标(用户看到的画面)
↕ 偏移 + 缩放(zoom crop 参数)
Sensor 全图坐标
↕ zone 网格映射
Zone index (row, col)
→ 取 PD(需从全图 PD 中定位对应区域)
→ 取 FV(直接按 zone index,AF stats 已对齐可视区域)
```
**zoom 时的处理**
| 数据 | zoom 影响 | 处理方式 |
|------|----------|---------|
| AF stats (FV) | 重新配置到可视区域,zone 数量不变(12×6),单 zone 覆盖面积变小 | ISP 重配 w/h,保持 col=12, row=6 |
| PD raw | 始终全图,不随 zoom 变化 | 需要将可视区域映射到全图,裁剪出对应 PD 区域计算(备选:直接裁剪 PD raw) |
| VPSS 坐标(人脸框等) | 基于可视区域 | WindowManager 负责转换到 zone index |
> **注意**:zoom 后单 zone 内容变少,PD 计算精度可能受影响,需实测验证。
---
## 四、决策层设计
### 目标选择(WindowManager)
| 模式 | 选择策略 |
|------|---------|
| 人脸模式 | AI 人脸框 → 映射到 zone |
| 跟踪模式 | 跟踪目标框 → 映射到 zone |
| 手动选区 | 用户指定区域 → 映射到 zone |
| 中心点对焦 | 直接取中心 zone |
| 大区域自动 | 加权选择最优 zone |
### 触发决策(PolicyModule)— 四层门控
```
第一层:场景变化检测(FV 下降?PD 偏移?)→ 粗筛,大部分帧在此过滤
第二层:稳定性确认(连续 N 帧)→ 防误触发
第三层:PD 分析 → 触发 PDAF 或 FvScan
第四层:合焦门控 → 动态 dead zone,防重复触发
高置信+强纹理 → dead zone 小(精度高)
低置信+弱纹理 → dead zone 大(容忍偏差)
```
对焦抑制信号:AE/AWB/SAD 剧烈变化时暂停触发判定
---
## 五、执行层设计
### 为什么选「粗跳 + 精调」而不是「每步微调」
每步微调(每帧读 PD → 小步推镜头 → 等信号更新 → 再推)的问题:
- 每次推镜头后需等 2-4 帧 PD 信号才稳定
- DCC 精度不够时需多次迭代才能收敛
- 3-5 次迭代 × 每次 2-3 帧延迟 = 9-15 帧(300-500ms)
粗跳 + 精调的思路:
- **一次算好目标位置,直接大步跳过去**,只吃一次延迟
- 到位后用 FV 精调确认峰值,不依赖 PD 收敛
- 总共 1-2 次跳转 + 1 次精调,耗时大幅缩短
这也是业界通用方案(Sony Hybrid AF、libcamera PDAF+CDAF)。
### PD 稳定性 → 步长调节
粗跳前评估连续 N 帧 PD 稳定性:
- **stable**(+3, +3, +3):幅度方差小 → 正常步长
- **noisy**(+3, +1, +4):方向一致但幅度波动 → 降低步长,避免过冲
### 对焦执行流程
```
Phase 1 (PD 粗跳) → Phase 2 (PD 修正, 可选) → Phase 3 (FV 三点抛物线精调)
```
- Phase 1:`deltaPulse = pd × dcc + 重力补偿 + 温度补偿`,大步跳到目标附近
- Phase 2:粗跳后 PD 偏差仍大时修正一次(最多 1 次,不无限迭代)
- Phase 3:三点采样 + 抛物线拟合求 FV 精确峰值(参考 libcamera findPeak)
**预期耗时:200-300ms(典型),最长 400ms**
### 对焦过程数据记录
每次对焦执行过程中,记录完整的 PD/FV 变化序列(独立于历史数据)。
- 粗跳分步过程中虽不等信号同步,但**被动记录**每帧的 PD/FV
- 推镜头过程中数据不稳(运动模糊、PD 延迟),**记录但不依赖**
- 价值:
- DCC 校准:对比"PD 预测位置" vs "FV 实际峰值位置",累积修正 DCC
- 方向验证:分步中 FV 趋势至少可判断方向对不对
- 调试诊断:dump 完整对焦曲线,排查问题
- 异常检测:PD 突然反向或 FV 骤降时可提前中止
### DCC 补偿:温度 & 重力
VCM 电机受重力和温度影响,同样 pulse 对应不同实际位置:
| 因素 | 影响 | 补偿方式 |
|------|------|---------|
| **重力**(影响较大) | 镜头朝上/朝下/水平,磁力抵抗重力不同 | `sin(angle) × K_gravity`,K 需标定 |
| **温度** | VCM 磁力随温度变化 | `(T - T_ref) × K_temp`,K 需标定 |
补偿加入 DCC 计算:`deltaPulse = pd × dcc + gravityOffset + tempOffset`
### 镜头到位等待
VCM 马达无位置反馈,发 pulse 后不知道何时真正到位。
方案:实测不同移动量下的到位耗时,按移动量分档固定等待。VCM 响应特性稳定,同样移动量到位时间基本一致。
| 移动量 | 等待时间 | 备注 |
|--------|---------|------|
| 小步(<30 pulse) | 待测 | 精调阶段 |
| 中步(30~100) | 待测 | 修正阶段 |
| 大步(>100) | 待测 | 粗跳阶段 |
### 状态机:新增 TRACKING 状态
```
IDLE → FOCUSING → TRACKING → IDLE
↑ │
└─────────┘ (PD 大变 → 重新对焦)
```
- FOCUSING:完整 Phase 1/2/3
- TRACKING:对焦完成后持续监测,小步跟随移动目标
- 解决移动目标场景反复全流程对焦的问题
---
## 六、S3/S5 统一设计
### 一套 lib,两个产品
pdaf lib 是纯算法层,不直接操作硬件。S3/S5 差异通过配置解决。
```
pdaf lib(统一算法)
├── ProcessModule / PolicyModule / ActuateModule / WindowManager / ...
└── 平台配置:
├── S3: pulse 0~1024, DCC 表, 12×6 zone, AIPD ~25fps
└── S5: pulse 范围, DCC 表, zone 配置, ...
```
### lib 职责边界
> 接受 PD 和 FV 数据流,根据对焦模式和对焦框设置等配置,输出镜头控制指令。
| 归 pdaf lib | 不归 pdaf lib |
|-------------|--------------|
| PD/FV 数据处理 | PD raw 采集 |
| 触发判定 + 执行算法 | ISP 配置 / 电机驱动 |
| 模式/对焦框管理 | 人脸/目标检测 AI |
### 调试模式
S5 硬件未就绪前,在 S3 上通过 FIFO 命令通道模拟 S5 操作:
```bash
echo "mode afs" > /tmp/pdaf_cmd # 切换模式
echo "key half_press" > /tmp/pdaf_cmd # 模拟快门半按
echo "motor goto 500" > /tmp/pdaf_cmd # 控制镜头
echo "dump pd" > /tmp/pdaf_cmd # 查看数据
```
量产后同样可用于现场诊断。
### 日志与数据上报
**问题**:之前排查对焦问题只能远程看用户电脑屏幕,耗时耗人力,无法定位根因。
**方案**
- **调试日志分级开关**:可运行时开关,不同级别(关键事件 / 详细数据 / 全量 dump)
- **debug 绘制开关**:OSD 叠加 zone 网格、PD 方向箭头、FV 热力图等,开发调试用
- **对焦流程记录**:每次对焦完整记录一条结构化数据,包含:
- 触发原因、触发时的 PD/FV 快照
- 各 Phase 耗时、步数、PD/FV 变化序列
- 最终结果(成功/失败)、最终 pulse 位置
- **数据上报**:对焦流程记录用于远程数据分析,测试和用户反馈问题时直接拉数据定位,不再依赖远程桌面
---
## 七、开发计划
### 原则:模块独立验证,再组合集成
每个模块先单独测试确认可靠,再组合。避免混到一起后定位不了问题出在哪。
| 模块 | 独立验证内容 |
|------|-------------|
| PD 计算 | 固定镜头位置,采集各 zone 的 PD 值,验证准度和一致性 |
| FV 计算 | 固定镜头位置,验证各 zone 的 FV 值是否反映真实清晰度 |
| DCC | 已知 PD → `deltaPulse = pd × dcc` → 推镜头 → 验证到位精度 |
| 触发模块 | 制造场景变化(遮挡/移物),验证触发是否及时、是否误触发 |
| 执行模块 | 手动触发对焦,验证 Phase 1/2/3 各阶段行为和耗时 |
| 坐标映射 | 不同 zoom 倍数下验证 zone 对齐、VPSS→zone 转换正确性 |
### 开发步骤
| 步骤 | 模块 | 说明 |
|------|------|------|
| 1 | DCC 简化 | 移除物距概念,`deltaPulse = pd × dcc常量`,最独立 |
| 2 | FV 优化 | 统一 12×6 zone,理清坐标映射 |
| 3 | AIPD 测试 | 测试工具采集 PD-pulse 曲线,摸清精度 |
| 4 | 触发重写 | 四层门控,信号逐个验证后组合 |
| 5 | 执行重写 | PD 粗跳 + FV 抛物线精调 |
| 6 | 集成测试 | 全链路调优 |
---
## 八、预期收益
| 指标 | 目标 |
|------|------|
| 对焦速度 | 200-300ms |
| 代码维护 | S3/S5 统一 lib |
| 数据可靠性 | PD/FV 独立更新、零坐标映射 |
| 可调试性 | 命令通道实时调参 |
| 移动目标 | TRACKING 小步跟随 |
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment