191 lines
17 KiB
Markdown
191 lines
17 KiB
Markdown
|
|
# 目标测光曝光控制模块
|
|||
|
|
|
|||
|
|
独立 C++17 静态库,依赖 OpenCV `core`、`imgproc`。可独立计算原始图像的全局/ROI 亮度;也可通过控制实例接收采集时间及实际曝光,返回下一帧曝光建议。支持源码子目录引用和安装后的 CMake 包引用。
|
|||
|
|
|
|||
|
|
模块负责测光、时间窗口历史、3/5 点预测、比例/PI/死区控制及 CAPTURE/TRACK/LOST 状态管理。识别、采图、相机 SDK、机器人、三维处理、自动增益和业务节拍均由调用方负责。
|
|||
|
|
|
|||
|
|
## 接入方式
|
|||
|
|
|
|||
|
|
源码引用:
|
|||
|
|
|
|||
|
|
```cmake
|
|||
|
|
add_subdirectory(path/to/TargetExposureControl)
|
|||
|
|
target_link_libraries(your_app PRIVATE TargetExposureControl::Control)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
安装包引用:
|
|||
|
|
|
|||
|
|
```cmake
|
|||
|
|
find_package(TargetExposureControl 1 CONFIG REQUIRED)
|
|||
|
|
target_link_libraries(your_app PRIVATE TargetExposureControl::Control)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
配置 CMake 时设置 `OpenCV_DIR`,安装方式还需将安装前缀加入 `CMAKE_PREFIX_PATH`。安装包要求使用构建时相同版本的 OpenCV;使用其他版本时重新构建本库。编译器、架构、运行库和 Debug/Release 也应与调用工程一致。OpenCV 的运行时 DLL 由使用方部署。本库没有相机 SDK 或 Qt 依赖。
|
|||
|
|
|
|||
|
|
每路相机/图像流使用一个 `target_exposure::ExposureControl` 实例。不同实例状态隔离,同一实例由调用方串行调用。所有曝光单位为 **微秒 us**,所有时间戳也使用同一时钟域下的非负微秒;两者语义不同,不可相互替代。
|
|||
|
|
|
|||
|
|
## 连续在线反馈(1.2.0)
|
|||
|
|
|
|||
|
|
需要每一帧都依据成像亮度调整曝光时,使用独立的 `ContinuousExposureControl`。它复用原比例/PI、死区、迟滞、单步限幅和积分保护,但没有“目标确认 → 丢失冻结 → 单次恢复预算”,不在库内拟合预测。调用方可提供已验证的下一帧灰度预测,进入同一个反馈控制器。原 `ExposureControl` 的状态和默认行为保持不变。
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
target_exposure::ContinuousExposureControl feedback; // 每路相机一个,跨帧保留
|
|||
|
|
target_exposure::ContinuousConfig config;
|
|||
|
|
config.min_exposure_us = camera_min_exposure_us;
|
|||
|
|
config.max_exposure_us = permitted_max_exposure_us;
|
|||
|
|
config.region_controller.target_gray = desired_region_gray;
|
|||
|
|
config.global_controller.target_gray = desired_global_gray;
|
|||
|
|
|
|||
|
|
target_exposure::ContinuousFrameInput input;
|
|||
|
|
input.metering = target_exposure::measureImage(original_image, final_roi, region_is_usable);
|
|||
|
|
input.source = target_exposure::FeedbackSource::YoloRegions; // 或 ManualRoi、Global
|
|||
|
|
input.timestamp_us = capture_monotonic_us;
|
|||
|
|
input.actual_exposure_us = exposure_read_back_for_this_frame;
|
|||
|
|
const auto result = feedback.update(input, config);
|
|||
|
|
// accepted 且 adjustment_suggested 时,由业务在下一帧前写入 suggested_exposure_us。
|
|||
|
|
// 写入失败、读回缺失或读回与请求不一致由业务记录并处理,不伪造实际曝光。
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`Global` 只使用全局均值;`ManualRoi` 和 `YoloRegions` 优先使用有效区域的 `gray`(截尾均值),没有可用区域或区域统计无效时显式回退全局。全局和区域各有独立的 `ControllerConfig`,参考灰度默认均为 118,但可以分别配置;全局和 ROI 的灰度目标无需相同。ROI 有效只是像素区域有效,不确认存在工件。无目标但全局正常时保持曝光,全局偏暗/偏亮时按误差方向连续调整。
|
|||
|
|
|
|||
|
|
`accepted=true` 时总是返回 `suggested_exposure_us`;保持时它等于当前实际曝光。每帧曝光值必须来自这一帧的设备读回,不能用上一条建议替代。输入时间为同一单调时钟的非负微秒,严格递增。非法配置、来源、时间、实际曝光或最终测光不会产生可应用建议,也不推进已接受帧时间水位或 PI 状态。
|
|||
|
|
|
|||
|
|
实际测光来源切换、配置变化或超过 `idle_timeout_us`(默认 300 秒)会清空积分与同向计数。`reset()` 也清空反馈状态;仅显式 `reset(FeedbackResetReason::ClockChanged)` 解除时间水位。调用方更换 ROI 几何或模型但来源枚举未变化时,应显式 `reset()`,避免跨工况沿用积分。正常流不需要 `preCapture()` 或经验曝光初始化;从相机当前读回曝光开始。
|
|||
|
|
|
|||
|
|
日志可直接记录返回的 `requested_source`、`effective_source`、`fallback_reason`、`reset_reason`、`state`、`reason`、实际/建议曝光、参考/反馈灰度、完整 `metering` 以及 `control`。`control` 包含当前/控制误差、积分、同向计数、原始/最终增量、步长限制、曝光限制和积分抗饱和标志。近黑率与饱和率作为测光事实保留,不额外引入新阈值控制策略。库不写日志文件,不接触相机,也不承诺现场识别率。
|
|||
|
|
|
|||
|
|
可选 `ContinuousFrameInput::predicted_gray` 表示下一帧在当前实际曝光下的灰度,由调用方保证相同参数、稳定区域和可信时序。仅在当前区域测光位于死区/迟滞保持区、没有重置或积分抑制时使用;全局回退和正在调节的实际反馈优先。预测与测量共用一套死区、迟滞、同向计数、时间确认、PI 和限幅,预测单步上限 `prediction_max_step_ratio` 默认 10%,还受原曝光/时间限幅约束。没有提供该字段的调用保持原行为。
|
|||
|
|
|
|||
|
|
`prediction_used` 只说明预测进入了本帧控制器,不能表示曝光已写入。`control_gray` 与控制误差反映控制器实际输入;`metering`、`raw_control_gray`、`control.filtered_gray` 始终保留本帧真实测量。`feedback_control_gray`、`feedback_reason`、`feedback_suggested_exposure_us` 保留实际反馈依据,供增益与保护策略使用;预测不进入测量滤波历史。
|
|||
|
|
|
|||
|
|
连续控制的 `state` 是 `Holding`、`Adjusting`、`Limited` 或拒绝输入时的 `Invalid`,不表示检测到/丢失工件。新增独立类型保持旧源码调用兼容,静态库与应用应一起重新编译。
|
|||
|
|
|
|||
|
|
## 独立离线测光(1.3.0,兼容原 1.1.0 调用)
|
|||
|
|
|
|||
|
|
只计算保存图像时,直接调用 `measureImage`,不创建控制实例、不设置曝光或时间,也不调用 `preCapture`。以下变量由调用方提供:
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
target_exposure::Roi roi;
|
|||
|
|
roi.rectangles = final_metering_rects; // 可换成旋转框或实际分割掩膜
|
|||
|
|
const auto measurement = target_exposure::measureImage(original_image, roi, roi_is_usable);
|
|||
|
|
// measurement.global.arithmetic_gray:全局普通平均灰度。
|
|||
|
|
// measurement.target.arithmetic_gray:ROI 普通平均灰度。
|
|||
|
|
// measurement.target.gray:ROI 截尾均值,保留既有控制算法的统计口径。
|
|||
|
|
// measurement.global.source_arithmetic_gray:图像原始标度的普通均值。
|
|||
|
|
// target_exposure::kVersion:当前测光库版本,供分析文件记录。
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
第四个参数为可选的 `MeteringConfig`,默认整图测光、目标区域两端各截尾 5%、至少 16 个原始像素和 8 个保留像素。只计算全局时传空 `Roi` 和 `false`。`roi_is_usable` 只表示存在可测光的区域;手绘 ROI 有效不代表工件已到位或已识别。
|
|||
|
|
|
|||
|
|
第五个参数 `validImageMask` 为可选的有效图像区域掩膜。它必须是与图像等尺寸的 `CV_8UC1`,非零像素表示有效;全局区域和目标区域并集都与它取交集。省略或传空 `cv::Mat` 时整图有效,不会根据像素颜色自动排除黑色工件或黑边。全部为零是有效但没有可测像素的掩膜,对应 `EmptyRoi`;格式错误对应 `InvalidRoi`,不静默退回整图。无目标时 `target` 仍为 `NoTarget`。`global_center_fraction` 如非 1,则只限制全局区域,不改变目标区域。
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
const auto measurement = target_exposure::measureImage(
|
|||
|
|
original_image, saved_regions, regions_are_usable, metering_config, valid_image_mask);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`CV_16UC1` 使用 65536 桶的源像素直方图;普通均值与截尾均值都先在 0~65535 标度精确统计,再线性映射到 0~255,不先转成 8 位图。`source_arithmetic_gray` 保留原始普通均值,`source_bit_depth/source_max_gray` 分别为 16/65535;8 位输入则为 8/255,原始与统一均值相同。`arithmetic_gray` 和 `gray` 始终采用 0~255 标度,近黑/饱和阈值及控制配置也维持此标度。图中 12 位或 10 位数据如果装在 16 位容器内,本库仍按 65535 解释;调用方应明确源数据标度,不能误称为满量程 16 位。16 位三通道、Bayer 和浮点图像仍不支持。
|
|||
|
|
|
|||
|
|
每个区域使用一次像素直方图,同时输出普通均值、截尾均值、原始/保留像素数和截尾前的近黑/饱和比例。目标样本不足时两个均值均为空,不能用零替代;全局不受目标区域无效影响。配置非法时两个区域均返回 `MeterStatus::InvalidConfiguration`;图像、ROI 错误由对应状态报告。无有效区域时不检查闲置 ROI 内容,全局照常计算。
|
|||
|
|
|
|||
|
|
这是无状态函数,同一输入可重复计算,结果不受曝光控制实例的历史影响;调用期间原图和 mask 均不被修改或保留。允许在输入数据不被并发修改的前提下并发测光。采集时间、处理耗时、模型来源及 CSV/JSON 存档由应用程序关联,不由测光函数生成。
|
|||
|
|
|
|||
|
|
`processFrame()` 使用同一测光实现,控制仍使用原 `gray` 字段。新增字段追加在原有数据字段之后,保留原源码的聚合初始化顺序;新增数据字段不保证旧二进制 ABI,应用和静态库应一起重新编译。
|
|||
|
|
|
|||
|
|
## 曝光控制调用顺序
|
|||
|
|
|
|||
|
|
1. 准备本工况的 `Config`。必须提供最小、最大和经验曝光;静止采图上限由业务节拍确定。
|
|||
|
|
2. **拍照前**调用 `preCapture(now_us, config)`。检查 `accepted`;若 `initialization_required`,由调用方先成功设置 `suggested_exposure_us`,再采集。
|
|||
|
|
3. 调用方采集原始图像并识别,读取这一帧真正使用的曝光值,构造 `FrameInput`。
|
|||
|
|
4. 调用 `processFrame(input)`,检查输入接收状态、测光有效性及原因。仅当 `adjustment_suggested=true` 时,由调用方将建议应用于下一帧。
|
|||
|
|
5. 每一帧都重新执行拍照前检查。建议未成功应用时,不把建议值当实际曝光。初始化设置失败时由业务重试设置或显式复位,不继续假装初始化成功。
|
|||
|
|
|
|||
|
|
可编译运行的完整示例见 [examples/minimal.cpp](examples/minimal.cpp),配置见 [examples/static_station_config.hpp](examples/static_station_config.hpp)。示例由外部循环模拟图像和曝光应用,模块本身不采图、不等待、不循环试曝光。
|
|||
|
|
|
|||
|
|
```cpp
|
|||
|
|
target_exposure::ExposureControl exposure;
|
|||
|
|
target_exposure::Config config;
|
|||
|
|
config.min_exposure_us = 100;
|
|||
|
|
config.max_exposure_us = 20000; // 仅作示例,替换为本工况上限
|
|||
|
|
config.empirical_exposure_us = 1000; // 替换为本工况经验值
|
|||
|
|
|
|||
|
|
// 拍照前:
|
|||
|
|
auto prepare = exposure.preCapture(now_us, config);
|
|||
|
|
// accepted=false 时处理错误;initialization_required=true 时先由业务设置曝光。
|
|||
|
|
// 业务完成采图、识别后:
|
|||
|
|
target_exposure::FrameInput input;
|
|||
|
|
input.image = original_image; // CV_8UC1 或 CV_8UC3(BGR)
|
|||
|
|
input.roi.rectangles = final_metering_rects; // 也可叠加旋转矩形或 masks
|
|||
|
|
input.has_usable_target = detector_has_target;
|
|||
|
|
input.timestamp_us = captured_at_us;
|
|||
|
|
input.actual_exposure_us = actual_frame_exposure_us;
|
|||
|
|
auto result = exposure.processFrame(input);
|
|||
|
|
// result.adjustment_suggested 为 true 时,由业务设置 result.suggested_exposure_us。
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
上面的变量来自调用工程,片段用于说明接口顺序;独立运行使用完整示例。
|
|||
|
|
|
|||
|
|
## 输入和测光契约
|
|||
|
|
|
|||
|
|
- 支持 Gray8、BGR8 和 Gray16。BGR 使用 OpenCV 固定 `COLOR_BGR2GRAY` 转换;Gray16 先统计原始像素再按 65535 线性映射到统一 0~255 标度。不做逐图 min-max 归一化、gamma 调整或自动增强,不接受画框图、增强图及未经解释的 Bayer 数据。
|
|||
|
|
- `Roi` 中矩形、旋转矩形及 mask 可以混用,按像素取并集,重叠只统计一次。mask 必须为与图像同尺寸的 `CV_8UC1`,非零即选中。
|
|||
|
|
- 普通矩形使用 OpenCV 的左闭右开范围,先裁剪到图像;旋转矩形按像素中心是否位于旋转后的半开矩形内判断,不使用其包围框直接测光。
|
|||
|
|
- 输入 ROI 被视为最终结果,不隐式内缩。`inset(rect, fraction_per_side)` 是独立、显式辅助函数;比例指每一侧内缩量,整数矩形向内取整。
|
|||
|
|
- `arithmetic_gray` 为截尾前的普通平均;全局 `gray` 与其相同,`global_center_fraction<1` 时统计固定比例的居中矩形。目标 `gray` 在并集内上下各删除 `floor(N × trim_fraction_each_side)` 个样本后求平均。
|
|||
|
|
- 无目标、ROI 空/格式错误、像素不足均有明确状态,`gray`、`arithmetic_gray`、`source_arithmetic_gray` 为 `std::nullopt`。真实黑图的灰度 0 可以是有效测量。
|
|||
|
|
- 非有限、非正或超出当前配置范围的实际曝光属于输入契约错误,返回 `InvalidActualExposure`,保留错误实参用于诊断,不伪造实际曝光或产生调整建议。调用方应修正配置/曝光状态后重走拍照前流程。
|
|||
|
|
- 同一实例内帧时间必须严格递增。重复、乱序及缺少拍照前检查的帧不更新控制状态。相机时间重置时显式 `reset(ResetReason::ClockChanged)`。
|
|||
|
|
- 图像只在调用期间读取,不改原图、不保存图像副本;调用期间调用方须保证数据有效且不被其他线程修改。
|
|||
|
|
|
|||
|
|
## 关键配置
|
|||
|
|
|
|||
|
|
所有控制参数都在公共头文件的强类型结构体中配置,无额外配置文件解析依赖。
|
|||
|
|
|
|||
|
|
| 配置 | 默认/示例 | 含义 |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `min/max/empirical_exposure_us` | 默认未设置,必须由调用方填写 | 曝光范围及初始化曝光 |
|
|||
|
|
| `profile_id`、`revision` | default、0 | 工况身份;任何参数或版本变化也会在拍照前初始化 |
|
|||
|
|
| `history_window_us`、`idle_timeout_us` | 各 300000000 us | 历史窗口与停机阈值分别判断 |
|
|||
|
|
| `history_capacity` | 512 | 帧诊断和有效目标各自保留最多此数量;不得小于预测点数 |
|
|||
|
|
| `detect_confirm_frames/lost_confirm_frames/freeze_frames` | 3/3/2 | 三态连续计数和冻结拍数 |
|
|||
|
|
| `metering.trim_fraction_each_side` | 0.05 | 每侧截尾比例 |
|
|||
|
|
| `metering.min_target_pixels/min_retained_pixels` | 16/8 | 截尾前、后最少像素 |
|
|||
|
|
| `metering.global_center_fraction` | 1.0 | 全图;可配置为 0~1 之间的中央区域比例 |
|
|||
|
|
| `controller.target_gray` | 118 | 8 bit 标度下的目标灰度示例 |
|
|||
|
|
| `controller.deadband_ratio/hysteresis_ratio/large_error_ratio` | 0.10/0.02/0.40 | 相对误差阈值,0.10 表示 10% |
|
|||
|
|
| `controller.proportional_gain/kp/ki` | 0.70/0.40/0.10 | 附件比例分支及离散 PI 系数示例 |
|
|||
|
|
| `controller.integral_abs_limit/max_step_ratio` | 1.0/0.25 | 积分幅度上限、大小误差统一步长上限 |
|
|||
|
|
| `controller.same_direction_frames` | 2 | 小误差连续同方向确认;大误差按已确认规则立即动作 |
|
|||
|
|
| `prediction.points` | 3,可选 5 | 只使用最近指定数量的有效目标记录 |
|
|||
|
|
| `prediction.enabled/photometric_model_confirmed` | true/false | 必须确认光度模型前提后才采用预测;未确认仍正常反馈控制 |
|
|||
|
|
| `prediction.black_level` | 0.0 | 已知黑电平偏置,不从场景中猜测 |
|
|||
|
|
| `prediction.nominal_interval_us/max_horizon_us` | 0/60000000 us | 未传下一拍时刻时的备用间隔;0 表示缺失,无时距则降级 |
|
|||
|
|
| `prediction.max_relative_fit_rmse` | 0.10 | 归一化响应拟合的相对残差上限 |
|
|||
|
|
| `prediction.max_near_black_fraction/max_saturated_fraction` | 0.10/0.05 | 排除失去线性响应的记录;截尾前统计 |
|
|||
|
|
| `prediction.min_predicted_gray/max_predicted_gray` | 1/254 | 预测适用范围 |
|
|||
|
|
| `recovery.global_reference_gray` | 未设置 | 独立全局参考,不自动等于目标灰度 |
|
|||
|
|
| `recovery.deadband_ratio/gain/max_step_ratio` | 0.30/0.30/0.10 | 单次恢复判据与保守比例修正 |
|
|||
|
|
|
|||
|
|
所有示例值只用于演示配置和逻辑,不能代表所有工件。调用方选择黑件/白件或不同工况配置。参数错误由 `validate(config)` 或 `preCapture().errors` 报告。
|
|||
|
|
|
|||
|
|
详细预测公式、三态和恢复预算语义见 [控制与状态说明](docs/控制与状态说明.md)。
|
|||
|
|
|
|||
|
|
## 构建与验证
|
|||
|
|
|
|||
|
|
Windows x64、VS 2022/MSVC 下,原控制版本使用 OpenCV 4.7 验证;当前独立测光版本使用与 MatchVerify 相同的 OpenCV 4.10 重新构建验证。源码构建不硬编码本机路径;以下命令使用本机实际依赖位置。
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
pwsh -NoProfile -File tools/verify.ps1 `
|
|||
|
|
-BuildRoot E:/05_Temp/TargetExposureControl/capture-metering `
|
|||
|
|
-OpenCVDir E:/06_EnvConfig/opencv/build/x64/vc16/lib `
|
|||
|
|
-OpenCVRuntimeDir E:/06_EnvConfig/opencv/build/x64/vc16/bin
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
脚本依次配置、构建 Release、运行全部分组 CTest 和最小示例、安装库,再构建并运行只引用安装包的独立消费工程。完整日志位于构建根目录的 `verification.log`。所有测试使用模拟时间和内存图像,不连接相机。
|
|||
|
|
|
|||
|
|
OpenCV 4.7 Windows 包的顶层配置不识别某些新 MSVC 版本。脚本发现已存在的 `x64/vc16/lib/OpenCVConfig.cmake` 时直接使用该包配置;没有修改 OpenCV。手动配置时可将 `OpenCV_DIR` 设到该目录。其他 OpenCV 布局可传具体包目录和 `-OpenCVRuntimeDir`。
|
|||
|
|
|
|||
|
|
源码作为子目录接入时默认不构建示例与测试;可通过 `TEC_BUILD_TESTS`、`TEC_BUILD_EXAMPLES` 控制。安装包内还包含中文说明与完整示例。
|
|||
|
|
|
|||
|
|
实际命令、结果及剩余验证边界见 [实施记录](实施记录.md)。构建、模拟控制测试和安装接入验证不能证明真实相机参数生效、实际成像改善或现场验收。
|