0x01

今天同学突然给我发了个图片,找不同

image-20200329010735656

这么复杂的图片突然萌生出一种想法,python图像识别领域这么强大,何不用python来实现一下自动寻找呢.

image-20200329011119103

原图:

raw

这里主要是涉及到模式匹配

0x02 思路

  1. 读取图片size,然后将图片均分为上下两部分
  2. 将下部分图片颠倒后和上部分图片比较,找出不同的地方
  3. 将不同的地方标注出来

但是经过操作后发现,原图上下并不是均分的…, 存在上下方向上的图像偏移.所以这个方法行不通

image-20200329012018772

这就需要找到上下两部分图片真正重合的位置了

  1. 读取图片size,然后将图片均分为上下两部分
  2. 提取上部分图像中的一小块,用于在下部分图像中定位
  3. 在下部分图像中搜索出和这一小块图像一样的地方,记录下相应坐标
  4. 利用这个坐标做相应的计算,裁剪出真正可用做对比的图像
  5. 对比最终裁剪的图像
  6. 将不同的地方标注出来

裁剪方式为: 分别从相同点出发,向四周裁剪同样的距离(由于这个图片左右方向没有平移,所以向左右两侧裁剪的宽度就可以是原图宽度)

image-20200329013229246

0x03 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# author : Lyc 2020-3-29
from PIL import Image
from PIL import ImageChops
import signal
import numpy as np
import math
import cv2 as cv
from skimage.measure import compare_ssim
import argparse
import imutils

# 原图尺寸
high=0
width=0

# 用于裁剪小图 对齐位置
rawy=200
rawx=200
# 上部分样本图片右下角在下部分中的位置 由于从一张图片切割 所以图片没有平移现象,不用考虑x轴
anchor_y=0
# 用于最终上下部分截取图片范围
down_y1=0
down_y2=0
up_y1=0
up_y2=0

# 最终截取范围 , 以锚点为中心向周围截取多少部分
m=160 # 向中心
h=200 # 向两侧

def picture_cut():
'''
进行图片上下裁剪,并截取上部分中的一个样本
'''
global width
global high
img = Image.open("./raw.jpg")
print(img.size)
width = img.size[0]
high = img.size[1] # 获取尺寸
cropped_up = img.crop((0, 0, width, high/2)) # 裁剪上部分 (left, upper, right, lower)

cropped_down = img.crop((0,high/2,width,high)) # 裁剪下部分图像
matrix=np.array(cropped_down)
img_down=Image.fromarray(matrix)
cropped_down=img_down.transpose(Image.FLIP_TOP_BOTTOM) # 将下部分图像颠倒

cropped_tem = img.crop((100,100,rawx,rawy)) # 取上部分一块样本 用作定位

cropped_up.save("./cut_up.jpg")
cropped_down.save("./cut_down.jpg")
cropped_tem.save("./tmp.jpg")

def template_demo():
'''
图片匹配,大图找小图
@target 原图
@tpl 小图
'''
global anchor_y
tpl =cv.imread("./tmp.jpg")
target = cv.imread("./cut_down.jpg")
#cv.namedWindow('template image', cv.WINDOW_NORMAL)
#cv.imshow("template image", tpl)
#cv.namedWindow('target image', cv.WINDOW_NORMAL)
#cv.imshow("target image", target)
methods = [cv.TM_SQDIFF_NORMED, cv.TM_CCORR_NORMED, cv.TM_CCOEFF_NORMED] #3种模板匹配方法
#获得模板图片的高宽尺寸
th, tw = tpl.shape[:2]
for md in methods:
print(md)
result = cv.matchTemplate(target, tpl, md)
#寻找矩阵(一维数组当做向量,用Mat定义)中的最大值和最小值的匹配结果及其位置
min_val, max_val, min_loc, max_loc = cv.minMaxLoc(result)
if md == cv.TM_SQDIFF_NORMED:
tl = min_loc
else:
tl = max_loc
br = (tl[0]+tw, tl[1]+th) #br是矩形右下角的点的坐标
anchor_y=tl[1]+th # 图片下部分锚点
print("小图右下角在原图中的的坐标{}".format(br))
#cv.rectangle(target, tl, br, (0, 0, 255), 2)
#cv.namedWindow("match-" + np.str(md), cv.WINDOW_NORMAL)
#cv.imshow("match-" + np.str(md), target) # 显示对比图片

