如何找到两张图片之间的差异矩形

7
我有两张大小相同的图片。最好的方法是找到它们不同之处所在的矩形区域。显然,我可以以不同的方向4次遍历图像,但我想知道是否有更简单的方法。

first image

second image

difference


1
背景是否完全相同? - Joscha
是的,我正在尝试找到最小的矩形,使得矩形外的所有内容都相同。 - Gelatin
6
你可以将图片(矩阵)相减 - 在任何非零位置上它们都是不同的。将这个矩阵传递给一个算法,该算法计算所有行和列的总和(从外到内),直到找到一个具有非零值的行或列为止,这应该能够给出红色矩形。 - Joscha
一个有趣的库可能是AForge。在减去图像后,您可以使用它的斑点处理功能:http://www.aforgenet.com/framework/features/blobs_processing.html,还可以参考这个相关问题:http://stackoverflow.com/questions/1162669/find-bounding-rectangle-of-objects-in-monochrome-bitmaps - Dirk Vollmar
7个回答

5
如果您想要一个单矩形,请将阈值设置为int.MaxValue。
var diff = new ImageDiffUtil(filename1, filename2);
var diffRectangles = diff.GetDiffRectangles(int.MaxValue);

enter image description here

如果您想要多个矩形,请使用较小的阈值。
var diff = new ImageDiffUtil(filename1, filename2);
var diffRectangles = diff.GetDiffRectangles(8);

enter image description here

ImageDiffUtil.cs

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;

namespace diff_images
{
    public class ImageDiffUtil
    {
        Bitmap image1;
        Bitmap image2;

        public ImageDiffUtil(string filename1, string filename2)
        {
            image1 = Image.FromFile(filename1) as Bitmap;
            image2 = Image.FromFile(filename2) as Bitmap;
        }

        public IList<Point> GetDiffPixels()
        {
            var widthRange = Enumerable.Range(0, image1.Width);
            var heightRange = Enumerable.Range(0, image1.Height);

            var result = widthRange
                            .SelectMany(x => heightRange, (x, y) => new Point(x, y))
                            .Select(point => new
                            {
                                Point = point,
                                Pixel1 = image1.GetPixel(point.X, point.Y),
                                Pixel2 = image2.GetPixel(point.X, point.Y)
                            })
                            .Where(pair => pair.Pixel1 != pair.Pixel2)
                            .Select(pair => pair.Point)
                            .ToList();

            return result;
        }

        public IEnumerable<Rectangle> GetDiffRectangles(double distanceThreshold)
        {
            var result = new List<Rectangle>();

            var differentPixels = GetDiffPixels();

            while (differentPixels.Count > 0)
            {
                var cluster = new List<Point>()
                {
                    differentPixels[0]
                };
                differentPixels.RemoveAt(0);

                while (true)
                {
                    var left = cluster.Min(p => p.X);
                    var right = cluster.Max(p => p.X);
                    var top = cluster.Min(p => p.Y);
                    var bottom = cluster.Max(p => p.Y);
                    var width = Math.Max(right - left, 1);
                    var height = Math.Max(bottom - top, 1);
                    var clusterBox = new Rectangle(left, top, width, height);

                    var proximal = differentPixels
                                        .Where(point => GetDistance(clusterBox, point) <= distanceThreshold)
                                        .ToList();
                    proximal.ForEach(point => differentPixels.Remove(point));

                    if (proximal.Count == 0)
                    {
                        result.Add(clusterBox);
                        break;
                    }
                    else
                    {
                        cluster.AddRange(proximal);
                    }
                };
            }

            return result;
        }

        static double GetDistance(Rectangle rect, Point p)
        {
            var dx = Math.Max(rect.Left - p.X, 0);
            dx = Math.Max(dx, p.X - rect.Right);

            var dy = Math.Max(rect.Top - p.Y, 0);
            dy = Math.Max(dy, p.Y - rect.Bottom);
            return Math.Sqrt(dx * dx + dy * dy);
        }
    }
}

Form1.cs

using System.Drawing;
using System.Linq;
using System.Windows.Forms;

