
算法 | 轮廓提取 ------ 关于像素、阈值和直觉的碎碎念
文章目录
- [算法 | 轮廓提取 ------ 关于像素、阈值和直觉的碎碎念](#算法 | 轮廓提取 —— 关于像素、阈值和直觉的碎碎念)
被圈出来的轮廓就是艺术,舒爽又轻松。
其实提取图形轮廓这件事儿,说难倒真不算难,逻辑也就那几步:像素扫一遍,不顺眼的丢掉,顺眼的凑一块儿,最后拉个线。但真动起手来你会发现,这活儿细碎得很,全是在跟几个参数玩微调。
趁着这时候脑子还热乎,赶紧把这今天的思考记下来,省得以后再掉进同样的坑里。
并不是所有像素都配得上"特征"二字
第一件事就是采样。面对一张 4000x4000 的大图,你不可能傻乎乎地每个像素都去算一遍,那电脑风扇还不得起飞?所以加了个"采样步长"不过分吧~
这步长挺玄乎的。你要是设成 1,那确实精细,连纸张的纹路都能给你抠出来,但没必要;你要是设成 5,那些尖尖的角全变圆了,有的细线直接被跳过去断开了。好比你走路,步子跨太大了容易扯着蛋,步子太小走得又慢。我试了好久,发现符合我的图像的黄金值,既能保证图形不散架,算得又快。
简单的流程,这思路其实一眼就能看明白:
步长 SampleStep
容差 GrayTolerance
聚类半径 Radius
平滑阈值 Epsilon
加载灰度原图
采样扫描
色彩过滤
点云聚类
轮廓漫游
最终矢量轮廓
你说的黑不是黑,你说的白是什么白
背景色的识别我下意识就忽略掉了。这玩意一眼过去就知道是北京,但是算法不知道,你说他是黑的,可实际上那黑里透着灰,灰里还带着噪点。我用Binning找出了出现频率最高的灰度值,算它是背景,但你总得给它一点Buffer啊,也就是灰度容差。
这参数就是个过滤器。你设大了,图形边缘就被吞了;设小了,满屏幕都是星星点点的噪点,聚类的时候能把你烦死。在这个环节,我意识到自己并不是在做纯数学计算,而是在做一种"模糊判断"。你得凭感觉去找那个临界点,既要让目标显现出来,又要让背后的杂音闭嘴。
谁跟谁是一家?这是个问题
等点都抓出来了,怎么把它们分堆儿又是另一门学问。我用的是最直接的距离聚类:两个点离得近,那就是兄弟。这里的核心是那个"搜索半径"。
半径这个数,真的是牵一发而动全身。它得跟刚才的采样步长配合着来。因为你采样的时候是跳着走的,假如你跳了 3 个像素,那你的搜索半径至少得大过 3,不然这些点在程序眼里就是互不相识的陌生人,最后出来的轮廓全是碎玻璃渣。
我在这儿卡了,就在想这些参数怎么能不打架,具体问题具体分析,得花人力搞这种平衡。
最后的一笔:多一点嫌碎,少一点嫌秃
等所有的点都凑成堆了,要把它们连成一条线。这时候,轮廓漫游算法会给你一堆密密麻麻的点。如果直接用,那三角形的边看起来就像是狗啃的一样,全是锯齿。所以得上 Douglas-Peucker 抽稀。
算法 | Douglas-Peucker 拯救"腰椎间盘突出的三角形"
这时候,如果发现隔三差五的算出来的三角形竟然有五六七八个点。那肯定是因为平滑阈值给小了。算法太"负责"了,连边缘上一个稍微突出的毛刺都不舍得丢,结果整出来一堆伪顶点。而如果你把 Epsilon 调得太狠,三角形可能瞬间就被削成了两个点------直接压扁了。
这最后一步的抽稀,其实是我们在跟现实妥协。像素是方的,图形是圆的(或者是直的),这种天生的矛盾没法消除,只能靠 Epsilon 去抹平那层不完美的伪装。
总结一下
现在回看,这整条线上的参数都不是孤立的:
- 采样步长 决定了你有多少底料;
- 灰度容差 决定了底料里有多少杂质;
- 搜索半径 决定了这些底料能不能捏成团;
- 平滑阈值 决定了最后这团东西好不好看。
写代码不难,难的是这种对数据的掌控力。每一次调参,其实都是在跟这张图背后的物理特性对话。