2. 镜头脏污与遮挡的自动检测

镜头脏污与遮挡的自动检测 学习卡片

固定视角下的脏污检测思路

固定视角相机意味着背景在时间上高度稳定,因此可以把“当前帧”与“干净参考帧”做逐像素比较,再结合局部纹理统计来区分真正的脏污/遮挡与场景中的正常变化。核心判据有两个:局部方差下降(脏污区域趋于平滑、失去纹理)和边缘密度下降(细节被遮挡)。两者同时显著下降的区域,才判定为脏污候选。

镜头脏污检测:参考帧差分 + 局部方差 + 边缘密度 干净参考帧 I_ref 固定视角 · 无脏污 当前帧 I_cur 同一场景 · 可能脏污 差分图 D D = |I_cur − I_ref| 阈值 T_diff = 25 局部方差 Var_local 窗口 15×15 脏污区方差显著下降 边缘密度 Edge_density Sobel 幅值 > 40 脏污区边缘密度下降 融合判定 D > T_diff Var < 0.6·Var_ref Edge < 0.5·Edge_ref 输出结果 脏污区域位置 (x,y,w,h) 严重程度评分 0–100 严重程度评分公式: Score = 100 × (0.5 × (1 − Var_ratio) + 0.3 × (1 − Edge_ratio) + 0.2 × Area_ratio) Var_ratio = Var_local / Var_ref,Edge_ratio = Edge_local / Edge_ref,Area_ratio = 脏污面积 / 画面面积 评分等级: 0–30 轻微 31–60 中度 61–85 严重 86–100 需立即清洁

检测算法实现

以下 Python 代码实现完整的检测流程:读取参考帧与当前帧,计算差分图、局部方差图和边缘密度图,融合三个判据生成脏污掩膜,并输出区域位置与严重程度评分。

import cv2
import numpy as np

def detect_lens_dirt(ref_path, cur_path, diff_thresh=25,
                     var_ratio_thresh=0.6, edge_ratio_thresh=0.5,
                     win_size=15, edge_thresh=40):
    """
    固定视角镜头脏污检测。
    返回: (mask, regions, score)
    """
    ref = cv2.imread(ref_path, cv2.IMREAD_GRAYSCALE)
    cur = cv2.imread(cur_path, cv2.IMREAD_GRAYSCALE)
    assert ref is not None and cur is not None, "图像读取失败"
    assert ref.shape == cur.shape, "参考帧与当前帧尺寸必须一致"

    # 1. 差分图
    diff = cv2.absdiff(cur, ref)
    diff_mask = (diff > diff_thresh).astype(np.uint8)

    # 2. 局部方差(用均值滤波加速计算)
    ref_f = ref.astype(np.float32)
    cur_f = cur.astype(np.float32)
    k = (win_size, win_size)

    ref_mean = cv2.blur(ref_f, k)
    ref_sq_mean = cv2.blur(ref_f * ref_f, k)
    ref_var = ref_sq_mean - ref_mean * ref_mean

    cur_mean = cv2.blur(cur_f, k)
    cur_sq_mean = cv2.blur(cur_f * cur_f, k)
    cur_var = cur_sq_mean - cur_mean * cur_mean

    # 避免除零
    var_ratio = cur_var / (ref_var + 1e-6)
    var_mask = (var_ratio < var_ratio_thresh).astype(np.uint8)

    # 3. 边缘密度(Sobel 幅值)
    ref_gx = cv2.Sobel(ref_f, cv2.CV_32F, 1, 0, ksize=3)
    ref_gy = cv2.Sobel(ref_f, cv2.CV_32F, 0, 1, ksize=3)
    ref_edge = np.sqrt(ref_gx**2 + ref_gy**2)

    cur_gx = cv2.Sobel(cur_f, cv2.CV_32F, 1, 0, ksize=3)
    cur_gy = cv2.Sobel(cur_f, cv2.CV_32F, 0, 1, ksize=3)
    cur_edge = np.sqrt(cur_gx**2 + cur_gy**2)

    ref_edge_bin = (ref_edge > edge_thresh).astype(np.float32)
    cur_edge_bin = (cur_edge > edge_thresh).astype(np.float32)

    ref_edge_density = cv2.blur(ref_edge_bin, k)
    cur_edge_density = cv2.blur(cur_edge_bin, k)

    edge_ratio = cur_edge_density / (ref_edge_density + 1e-6)
    edge_mask = (edge_ratio < edge_ratio_thresh).astype(np.uint8)

    # 4. 融合:三个条件同时满足
    mask = diff_mask & var_mask & edge_mask

    # 5. 形态学清理
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
    mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
    mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

    # 6. 连通域分析
    num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(
        mask, connectivity=8)

    h, w = ref.shape
    total_area = h * w
    regions = []
    total_dirt_area = 0

    for i in range(1, num_labels):
        x, y, bw, bh, area = stats[i]
        if area < 50:  # 忽略过小噪点
            continue
        cx, cy = centroids[i]
        regions.append({
            "bbox": (int(x), int(y), int(bw), int(bh)),
            "center": (round(cx, 1), round(cy, 1)),
            "area": int(area)
        })
        total_dirt_area += area

    # 7. 严重程度评分
    if total_dirt_area == 0:
        return mask, [], 0.0

    # 取脏污区域内的平均比值
    dirt_pixels = mask > 0
    avg_var_ratio = float(np.mean(var_ratio[dirt_pixels]))
    avg_edge_ratio = float(np.mean(edge_ratio[dirt_pixels]))
    area_ratio = total_dirt_area / total_area

    score = 100.0 * (0.5 * (1 - avg_var_ratio) +
                     0.3 * (1 - avg_edge_ratio) +
                     0.2 * area_ratio)
    score = float(np.clip(score, 0, 100))

    return mask, regions, round(score, 1)

if __name__ == "__main__":
    mask, regions, score = detect_lens_dirt(
        "ref_clean.png", "cur_dirty.png")

    print(f"严重程度评分: {score}")
    print(f"检测到 {len(regions)} 个脏污区域:")
    for r in regions:
        print(f"  位置={r['bbox']}  中心={r['center']}  面积={r['area']}px")

    # 可视化
    vis = cv2.imread("cur_dirty.png")
    vis[mask > 0] = (0, 0, 255)
    for r in regions:
        x, y, bw, bh = r["bbox"]
        cv2.rectangle(vis, (x, y), (x + bw, y + bh), (0, 255, 0), 2)
    cv2.putText(vis, f"Score: {score}", (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2)
    cv2.imwrite("dirt_result.png", vis)

参数选择与结果判读

参数 含义 推荐值 调整方向
diff_thresh 差分灰度阈值 25 光照波动大时调高至 35–40
var_ratio_thresh 局部方差比值阈值 0.6 纹理丰富场景可降至 0.5
edge_ratio_thresh 边缘密度比值阈值 0.5 低纹理场景可放宽至 0.6
win_size 局部统计窗口 15×15 脏污面积大时增大至 21–31
edge_thresh Sobel 边缘判定阈值 40 噪声大时调高至 60
结果判读要点:评分 0–30 为轻微脏污,可继续使用;31–60 为中度,建议安排清洁;61–85 为严重,图像质量已明显下降;86–100 为需立即清洁。若检测到多个区域,以最大区域面积和最高评分作为整体判断依据。
注意事项:参考帧必须在镜头干净、光照稳定的条件下采集。若场景本身存在运动物体(如行人、车辆),差分图会产生大量误检,此时应改用中值背景建模或加入运动掩膜排除动态区域。