【问题标题】:how to find all shared regions between two images如何找到两个图像之间的所有共享区域
【发布时间】:2021-04-30 12:31:07
【问题描述】:

我一直在尝试找到能够自动找到两个图像之间所有共享区域的东西,明确不是基于像素匹配或差异,我基本上在公平之后什么都没有有点搜索。

假设我有以下两张图片,在这种情况下是网站截图。第一个“基线”:

和第二个非常相似,但有一些修改的 CSS,所以整个块都被移动了。文本内容没有变化,盒子尺寸没有变化,只是重新定位了一些元素:

在这种情况下(但实际上在所有其他情况下,其中一个是另一个的派生的两个图像将被比较),它们的像素差异实际上对于查看发生了什么变化是无用的:

事实上,即使我们应用一些简单的差异夸大,结果仍然相当无用,因为我们仍在查看像素差异,而不是基于 更改的差异 em>,所以我们不会(以任何方式)查看对视觉信息的实际修改:

这就像比较两本书,然后根据n 的多少个值来确定这本书是不同的,我们可以找到哪个book1.letters[n] != book2.letters[n]...

所以,我正在寻找一种计算相似区域的方法,显示两个图像的哪些部分编码相同的信息,但不一定在相同的边界框中。

例如,在上面的两个图像中,几乎所有的数据都是相同的,只是有些部分重新定位。唯一真正的区别是有神秘的空白。

用颜色编码的相似区域:

以及对应关系:

我找不到一个工具来做到这一点,我什至找不到允许使用 opencv 或类似技术实现这一点的教程。也许我正在寻找错误的术语,也许实际上没有人为此编写过图像比较工具(这似乎令人难以置信?),所以冒着偏离主题的风险:我尽可能多地searched and researched,这里。如果我需要它作为可以作为 QA/测试的正常(开源)工具链的一部分运行的工具,我有什么选择? (所以:对于同样昂贵的商业软件来说,不是一些昂贵的插件)。

【问题讨论】:

  • 我在很久以前做过类似的事情,所以也许看看那里...stackoverflow.com/a/27342865/2836621
  • 这看起来是一个非常复杂的问题,在达到这个结果之前你需要做几项任务:你需要将你的图像绑定在分割区域中,并比较它们中的每一个,以确定它们中的哪一个是相同,然后您需要确定是否位于同一个地方或放错地方
  • @MarkSetchell,这真是太棒了
  • 当然这是一个复杂的问题@Nightmerker。如果不是,那么已经有很多可用的实现了。复杂问题是那些在弄清楚后实际上有助于解决实际问题的问题(在这种情况下,实际上是帮助团队以有效的方式进行样式和演示 QA)。
  • “不基于像素匹配或差异”有点荒谬。您必须以某种方式比较像素。

标签: image-processing


【解决方案1】:

这是对初始区域聚类的建议。

首先,我们减去 2 张图像以找出不同的区域。然后我们将其调整为更小的规模,以实现更快的速度和更轻松的聚类。

然后我们运行形态关闭操作将所有附近的对象聚集在一起。

对结果进行阈值化以获得强信号

运行连通分量分析以获取所有边界框。

然后检查所有方框交叉点并将它们联合起来。就我而言,我只是在实体模式下重新绘制所有边界框并重新分析组件以获得区域

一旦我们有了这个,我们就可以在第二张图像上运行相同的过程,并使用简单的互相关匹配方法或任何其他奇特的匹配方法对提取的每个区域进行交叉匹配。在这种情况下,区域之间的简单宽度和高度匹配也可以。

这是我编写的代码。希望对你有帮助。

import cv2
import numpy as np


# Function to fill all the bounding box
def fill_rects(image, stats):

    for i,stat in enumerate(stats):
        if i > 0:
            p1 = (stat[0],stat[1])
            p2 = (stat[0] + stat[2],stat[1] + stat[3])
            cv2.rectangle(image,p1,p2,255,-1)


# Load image file
img1 = cv2.imread('img1.jpg',0)
img2 = cv2.imread('img2.jpg',0)

# Subtract the 2 image to get the difference region
img3 = cv2.subtract(img1,img2)

# Make it smaller to speed up everything and easier to cluster
small_img = cv2.resize(img3,(0,0),fx = 0.25, fy = 0.25)


