1. 图像采集链路质量基线建立
链路对象与采集约束
本案例把“相机→采集卡/网口→驱动缓冲→落盘”视为一条可测量的信号链。目标是让同一静态场景的多帧图像在固定曝光、固定增益、固定白平衡、关闭自动算法的条件下进入磁盘,再用统一指标描述这条链路的基线水平。基线不是单张图的“好看程度”,而是多帧统计量的分布范围:后续例2的脏污检测、例3的自动曝光评估,都要以这里的均值和波动带作为参照。
采集前需要固定以下条件,否则基线不可复现:
- 场景:均匀漫反射标板或纹理丰富的静态目标,避免运动物体与频闪光源。
- 相机:手动曝光、手动增益、手动白平衡,关闭自动对焦与降噪以外的时域滤波。
- 格式:统一像素格式(如 Mono8 或 BayerRG8),统一分辨率与位深。
- 落盘:无损格式(PNG/TIFF/BMP),避免 JPEG 压缩改变清晰度指标。
- 帧数:连续采集 N≥30 帧,丢弃前 5 帧预热帧。
指标定义与计算口径
三个基础指标必须给出明确公式,否则不同人算出的“基线”无法比较。设图像为 I(x,y),灰度范围 0–255,像素总数 M·N。
- 亮度均值:
mean = (1/(M·N)) · Σ I(x,y),反映整体曝光水平。 - 对比度:用灰度标准差
std = sqrt( (1/(M·N)) · Σ (I-mean)² ),反映明暗分布宽度。 - 清晰度:用 Tenengrad 梯度能量,
focus = (1/(M·N)) · Σ (Gx² + Gy²),其中 Gx、Gy 为 Sobel 水平/垂直梯度。清晰度对焦面变化敏感,对整体亮度变化不敏感。
为避免单帧偶然性,基线取多帧统计:mean_avg、mean_std、contrast_avg、focus_avg,并给出 ±3σ 波动带作为后续判异阈值。
采集与计算实现
下面给出可直接运行的 Python 实现,使用 OpenCV 采集、NumPy 计算指标、逐帧落盘。相机索引、分辨率、帧数按实际设备修改即可。
import cv2
import numpy as np
import os
import json
SAVE_DIR = "baseline_frames"
os.makedirs(SAVE_DIR, exist_ok=True)
CAM_INDEX = 0
WIDTH, HEIGHT = 1280, 1024
TOTAL_FRAMES = 35 # 含预热帧
WARMUP = 5 # 丢弃前5帧
cap = cv2.VideoCapture(CAM_INDEX)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 手动曝光模式
cap.set(cv2.CAP_PROP_EXPOSURE, -6) # 示例值,按实际光照调整
cap.set(cv2.CAP_PROP_GAIN, 0)
cap.set(cv2.CAP_PROP_AUTO_WB, 0)
def metrics(gray):
mean = float(np.mean(gray))
std = float(np.std(gray))
gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)
gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)
focus = float(np.mean(gx**2 + gy**2))
return mean, std, focus
records = []
for i in range(TOTAL_FRAMES):
ok, frame = cap.read()
if not ok:
raise RuntimeError(f"frame {i} grab failed")
if i < WARMUP:
continue
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
m, s, f = metrics(gray)
records.append({"frame": i, "mean": m, "contrast": s, "focus": f})
cv2.imwrite(os.path.join(SAVE_DIR, f"frame_{i:03d}.png"), frame)
cap.release()
arr = np.array([[r["mean"], r["contrast"], r["focus"]] for r in records])
baseline = {
"frames_used": len(records),
"mean_avg": float(arr[:,0].mean()),
"mean_std": float(arr[:,0].std()),
"contrast_avg": float(arr[:,1].mean()),
"contrast_std": float(arr[:,1].std()),
"focus_avg": float(arr[:,2].mean()),
"focus_std": float(arr[:,2].std()),
}
baseline["mean_band"] = [baseline["mean_avg"]-3*baseline["mean_std"],
baseline["mean_avg"]+3*baseline["mean_std"]]
baseline["focus_band"] = [baseline["focus_avg"]-3*baseline["focus_std"],
baseline["focus_avg"]+3*baseline["focus_std"]]
with open("baseline.json", "w") as fp:
json.dump(baseline, fp, indent=2)
print(json.dumps(baseline, indent=2))
基线数据与判读
下表为一次示例采集的基线结果(示例数据,用于说明字段与判读方式,非实测值)。实际运行时以 baseline.json 输出为准。
| 指标 | 均值 | 帧间标准差 | ±3σ 波动带 | 判读要点 |
|---|---|---|---|---|
| 亮度均值 | 128.4 | 0.9 | [125.7, 131.1] | 偏离带外说明曝光或光源漂移 |
| 对比度(std) | 42.6 | 0.7 | [40.5, 44.7] | 下降提示雾化、脏污或散焦 |
| 清晰度(Tenengrad) | 1860 | 55 | [1695, 2025] | 显著下降提示失焦或镜头污染 |
判读规则:连续 3 帧以上指标落在 ±3σ 带外,视为链路状态变化;单帧越界先排查采集丢帧或压缩伪影。基线建立后,把 baseline.json 与帧序列一起归档,作为后续检测算法的参照。
若相机不支持
CAP_PROP_AUTO_EXPOSURE 的 0.25 约定,可改用厂商 SDK 的曝光/增益接口,但必须保证采集期间参数不变,并在日志中记录实际曝光时间与增益值。结果验证与归档
验证基线是否可信,检查三点:
- 帧间标准差应远小于均值本身(例如亮度 std < 2 灰度级),否则说明光源或曝光在漂移。
- 清晰度指标在丢弃预热帧后应趋于稳定,若持续下降,检查对焦是否被自动算法改动。
- 落盘图像与内存帧的指标应一致,若不一致,说明写盘过程发生了压缩或位深转换。
归档内容:baseline_frames/ 原始帧、baseline.json 统计结果、采集时的曝光/增益/分辨率日志。后续例2、例3直接读取该 JSON 作为参照,不再重新定义基线口径。