操操操

实战角度!图片去水印及图片匹配替换几种方法分析

最近手上有一批图片需要去水印,同时也要对于大图中某个小部分做替换。之前网站的很多图片水印的处理方式都比较简单粗暴,确定水印加在图片上的大致位置,然后做一个不透明度100%的图片覆盖上去,完美解决问题,但是不理想的地方也显而易见,用户观感特别不好。所以,借着这次处理的机会,想把问题根除掉。本文会分四部分,零部分(你没有看错!)主要是自己尝试的路径,如果想简单直接,不失为一种有效方式。第一部分把可以应用的计算机视觉领域可能会用到的算法或者对思路有拓展的算法进行总结,同时对于有些算法的使用过程中遇到的问题,结合我自己的实战经验给出一些实践避坑指南。第二部分,对应第一部分的总结,会给出通用的实现demo,第三部分,会对本文进行总结,相信你在图片匹配替换或者去水印领域遇到相关问题,我的文章都能给你些许思路。

阅读本文之前,先把一些需要你有点概念的东西一股脑怼给你。

    1. 引子,拓展思路,因为我们这里主要说特征匹配!讲模板匹配意在表达尝试路径,也算是一种思路的拓展吧
有啥简单粗暴,直接上手的替换图片中指定区域的方法?

模板匹配,cv2.matchTemplate在这个领域内有发挥空间,但是threhold设置的值对于匹配结果有着非常明显的影响。通常这类处理,都是先把图片转成灰度图,对于rgb通道里某个颜色用控制变量法完全加强,让其出现物体的轮廓,用这种方法完成匹配,我之前把threhold调成0.98,匹配来说是比较精准的,但是问题在于加大了工作量,有些匹配的overlay在原图上不是完整出现的,这个时候,如果算法可以从overlay上自动截取坐标点的宽度和高度,然后自己匹配替换上去,思路看似完美,想必实现出来替换的效果会是极好。

但是事与愿违,我就按着这个思路,按1px的截取offset,把111px的图正反方向都生成了模板图和替换图,针对了十几张图片做测试,大家可以想象一下,十几张图片,模板图片几百张,遍历下来就是几万次,其实效率还是挺低的,但是并不急用,所以如果能跑出结果,也就不会有后面特征匹配的继续尝试了,可见效果并不好。后来想到可能是精度问题,因为我上面有讲threhold设置值越大越精确,但是对于图片角点之类的相似,但是图片上有些许变化的部分,设置过大的threshold就无法覆盖到这些图片,于是我调小了,发现匹配变得更加面目全非,于是搜寻可以确保一定精度的情况下缩小可用匹配数量的方法,于是non-maxmium-supression(非极大值抑制)出现在了我的视野中。

这个概念是我面临问题非常好的一个解决方向,我之前没有使用这个限定条件前,如果我降低threshold的值(建议值范围是0.80~0.95),那么,什么出乎意料的匹配结果都能给你搞出来,非常的不精准,但是引入了这个函数之后,匹配结果就很精准了,基本没有匹配失误的地方。它的原理也被广泛应用到人工智能大方异彩的视觉技术相关领域,比如说边缘检测、人眼检测、目标检测(DPM、YOLO、SSD、Faster RNN等等)

再比如,你进带门禁卡的大楼,我们公司使用的是刷工卡磁条的方式,这比较原始,现在有些公司已经使用人脸识别技术。识别的时候,可能同一时刻,入镜几个不同的人脸,以哪个作为target来识别呢?相对科技不进步的处理方式,就是“麻烦后面的兄弟,你往出站站,机器识别不到我了”。而先进一点的,它会根据人头在画面中的比例,来确定权重,甚至几个头像在里面一次都能给你识别到,显然如果用户体验更好一点,权重这种就有种比较无缝对接的感觉。

这里就要再强调下,如果想搞计算机视觉方向,那numpy这个人工智能库一定要学好,就比如说有了这个non-maxminum-supression帮你过滤掉匹配度不高的元素,但是由于我是要做图片替换的,如果你不了解numpy,往下的工作你也进行不了,因为所谓的图片替换,其本质原理就是识别出你传入的模板图片,根据这个模板图片匹配整张图中匹配程度大于80%或更高的部分,返回它的坐标点。non-maxmium-supression起的作用就到此为止了,后面你只有了解怎么把overlay或者mask给overwrite上去,你的工作才能真的完活。亦或是,后续涉及到图像、视频处理的领域,比如你打算给视频去水印加水印、拼接图片、做视频的转场特效、做视频的ken-Burns特效等等等等,你都要依赖numpy的二、三、四维来帮你确定维度点或者填数据。