# Morphological close process to cluster nearby objects
fat_img = cv2.dilate(small_img, None,iterations = 3)
fat_img = cv2.erode(fat_img, None,iterations = 3)

fat_img = cv2.dilate(fat_img, None,iterations = 3)
fat_img = cv2.erode(fat_img, None,iterations = 3)

# Threshold strong signals
_, bin_img = cv2.threshold(fat_img,20,255,cv2.THRESH_BINARY)

# Analyse connected components
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(bin_img)

# Cluster all the intersected bounding box together
rsmall, csmall = np.shape(small_img)
new_img1 = np.zeros((rsmall, csmall), dtype=np.uint8)

fill_rects(new_img1,stats)


# Analyse New connected components to get final regions
num_labels_new, labels_new, stats_new, centroids_new = cv2.connectedComponentsWithStats(new_img1)


labels_disp = np.uint8(200*labels/np.max(labels)) + 50
labels_disp2 = np.uint8(200*labels_new/np.max(labels_new)) + 50



cv2.imshow('diff',img3)
cv2.imshow('small_img',small_img)
cv2.imshow('fat_img',fat_img)
cv2.imshow('bin_img',bin_img)
cv2.imshow("labels",labels_disp)
cv2.imshow("labels_disp2",labels_disp2)
cv2.waitKey(0)

【讨论】:

  • 虽然是一种找到“差异区域”的有效方法,当然值得一票,但这种方法仍然遇到与标准像素匹配相同的问题:它不会突出显示两个图像之间的实际差异就视觉数据位置而言,它只是更有效地突出像素不同的区域。想想文本差异:如果我在文本文件的开头添加字母“a”,则差异是“字母 a,在开头”,而不是“文本文件中的每个字母都已更改”。我们想要前者,但这种方法仍然是后者的图像等价物。
  • (为了确认这一点,假设有人不小心设置了body { padding-top: 10em; } 而不是body { padding-top: 10px; }。第一步像素差异将产生一个初始差异,它实际上是整个页面虽然什么都没有改变:我们只是在顶部有额外的空白。我们应该找到的实际差异是那个空白边界框)
  • 我想我第一次误解了你的意图。但是,在我们找到不同的区域并将它们重新定位以匹配初始图像后,我们可以再次在 2 图像之间进行减法,并获得您正在寻找的白色区域差异。
  • 至于在文本开头添加“a”,如果该“a”导致整个文本的段落发生变化,从而基本上禁用任何全局视觉效果,那将是一个超级困难的问题申述。我能想到的唯一方法是逐字处理文本.....
  • 这不是我要说的。我想要一个图像数据的图像差异,其工作方式与文本差异的工作方式相同:显示添加/删除/重定位而不是比较像素坐标处的单个像素(这就像通过查看数组位置而不是...你知道...文本)。在图像上下文中,如果图像中的某些文本添加了 a 以重排它所在的整个段落(但不是后续段落,当然;那些只会被向下移动),那么视觉差异当然应该显示整个段落完全不同,因为它是。
【解决方案2】:

回答我自己的问题:opencv (for python) 与 scikit-image 配对几乎可以让我们分两步到达那里。

  1. 在两张图片之间执行SSIM 比较,捕捉 bbox 轮廓相对于第二张图片的各种差异
  2. 对于第二张图片中的每个轮廓,对第一张图片执行template matching,这会告诉我们差异轮廓是“变化”还是“平移”。

在代码中,假设两个图像imageAimageB,具有相同的尺寸:

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,在这种情况下,空白框最终也会匹配到原始图像,但是因为它只是空白,所以它会匹配到几乎没有意义的东西(在这种特殊情况下,它将与原件右下角的空白匹配)。

当然,提高生活质量的方法是循环使用颜色,以便各个移动的部分都可以通过它们共享的颜色相互关联,但这是任何人都可能自己在此代码之上添加的东西.