def final_cut(up_y1,up_y2,down_y1,down_y2):
'''
裁剪最终可以用来对比的图像
'''
img = Image.open("./raw.jpg")
print("final")
cropped_up = img.crop((0, up_y1, width, up_y2)) # 裁剪上部分 (left, upper, right, lower)
cropped_down = img.crop((0,down_y1,width,down_y2)) # 裁剪下部分图像
matrix=np.array(cropped_down)
img_down=Image.fromarray(matrix)
cropped_down=img_down.transpose(Image.FLIP_TOP_BOTTOM) # 将下部分图像颠倒

cropped_up.save("./cut_final_up.jpg")
cropped_down.save("./cut_final_down.jpg")


def compare_images(path_one, path_two, diff_save_location):
"""
比较图片,如果有不同则生成展示不同的图片

@参数一: path_one: 第一张图片的路径
@参数二: path_two: 第二张图片的路径
@参数三: diff_save_location: 不同图的保存路径
"""
image_one = Image.open(path_one)
image_two = Image.open(path_two)
try:
diff = ImageChops.difference(image_one, image_two)


if diff.getbbox() is None:
# 图片间没有任何不同则直接退出
print("【+】We are the same!")
else:
diff.save(diff_save_location)
except ValueError as e:
text = ("表示图片大小和box对应的宽度不一致,参考API说明:Pastes another image into this image."
"The box argument is either a 2-tuple giving the upper left corner, a 4-tuple defining the left, upper, "
"right, and lower pixel coordinate, or None (same as (0, 0)). If a 4-tuple is given, the size of the pasted "
"image must match the size of the region.使用2纬的box避免上述问题")
print("【{0}】{1}".format(e,text))


def mark():
'''
标注出图片的不同处
'''
imageA = cv.imread("./cut_final_down.jpg")
imageB = cv.imread("./cut_final_up.jpg")
# 加载两张图片并将他们转换为灰度:
grayA = cv.cvtColor(imageA,cv.COLOR_BGR2GRAY)
grayB = cv.cvtColor(imageB,cv.COLOR_BGR2GRAY)

# 计算两个灰度图像之间的结构相似度指数:
(score,diff) = compare_ssim(grayA,grayB,full = True)
diff = (diff *255).astype("uint8")
print("SSIM:{}".format(score))

# 找到不同点的轮廓以致于我们可以在被标识为“不同”的区域周围放置矩形:
thresh = cv.threshold(diff,0,255,cv.THRESH_BINARY_INV | cv.THRESH_OTSU)[1]
cnts = cv.findContours(thresh.copy(),cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)
#cnts = cnts[0] if imutils.is_cv2() else cnts[1]
cnts = imutils.grab_contours(cnts)

# 找到一系列区域,在区域周围放置矩形:
for c in cnts:
(x,y,w,h) = cv.boundingRect(c)
cv.rectangle(imageA,(x,y),(x+w,y+h),(0,0,255),2)
cv.rectangle(imageB,(x,y),(x+w,y+h),(0,0,255),2)

# 用cv2.imshow 展现最终对比之后的图片, cv2.imwrite 保存最终的结果图片
cv.imshow("Modified",imageB)
cv.imwrite("final.png",imageB)
cv.waitKey(0)


if __name__ == '__main__':
picture_cut() #裁剪图片
template_demo()
print("在图片窗口按任意键退出")
#cv.waitKey(0)
#cv.destroyAllWindows()

up_y1=rawx-h
up_y2=rawy+m
print("high: {} anchor_y: {} ".format(high,anchor_y))
down_y1=high-anchor_y-m
down_y2=high-anchor_y+h
print("{} {} {} {}".format(up_y1,up_y2,down_y1,down_y2))
final_cut(up_y1,up_y2,down_y1,down_y2) # 裁剪最终用来对比的图像

compare_images('./cut_final_up.jpg',
'./cut_final_down.jpg',
'res_diff.jpg') # 比较两图的不同

print("最终对比图片为: res_diff.jpg final.png")
mark() # 标出不同的地方

脚本中用到的库文件下载方式:

1
2
3
4
pip3 install numpy
pip3 install opencv-python
pip3 install scikit-image
pip3 install imutils

0x04 效果

final

这些小点点还没有去除,可能与选取的阈值,图片噪声有关,但是比较大的方块圈出来的,都是可以人眼分辨出不同的地方,一共有5处

下面这个图可能更好看一些,不同的位置色彩不同

res_diff