总之,使用模板匹配,并且针对它的不断探索,实现了一些优化,但是最终还是没有解决我的问题,于是我就改弦更张,于是有了下面这些特征匹配的算法出现在视野里。

    1. 在特征匹配领域有哪些常见的算法?

总结如下:

角点检测

这个就是很原始了,但是问题也是最显示的,下面对比的算法会以它为对照组,说明它存在的劣势,但是诞生的比较早,不需要特别高精度的场景可以使用。

SIFT算法

都是脱胎于最早的角点检测算法,但是图像中像素大小会对检测结果造成影响,不匹配率比较高,因此就引入了DoG和SIFT算法进行检测。Opencv的SIFT类实际是对DoG和SIFT算法的组合。进一步精细,DoG是对同一图像使用不同的高斯滤波所得的结果,SITF是通过特征向量来描述关键点周围区域的情况。

SURF算法

opencv-python(也就是我们做计算机视觉常用的cv2,如果你使用上述方法的算法抽象出的封装函数的时候,如果报这些函数不存在,可能你需要安装opencv-contrib-python这个library,它与opencv-python不是一个东西,这点要注意了),它是Hessian算法检测的关键点,而SURF是提到关键特征,这与SIFT很像了,Opencv的SURF类是Hessian算法和SURF算法的组合。

ORB算法

ORB算法是2011年左右新推出的特征匹配算法,特点是效率更高(相比于前两者),它是基于FAST关键点检测和BRIEF描述技术的组合。其中FAST是新兴的特征检测算法,BRIEF只是一种描述符,是图像的一种表示方法,可以用于比较两个图像的关键点描述,可作为特征匹配的一种方法。

暴力匹配

看这名字就知道咋回事了。我也举一个我之前使用比较多的方式做举例,cv2.matchTemplate,称为模板匹配。由于我之前做一些图片位替换(把大图中小图替换成我指定的小图),图片大小固定,不用做scale变换匹配,也不用做rotate变换匹配,这种算法还是很简单高效的。比如我替换一个方块图片成另外一个广块图片,那我直接先用img[y_axis:y_axis+h,x_axis:x_axis+w]把要匹配的图像先抽取成独立的图片(当然你也可以用photoshop等图像编辑软件,但是如果数量太大,你用photoshop就不现实了),然后遍历所有要处理的图像,把所有要匹配的图像按像素级别全部生成template(因为有些图像跨图截取,并不是完整的正方形平面图像,而是它的上部分或下部分,总之只是一部分),再把替换的mask依照一样原理生出对等数量mask,然后用glob.glob按前缀来匹配进行替换,threshold设置成0.80~0.95(这也是官方建议的通道值,匹配效果最好),但是这种匹配下来,不是特别理想,而且模板和替换图生成数量巨大,即使开多进程(并行执行)来跑,再配以多线程(高并发,但是python的并发执行效果都知道怎么回事),效率依然很低。当然,这也是我一直心心念念,想通过特征匹配方式一劳永逸解决问题的原因。这个举例也只意在说明暴力匹配方式的运行逻辑,不只计算机视觉领域、常规业务编程、自然语言处理领域这种逻辑也有着广泛的使用,毕竟凡事都讲投产比的。

了解了暴力匹配的定义,我们很多算法的拓展或者实际的使用过程中,都是将上述那些基础算法与暴力方法结合着使用,就是用暴力匹配或者比暴力匹配更优一点的算法进行粗筛,再用上述那些算法得到最终结果
K-最近邻匹配(KNN),也依托于BFMatcher暴力匹配

计算机视觉领域,KNN算法属于最基础常用的算法了。特征匹配领域的暴力匹配算法中,BFMatcher(遍历描述符,确定描述符是否匹配,然后计算匹配距离并排序)算法是可以与其它算法最大程度整合的暴力匹配专属算法了。

FLANN匹配,相对于BFMatcher算法来讲,FLANN算法更加准确、快速、方便

FLANN库全称是Fast Library for Approximate Nearest Neighbors,它是目前最完整的(近似)最近邻开源库。不但实现了一系列查找算法,还包含了一种自动选取最快算法的机制,可以根据数据本身特征选取最佳算法来处理数据集,值得注意的是FLANN匹配只能使用SURF和SIFT算法。

    1. 对于上述引伸出的算法,给出具体的实现demo
角点检测
import cv2
import numpy as np