【讨论】:

    【解决方案3】:

    建议:

    如果你能分割出蓝色的句子,这个问题就会大大缓解,这可以通过形态扩张然后二值化来实现。如果膨胀足够强以至于所有字符都接触(尽管不同的文本行保持分开),则连接组件标签可以提取孔线。

    现在你有了边界框,要尝试的位置数量大大减少了。

    还可以查看 diff 算法,它可能与顺序文本相关。 https://en.wikipedia.org/wiki/Diff

    【讨论】:

    • 你指的是什么“蓝色句子”?
    • @Mike'Pomax'Kamermans 呃,再清楚不过了。它们是句子,它们是蓝色的。
    • 而且我不知道我们在说什么:我不想区分 Stackoverflow,所以我不关心“蓝色句子”,甚至这里的任何可能方式的“句子” ,我关心的是找到两个图像之间的最大共享区域,所以截图是最快的例子。您显示的差异突出了错误的东西:它突出了所有局部差异,而它需要突出显示两个图像之间匹配的整个区域。
    【解决方案4】:

    不幸的是,我无法产生确切的预期结果,但通过一个相当生硬的算法,我得到了一些接近。一般算法是:

    1. 为每张图片添加相同的随机噪声。

      请参见图 1 中的第一个和第三个窗格。向两个图像添加相同的噪声可确保可以通过相位相关(如下)比较无特征区域(例如白色背景)。

    2. 用图像 1 中的盒装子部分填充零矩阵

    此矩阵的示例在图 1 的中间窗格中给出。此图像必须与图像 1 和图像 2 具有相同的尺寸。

    1. 在步骤 2 的矩阵和噪声图像 2 之间执行相位相关。

    您可以在此处转动几个旋钮来改善最终结果。见Performing a phase correlation with fft in R

    1. 提取与最高相关值相关的 x 和 y 值“偏移”

    这些值表示步骤 2 中的矩阵应如何在 x 和 y 方向上移动,以使其最好地匹配噪声图像 2。

    1. 在步骤 3 中调整框形小节的位置,使噪声图像 1 中的所有区域都被循环覆盖。然后重复步骤 3 和 4。

    这是通过循环遍历图像 1 中的行和列来完成的。您可以遍历每个索引或跳过多个索引。

    1. 创建一个包含 x 和 y 位移的矩阵并绘制图表以观察图像 1 中的区域与图像 2 的比较情况。

    请注意,结果并不完全符合您的要求,而是接近。基本上,红色区域表示图像 1 中的相应区域不必移动。黄色区域(在这种情况下)需要稍微向下移动,橙色区域更是如此,白色区域需要向上移动。

    同样,向图像 1 和 2 添加相同的噪声是重要的一步。该算法依赖于隔离小框区域(在示例代码中,我使用了 50x50 像素框)。当您遍历图像 1 的行和列并隔离相应的加框区域时,几个区域将包含没有特征的区域。这会给相位相关带来问题,因为盒装的无特征区域在具有相似无特征背景的所有区域中将具有多个高相关值。实际上,添加噪声会为两幅图像添加特征,以减少不明确的相位相关性。

    此算法未产生与预期相同的结果的原因是,没有以巧妙的方式选择加框区域 - 当您在图像 1 的行和列上循环时选择它们。因此,取决于在您选择的框尺寸上,与图 2 相比,某些框区域的转换特征会有所不同。在 yapws87 提出的区域聚类算法之后,该算法可能会更好地工作

    这是产生这些结果的 R 代码:

    ## read in the images 
    img1 <- readJPEG('./img1.jpg')
    img2 <- readJPEG('./img2.jpg')
    
    ## grayscale the images
    img1 <- (img1[,,1]+img1[,,2]+img1[,,3])/3
    img2 <- (img2[,,1]+img2[,,2]+img2[,,3])/3
    
    
    ## rotate the images for more intuitive R plotting
    img1 <- t(apply(img1,2,rev))
    img2 <- t(apply(img2,2,rev))
    
    ## create some uniform noise 
    noise <- matrix(runif(n=nrow(img1)*ncol(img1)),nrow=nrow(img1),ncol=ncol(img1))*0.1
    
    ## add the SAME noise to both images
    img1 <- noise+img1
    img2 <- noise+img2
    
    ## remove the mean from both images (this may not be necessary) 
    img1 <- img1/mean(img1)
    img2 <- img2/mean(img2)
    
    ## Take the conjugate of the fft of the second image
    IMG2c <- Conj(fft(img2))
    
    ## define how to loop through the first image
    row.step=50
    col.step=50
    
    ## create a zero image (made with all 0s)
    zero.img <- matrix(0,ncol=ncol(img1),nrow=nrow(img1))
    
    ## initialize some vectors to hold the x and y
    ## shifts that correspond to the highest phase correlation value
    shift.x.vec=NULL
    shift.y.vec=NULL
    
    ## keep track of how many iterations you go through
    i.iters=1
    
    ## loop over the columns
    i=1
    while((i+col.step-1)<nrow(img1)) {
    
        ## keep track of how many iterations you go through
        j.iters=1
    
        ## loop over the rows
        j=1
        while((j+col.step-1)<ncol(img1)) {
    
            ## define a current 'box' as the zero image
            cbox1 <- zero.img
    
            ## then populate a small box with values from image 1
            cbox1[i:(i+row.step-1),j:(j+col.step-1)] <- img1[i:(i+row.step-1),j:(j+col.step-1)]
    
            ## PERFORM THE PHASE CORRELATION
    
            ## go into the frequency domain
            CBOX1 <- fft(cbox1)
    
            ## find a normalized value
            norm <- abs(CBOX1 * IMG2c)
    
            ## perform the phase correlation and go back to the space domain
            corr <- Re(fft((CBOX1 * IMG2c)/norm,inv=TRUE)/length(CBOX1))
    
            ## this rearranges the quadrants of the matrix see
            ## matlabs function fftshift
            corr <- fftshift(corr)
    
            ## find the x and y index values associated with the
            ## highest correlation value.
            shift <- which(corr==max(corr),arr.ind=TRUE)
            shift.x <- shift[1]
            shift.y <- shift[2]
    
            ## populate the x and y shift vectors
            shift.x.vec <- c(shift.x.vec,shift.x)
            shift.y.vec <- c(shift.y.vec,shift.y)
    
            ## THIS IS ADDITIONAL PLOTTING AND CAN BE IGNORED
            if(i.iters==6 & j.iters==6) {
                dev.new()
                ##jpeg('./example.jpeg',width=900,height=700)
                split.screen(c(1,3))
                screen(1)
                image(1:nrow(img1),1:ncol(img1),img1,col=gray.colors(200),axes=FALSE,ylab="",xlab="",useRaster=TRUE,main='Noisy Image 1')
                rect(j,i,(j+col.step-1),(i+row.step-1))
    
                screen(2)
                image(cbox1,col=gray.colors(200),axes=FALSE,useRaster=TRUE,main='Current Box')
    
                screen(3)
                image(img2,col=gray.colors(200),axes=FALSE,useRaster=TRUE,main='Noisy Image 2')
    
                ##dev.off()
            }
    
                
                
                    
            j.iters=j.iters+1
            j=j+row.step
        }
    
        i.iters=i.iters+1
        i=i+col.step
            
    }
    
    ## make a matrix of shifts values
    ## in this example, only the y shifts are interesting though
    shift.x.mat <- matrix(shift.x.vec,ncol=j.iters-1,nrow=i.iters-1,byrow=TRUE)
    shift.y.mat <- matrix(shift.y.vec,ncol=j.iters-1,nrow=i.iters-1,byrow=TRUE)
    
    
    ##jpeg('./final.jpeg',width=800,height=800)
    image(shift.y.mat,axes=FALSE,useRaster=TRUE)
    ##dev.off()
    
    
    
     
    
     
    

    【讨论】:

    • 嗯,为什么会有噪音?
    • 您说的是添加到图像 1 和 2 的噪声还是最终结果中的“噪声”?无论如何,我在最终图像之后和希望回答您问题的代码之前的另外两段中解决了这两个问题。
    • 嗯,感觉没有特征的区域在这里应该没问题:只要我们跳过它们作为寻找对应的潜在针头,我们就可以增长我们发现的任何对应,以包含尽可能多的“空" 适合的图块(我们知道初始对应坐标,当我们最终确实想要像素匹配时,比较现在已知的有界区域是有效的,具有一些受控容差)。
    • 这会减少源空间和目标空间,因为我们找到并删除对应图块,直到我们在左侧图像中留下不匹配的区域(“删除”)和不匹配的区域右图(“添加”)。
    • 有趣的概念。这需要一些技巧来确定哪些区域没有特征,然后如何种植它们,但我总体上喜欢这个想法。我会让你(或另一个堆垛机)去实现,但我很好奇结果如何!
    猜你喜欢
    • 2019-11-11
    • 2012-08-03
    • 2013-02-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-01-02
    • 2020-06-14
    相关资源
    最近更新 更多