上一篇让程序「盯住」了运动的东西。这一篇换两个反方向的活------一个把前景从背景里抠出来,一个把划痕水印填回去。图像分割和图像修复,看起来一进一出,其实用的是同一套底层思想:让程序理解「这块像素属于哪」。而 OpenCV 里这两个功能的代表作,就是 GrabCut 和 inpaint。
一、效果先行
先看抠图。左边原图用绿框框住球员,右边三种配置的抠图结果------第一张 1 次迭代还残留着黑色块状伪影,第二张 5 次迭代干净了但足球被当成背景丢了,第三张手动涂了几笔后足球保住了:

再看去水印。我在水果图上程序化划了三条划痕、盖了一个水印块,然后让两种算法各自修复:左边 TELEA、中间 NS,几乎看不出差别,右边是把修复误差放大 10 倍的热图------划痕处最亮,说明那里最难修:

两张图记住两件事:GrabCut 的矩形框和掩码两种玩法结果可能完全不同;inpaint 只能「糊」不能「还原」。
二、GrabCut:把图「割」成两块
GrabCut 名字里的 「Cut」 是图割(Graph Cut)。它把整幅图像建模成一张图:每个像素是一个节点,相邻像素之间连边,每个像素还和「前景」「背景」两个虚拟终点各连一条边。边的权重编码了像素间的颜色相似度和「它更像前景还是背景」的先验。
然后做最小割------找一组边,切断后把图分成前景、背景两块,且割掉的边总权重最小。翻译成人话:前景和背景的边界,恰好落在颜色差异最大的地方。
OpenCV 的实现里,前景和背景的颜色分布各自用一个高斯混合模型(GMM)建模。它不停循环「用当前分割结果重新估计 GMM → 用新 GMM 重新切图」,迭代次数越多,边界越精细。
三、矩形框 vs 掩码:足球去哪了
GrabCut 有两种初始化方式,这是本课最有意思的区别:
cpp
// 方式一:矩形框初始化(最省事,用户框一下就行)
grabCut(img, mask, rect, bgdModel, fgdModel, 5, GC_INIT_WITH_RECT);
// 框外自动当背景,框内当"可能前景",靠 GMM 自动分辨
// 方式二:掩码初始化(手动涂前景/背景笔刷)
grabCut(img, mask, rect, bgdModel, fgdModel, 5, GC_INIT_WITH_MASK);
// mask 里已经标好了确定前景/确定背景,算法按这些种子分割
回到效果图,那个黄色足球就是关键线索:矩形框模式下,足球是黄色的、亮度高,和背景看台、草地颜色都不像,但 GMM 没有任何「足球是前景」的先验,直接把它归到了背景------颜色分割对「和背景撞色」的前景无能为力。而掩码模式手动在球衣上涂几笔前景、草地上涂几笔背景,等于给算法注入硬约束,足球就保住了。
这就是两种方式的取舍:矩形框全自动但可能误判,掩码要人工交互但更准。
四、inpaint:去水印的两种算法
inpaint 的思路很朴素:用损伤区域周围的像素「填充」损伤区。划痕、水印、遮挡,都能修。OpenCV 给了两种算法:
cpp
inpaint(damaged, mask, telea, 3, INPAINT_TELEA); // 快速行进法
inpaint(damaged, mask, ns, 3, INPAINT_NS); // Navier-Stokes
TELEA(快速行进法)从损伤边缘向内推进,用「到边界的距离」加权邻近像素填充,快而简单。NS 把图像强度当流体,沿等照度线(等亮度线)方向传播信息,对直线边缘更平滑。
mask 是关键------白色区域就是要修的地方。所以我先程序化画了三条划痕加一个水印块,图和 mask 同步画,这样损伤位置是「已知的」,修完能精确对账:

五、实测对账
这系列一直坚持「已知值 vs 检测值」的对账,这次也不例外:
GrabCut 三种配置的前景像素占比------矩形框 1 次迭代 0.133,5 次迭代 0.112,掩码 5 次迭代 0.114。从 0.133 降到 0.112,是 GMM 在迭代中把「框内误入的背景」一点点修正掉。
inpaint 的修复质量用 PSNR 衡量(越高越好,40 dB 以上视觉无损)。损伤区域的 PSNR:TELEA 22.43 dB,NS 22.35 dB,几乎打平。但注意------全图 PSNR 高达 35.6 dB,因为损伤区只占一小部分,大部分像素没动,这个数字是虚高的。评估修复质量,一定要看损伤区域的 PSNR。
22 dB 偏低,意味着有明显的涂抹痕迹。这正好印证了 inpaint 的本质:它只能「糊过去」,恢复不出水果果肉那种高频纹理。大块遮挡,还是要靠深度学习修复。
六、踩坑记录
| 坑 | 现象 | 原因 | 正确姿势 |
|---|---|---|---|
| grabCut 掩码几乎全黑 | 当二值图显示啥都看不见 | mask 值是 0/1/2/3 四类标签,不是 0/255 | 用 `(mask==GC_FGD) |
| 手动预置 mask 失效 | 涂的笔刷没起作用 | GC_INIT_WITH_RECT 会覆盖预置 mask | 想用笔刷必须走 GC_INIT_WITH_MASK |
| inpaint 编译报错 | 未声明 | inpaint 在 photo 模块不在 imgproc | #include <opencv2/photo.hpp> |
| PSNR 虚高 | 修复看着差但分数高 | 全图算把没动的像素也算进去 | 用 mask 限定只算损伤区 |
七、AI 与 LLM Wiki:这一课沉淀了什么
LLM Wiki 从 20 页涨到 22 页,这一课新增「图像分割:GrabCut图割」和「图像修复:inpaint去水印」两个概念页,沉淀了图割最小割思想、GMM 迭代、两种初始化模式的区别、以及「损伤区 PSNR 对账」这个验证方法。四张截图归档进素材目录,进度表上 grabcut 和 inpaint 勾选完成,从 26 个变成 28 个,进度 29%。

这一课最值得进知识库的,是「用已知损伤对账」这个方法论------不是肉眼看图说「修得不错」,而是先制造已知损伤,再算 PSNR 精确度量。这种「造一个已知问题去验证解法」的思路,比 GrabCut 和 inpaint 本身的 API 更值钱。
写在最后
97 个实例,今天完成第 28 个,进度 29%。图像分割这一站,GrabCut 给了「全局图割」的分割思路,inpaint 给了「周边填充」的修复思路,一进一出,把「这块像素属于哪」这个核心问题讲透了。
下一篇做几何校正------透视变换(warpPerspective)和 ECC 图像对齐,一个是文档扫描矫正,一个是亚像素配准。到时候见。