namespace diff_images
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            var filename1 = @"Gelatin1.PNG";
            var filename2 = @"Gelatin2.PNG";

            var diff = new ImageDiffUtil(filename1, filename2);
            var diffRectangles = diff.GetDiffRectangles(8);

            var img3 = Image.FromFile(filename2);
            Pen redPen = new Pen(Color.Red, 1);
            var padding = 3;
            using (var graphics = Graphics.FromImage(img3))
            {
                diffRectangles
                    .ToList()
                    .ForEach(rect =>
                    {
                        var largerRect = new Rectangle(rect.X - padding, rect.Y - padding, rect.Width + padding * 2, rect.Height + padding * 2);
                        graphics.DrawRectangle(redPen, largerRect);
                    });
            }

            var pb1 = new PictureBox()
            {
                Image = Image.FromFile(filename1),
                Left = 8,
                Top = 8,
                SizeMode = PictureBoxSizeMode.AutoSize
            };

            var pb2 = new PictureBox()
            {
                Image = Image.FromFile(filename2),
                Left = pb1.Left + pb1.Width + 16,
                Top = 8,
                SizeMode = PictureBoxSizeMode.AutoSize
            };

            var pb3 = new PictureBox()
            {
                Image = img3,
                Left = pb2.Left + pb2.Width + 16,
                Top = 8,
                SizeMode = PictureBoxSizeMode.AutoSize
            };

            Controls.Add(pb1);
            Controls.Add(pb2);
            Controls.Add(pb3);
        }
    }
}

4
一个简单的方法是从原点开始,逐行逐列地工作。比较每个像素,记录最上面、最左边、最右边和最下面的像素,从中可以计算出矩形。在一些情况下,这种单次遍历的方法会更快(即存在非常小的差异区域)。

3

像这样的图像处理很昂贵,需要查看大量位。在实际应用中,您几乎总是需要过滤图像以消除由不完美的图像捕获引起的伪影。

用于这种位操作的常见库是OpenCV,它利用可用的专用CPU指令使其快速。有几个.NET包装器可用于它,Emgu是其中之一


谈论工件,所以如果我这样做这个,会得到错误的尺寸吗?因为如果您使用我的代码并隐藏前两张图片,您将看到它几乎适合高度,但宽度混乱了。 - WiiMaxx

2

我认为没有更简单的方法。

实际上,这只需要(非常)少量的代码,所以除非你找到一个可以直接为你完成这个任务的库,否则你不会找到更短的方法。


1

想法:

将图像视为一个二维数组,其中每个数组元素都是图像的像素。因此,我认为图像差异只是二维数组差异。

想法就是仅仅通过扫描数组元素的宽度,找到像素值存在差异的位置。如果两个二维数组的[x,y]坐标不同,则启动我们的矩形查找逻辑。随后,这些矩形将用于修补最后更新的帧缓冲区。

我们需要扫描矩形的边界以查找差异,如果在矩形的边界上发现任何差异,则根据所进行的扫描类型增加宽度或高度。

例如:如果我沿着二维数组的宽度进行扫描,并发现存在一个坐标在两个二维数组中不同的位置,则我将创建一个矩形,其起始位置为[x-1,y-1],宽度和高度分别为2和2。请注意,宽度和高度指的是像素数。

例如:矩形信息: X = 20 Y = 35 W = 26 H = 23

例如,矩形的宽度从坐标[20, 35]开始-> [20, 35 + 26-1]。也许当您找到代码时,您可能能够更好地理解它。

此外,您找到的大矩形内部可能会有较小的矩形,因此我们需要从参考中删除这些较小的矩形,因为它们对我们来说毫无意义,除了占用我宝贵的空间!

上述逻辑在VNC服务器实现的情况下将非常有用,因为需要矩形来表示当前拍摄的图像中的差异。这些矩形可以通过网络发送到VNC客户端,后者可以将矩形打补丁到其所拥有的本地帧缓冲区的矩形上,从而在VNC客户端显示板上显示它。

P.S.:

