4. 阴影区域几何特征描述与筛选

几何特征的定义与计算口径

阈值分割得到的候选阴影掩膜只是“暗区”,其中既有真实目标阴影,也有礁石暗斑、底质纹理、拖鱼阴影和噪声团块。要筛出典型水下目标阴影,需要把每个连通域压缩成一组可比较的几何量。本例统一采用以下口径(像素坐标系,x 向右、y 向下,面积以像素计):

  • 面积 A:连通域内前景像素总数,用于剔除过小噪声与超大背景暗区。
  • 外接矩形:取最小外接矩形(旋转矩形),宽 w 为长边、高 h 为短边,长宽比 R = w / h ≥ 1。
  • 矩形度:F = A / (w·h),反映区域填充外接矩形的程度,细长或分叉形状该值偏低。
  • 紧致度:C = 4πA / P²,P 为周长,圆为 1,形状越不规则越小。
  • 主轴方向:由二阶中心矩得到,用于判断阴影长轴是否与声呐航迹/距离向一致。

下面这张图给出从掩膜到特征向量再到规则筛选的完整数据流,并标出各特征对典型目标阴影的期望区间,便于对照实现结果。

候选阴影掩膜 连通域标签图 (已剔除小面积) 逐连通域遍历 几何特征计算 A = 前景像素数 w, h = 最小外接矩形长/短边 R = w / h(长宽比) F = A / (w·h)(矩形度) C = 4πA / P²(紧致度) θ = 主轴方向角 特征向量 [A, R, F, C, θ] 每区域一行 写入特征表 规则筛选 A ∈ [Amin, Amax] R ∈ [1.5, 6.0] F ≥ 0.45 C ≥ 0.15 θ 与航迹夹角 ≤ 30° 全部满足 → 保留 保留:典型目标阴影 进入回波配对环节 剔除:非目标暗区 礁石/纹理/噪声/拖鱼 典型形态对照(示例值,非实测) 细长椭圆阴影 R≈3.0 F≈0.78 C≈0.72 近圆暗斑 R≈1.1 F≈0.79 C≈0.85 分叉纹理 F≈0.28 C≈0.09 规则长条(拖鱼) R≈3.3 F≈0.95 C≈0.80 关键区分点 矩形度低 → 分叉/破碎 长宽比过小 → 圆斑 方向偏离 → 非目标

用 OpenCV 实现特征提取

下面的脚本对单帧掩膜逐连通域计算上述特征,并按规则输出保留/剔除结果。输入为二值掩膜(前景 255),输出为特征表与筛选后的掩膜。示例阈值仅作教学起点,实际需按声呐量程与像素分辨率标定。

import cv2
import numpy as np
import math

def region_features(mask):
    """mask: uint8 二值图,前景=255。返回每个连通域的特征字典列表。"""
    num, labels, stats, cents = cv2.connectedComponentsWithStats(mask, connectivity=8)
    feats = []
    for i in range(1, num):  # 跳过背景 0
        area = int(stats[i, cv2.CC_STAT_AREA])
        if area < 20:
            continue
        comp = (labels == i).astype(np.uint8)
        cnts, _ = cv2.findContours(comp, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
        c = max(cnts, key=cv2.contourArea)
        (cx, cy), (w, h), ang = cv2.minAreaRect(c)
        w, h = max(w, h), min(w, h)          # 长边为 w
        rect_ratio = area / (w * h + 1e-6)   # 矩形度
        peri = cv2.arcLength(c, True)
        compact = 4 * math.pi * area / (peri * peri + 1e-6)  # 紧致度
        # 主轴方向:二阶中心矩
        ys, xs = np.nonzero(comp)
        xs = xs - xs.mean(); ys = ys - ys.mean()
        mu20 = (xs * xs).mean(); mu02 = (ys * ys).mean(); mu11 = (xs * ys).mean()
        theta = 0.5 * math.atan2(2 * mu11, mu20 - mu02)
        feats.append(dict(label=i, area=area, w=w, h=h,
                          aspect=w / (h + 1e-6), rect=rect_ratio,
                          compact=compact, theta=math.degrees(theta),
                          cx=cx, cy=cy))
    return feats

def filter_shadows(feats, amin=80, amax=8000,
                   rmin=1.5, rmax=6.0, fmin=0.45, cmin=0.15,
                   track_deg=0.0, max_dev=30.0):
    keep, drop = [], []
    for f in feats:
        dev = abs(((f["theta"] - track_deg + 90) % 180) - 90)
        ok = (amin <= f["area"] <= amax and
              rmin <= f["aspect"] <= rmax and
              f["rect"] >= fmin and
              f["compact"] >= cmin and
              dev <= max_dev)
        (keep if ok else drop).append(f)
    return keep, drop

# 使用示例
mask = cv2.imread("shadow_mask.png", cv2.IMREAD_GRAYSCALE)
feats = region_features(mask)
keep, drop = filter_shadows(feats)
print(f"候选 {len(feats)},保留 {len(keep)},剔除 {len(drop)}")
for f in keep:
    print(f"label={f['label']} A={f['area']} R={f['aspect']:.2f} "
          f"F={f['rect']:.2f} C={f['compact']:.2f} θ={f['theta']:.1f}°")

规则阈值怎么定:用分布而不是拍脑袋

阈值应来自本批数据的特征分布。建议先对若干帧的候选区域做统计,观察目标阴影与干扰暗区在 R、F、C 上的分离程度,再取分位数作为初值。下表给出一组教学示例的统计口径与建议区间(数值为示例,需按实际数据重标定):

特征典型目标阴影礁石/圆斑分叉纹理建议初值
面积 A(像素)200–300050–400100–1500[80, 8000]
长宽比 R2.0–5.01.0–1.31.2–2.5[1.5, 6.0]
矩形度 F0.60–0.850.70–0.850.20–0.40≥ 0.45
紧致度 C0.45–0.800.75–0.900.05–0.15≥ 0.15
主轴偏差≤ 20°随机随机≤ 30°
矩形度与紧致度联合使用能有效剔除分叉纹理:分叉形状外接矩形大而填充少,F 低;周长长而面积小,C 低。两者同时偏低时几乎可判定为非目标。
长宽比阈值不能过窄。目标斜航或近距成像时阴影可能接近方形,R 会降到 1.5 附近;若把下限设到 2.0 会漏检。建议先用召回率评估下限,再用精确率收紧上限。

结果验证与调参闭环

筛选效果用两个指标判断:保留区域中真实目标阴影的比例(精确率)与真实目标阴影被保留的比例(召回率)。做法是对一批帧人工标注目标阴影标签,将脚本输出的 keep 列表与标签比对。若召回偏低,优先放宽 R 下限与主轴偏差;若精确率偏低,优先提高 F 与 C 下限。每次只调一个参数并记录指标变化,避免多参数同时变动导致无法归因。

此外应检查被剔除样本的典型图像:若大量真实阴影因面积超上限被剔除,说明 Amax 与量程不匹配;若因 C 偏低被剔除,往往是分割掩膜边缘破碎,应回到预处理环节检查形态学闭运算的核尺寸,而不是继续放宽几何阈值。