img = cv2.imread('chess_board.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = np.float32(gray)
dst = cv2.cornerHarris(src=gray, blockSize=9, ksize=23, k=0.04)
a = dst>0.01 * dst.max()
img[a] = [0, 0, 255]
while (True):
 cv2.imshow('corners', img)
 if cv2.waitKey(120) & 0xff == ord("q"):
  break
 cv2.destroyAllWindows()
SIFT算法
import cv2
imgpath = 'varese.jpg'
img = cv2.imread(imgpath)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
sift = detector = cv.SIFT_create() #我使用的是4.7.x版本 如果是3.x.x要查文档看对应封装的函数
keypoints, descriptor = sift.detectAndCompute(gray, None)

img = cv2.drawKeypoints(image=img, outImage=img, keypoints = keypoints, flags=cv2.DRAW_MATCHES_FLAGS_DEFAULT, color = (51, 163, 236))

cv2.imshow('sift_keypoints', img)
while (True):
 if cv2.waitKey(120) & 0xff == ord("q"):
  break
cv2.destroyAllWindows()
SURF算法
import cv2
imgpath = 'varese.jpg'
img = cv2.imread(imgpath)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
sift = cv2.xfeatures2d.SURF_create(float(4000))
keypoints, descriptor = sift.detectAndCompute(gray, None)

img = cv2.drawKeypoints(image=img, outImage=img, keypoints = keypoints, flags=cv2.DRAW_MATCHES_FLAGS_DEFAULT, color = (51, 163, 236))

cv2.imshow('sift_keypoints', img)
while (True):
 if cv2.waitKey(120) & 0xff == ord("q"):
  break
cv2.destroyAllWindows()
ORB算法
import numpy as np
import cv2
from matplotlib import pyplot as plt

img1 = cv2.imread('aa.jpg',0)
img2 = cv2.imread('bb.png',0)

orb = cv2.ORB_create()
kp1, des1 = orb.detectAndCompute(img1,None)
kp2, des2 = orb.detectAndCompute(img2,None)


bf = cv2.BFMatcher(normType=cv2.NORM_HAMMING, crossCheck=True)
matches = bf.match(des1,des2)
matches = sorted(matches, key = lambda x:x.distance)
img3 = cv2.drawMatches(img1=img1,keypoints1=kp1,img2=img2,keypoints2=kp2, matches1to2=matches, outImg=img2, flags=2)
plt.imshow(img3),plt.show()
暴力匹配
#SURF和SIFT算法+暴力匹配
将orb = cv2.ORB_create()改为
orb = cv2.xfeatures2d.SURF_create(float(4000))

将bf = cv2.BFMatcher(normType=cv2.NORM_HAMMING, crossCheck=True)改为
bf = cv2.BFMatcher(normType=cv2.NORM_L1, crossCheck=True)

#获取匹配关键点的位置 
x,y = kp1[matches[0].queryIdx].pt
cv2.rectangle(img1, (int(x),int(y)), (int(x) + 5, int(y) + 5), (0, 255, 0), 2)
cv2.imshow('a', img1)

x,y = kp2[matches[0].trainIdx].pt
cv2.rectangle(img2, (int(x1),int(y1)), (int(x1) + 5, int(y1) + 5), (0, 255, 0), 2)
cv2.imshow('b', img2)

img3 = cv2.drawMatches(img1=img1,keypoints1=kp1,img2=img2,keypoints2=kp2, matches1to2=matches[:1], outImg=img2, flags=2)
plt.imshow(img3),plt.show()
K-最近邻匹配(KNN),也依托于BFMatcher暴力匹配
import numpy as np
import cv2
from matplotlib import pyplot as plt

img1 = cv2.imread('aa.jpg',0)
img2 = cv2.imread('bb.png',0)

orb = cv2.ORB_create()
kp1, des1 = orb.detectAndCompute(img1,None)
kp2, des2 = orb.detectAndCompute(img2,None)

bf = cv2.BFMatcher(normType=cv2.NORM_HAMMING, crossCheck=True)
matches = bf.knnMatch(des1,des2,k=1)

img3 = cv2.drawMatchesKnn(img1=img1,keypoints1=kp1,img2=img2,keypoints2=kp2, matches1to2=matches, outImg=img2, flags=2)
plt.imshow(img3),plt.show()
FLANN匹配,相对于BFMatcher算法来讲,FLANN算法更加准确、快速、方便
import numpy as np
import cv2
from matplotlib import pyplot as plt

queryImage = cv2.imread('aa.jpg',0)
trainingImage = cv2.imread('bb.png',0)

sift = detector = cv.SIFT_create() #我使用的是4.7.x版本 如果是3.x.x要查文档看对应封装的函数
# sift = cv2.xfeatures2d.SURF_create(float(4000))
kp1, des1 = sift.detectAndCompute(queryImage,None)
kp2, des2 = sift.detectAndCompute(trainingImage,None)

indexParams = dict(algorithm=0, trees=5)
searchParams = dict(checks=50)
flann = cv2.FlannBasedMatcher(indexParams,searchParams)
matches = flann.knnMatch(des1,des2,k=2)

matchesMask = [[0,0] for i in range(len(matches))]

for i,(m,n) in enumerate(matches):
  if m.distance < 0.7*n.distance:
    matchesMask[i] = [1,0]

drawParams = dict(matchColor = (0,255,0),
          singlePointColor = (255,0,0),
          matchesMask = matchesMask,
          flags = 0)
resultImage = cv2.drawMatchesKnn(queryImage,kp1,trainingImage,kp2,matches,None,**drawParams)
plt.imshow(resultImage,),plt.show()

#FLANN的单应性匹配
import numpy as np
import cv2
from matplotlib import pyplot as plt

MIN_MATCH_COUNT = 10

img1 = cv2.imread('tattoo_seed.jpg',0)
img2 = cv2.imread('hush.jpg',0)

# 使用SIFT检测角点
sift = cv2.xfeatures2d.SIFT_create()
# 获取关键点和描述符
kp1, des1 = sift.detectAndCompute(img1,None)
kp2, des2 = sift.detectAndCompute(img2,None)

# 定义FLANN匹配器
index_params = dict(algorithm = 1, trees = 5)
search_params = dict(checks = 50)
flann = cv2.FlannBasedMatcher(index_params, search_params)
# 使用KNN算法匹配
matches = flann.knnMatch(des1,des2,k=2)

# 去除错误匹配
good = []
for m,n in matches:
  if m.distance < 0.7*n.distance:
    good.append(m)

# 单应性
if len(good)>MIN_MATCH_COUNT:
  src_pts = np.float32([ kp1[m.queryIdx].pt for m in good ]).reshape(-1,1,2)
  dst_pts = np.float32([ kp2[m.trainIdx].pt for m in good ]).reshape(-1,1,2)
  M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC,5.0)
  matchesMask = mask.ravel().tolist()
  h,w = img1.shape
  pts = np.float32([[0,0],[0,h-1],[w-1,h-1],[w-1,0]]).reshape(-1,1,2)
  dst = cv2.perspectiveTransform(pts,M)

  img2 = cv2.polylines(img2,[np.int32(dst)],True,(255,0,0),3, cv2.LINE_AA)

