CommonLibraries/README.md
Ben 11236718ea chore: preserve existing library source changes for MatchVerify migration
Isolated source snapshot; original checkout and index unchanged. This migration does not claim new build or hardware validation.
2026-10-20 23:59:35 +08:00

191 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 目标测光曝光控制模块
独立 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)。构建、模拟控制测试和安装接入验证不能证明真实相机参数生效、实际成像改善或现场验收。