3. 基于阈值分割的声学阴影初步提取

任务定位与输入约定

本节的输入是一帧已预处理的声呐灰度图(侧扫或前视均可),目标是输出一张二值阴影掩膜:暗区为 1,其余为 0。关键约束有三点:第一,阈值必须由图像自身统计量决定,不能手工指定;第二,分割后必须做连通域筛选,剔除小面积噪声;第三,输出掩膜要与原图同尺寸,便于后续几何特征计算。

约定灰度范围 0–255,暗区灰度低。示例使用一张 480×320 的合成侧扫声呐图:背景灰度约 150,目标阴影区灰度约 45,叠加高斯噪声与若干小暗斑作为干扰。以下参数均为教学示例值,实际使用时按设备与场景调整。

阈值分割:Otsu 与最大熵的取舍

Otsu 法最大化类间方差,适合双峰明显的图像;最大熵法最大化前景与背景熵之和,对阴影占比小、直方图单峰偏斜的图像更稳健。声呐阴影往往面积占比小(常低于 10%),因此两者结果会有差异,实践中建议同时计算并比较。

import numpy as np
from scipy import ndimage as ndi

def otsu_threshold(img):
    hist, _ = np.histogram(img, bins=256, range=(0, 256))
    total = img.size
    sum_all = np.dot(np.arange(256), hist)
    w_b = np.cumsum(hist)
    sum_b = np.cumsum(hist * np.arange(256))
    w_f = total - w_b
    valid = (w_b > 0) & (w_f > 0)
    mu_b = np.divide(sum_b, w_b, out=np.zeros(256), where=w_b > 0)
    mu_f = np.divide(sum_all - sum_b, w_f, out=np.zeros(256), where=w_f > 0)
    between = w_b * w_f * (mu_b - mu_f) ** 2
    between[~valid] = -1
    return int(np.argmax(between))

def max_entropy_threshold(img):
    hist, _ = np.histogram(img, bins=256, range=(0, 256))
    p = hist / hist.sum()
    p = p[p > 0]
    best_t, best_h = 0, -np.inf
    for t in range(1, 255):
        p0 = p[:t]; p1 = p[t:]
        if p0.size == 0 or p1.size == 0:
            continue
        h0 = -np.sum(p0 * np.log(p0 + 1e-12))
        h1 = -np.sum(p1 * np.log(p1 + 1e-12))
        if h0 + h1 > best_h:
            best_h, best_t = h0 + h1, t
    return best_t

注意 Otsu 返回的是“暗类/亮类”的分界灰度,阴影取 img <= t;最大熵同理。两者阈值通常相差 5–20 个灰度级,阴影面积占比越小,最大熵往往给出更保守(更低)的阈值。

连通域筛选与掩膜输出

阈值化后图像中会残留孤立暗斑、边缘毛刺。用 8 邻域连通域标记,按面积与形状做两级筛选:面积小于 min_area 的直接剔除;面积达标但长宽比极端(如 > 8)的细长条也剔除,因为典型目标阴影呈块状而非线状。

def extract_shadow_mask(img, method="otsu", min_area=120, max_aspect=8.0):
    t = otsu_threshold(img) if method == "otsu" else max_entropy_threshold(img)
    binary = (img <= t).astype(np.uint8)
    labels, n = ndi.label(binary, structure=np.ones((3, 3)))
    mask = np.zeros_like(binary)
    kept = []
    for i in range(1, n + 1):
        ys, xs = np.where(labels == i)
        area = ys.size
        if area < min_area:
            continue
        h = ys.max() - ys.min() + 1
        w = xs.max() - xs.min() + 1
        aspect = max(h, w) / max(1, min(h, w))
        if aspect > max_aspect:
            continue
        mask[labels == i] = 1
        kept.append({"id": i, "area": int(area), "aspect": round(float(aspect), 2)})
    return mask, t, kept

示例运行结果(合成图,非实测):Otsu 阈值约 96,最大熵阈值约 88;Otsu 掩膜保留 3 个连通域,最大熵保留 2 个。两者都能去掉面积 < 120 像素的噪声斑,差异在于最大熵对阴影边缘更保守,掩膜略小。

若阴影与背景对比度低,可先做 3×3 中值滤波再阈值化;若阴影被高亮回波切断,可先做闭运算(结构元 5×5)再连通域标记,避免同一目标被拆成多块。

结果判读与掩膜质量检查

掩膜是否可用,看三个量:候选区域数量(应与目视目标数接近)、最大区域面积占比(典型目标阴影占全图 1%–15%)、掩膜与原图暗区的重合度。重合度可用简单指标:掩膜内像素的平均灰度应显著低于全图均值,示例中前者约 48、后者约 142,说明掩膜确实落在暗区上。

若候选数量远多于目视目标,优先调大 min_area;若目标阴影被切碎,优先降低阈值或加闭运算;若掩膜把背景暗纹理也纳入,优先改用最大熵并收紧 max_aspect。

声学阴影初步提取:从灰度图到阴影掩膜 输入灰度图 480×320 阈值计算 Otsu:类间方差最大 最大熵:H₀+H₁ 最大 示例 t≈96 / 88 二值化 img ≤ t → 1 其余 → 0 连通域筛选 8 邻域标记 面积 ≥ 120 保留 长宽比 ≤ 8 保留 输出阴影掩膜 保留 3 个候选区域(示例) 掩膜质量判读 • 候选数量 ≈ 目视目标数 • 最大区域面积占比 1%–15% • 掩膜内平均灰度 ≪ 全图均值 • 示例:48 vs 142(合成图) 异常时:调 min_area / 换阈值法 / 加闭运算

可复用的参数与检查清单

参数/检查项示例取值调整方向
阈值方法Otsu 与最大熵并行阴影占比小优先最大熵
min_area120 像素噪声多则调大
max_aspect8.0细长伪影多则调小
连通结构元3×3 全 1阴影断裂时先闭运算
掩膜内平均灰度约 48接近全图均值说明误检