2. 镜头脏污与遮挡的自动检测
固定视角下的脏污检测思路
固定视角相机意味着背景在时间上高度稳定,因此可以把“当前帧”与“干净参考帧”做逐像素比较,再结合局部纹理统计来区分真正的脏污/遮挡与场景中的正常变化。核心判据有两个:局部方差下降(脏污区域趋于平滑、失去纹理)和边缘密度下降(细节被遮挡)。两者同时显著下降的区域,才判定为脏污候选。
检测算法实现
以下 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 为需立即清洁。若检测到多个区域,以最大区域面积和最高评分作为整体判断依据。
注意事项:参考帧必须在镜头干净、光照稳定的条件下采集。若场景本身存在运动物体(如行人、车辆),差分图会产生大量误检,此时应改用中值背景建模或加入运动掩膜排除动态区域。