image border
最近项目中搞了个图像描边的需求,常见的美图工具App都有类似的功能,典型的如美图秀秀,一开始觉得应该不太复杂,正常评估时间,实际做的时候,发现问题比想象中的复杂多了,结果项目不得不延期 😭,所以有必要搞篇文章来总结下教训。
先说整体的流程: 1、原图 -> 2、抠图 -> 3、边缘检测 -> 4、绘制边缘 -> 5、结果导出
这个流程还是很容易想到,但是除了最后一步相对来说容易点,2、3、4都是一路坑 😞
image matting
首先是抠图,就是这样的
跟我们这边的算法同学对接,爬虫收集图像、标注、模型训练 一套组合下来,效果不理想,生产环境不可用,第一步就卡住了 😞
为了赶项目周期,最后使用了阿里云的方案,这里就不多说了,算法同学持续优化模型,待成熟之后替换阿里云。
Edge detection
这一步相对来说是最复杂的,这里遇到的问题也是最大,耗时最久
最初的方案大致是这样的:抠图结果->采样缩放图像->遍历图像bitmap取满足条件的点,条件简单的理解就是点周围3x3范围的点像素值取平均值。
为什么要检测边缘,是因为要做虚线描边,获取连续的边缘点之后然后在画布上连接点画出来。
代码大概是这样的…
1 | + (NSArray *)imageFindContours:(UIImage *)image { |
这个有个致命的问题,就是找到点之后,但是没办法有序的连起来,也试过一些方案,但是图像的边缘情况太复杂了,总是有问题,这里就不多说了。
接下来就需求其他的方案,大名鼎鼎的OpenCV出场了,参考官方文档,编译产物,接入app 调试下来就能获取正确的结果了,这里就不多说了,直接看下代码,对应的节点有注释说明
1 | + (NSArray *)findContours:(UIImage *)image { |
这个方案唯一的缺陷就是需要引入OpenCV静态库,增加包大小,也想过咱只用到了边缘检测,其他的牛逼功能暂时也用不到,裁剪下只保留需要的类是不是就可以,但是大致翻了下,牵扯的太多,最终放弃了, 最后也并没有使用OpenCV的方案。
因为发现了更轻量级的方案,Suzuki边缘检测算法,后面也了解该算法其实就是OpenCV内部的一种边缘检测方案。 恰好Android同学找到了一个开源库,java版本的Suzuki边缘检测算法。
代码拉下来结合算法文档来回撸几遍,大致能理解了,android直接java拖进去用上,ios翻译成OC,也不复杂,因为算法核心方法也不过几十行代码,就算不理解,硬翻也能翻译过来。
贴一下翻译成OC的代码:
1 | + (NSArray *)findContours:(UIImage *)img threshold:(CGFloat)threshold { |
SDF
上面提到边缘检测找连续的边缘点只是为了解决虚线描边,其他的描边情况其实是用不到这些点的,但是这里也遇到问题了。
一开始的想法跟上面通过3x3范围取平均值,通过条件过滤来做的,kernel code大概是这样
1 | static NSString *KernelString = @"\ |
这套方案做demo的时候,感觉效果还行,一点点毛疵,以为是条件判断不严谨,以为后续调整下可以解决,还有个严重的问题,就是这个方案计算量太大,图片分辨率1080左右,表现就有点卡,尤其是拖动滑竿调整,描边粗细 、间距 ,实时渲染有明显的卡顿,但是我们又不能降低图片质量,所以这个方案最终也就是停留在demo阶段了。
因为计算量太大,所以想办法降低像素计算量,SDF出场了,通过距离场,可以生成一张图,这张图可以告知像素边界信息,直接通过边界信息,省去了极大的计算量。
SDF :signed distance filed
有向距离场
sdf有两种方式,一种是循环(横向x纵向) 一种是双线性(横向+纵向),很明显前一种计算量远远大于后一种,联调下来第二种方案实际效果也是相当不错了。
这里还要考虑一个问题,因为描边是有粗细跟间距的,所以可以通过调整距离参数生成图,很好的解决了描边粗细跟间距问题的,SDF方案在虚线描边的case也是有用的,通过把SDF生成图拿去做边缘检测找连续点。
来看下SDF生成图的效果
抠图
横向SDF结果
接纵向SDF结果
贴一下Metal版本的SDF计算逻辑
横向SDF
1 | extern "C" { namespace coreimage { |
纵向SDF
1 | extern "C" { |
border
有了距离场,描边的工作就一下子简单多了。 目前实现的五种描边效果就是这样式的
项目中使用coreimage自定义kernel做的,当然也可以metal搞定
sdfsourceKernelString 对应上面第三个效果
1 | static NSString *sdfsourceKernelString = @"\ |
最后一个虚线描边就是常规的连接边缘点安排画布绘制,然后跟抠图做一个合并导出,就不展开说了。
Othter
最后还有一些注意点,比如抠图图像是在边缘,则需要考虑下预留描边空间,判断是否有落在边缘,如果有则补充点空间。
还有一个就是最小包围盒,抠图很可能只占据原图的一部分预期,为了展示效果,需要把抠图的最小包围盒找到,找这个最小包围盒,不需要那么精确,找出一个差不多的最小矩形框就行,项目上用的就是粗暴的像素遍历,找四个角的位置就可以了,通过最小包围盒,也能判断抠图是否靠近边缘。
The Last
综上,关键的几个步骤基本都尝试了多种方式,分析比较得出最合适项目需求的技术方案, 最终从性能、体验等维度拿到相对不错的结果,单纯从描边功能上来说,对比修复工具也不输 O(∩_∩)O哈哈~