
训练医学图像分割或检测模型时病灶样本不足是比网络结构更常见的瓶颈。OTLesMix 这个名称指向一种合成病灶生成方案通过 Wasserstein Barycenter 和 Optimal Transport Map 构造新的病灶使其形状和位置具备更多多样性。真正训练一个病灶分割模型往往面临三方面困难阳性样本数量少、不同病灶形状差异大、病灶在解剖图像中出现的位置跨度广。如果只用旋转、缩放、弹性形变这类数据增强很难让模型见到“从未组合过”的病灶位置和形态。这篇内容从工程实现角度拆解 OTLesMix 背后的数学工具和落地步骤。先理解最优传输、Wasserstein 距离、Wasserstein Barycenter 和最优传输映射各自解决什么问题再用 Python 与 POT 库跑通一个最小模拟最后把合成病灶流程接入分割训练管线并讨论常见参数、排错路径和验证清单。目标不是替代原始论文源码而是让读者在自己的数据集上也能搭建一条类似的可复现合成管线。1. OTLesMix 解决什么问题病灶数据的三重稀缺1.1 病灶样本数量少之外更麻烦的是形状和位置不平衡在一个典型的医学图像分割任务里模型需要的不是“固定的几千张训练图”而是足够覆盖真实分布的训练样本。病灶数据的稀缺通常体现在三个层次数量稀缺阳性样本可能是几十例到几百例远少于自然图像数据集。形状稀缺某些病灶形态只在个别样本中出现例如边缘不规则的结节、带分叶的肿瘤、跨叶病灶。位置稀缺病灶在 CT、MRI 或超声图像中出现的位置受到解剖结构约束现有训练集可能只覆盖少数几个区域。数量问题可以通过类别权重、重采样、半监督等方式缓解。但形状和位置的不平衡无法简单解决。一个模型如果只见过肝脏右下缘的肿瘤遇到肝顶部或贴近血管的肿瘤时边界预测很容易失准。合成病灶的目标就是让训练集覆盖更多“病灶形态 × 病灶位置”的组合。1.2 为什么传统数据增强无法完成这种覆盖传统数据增强做的是同一样本内部的变换旋转改变病灶方向但不改变它相对于器官的位置。缩放改变病灶大小但很难同时适配不同解剖区域的相对比例。平移在固定范围内移动整张图像可能把病灶移动到背景或解剖关系错误的位置。弹性形变改变局部形状但强度有限并且容易出现不自然的拉伸。灰度扰动只改变亮度、对比度不改变形态和位置。这些方法有一个共同缺陷它们没有建立“病灶外观”和“目标区域”之间的对应关系。把一个真实病灶从一个器官区域搬到另一个器官区域需要知道病灶到目标区域的形状映射同时保证边界自然、灰度分布合理。简单地把病灶 mask 贴到另一张图上会产生明显贴纸感和边缘伪影。1.3 OTLesMix 的切入点把病灶当作概率分布来搬移OTLesMix 的关键转变是把病灶图像看作一个概率分布而不是普通像素矩阵。每一幅病灶 patch 可以理解成在二维图像空间上的一堆“质量”灰度值就是质量密度。在这个视角下两个病灶之间的差异可以通过把一份质量搬移到另一份质量所在位置的最小成本来衡量这就是最优传输问题。多个病灶之间的“折中形态”可以通过 Wasserstein Barycenter 得到它不是在像素上做平均而是在空间分布上做传输后的加权平均。把一个病灶放到一个新位置可以使用 Optimal Transport Map 把源病灶的质量映射到目标区域从而形成新位置、新大小的病灶。因此OTLesMix 可以理解为由两个组件配合完成合成任务Wasserstein Barycenter 负责形状多样性。Optimal Transport Map 负责位置多样性。如果原始论文没有给出完整实现这个理解依然可以用作搭建合成管线的设计基础。过程中需要格外注意合成病灶不一定在医学上合理必须结合解剖约束、质量评估和真实数据验证来使用。2. 理解标题中的两个数学工具Wasserstein 距离与 Barycenter2.1 最优传输把一堆土搬到另一堆土的最小成本最优传输问题的雏形是“搬土问题”。假设源图像 (A) 的灰度值总和是一堆土目标图像 (B) 是另一堆土。现在要把源图像上的灰度质量搬到目标图像位置搬运成本等于“搬运质量 × 搬运距离”。在离散图像中Kantorovich 形式可以写成[ \min_{\pi \in \Pi(\mu,\nu)} \sum_{i,j} \pi_{i,j} c(x_i, y_j) ]其中 (\mu) 是源分布(\nu) 是目标分布(\pi) 是传输矩阵(c(x_i, y_j)) 是源像素 (x_i) 到目标像素 (y_j) 的搬运成本通常取欧氏距离或欧氏距离平方。传输矩阵 (\pi) 的行对应源像素列对应目标像素矩阵元素表示从源像素搬到目标像素的质量。这个矩阵就是 Optimal Transport Plan也就是离散情况下的 Optimal Transport Map。2.2 Wasserstein 距离为什么适合病灶图像比较Wasserstein 距离是由最优传输成本诱导出的分布距离。对于 (p) 阶 Wasserstein 距离[ W_p(\mu,\nu)\left(\inf_{\pi \in \Pi(\mu,\nu)} \int c(x,y)^p d\pi(x,y)\right)^{1/p} ]当 (c) 取欧氏距离平方时得到 (W_2) 距离。Wasserstein 距离和普通逐像素损失的区别在于它考虑空间位移。两幅病灶图像即使灰度分布类似如果病灶中心从左上角移到右下角逐像素 L2 距离会很大而 Wasserstein 距离恰好能反映这种位移成本。同样一个圆形病灶和一个椭圆病灶的 (W_2) 距离会体现出形状变化带来的搬运成本。在 OTLesMix 场景下Wasserstein 距离的意义有两个它是构建 Barycenter 的优化目标。它是衡量合成病灶是否接近真实病灶分布的可选指标。2.3 Wasserstein Barycenter多个病灶之间的“形状重心”Wasserstein Barycenter 解决的是“在多个分布之间找一个折中分布”的问题。给定 (k) 个病灶分布 (\mu_1,\dots,\mu_k) 和权重 (w_1,\dots,w_k)Barycenter 定义为[ \min_{\mu} \sum_{i1}^k w_i W_2^2(\mu, \mu_i) ]可以把它看成几何中心的推广。普通像素平均会产生模糊的灰度重叠而 Wasserstein Barycenter 通过最优传输重新分配质量得到的结果是在空间形态上“离所有输入都不太远”的中间分布。在病灶生成中Wasserstein Barycenter 的典型用途是从多个真实病灶合成一个中间形态的病灶。改变不同病灶的权重控制合成结果更接近哪一类形态。在形状轴上采样生成从病灶 A 到病灶 B 的中间样本。这也解释了 OTLesMix 中“Diverse Shapes”的部分。2.4 Optimal Transport Map从源病灶到目标区域的位置控制最优传输映射可以把源分布的质量搬运到目标分布。离散情况下这个映射对应一个传输矩阵。如果目标分布来自一个参考区域 mask那么最优传输映射就可以把源病灶搬运到目标区域。例如源病灶是一个左肺上叶的小结节目标区域是右肺下叶的某个局部 patch。将源病灶的灰度分布与目标区域位置分布做最优传输可以得到一个传输矩阵。这个矩阵会把源病灶的质量“搬”到目标区域形成位于新位置的病灶。这样做的好处是位置由目标区域 mask 控制不会随机飘移。形状会向目标区域的结构靠近而不是简单平移。病灶灰度来自真实病灶不是从噪声或随机纹理生成的。这也对应了标题中“Diverse Locations”和“Optimal Transport Map”。2.5 两个组件如何配合在实际实验设计里可以把两个组件放在同一条流水线上从训练集中收集一批真实病灶 patch 和对应 mask。选择一小批病灶使用 Wasserstein Barycenter 生成新的中间形状病灶。从健康图像中选取一组可放置病灶的目标区域。对每个新病灶和目标区域计算最优传输矩阵。用传输矩阵把新病灶灰度搬到目标区域并同步生成病灶 mask。将合成病灶融合回健康图像得到新的训练样本。这样Barycenter 负责“造出没见过但合理的新形状”Optimal Transport Map 负责“把这些形状放到不同位置”。两者组合后训练样本的数量、形状覆盖和位置覆盖都能提升。3. 先跑通最小示例用 Python 和 POT 模拟病灶合成3.1 环境准备最小示例只需要 Python、NumPy、OpenCV、Matplotlib 和 POT。POT 是 Python Optimal Transport 库提供 EMD、Sinkhorn、Wasserstein 距离和 Barycenter 等实现。安装命令pip install numpy opencv-python matplotlib pot安装完成后验证是否可用python -c import ot; print(ot.__version__)依赖说明如下表依赖用途检查方式numpy图像矩阵和分布计算import numpyopencv-python生成模拟病灶、图像融合import cv2matplotlib可视化结果import matplotlibpot最优传输、Sinkhorn、Barycenterimport ot; print(ot.__version__)版本以 pip 安装时的最新稳定版为准不同版本的 POT 在部分 API 参数上可能有细微差异。3.2 生成两个模拟病灶图像先创建两个不同位置、不同大小、不同旋转角度的椭圆病灶。OpenCV 的ellipse可以直接画实心椭圆。import numpy as np import cv2 import matplotlib.pyplot as plt import ot def make_lesion(size64, center, axes, angle30, intensity1.0): img np.zeros((size, size), dtypenp.float32) img cv2.ellipse( img, centercenter, axesaxes, angleangle, startAngle0, endAngle360, colorintensity, thickness-1, ) return img lesion_a make_lesion(64, center(20, 22), axes(8, 5), angle30) lesion_b make_lesion(64, center(45, 40), axes(12, 7), angle75)这段代码生成两个椭圆病灶lesion_a偏左上lesion_b偏右下形状和方向都不同。后续最优传输会在两个病灶之间寻找映射。3.3 把图像变成概率分布并计算最优传输矩阵把图像展平成一个一维向量再除以总和就得到离散概率分布。代价矩阵使用像素坐标的欧氏距离平方对应 Wasserstein-2 距离。def to_distribution(img): flat img.ravel().astype(np.float64) s flat.sum() if s 1e-12: flat[:] 0.0 flat[0] 1.0 else: flat / s return flat size 64 xs np.arange(size) / size ys np.arange(size) / size xx, yy np.meshgrid(xs, ys) coords np.stack([xx.ravel(), yy.ravel()], axis1) M ot.dist(coords, coords, metricsqeuclidean) a to_distribution(lesion_a) b to_distribution(lesion_b) T ot.emd(a, b, M) print(transport matrix shape:, T.shape) print(row sum:, T.sum(axis1).min(), T.sum(axis1).max())在 64×64 图像上N4096矩阵 (M) 和 (T) 的大小约为 128MB 每个可以接受。如果把分辨率提高到 128×128每个矩阵会超过 2GB需要改成 Sinkhorn 或分块计算。3.4 用 Wasserstein Barycenter 合成中间病灶使用 POT 的barycenter_sinkhorn计算两个病灶之间的 barycenter。输入参数中A的每一列是一个分布weights是各分布的权重reg是熵正则化系数。A np.stack([a, b], axis1) reg 0.01 weights [0.5, 0.5] bary_hist ot.barycenter_sinkhorn( A, M, regreg, weightsweights, numItermax1000, ) bary_img bary_hist.reshape(size, size)reg越小结果越接近严格最优传输但也越容易出现数值不稳定性reg越大结果越平滑但可能丢失病灶的锐利边界。权重决定了合成结果更接近哪一个输入病灶。3.5 把病灶迁移到目标位置现在模拟“把病灶放到新的位置”。先创建一个圆形目标区域 mask然后以这个 mask 作为目标分布计算从源病灶到目标区域的传输矩阵。target_mask np.zeros((size, size), dtypenp.float32) target_mask cv2.circle(target_mask, center(45, 20), radius12, color1.0, thickness-1) t to_distribution(target_mask) T2 ot.emd(a, t, M) moved_flat T2.T lesion_a.ravel() moved moved_flat.reshape(size, size) moved / moved.max() if moved.max() 0 else 1.0这里的矩阵方向需要特别注意。T2的行对应源像素列对应目标区域像素。要从源图像得到目标位置的灰度图需要使用T2.T source_flat把源像素质量按传输矩阵聚合到目标像素上。3.6 可视化与结果解读把源病灶、真实病灶、barycenter 合成结果、传输后结果放在同一张图里。fig, axes plt.subplots(1, 4, figsize(12, 3)) axes[0].imshow(lesion_a, cmapgray) axes[0].set_title(source lesion) axes[1].imshow(lesion_b, cmapgray) axes[1].set_title(real lesion) axes[2].imshow(bary_img, cmapgray) axes[2].set_title(barycenter) axes[3].imshow(moved, cmapgray) axes[3].set_title(moved to target) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(otlesmix_demo.png, dpi150)正常结果应该是barycenter 的形状介于两个真实病灶之间moved的图像主体出现在右侧中间位置的目标区域附近。如果moved出现大量碎裂点说明离散传输矩阵过于严格可以改用ot.sinkhorn并增加正则化系数。4. 从示例到实际病灶分割管线4.1 整体流程把最小示例扩展成可训练的分割数据增强管线需要以下步骤准备真实病灶 patch 和对应分割 mask。选择健康背景图像并明确病灶可以放置的解剖区域。对病灶 patch 做强度预处理转换为概率分布。使用 Wasserstein Barycenter 生成中间形态病灶。使用最优传输映射把病灶迁移到目标区域。将迁移后的病灶融合回健康图像。同步生成病灶 mask保存为离线训练样本。混合真实样本和合成样本训练分割模型。这里最关键的一点是病灶迁移的不仅是图像还包括 mask。最优传输矩阵作用于图像灰度也必须作用于 mask才能保证标签和图像一致。4.2 真实医学图像中如何构造病灶分布真实医学图像通常来自 NIfTI 或 DICOM 文件。把病灶区域裁剪成 patch 后需要先做强度归一化。以 CT 为例通常会先做窗宽窗位处理再裁剪到 [0,1] 区间。def normalize_from_ct(patch, window_center-400, window_width1500): low window_center - window_width / 2.0 high window_center window_width / 2.0 patch np.clip(patch, low, high) patch (patch - low) / (high - low) return patch.astype(np.float32)病灶分布可以取 patch 的灰度值也可以取病灶 mask 的距离图或权重图。使用 mask 的缺点是忽略病灶内部纹理使用灰度图则想保留更多外观信息。建议根据任务选择只关注病灶几何形态使用 mask 的二值图。关注病灶内部密度或信号变化使用灰度 patch。两者都关注使用 mask 和灰度 patch 的加权组合。4.3 用最优传输生成病灶并覆盖到健康图像传输完成后需要把合成病灶融合回背景图像。最简单的做法是直接覆盖但边界可能很硬。更稳妥的做法是使用 alpha 融合或泊松融合。def overlay_synthetic_lesion(bg_image, bg_mask, moved, moved_mask, x, y, alpha0.8): patch_h, patch_w moved.shape[:2] roi bg_image[y:ypatch_h, x:xpatch_w] blended np.clip(alpha * moved (1 - alpha) * roi, 0, 1) bg_out bg_image.copy() bg_out[y:ypatch_h, x:xpatch_w] blended mask_out bg_mask.copy() roi_mask mask_out[y:ypatch_h, x:xpatch_w] roi_mask np.maximum(roi_mask, moved_mask) mask_out[y:ypatch_h, x:xpatch_w] roi_mask return bg_out, mask_out实际覆盖时要注意提前检查x patch_w和y patch_h是否超出边界。对于不同模态图像alpha的选择也不同。背景纹理较细的图像alpha太高会显得像贴纸背景均匀的图像alpha可以高一些。4.4 掩膜同步源病灶 mask 和目标位置 mask 都要经过同一变换如果在最小示例中对病灶灰度使用了传输矩阵T2那么对 mask 也应该使用同样的矩阵source_mask (lesion_a 0.5).astype(np.float32) moved_mask_flat T2.T source_mask.ravel() moved_mask (moved_mask_flat.reshape(size, size) 0.5).astype(np.uint8)由于离散传输会拆分质量mask 的阈值可能需要调低例如从0.5调成0.2然后再做一次形态学闭运算避免内部出现空洞。在真实医学图像中病灶 mask 不只是二元区域有时需要保留边界距离信息。可以用合成病灶的边界距离图来生成更平滑的 mask 边缘但前提是合成病灶质量足够高否则反而会引入错误标注。4.5 与 PyTorch 训练管线集成推荐把合成病灶离线生成并缓存到磁盘而不是在训练时实时计算最优传输。训练阶段每次读取大量样本实时算 N×N 传输矩阵会显著拖慢数据加载。缓存后的训练集可以这样加载from torch.utils.data import Dataset import numpy as np class MixedLesionDataset(Dataset): def __init__(self, real_samples, synthetic_samples): self.samples real_samples synthetic_samples def __len__(self): return len(self.samples) def __getitem__(self, idx): image_path, mask_path self.samples[idx] image np.load(image_path).astype(np.float32) mask np.load(mask_path).astype(np.float32) return image, mask混合比例可以先用真实样本和合成样本 1:1再根据验证集 Dice 或 mIoU 调整。比较稳妥的做法是先只加少量合成样本确认验证集不掉点后再逐步增加。5. 关键参数理解与实验设置5.1 Barycenter 权重weights参数决定了 barycenter 更接近哪些输入病灶。如果希望通过合成样本平衡不同类别的数量可以让权重与类别样本数成反比。例如 A 类病灶有 100 个B 类病灶有 20 个那么 B 类的单个样本权重可以更高但不能简单设置一个固定值需要根据实验调整。表Barycenter 权重的影响权重设置影响适用场景均匀权重合成结果居中不偏向任何一类类别均衡时偏向某类合成结果更像该类类别不平衡或形状覆盖不足权重大于 1无意义权重会被归一化不推荐权重全为 0优化不确定不推荐5.2 熵正则化系数使用 Sinkhorn 计算时reg是不可回避的参数。reg偏大时结果平滑。数值稳定。收敛快。但可能丢失病灶边界细节。reg偏小时结果更接近严格最优传输。边界更锐利。但收敛慢甚至出现 NaN。矩阵中的质量分布更集中合成可能产生碎片化。建议从reg0.01到0.1开始针对 64×64 patch 的实验不需要一开始选择过大值。如果看到明显模糊可以逐步调小。5.3 图像分辨率与计算内存最优传输的时间复杂度和内存开销都依赖像素数量 (N)。稠密矩阵的内存是 (N^2) 数量级。图像尺寸N单矩阵大约内存建议32×3210248MB快速验证64×644096128MB常用模拟128×128163842GB谨慎使用256×2566553632GB不建议稠密计算实际医学图像通常远大于 256×256。因此不要把整张 CT 放进最优传输而是裁剪成病灶 patch 或下采样到低分辨率计算完传输后再放大回原分辨率。这样既能保留形状信息又避免内存爆炸。5.4 学习环境和生产环境的差异学习环境里单张 patch 离线生成就能满足验证。生产环境则需要考虑批量生成、缓存目录、版本管理和质量过滤。维度学习环境生产环境图像尺寸64×64 patch按器官裁剪通常 128×128 或 192×192 patch计算方式单进程离线执行多进程并行合成写入缓存参数保存代码内写死使用 YAML 或 JSON 保存质量检查肉眼查看FID、边界距离、人工抽检缓存管理临时目录版本化缓存避免重复计算6. 常见问题与排查路径6.1 合成图像出现空洞和锯齿现象moved图像出现大量离散亮点病灶不连续。原因严格最优传输ot.emd会把质量完全搬运到成本最低的像素离散化后容易出现“像素级跳变”。目标分布和源分布形状差异大时这种问题更明显。检查方式打印T2的行和列最小、最大值看是否有大量接近 0。调大reg使用ot.sinkhorn。观察不同reg下moved的平滑度。处理建议将ot.emd换成ot.sinkhorn。对moved做轻度高斯滤波。后面融合时加入泊松融合或 alpha 融合。6.2 传输矩阵和代价矩阵内存过高现象程序在构建M或计算T时内存溢出。原因图像分辨率提高后(N^2) 矩阵内存增长非常快。检查方式查看任务管理器或系统监控中的内存占用。打印M.nbytes和T.nbytes。确认是不是在整张大图上直接计算。处理建议把输入图像裁剪成 patch。使用下采样后的低分辨率图计算传输矩阵。如果必须计算大图使用 Sinkhorn但代价矩阵同样很大。可以将代价矩阵分段计算或者使用稀疏表示但实践复杂度高不建议一开始做。6.3 病灶放到新位置后看起来像“贴纸”现象合成病灶边界清晰内部纹理和周围组织差异过大训练时模型学到的是“贴纸特征”而不是病灶特征。原因直接覆盖只移动了病灶灰度没有考虑目标区域的局部亮度、噪声水平和边缘过渡。检查方式把合成图像和真实图像放在同一窗口下对比。计算合成病灶边界处的梯度。分别训练“真实 only”和“合成 only”分割模型观察是否在真实验证集上退化。处理建议使用泊松融合处理边界。根据目标区域的局部均值和方差对合成病灶做亮度归一化。在边界的 mask 上做腐蚀或膨胀让 mask 边缘更符合图像边缘。合成样本只作为补充不全部替代真实样本。6.4 Mask 与图像错位或 mask 出现虚影现象合成图像的病灶位置和对应 mask 位置不一致或者 mask 边缘模糊。原因传输矩阵方向使用错误给 mask 和图像用了不同的处理流阈值选择不当。检查方式确认T2的行是源像素、列是目标像素。打印moved和moved_mask的质心坐标。可视化 overlay把 mask 边缘画在图像上检查是否对齐。处理建议强制复用同一份T2计算图像和 mask。mask 阈值调低后做形态学闭运算。在代码里加断言确保数组坐标范围一致。6.5 分割模型在合成数据上训练后真实数据指标下降现象训练集上 Dice 很高但真实验证集 Dice 明显下降。原因合成数据与真实数据之间存在域偏移。模型可能更依赖合成病灶中某些不自然的边界、纹理或灰度特征。检查方式分别记录 real-only、synthetic-only、mixed 三个实验的验证集 Dice 和 mIoU。使用 FID 或 Kernel MMD 比较合成病灶和真实病灶的特征分布。对错误分割样例做可视化观察模型是否在病灶边界处持续误判。处理建议降低合成样本在总训练集中的比例例如从 10% 开始。合成样本只参与某些训练阶段比如先在真实数据上预训练再混合合成数据微调。对合成病灶做更严格的解剖约束不要放到解剖关系错误的位置。在评估指标上同时关注边界距离不只关注 Dice。7. 最佳实践与可复用检查清单7.1 在病灶合成前先确定解剖约束最优传输只保证数学上的分布映射不保证医学上的合理性。一个病灶被搬到血管外、器官边界外或骨骼内部即使形状很真实对训练也是噪声。实际操作中需要准备一份“可放置区域 mask”。可以在正常图像上手动标注也可以用器官分割结果减去真实病灶区域得到。放置区域不能覆盖目标结构以外的组织。7.2 多尺度合成病灶大小差异大时单一 patch 尺寸无法覆盖所有情况。可以在 32×32、64×64、128×128 多个尺度上分别计算 barycenter 和传输映射。不同尺度对应不同病理表现也避免模型只看到固定大小的病灶。7.3 质量验证不能只看 Dice合成数据的质量指标分为两层图像质量合成病灶是否真实可以用 FID、Gram 矩阵距离、人工评分。任务收益加入合成数据后模型在保留的真实测试集上是否变好。这两者不一定一致。图像质量很高的合成数据可能对模型收益不大反而引入过拟合。最直接的标准是真实验证集上的分割指标而不是合成图像是否“以假乱真”。7.4 混合比例和重采样策略推荐从合成样本占比 10% 开始实验。合成样本占比建议场景0%基线10%-20%小样本数据增强30%-50%病灶数量严重不足大于50%需要严格控制域偏移不推荐直接使用