做视觉感知的项目,ISP(Image Signal Processor)的调教质量直接决定了后续AI模型的输入质量。征程6内置了ISP硬件pipeline,支持AE(自动曝光)、AWB(自动白平衡)、LSC(镜头阴影校正)、NR(降噪)等一系列处理。这篇记录我在一个AGV项目上对征程6 ISP做调优的过程,以及图像数据回灌(image data replay)的使用经验。
一、征程6 ISP Pipeline概览
征程6的ISP硬件pipeline大概长这样:
```
Raw Sensor Data
↓
BLC (Black Level Correction)
↓
LSC (Lens Shading Correction)
↓
AE/AWB统计
↓
Demosaic (去马赛克)
↓
CCM (Color Correction Matrix)
↓
Gamma校正
↓
NR (Noise Reduction)
↓
Sharpen
↓
Output (YUV/RGB)
```
ISP调优的核心思路是:根据场景特点(室内/室外/夜间/逆光等),调整各模块的参数,让输出图像的亮度、对比度、色彩还原度符合后续AI模型的需求。
二、ISP参数配置
ISP参数通过设备树(device tree)和运行时API两种方式配置。静态参数在device tree里配,动态参数在运行时通过V4L2 ioctl调。
静态参数(Device Tree)
```
// arch/arm64/boot/dts/horizon/j6.dts
&isp {
// 分辨率
width = ;
height = ;
// 输出格式
output-format = "NV12"; // 或者"RGB"
// BLC参数
black-level = ; // 12bit raw,black level=64
// LSC参数(镜头阴影校正)
lsc-enable = ;
lsc-table = ; // 镜头标定数据
// AE参数
ae-mode = "auto"; // auto/manual/histogram
ae-target-brightness = ; // 目标亮度,8bit范围0-255
// AWB参数
awb-mode = "auto"; // auto/manual
awb-gain-r = ;
awb-gain-g = ;
awb-gain-b = ;
// Gamma
gamma-enable = ;
gamma-table = ;
// NR
nr-enable = ;
nr-strength = ; // 0-100
// Sharpen
sharpen-enable = ;
sharpen-strength = ;
};
```
运行时参数(V4L2)
```cpp
#include
int fd = open("/dev/video0", O_RDWR);
// 设置曝光时间
struct v4l2_control ctrl;
ctrl.id = V4L2_CID_EXPOSURE_ABSOLUTE;
ctrl.value = 10000; // 曝光时间,单位微秒
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
// 设置增益
ctrl.id = V4L2_CID_GAIN;
ctrl.value = 100; // ISO增益
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
// 设置AWB模式
ctrl.id = V4L2_CID_AUTO_WHITE_BALANCE;
ctrl.value = 1; // 1=auto, 0=manual
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
```
三、ISP调优实战
场景1:室内 fluorescent 灯光
室内 fluorescent 灯有频闪问题(50Hz/60Hz),如果曝光时间和频闪周期不匹配,画面会有明暗条纹。
解决: 把曝光时间设成频闪周期的整数倍。
```cpp
// 50Hz市电,周期=20ms
// 曝光时间 = 20ms / N(N=1,2,3...)
// 比如 N=2,曝光时间=10ms=10000us
ctrl.id = V4L2_CID_EXPOSURE_ABSOLUTE;
ctrl.value = 10000;
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
```
场景2:夜间低光照
夜间图像信噪比低,ISP的NR模块要开强一些,但 sharpening 要减弱,否则噪声会被放大。
```cpp
// 夜间参数
ctrl.id = V4L2_CID_NOISE_REDUCTION;
ctrl.value = 80; // NR强度加强
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
ctrl.id = V4L2_CID_SHARPNESS;
ctrl.value = 10; // Sharpen减弱
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
```
注意:
NR强度太高会导致图像模糊,细节丢失,可能影响小目标的检测精度。建议夜间场景把小目标的检测阈值适当降低,补偿图像模糊带来的影响。
场景3:逆光(背光)
逆光场景下前景很暗,背景很亮,AE如果按全局亮度测光,前景会过暗。
解决: 用局部测光或者手动设置曝光。
```cpp
// 手动曝光,根据前景亮度调整
ctrl.id = V4L2_CID_EXPOSURE_AUTO;
ctrl.value = V4L2_EXPOSURE_MANUAL;
ioctrl(fd, VIDIOC_S_CTRL, &ctrl);
ctrl.id = V4L2_CID_EXPOSURE_ABSOLUTE;
ctrl.value = 50000; // 手动增加曝光时间,让前景亮起来
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
```
代价是背景会过曝,但对于检测前景目标(行人、车辆)的应用来说,前景清晰更重要。
场景4:白天高动态范围(HDR)
白天同时有阳光直射区域和阴影区域,普通ISP处理不了这么大的亮度差异。
征程6支持硬件HDR,但需要在sensor端就开启HDR模式:
```cpp
// 开启HDR(sensor端)
ctrl.id = V4L2_CID_HDR_MODE;
ctrl.value = 1; // 1=HDR on, 0=off
ioctl(fd, VIDIOC_S_CTRL, &ctrl);
```
HDR模式会输出更宽的动态范围,但帧率可能会降低(因为需要多帧合成)。我项目里开了HDR后帧率从30fps降到了20fps,但画面质量提升明显,阴影里的目标也能看清了。
四、图像数据回灌(Image Data Replay)
图像数据回灌是调试ISP和感知pipeline的神器。它的原理是把之前采集的raw图像数据(从sensor出来的原始数据)保存下来,然后重复灌入ISP和BPU,方便复现问题和对比不同ISP参数的效果。
回灌数据的采集
```bash
# 从V4L2采集raw数据
v4l2-ctl --device=/dev/video0 \
--set-fmt-video=width=1920,height=1080,pixelformat=RG12 \
--stream-mmap \
--stream-count=100 \
--stream-to=./raw_data.raw
```
pixelformat=RG12表示12bit raw数据。采集100帧保存到raw_data.raw。
回灌数据的播放
```bash
# 把raw数据回灌到ISP
v4l2-ctl --device=/dev/video0 \
--set-fmt-video=width=1920,height=1080,pixelformat=RG12 \
--stream-mmap \
--stream-from=./raw_data.raw \
--stream-count=100
```
回灌过程中可以实时调整ISP参数,看不同参数下的输出效果。
在代码里做回灌
```cpp
#include
#include
// 打开回灌文件
int raw_fd = open("./raw_data.raw", O_RDONLY);
void* raw_data = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, raw_fd, 0);
// 设置ISP为回灌模式
struct v4l2_format fmt;
fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
fmt.fmt.pix.width = 1920;
fmt.fmt.pix.height = 1080;
fmt.fmt.pix.pixelformat = v4l2_fourcc('R', 'G', '1', '2');
ioctl(isp_fd, VIDIOC_S_FMT, &fmt);
// 发送回灌数据到ISP
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;
buf.index = 0;
buf.bytesused = 1920 * 1080 * 2; // 12bit = 2 bytes per pixel (packed)
buf.m.userptr = (unsigned long)raw_data;
ioctl(isp_fd, VIDIOC_QBUF, &buf);
// 启动回灌
ioctl(isp_fd, VIDIOC_STREAMON, &buf.type);
// ISP处理完成后,从capture端读取处理后的图像
struct v4l2_buffer cap_buf;
cap_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(isp_fd, VIDIOC_DQBUF, &cap_buf);
// cap_buf里的数据就是ISP处理后的图像
```
回灌调试工作流
我的工作流:
1. 采集问题场景的raw数据:在出现问题的场景下采集几秒的raw数据
2. 保存不同ISP参数的YAML配置:记录下当时的ISP参数
3. 回灌raw数据,调整ISP参数:在实验室里反复回灌,调整参数看效果
4. 对比不同参数的输出:把不同参数下的输出图像保存下来,肉眼对比
5. 找到最优参数后更新设备树:把最优参数固化到device tree里
一个小技巧:
采集raw数据的时候,同时采集ISP输出的YUV/RGB图像和BPU的推理结果。这样回灌的时候不仅能调ISP,还能验证ISP调优后对BPU推理精度的影响。
五、ISP调优对AI模型精度的影响
ISP调优不只是让画面"好看",更重要的是让画面"对AI友好"。
案例:
我项目里NR强度从50调到80后,画面干净了,但小目标的检测精度从72%掉到了65%。原因是NR把目标的边缘细节也抹掉了,小目标本来就只有几个像素,再一抹就融进背景里了。
ISP-AI联合调优策略:
1. 先固定AI模型,调ISP参数,看不同参数下的检测精度
2. 找到ISP参数的"sweet spot":精度最高的参数组合
3. 如果ISP调到头了精度还不够,再考虑调整AI模型(比如增加小目标检测头的分辨率)
```python
# ISP-AI联合调优脚本
isp_params = {
'nr_strength': [30, 50, 70, 90],
'sharpen_strength': [10, 30, 50, 70],
'gamma': [0.8, 1.0, 1.2],
}
best_map = 0
best_params = {}
for nr in isp_params['nr_strength']:
for sharpen in isp_params['sharpen_strength']:
for gamma in isp_params['gamma']:
# 设置ISP参数
set_isp_params(nr=nr, sharpen=sharpen, gamma=gamma)
# 回灌数据
replay_raw_data()
# 跑BPU推理
results = run_inference()
# 计算mAP
mAP = compute_map(results, ground_truth)
if mAP > best_map:
best_map = mAP
best_params = {'nr': nr, 'sharpen': sharpen, 'gamma': gamma}
print(f"Best mAP: {best_map}, params: {best_params}")
```
六、注意事项总结
1. ISP调优要和AI模型联合优化:不要只追求画面好看,要追求对AI检测友好。
2. 频闪问题通过曝光时间解决:50Hz用10ms/20ms曝光,60Hz用8.33ms/16.67ms。
3. 夜间NR和sharpen要平衡:NR太高丢细节,sharpen太高放大噪声。
4. 逆光用局部测光或手动曝光:前景清晰比背景不过曝更重要。
5. HDR提升动态范围但降帧率:根据应用需求权衡。
6. 回灌是调试神器:采集问题场景的raw数据,实验室里反复调参。
7. 回灌时同步采集BPU结果:验证ISP调优对AI精度的影响。
8. ISP参数要分场景配置:白天/夜晚/室内/室外各一套参数,根据环境光自动切换。
9. 12bit raw比8bit YUV信息量大:采集raw数据而不是压缩后的YUV,保留更多信息。
10. 设备树参数修改后重启生效:运行时参数通过V4L2 ioctl动态调整。
