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.40.9[125.7, 131.1]偏离带外说明曝光或光源漂移
对比度(std)42.60.7[40.5, 44.7]下降提示雾化、脏污或散焦
清晰度(Tenengrad)186055[1695, 2025]显著下降提示失焦或镜头污染

判读规则:连续 3 帧以上指标落在 ±3σ 带外,视为链路状态变化;单帧越界先排查采集丢帧或压缩伪影。基线建立后,把 baseline.json 与帧序列一起归档,作为后续检测算法的参照。

若相机不支持 CAP_PROP_AUTO_EXPOSURE 的 0.25 约定,可改用厂商 SDK 的曝光/增益接口,但必须保证采集期间参数不变,并在日志中记录实际曝光时间与增益值。
图像采集链路质量基线建立 静态场景 均匀/纹理标板 相机 手动曝光/增益/WB 采集接口 USB3/GigE/采集卡 驱动缓冲 帧队列/丢帧检测 落盘 PNG/TIFF 无损 指标计算(逐帧) 亮度均值 mean Σ I(x,y) / (M·N) 反映整体曝光水平 对比度 std sqrt(Σ(I-mean)²/(M·N)) 反映明暗分布宽度 清晰度 Tenengrad Σ(Gx²+Gy²)/(M·N) Sobel 梯度能量 多帧基线统计(N≥30,丢弃前5帧) 均值与波动 mean_avg ± 3·mean_std contrast_avg ± 3·contrast_std 清晰度基线 focus_avg ± 3·focus_std 作为失焦/脏污参照 判异规则 连续≥3帧越界→链路变化 单帧越界→先查丢帧/伪影

结果验证与归档

验证基线是否可信,检查三点:

  1. 帧间标准差应远小于均值本身(例如亮度 std < 2 灰度级),否则说明光源或曝光在漂移。
  2. 清晰度指标在丢弃预热帧后应趋于稳定,若持续下降,检查对焦是否被自动算法改动。
  3. 落盘图像与内存帧的指标应一致,若不一致,说明写盘过程发生了压缩或位深转换。

归档内容:baseline_frames/ 原始帧、baseline.json 统计结果、采集时的曝光/增益/分辨率日志。后续例2、例3直接读取该 JSON 作为参照,不再重新定义基线口径。