我将附上我实现自己算法的代码。我请求观众指出任何错误或性能调整。我还请求观众评论是否有更好的算法可以使生活更简单。

代码:

类矩形:

public class Rect {
    public int x; // Array Index
    public int y; // Array Index
    public int w; // Number of hops along the Horizontal
    public int h; // Number of hops along the Vertical

    @Override
    public boolean equals(Object obj) {
        Rect rect = (Rect) obj;
        if(rect.x == this.x && rect.y == this.y && rect.w == this.w && rect.h == this.h) {
            return true;
        }
        return false;
    }
}

图像差异类:

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;

import javax.imageio.ImageIO;

public class ImageDifference {
 long start = 0, end = 0;

 public LinkedList<Rect> differenceImage(int[][] baseFrame, int[][] screenShot, int xOffset, int yOffset, int width, int height) {
  // Code starts here
  int xRover = 0;
  int yRover = 0;
  int index = 0;
  int limit = 0;
  int rover = 0;

  boolean isRectChanged = false;
  boolean shouldSkip = false;

  LinkedList<Rect> rectangles = new LinkedList<Rect>();
  Rect rect = null;

  start = System.nanoTime();

  // xRover - Rovers over the height of 2D Array
  // yRover - Rovers over the width of 2D Array
  int verticalLimit = xOffset + height;
  int horizontalLimit = yOffset + width;

  for(xRover = xOffset; xRover < verticalLimit; xRover += 1) {
   for(yRover = yOffset; yRover < horizontalLimit; yRover += 1) {

    if(baseFrame[xRover][yRover] != screenShot[xRover][yRover]) {
     // Skip over the already processed Rectangles
     for(Rect itrRect : rectangles) {
      if(( (xRover < itrRect.x + itrRect.h) && (xRover >= itrRect.x) ) && ( (yRover < itrRect.y + itrRect.w) && (yRover >= itrRect.y) )) {
       shouldSkip = true;
       yRover = itrRect.y + itrRect.w - 1;
       break;
      } // End if(( (xRover < itrRect.x + itrRect.h) && (xRover >= itrRect.x) ) && ( (yRover < itrRect.y + itrRect.w) && (yRover >= itrRect.y) ))
     } // End for(Rect itrRect : rectangles)

     if(shouldSkip) {
      shouldSkip = false;
      // Need to come out of the if condition as below that is why "continue" has been provided
      // if(( (xRover <= (itrRect.x + itrRect.h)) && (xRover >= itrRect.x) ) && ( (yRover <= (itrRect.y + itrRect.w)) && (yRover >= itrRect.y) ))
      continue;
     } // End if(shouldSkip)

     rect = new Rect();

     rect.x = ((xRover - 1) < xOffset) ? xOffset : (xRover - 1);
     rect.y = ((yRover - 1) < yOffset) ? yOffset : (yRover - 1);
     rect.w = 2;
     rect.h = 2;

     /* Boolean variable used to re-scan the currently found rectangle
      for any change due to previous scanning of boundaries */
     isRectChanged = true;

     while(isRectChanged) {
      isRectChanged = false;
      index = 0;


      /*      I      */
      /* Scanning of left-side boundary of rectangle */
      index = rect.x;
      limit = rect.x + rect.h;
      while(index < limit && rect.y != yOffset) {
       if(baseFrame[index][rect.y] != screenShot[index][rect.y]) {        
        isRectChanged = true;
        rect.y = rect.y - 1;
        rect.w = rect.w + 1;
        index = rect.x;
        continue;
       } // End if(baseFrame[index][rect.y] != screenShot[index][rect.y])

       index = index + 1;;
      } // End while(index < limit && rect.y != yOffset)


      /*      II      */
      /* Scanning of bottom boundary of rectangle */
      index = rect.y;
      limit = rect.y + rect.w;
      while( (index < limit) && (rect.x + rect.h != verticalLimit) ) {
       rover = rect.x + rect.h - 1;
       if(baseFrame[rover][index] != screenShot[rover][index]) {
        isRectChanged = true;
        rect.h = rect.h + 1;        
        index = rect.y;
        continue;
       } // End if(baseFrame[rover][index] != screenShot[rover][index])

       index = index + 1;
      } // End while( (index < limit) && (rect.x + rect.h != verticalLimit) )


      /*      III      */
      /* Scanning of right-side boundary of rectangle */
      index = rect.x;
      limit = rect.x + rect.h;
      while( (index < limit) && (rect.y + rect.w != horizontalLimit) ) {
       rover = rect.y + rect.w - 1;
       if(baseFrame[index][rover] != screenShot[index][rover]) {
        isRectChanged = true;
        rect.w = rect.w + 1;
        index = rect.x;
        continue;
       } // End if(baseFrame[index][rover] != screenShot[index][rover])

       index = index + 1;
      } // End while( (index < limit) && (rect.y + rect.w != horizontalLimit) )

     } // while(isRectChanged)


     // Remove those rectangles that come inside "rect" rectangle.
     int idx = 0;
     while(idx < rectangles.size()) {
      Rect r = rectangles.get(idx);
      if( ( (rect.x <= r.x) && (rect.x + rect.h >= r.x + r.h) ) && ( (rect.y <= r.y) && (rect.y + rect.w >= r.y + r.w) ) ) {
       rectangles.remove(r);
      } else {
       idx += 1;
      }  // End if( ( (rect.x <= r.x) && (rect.x + rect.h >= r.x + r.h) ) && ( (rect.y <= r.y) && (rect.y + rect.w >= r.y + r.w) ) ) 
     } // End while(idx < rectangles.size())

     // Giving a head start to the yRover when a rectangle is found
     rectangles.addFirst(rect);

     yRover = rect.y + rect.w - 1;
     rect = null;

    } // End if(baseFrame[xRover][yRover] != screenShot[xRover][yRover])
   } // End for(yRover = yOffset; yRover < horizontalLimit; yRover += 1)
  } // End for(xRover = xOffset; xRover < verticalLimit; xRover += 1)

  end = System.nanoTime();    
  return rectangles;
 }

