回答我自己的问题:opencv (for python) 与 scikit-image 配对几乎可以让我们分两步到达那里。
- 在两张图片之间执行SSIM 比较,捕捉 bbox 轮廓相对于第二张图片的各种差异
- 对于第二张图片中的每个轮廓,对第一张图片执行template matching,这会告诉我们差异轮廓是“变化”还是“平移”。
在代码中,假设两个图像imageA 和imageB,具有相同的尺寸:
import cv2
import imutils
from skimage.metrics import structural_similarity
# ...a bunch of functions will be going here...
diffs = compare(imageA, imageB, gray(imageA), gray(imageB), [])
if len(diffs) > 0:
highlight_diffs(imageA, imageB, diffs)
else:
print("no differences detected")
与:
def gray(img):
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
def compare(or1, or2, im1, img2, diffs):
(score, diff) = structural_similarity(im1, img2, full=True)
diff = (diff * 255).astype("uint8")
thresh = cv2.threshold(diff, 0, 255,
cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]
contours = cv2.findContours(thresh.copy(),
cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
contours = imutils.grab_contours(contours)
# aggregate the contours, throwing away duplicates
for c in contours:
(x, y, w, h) = cv2.boundingRect(c)
region = [x, y, x + w, y + h]
try:
diffs.index(region)
except ValueError:
diffs.append(region)
return diffs
现在,cv2.RETR_EXTERNAL 应该只产生“外部轮廓”,例如如果有差异inside 其他差异(比如说一个盒子的边框颜色改变了,盒子里面的一些文字也改变了),它应该只产生一个盒子,作为外部(“外部”)盒子。
除了那不是它的作用,所以我写了一个愚蠢的函数,天真地除掉内盒:
def filter_diffs(diffs):
def not_contained(e, diffs):
for t in diffs:
if e[0] > t[0] and e[2] < t[2] and e[1] > t[1] and e[3] < t[3]:
return False
return True
return [e for e in diffs if not_contained(e, diffs)]
然后在使用颜色矩形突出显示差异的函数中使用。
RED = (0,0,255)
def highlight_diffs(a, b, diffs):
diffed = b.copy()
for area in filter_diffs(diffs):
x1, y1, x2, y2 = area
cv2.rectangle(diffed, (x1, y1), (x2, y2), RED, 2)
cv2.imshow("Diffed", diffed)
这是我们的第一部分。截取 Stackoverflow 的截图,然后在将左侧广告向下移动并重新着色 --yellow-100 CSS 变量后再次截取:
这会找到五个差异,但其中两个并不是真正的“差异”,因为它是新内容或已删除的内容,而是“我们将某事物向下移动”的结果。
那么,让我们添加模板匹配:
def highlight_diffs(a, b, diffs):
diffed = b.copy()
for area in filter_diffs(diffs):
x1, y1, x2, y2 = area
# is this a relocation, or an addition/deletion?
org = find_in_original(a, b, area)
if org is not None:
cv2.rectangle(a, (org[0], org[1]), (org[2], org[3]), BLUE, 2)
cv2.rectangle(diffed, (x1, y1), (x2, y2), BLUE, 2)
else:
cv2.rectangle(diffed, (x1+2, y1+2), (x2-2, y2-2), GREEN, 1)
cv2.rectangle(diffed, (x1, y1), (x2, y2), RED, 2)
cv2.imshow("Original", a)
cv2.imshow("Diffed", diffed)
cv2.waitKey(0)
使用以下模板匹配代码,非常严格阈值为“我们发现的匹配是否真的很好”:
def find_in_original(a, b, area):
crop = b[area[1]:area[3], area[0]:area[2]]
result = cv2.matchTemplate(crop, a, cv2.TM_CCOEFF_NORMED)
(minVal, maxVal, minLoc, maxLoc) = cv2.minMaxLoc(result)
(startX, startY) = maxLoc
endX = startX + (area[2] - area[0])
endY = startY + (area[3] - area[1])
ocrop = a[startY:endY, startX:endX]
# this basically needs to be a near-perfect match
# for us to consider it a "moved" region rather than
# a genuine difference between A and B.
if structural_similarity(gray(ocrop), gray(crop)) >= 0.99:
return [startX, startY, endX, endY]
我们现在可以比较原始图片和修改后的图片,看到广告在修改后的图片中移动了,而不是“新内容”,我们可以看到它在原始图片中的位置:
就是这样,我们有一个视觉差异,它实际上告诉我们一些有用的变化,而不是告诉我们哪个像素恰好是不同的颜色。
我们可以将模板匹配阈值降低一点,例如 0.95,在这种情况下,空白框最终也会匹配到原始图像,但是因为它只是空白,所以它会匹配到几乎没有意义的东西(在这种特殊情况下,它将与原件右下角的空白匹配)。
当然,提高生活质量的方法是循环使用颜色,以便各个移动的部分都可以通过它们共享的颜色相互关联,但这是任何人都可能自己在此代码之上添加的东西.