else:
  print("Not enough matches are found - %d/%d") % (len(good),MIN_MATCH_COUNT)
  matchesMask = None

draw_params = dict(matchColor = (0,255,0), # draw matches in green color
          singlePointColor = None,
          matchesMask = matchesMask, # draw only inliers
          flags = 2)
img3 = cv2.drawMatches(img1,kp1,img2,kp2,good,None,**draw_params)
plt.imshow(img3, 'gray'),plt.show()
    1. 对于本文进行总结

至此,文章也至结尾,对于本文进行一下总结。开篇通过我自己模板匹配没有达到理想结果的例子,引出了特征匹配。我们先介绍了特征匹配领域常用的一些算法,以及这些算法间的灵活组合导致擅长的特定领域。接着在第二部分,针对于提到的特征匹配算法,给出了一些实践性的demo,由于tensorflow的版本问题,有些网络文章copy下来的代码会报错,这时候,可能就是package版本升级,把一些方法重命名了,或者进一步做了封装导致的结果,解决方案就是拿到官方的最新版本的文档,利用关键字查询等方式,来确定变更部分,并在代码里进行调整。计算机视觉领域算法的落地算是现实生活所有人感受度最高的一个人工智能分支,像人脸识别、自动收费停车场、电商物流公司的自动机器人流转等等,这些商业上的的落地,都离不开边缘检测、人眼检测、物体检测这些基础的算法,所以,如果对于它们了解不深入的话,想把这些技术进步对生产效能的提升应用在自己的工作和日常中也是不可能的,功夫一定是要花到的。才能真正的为已所用,感谢阅读。

Avatar

Aisen

Be water,my friend.
扫码关注公众号,可领取以下赠品:
《夯实基础的go语言体系建设》645页涵盖golang各大厂全部面试题,针对云原生领域更是面面俱到;
扫码加微信,可领取以下赠品:
【完整版】本人所著,原价1299元的《爱情困惑者必学的七堂课》; 50个搞定正妹完整聊天记录列表详情点这里
【完整版】时长7小时,原价699元《中国各阶层男性脱单上娶指南》;