 public static void main(String[] args) throws IOException { 
  LinkedList<Rect> rectangles = null;

  // Buffering the Base image and Screen Shot Image
  BufferedImage screenShotImg = ImageIO.read(new File("screenShotImg.png"));
  BufferedImage baseImg   = ImageIO.read(new File("baseImg.png"));

  int width  = baseImg.getWidth();
  int height = baseImg.getHeight();
  int xOffset = 0;
  int yOffset = 0;
  int length = baseImg.getWidth() * baseImg.getHeight();

  // Creating 2 Two Dimensional Arrays for Image Processing
  int[][] baseFrame = new int[height][width];
  int[][] screenShot = new int[height][width];

  // Creating 2 Single Dimensional Arrays to retrieve the Pixel Values  
  int[] baseImgPix   = new int[length];
  int[] screenShotImgPix  = new int[length];

  // Reading the Pixels from the Buffered Image
  baseImg.getRGB(0, 0, baseImg.getWidth(), baseImg.getHeight(), baseImgPix, 0, baseImg.getWidth());
  screenShotImg.getRGB(0, 0, screenShotImg.getWidth(), screenShotImg.getHeight(), screenShotImgPix, 0, screenShotImg.getWidth());

  // Transporting the Single Dimensional Arrays to Two Dimensional Array
  long start = System.nanoTime();

  for(int row = 0; row < height; row++) {
   System.arraycopy(baseImgPix, (row * width), baseFrame[row], 0, width);
   System.arraycopy(screenShotImgPix, (row * width), screenShot[row], 0, width);
  }

  long end = System.nanoTime();
  System.out.println("Array Copy : " + ((double)(end - start) / 1000000));

  // Finding Differences between the Base Image and ScreenShot Image
  ImageDifference imDiff = new ImageDifference();
  rectangles = imDiff.differenceImage(baseFrame, screenShot, xOffset, yOffset, width, height);

  // Displaying the rectangles found
  int index = 0;
  for(Rect rect : rectangles) {
   System.out.println("\nRect info : " + (++index));
   System.out.println("X : " + rect.x);
   System.out.println("Y : " + rect.y);
   System.out.println("W : " + rect.w);
   System.out.println("H : " + rect.h);

   // Creating Bounding Box
   for(int i = rect.y; i < rect.y + rect.w; i++) {    
    screenShotImgPix[ ( rect.x               * width) + i ] = 0xFFFF0000;
    screenShotImgPix[ ((rect.x + rect.h - 1) * width) + i ] = 0xFFFF0000;
   }

   for(int j = rect.x; j < rect.x + rect.h; j++) {
    screenShotImgPix[ (j * width) + rect.y                ] = 0xFFFF0000;
    screenShotImgPix[ (j * width) + (rect.y + rect.w - 1) ] = 0xFFFF0000;
   }

  }

  // Creating the Resultant Image
  screenShotImg.setRGB(0, 0, width, height, screenShotImgPix, 0, width);
  ImageIO.write(screenShotImg, "PNG", new File("result.png"));

  double d = ((double)(imDiff.end - imDiff.start) / 1000000);
  System.out.println("\nTotal Time : " + d + " ms" + "  Array Copy : " + ((double)(end - start) / 1000000) + " ms");

 }
}

