前段时间公司要开发一个自拍换背景的证件照软件,之前从来没有接触过这个方面。于是看了很多相关文章,慢慢的有了思路。开始搞事情。。
2019.1.3 补充了uiimage和cv::mat相互转换,以及放大镜和擦除
简单的介绍一下流程,只需要做以下三步:
第一步:在原图上面画线,得到 mask 图
第二步:调用已经下载好的抠图算法,原图+mask图 融合出
第三步:用原图+融合图+背景图(以蓝色背景为例) 做融合
好了 更换背景成功,是不是觉得很神奇。
那下面我们来详细的讲解一下以上三步在iOS中是怎么实现的(大家如果有更好的思路可以提出来互相学习)
第一步:在原图上面画线,得到 mask 图(详解)
1.画线(标记需要处理的区域)
在这里 讲解一下得到mask图原理,以便于理解下面详细的步骤
原理: 一张图片,分为前景和背景,想更换背景,就要把前景和背景分离。已经确认的前景和背景我们不对它进行处理,真正要分离的就是背景和前景的交界处。那么这块要处理的区域我们通过画线来标记,标记为待处理区域。画完线之后,那些没有被画线的点我们如何来标记为前景和背景呢。这里就用到了种子生长算法(不知道的可以去百度一下),首先我们在左上角取一个生长点进行区域生长,生长过的区域我们把它标记为背景,遇到待处理区域,就停止生长。没有生长过 ,也没有标记过的地方把它标记为前景。这样mask图就出来了。说这么多,来一张图吧,如图
前期工作:创建一个全局的可变二维数组和原图矩阵(就原图image转换cv::mat)
思路:创建一个touchview 。根据手势划过的地方,如果设置线宽为20,取到 touchmove 走过的每一个点为中心边长为20的正方形内的点。同时去计算正方形中所有的点距离中心点的距离,把原图矩阵上 距离小于等于10的所有点的rgb值置为你想要设置的颜色,同时在二维数组上面也将这些点置为128。这样两个矩阵中就形成了和贝塞尔曲线 一样的线。现在我们创建一个矩阵大小的cv::mat格式的空白区域,要求8bit,无符号整形, 4通道。然后遍历数组,把矩阵置为跟二维数组一样的值。说到这里你肯定有点懵,来一段代码清醒一下
补充一下画线方法:这里没有采用贝塞尔曲线,而是直接在原图上面修改像素点。将划过的像素点置为255,0,0。 至于线的粗细,可以通过for循环来置。例如线宽10,那么循环就是 x-10--->x+10, y-10--->y+10
得到的是正方形。
处理一下变成圆形:计算当前点 距离中心点(x,y)这个点的距离,小于半径就可以了。只置半径内的像素点
在画线的事件里面有一点需要非常注意的:
在划的过程中一定要去判断这个点是否被置过,如果置过就不要重复再置了。如果半径为30,每划过一个点都要置3600个点。判断之后只需要置60个点。
2019.1.3 补充:
有很多人问UIimage转cv::mat 和 cv::mat 转UIimage怎么转 贴一下我的转换代码
现在需要处理的区域是标记了,我们来标记前景和背景
2.种子生长标记前景和背景
选取一个左上角的点对mask矩阵进行种子生长。把生长过的区域置为0,把没有生长过,也没有划过的部分置为255。mask的矩阵就出来了。看代码
(喜欢学习的人可以看一下)错误的思路:创建一个touchview ,创建和图片一样大小的矩阵每个点置为255。根据手势划过的地方用贝塞尔曲线连接,如果设置线宽为20,以 touchmove 走过的每一个点为中心画边长为20的正方形。同时去计算正方形中所有的点距离中心点的距离,把距离小于等于10的所有点置为128。这样矩阵中就形成了和touchview上贝塞尔曲线 一样的线。选取一个左上角的点对矩阵进行种子生长。把生长过的区域置为0,把没有生长过,也没有划过的部分置为255。mask的矩阵就出来了。然后创建一个矩阵大小的cv::mat格式的空白区域,要求8bit,无符号整形, 4通道。这个思路为什么是错的,因为用贝塞尔曲线的思路,如果要实现擦除功能是可以的,但是原图和mask上的点需要一一对应去执行,这点就比较难做到。
第一步走完了,不知道我说明白了没有。第一步能理解,很重要。让我们进入第二步
第二步:调用已经下载好的抠图算法,原图+mask图 =融合图
SharedMatting sm;
sm.loadImage(pathToImage); // load image from pathToImage
sm.loadTrimap(pathToTrimap); // load Trimap from pathToTrimap
sm.solveAlpha(); // do the shared matting algorithm
sm.save(pathToSave); // save result image
以上就是github上面的算法提供的接口,什么意思呢。
传入原图-->传入mask图-->经过吧啦吧啦一系列处理-->得到融合图
传入的是照片本地地址,我看了下它里面还是转成cv::mat格式去执行了,建议修改一下它里面的源码,让这几个接口直接传入cv::mat。这样我们就可以不用保存到本地再传入了。最后一个接口是做本地存储,不想做存储怎么办,在它的代码里面可以新加一个接口。直接把得到的cv::mat返回回来。转换成uiimage就可以展示了。来一张效果图
这一步需要注意的地方:cv::mat格式的原图和mask图在大小,字节和通道上一定要保持一致,不然报错了找都找不到。
第三步:用原图+融合图+背景图(以蓝色背景为例) 做融合
列一下融合公式(以下都是cv::mat格式的矩阵)
最终的结果图=原图矩阵 x( 融合图矩阵 / 255矩阵) + 背景矩阵 x(255矩阵-融合图矩阵)/255矩阵
简单的解释一下:因为( 融合图矩阵 / 255矩阵)只有0和1. ➕号之前得到的是前景,➕号之后得到的是背景。 相加就是全景。
该踩的坑都踩过了,应该会简便一些。
因为画线的时候 手指会挡住图片,需要画线的时候放大镜显示,以及画错之后小面积擦除。本人已经做好了,下次找个时间更新文章吧。难以掩盖即将要过元旦节的激动,提前祝大家新年快乐。
2018.12.29 下午 5:27 下班了
2019.1.3 下午3:38 更新
如何实现画线过程中的放大镜(效果图如下)
第一步:在touchbegan中截屏,在截屏的图上取手势划过的地方(范围自己取)显示在放大镜控件(uiimageview)中
第二步:在touchmove中取手势划过的地方(范围自己取),显示在放大镜控件(uiimageview)中,在事件中不断改变放大镜的位置。这一步关键在于原图已经画线了,放大的部位是从截屏上取的没有画线的,那怎么处理这一步呢。不要重复的去截屏来保持同步显示。在截屏的图中同步画线就行了,这一步很关键
(画线的方法在之前说过了)。
简单的贴下代码
实现擦除功能(这一步比较简单)
实际上就是手势触及到的部位要恢复成原图。
1.做操作之前,保存原图,保存截屏图(用于放大镜的截屏图)
2.手势触及的部位(范围自己取),从原图上取这一块的rgb值,通过for循环来修改已经画线的图对应的位置。
贴一下for循环里面的代码,应该比较好理解
因为代码是属于公司的,不方便透漏,所以就没上传代码。如果我有什么地方说得不明白可以私信我。很乐意一起学习。觉得有用的话,顺手点个赞。谢谢🙏