描述:

将会有一个名为的函数

public LinkedList<Rect> differenceImage(int[][] baseFrame, int[][] screenShot, int width, int height)

这段代码的功能是找出两张图片之间的差异,并返回一个对象的链表,这些对象就是矩形。

主函数会测试这个算法。

主函数中传入了两张样例图片,它们分别是“baseFrame”和“screenShot”,从而生成了结果图片“result”。

我没有足够的声望来发布结果图片,但它会很有趣。

这篇博客将提供输出Image Difference


0

如果你知道如何使用Lockbit,那么这里就有一个简单的方法 :)

        Bitmap originalBMP = new Bitmap(pictureBox1.ImageLocation);
        Bitmap changedBMP = new Bitmap(pictureBox2.ImageLocation);

        int width = Math.Min(originalBMP.Width, changedBMP.Width),
            height = Math.Min(originalBMP.Height, changedBMP.Height),

            xMin = int.MaxValue,
            xMax = int.MinValue,

            yMin = int.MaxValue,
            yMax = int.MinValue;

        var originalLock = originalBMP.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, originalBMP.PixelFormat);
        var changedLock = changedBMP.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, changedBMP.PixelFormat);

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                //generate the address of the colour pixel
                int pixelIdxOrg = y * originalLock.Stride + (x * 4);
                int pixelIdxCh = y * changedLock.Stride + (x * 4);


                if (( Marshal.ReadByte(originalLock.Scan0, pixelIdxOrg + 2)!= Marshal.ReadByte(changedLock.Scan0, pixelIdxCh + 2))
                    || (Marshal.ReadByte(originalLock.Scan0, pixelIdxOrg + 1) != Marshal.ReadByte(changedLock.Scan0, pixelIdxCh + 1))
                    || (Marshal.ReadByte(originalLock.Scan0, pixelIdxOrg) != Marshal.ReadByte(changedLock.Scan0, pixelIdxCh))
                    )
                {
                    xMin = Math.Min(xMin, x);
                    xMax = Math.Max(xMax, x);

                    yMin = Math.Min(yMin, y);
                    yMax = Math.Max(yMax, y);
                }
            }
        }

        originalBMP.UnlockBits(originalLock);
        changedBMP.UnlockBits(changedLock);

        var result = changedBMP.Clone(new Rectangle(xMin, yMin, xMax - xMin, yMax - yMin), changedBMP.PixelFormat);

        pictureBox3.Image = result;

看起来你的两张图片包含的差异超过了肉眼可见的范围,所以结果会比你预期的更宽泛。但是你可以添加一个容差,这样即使其余部分不完全相同,也能够适应。
为了加快速度,你可能可以使用Parallel.For,但只在外层循环中使用。

0

我认为,在每个方向上彻底搜索第一个不同点是最好的方法,除非你知道某个事实以某种方式限制了不同点的集合。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接