如何扫描两个图像的差异?

Sla*_*shy 19 c# screenshot image-processing

我正在尝试扫描2个图像(32bppArgb格式),识别何时存在差异并将差异块的边界存储在矩形列表中.

假设这些是图像: 在此输入图像描述

第二: 在此输入图像描述

我想获得不同的矩形边界(在我们的例子中打开的目录窗口).

这就是我所做的:

private unsafe List<Rectangle> CodeImage(Bitmap bmp,Bitmap bmp2)
    {

        List<Rectangle> rec = new List<Rectangle>();
        bmData = bmp.LockBits(new System.Drawing.Rectangle(0, 0, 1920, 1080), System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp.PixelFormat);
        bmData2 = bmp2.LockBits(new System.Drawing.Rectangle(0, 0, 1920, 1080), System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp2.PixelFormat);

        IntPtr scan0 = bmData.Scan0;
        IntPtr scan02 = bmData2.Scan0;
        int stride = bmData.Stride;
        int stride2 = bmData2.Stride;
        int nWidth = bmp.Width;
        int nHeight = bmp.Height;
        int minX = int.MaxValue; ;
        int minY = int.MaxValue;
        int maxX = 0;
        bool found = false;


        for (int y = 0; y < nHeight; y++)
        {
            byte* p = (byte*)scan0.ToPointer();
            p += y * stride;
            byte* p2 = (byte*)scan02.ToPointer();
            p2 += y * stride2;
            for (int x = 0; x < nWidth; x++)
            {

                if (p[0] != p2[0] || p[1] != p2[1] || p[2] != p2[2] || p[3] != p2[3])//found differences-began to store positions.
                {

                    found = true;
                    if (x < minX)
                        minX = x;
                    if (x > maxX)
                        maxX = x;
                    if (y < minY)
                        minY = y;

                }

                else
                {

                    if (found)
                    {

                            int height = getBlockHeight(stride, scan0, maxX, minY,scan02,stride2);
                            found = false;
                            Rectangle temp = new Rectangle(minX, minY, maxX - minX, height);
                            rec.Add(temp);
                            //x += minX;
                            y += height;
                            minX = int.MaxValue;
                            minY = int.MaxValue;
                            maxX = 0;

                    }
                }
                p += 4;
                p2 += 4;
            }
        }

        return rec;
    }
    public unsafe int getBlockHeight(int stride, IntPtr scan, int x, int y1,IntPtr scan02,int stride2)//a function to get  an existing block height.
    {
        int height = 0; ;
        for (int y = y1; y < 1080; y++)//only for example- in our case its 1080 height.
        {
            byte* p = (byte*)scan.ToPointer();
            p += (y * stride) + (x * 4);//set the pointer to a specific potential point. 
             byte* p2 = (byte*)scan02.ToPointer();
            p2 += (y * stride2) + (x * 4); //set the pointer to a specific potential point. 
            if (p[0] != p2[0] || p[1] != p2[1] || p[2] != p2[2] || p[3] != p2[3])//still change on the height in the increasing **y** of the block.

                height++;
            }

        }
        return height;
    }
Run Code Online (Sandbox Code Playgroud)

这实际上是我调用方法的方式:

    Bitmap a = Image.FromFile(@"C:\Users\itapi\Desktop\1.png") as Bitmap;//generates a 32bppRgba bitmap;
        Bitmap b = Image.FromFile(@"C:\Users\itapi\Desktop\2.png") as Bitmap;//

        List<Rectangle> l1 = CodeImage(a, b);
        int i = 0;
        foreach (Rectangle rec in l1)
        {
            i++;
            Bitmap tmp = b.Clone(rec, a.PixelFormat);
            tmp.Save(i.ToString() + ".png");
        }
Run Code Online (Sandbox Code Playgroud)

但我没有得到确切的矩形......我只得到了一半,有时甚至更糟.我认为代码逻辑中的某些内容是错误的.

@nico代码

    private unsafe List<Rectangle> CodeImage(Bitmap bmp, Bitmap bmp2)
    {


        List<Rectangle> rec = new List<Rectangle>();
        var bmData1 = bmp.LockBits(new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp.PixelFormat);
        var bmData2 = bmp2.LockBits(new System.Drawing.Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp2.PixelFormat);

        int bytesPerPixel = 3;

        IntPtr scan01 = bmData1.Scan0;
        IntPtr scan02 = bmData2.Scan0;
        int stride1 = bmData1.Stride;
        int stride2 = bmData2.Stride;
        int nWidth = bmp.Width;
        int nHeight = bmp.Height;

        bool[] visited = new bool[nWidth * nHeight];

        byte* base1 = (byte*)scan01.ToPointer();
        byte* base2 = (byte*)scan02.ToPointer();

        for (int y = 0; y < nHeight; y+=5)
        {
            byte* p1 = base1;
            byte* p2 = base2;

            for (int x = 0; x < nWidth; x+=5)
            {
                if (!ArePixelsEqual(p1, p2, bytesPerPixel) && !(visited[x + nWidth * y]))
                {
                    // fill the different area
                    int minX = x;
                    int maxX = x;
                    int minY = y;
                    int maxY = y;

                    var pt = new Point(x, y);

                    Stack<Point> toBeProcessed = new Stack<Point>();
                    visited[x + nWidth * y] = true;
                    toBeProcessed.Push(pt);
                    while (toBeProcessed.Count > 0)
                    {
                        var process = toBeProcessed.Pop();
                        var ptr1 = (byte*)scan01.ToPointer() + process.Y * stride1 + process.X * bytesPerPixel;
                        var ptr2 = (byte*)scan02.ToPointer() + process.Y * stride2 + process.X * bytesPerPixel;
                        //Check pixel equality
                        if (ArePixelsEqual(ptr1, ptr2, bytesPerPixel))
                            continue;

                        //This pixel is different
                        //Update the rectangle
                        if (process.X < minX) minX = process.X;
                        if (process.X > maxX) maxX = process.X;
                        if (process.Y < minY) minY = process.Y;
                        if (process.Y > maxY) maxY = process.Y;

                        Point n; int idx;
                        //Put neighbors in stack
                        if (process.X - 1 >= 0)
                        {
                            n = new Point(process.X - 1, process.Y); idx = n.X + nWidth * n.Y;
                            if (!visited[idx]) { visited[idx] = true; toBeProcessed.Push(n); }
                        }

                        if (process.X + 1 < nWidth)
                        {
                            n = new Point(process.X + 1, process.Y); idx = n.X + nWidth * n.Y;
                            if (!visited[idx]) { visited[idx] = true; toBeProcessed.Push(n); }
                        }

                        if (process.Y - 1 >= 0)
                        {
                            n = new Point(process.X, process.Y - 1); idx = n.X + nWidth * n.Y;
                            if (!visited[idx]) { visited[idx] = true; toBeProcessed.Push(n); }
                        }

                        if (process.Y + 1 < nHeight)
                        {
                            n = new Point(process.X, process.Y + 1); idx = n.X + nWidth * n.Y;
                            if (!visited[idx]) { visited[idx] = true; toBeProcessed.Push(n); }
                        }
                    }

                    if (((maxX - minX + 1 )>5) & ((maxY - minY + 1)>5))
                    rec.Add(new Rectangle(minX, minY, maxX - minX + 1, maxY - minY + 1));
                }

                p1 += 5 * bytesPerPixel;
                p2 += 5 * bytesPerPixel;
            }

            base1 += 5 * stride1;
            base2 += 5 * stride2;
        }


        bmp.UnlockBits(bmData1);
        bmp2.UnlockBits(bmData2);

        return rec;
    }
Run Code Online (Sandbox Code Playgroud)

Nik*_*iki 7

我发现你的代码存在一些问题.如果我理解正确,你

  1. 找到两个图像之间不同的像素.
  2. 然后你继续从那里向右扫描,直到找到两个图像再次相同的位置.
  3. 然后从最后一个"不同"像素扫描到底部,直到找到两个图像再次相同的位置.
  4. 然后你存储那个矩形,并从它下面的下一行开始

在此输入图像描述

我到目前为止对吗?

这里有两个明显的问题:

  • 如果两个矩形具有重叠的y范围,则会遇到麻烦:您将找到第一个矩形,然后跳到底部Y坐标,忽略刚刚找到的矩形的左侧或右侧的所有像素.
  • 即使只有一个矩形,您也可以假设矩形边框上的每个像素都不同,而所有其他像素都是相同的.如果该假设无效,您将过早停止搜索,并且只能找到部分矩形.

如果您的图像来自扫描仪或数码相机,或者它们包含有损压缩(jpeg)伪像,则第二个假设几乎肯定是错误的.为了说明这一点,这就是当我将每个相同的像素标记为黑色链接的两个jpg图像以及每个不同的像素白色时,我得到的结果:

在此输入图像描述

你看到的不是一个矩形.相反,您正在寻找的矩形周围的许多像素是不同的:

在此输入图像描述

这是因为jpeg压缩工件.但即使您使用了无损源图像,边界处的像素也可能无法形成完美的矩形,因为抗锯齿或因为背景恰好在该区域中具有相似的颜色.

你可以尝试来改善你的算法,但如果你看看这条边界,你会发现所有的各种丑恶的反以任何几何假设你会成功.

实施这种"正确的方式"可能会更好.含义:

  • 实现擦除不同像素的泛洪填充算法(例如,通过将它们设置为相同或通过在单独的掩码中存储标志),然后递归地检查4个相邻像素.
  • 或者实现连接组件标记算法,该算法使用临时整数标签标记每个不同的像素,使用巧妙的数据结构来跟踪连接的临时标签.如果您只对边界框感兴趣,则甚至不必合并临时标签,只需合并相邻标记区域的边界框即可.

连接组件标签通常要快一点,但要比洪水填充更难.

最后一条建议:如果我是你,我会重新考虑你的"没有第三方图书馆"的政策.即使您的最终产品不包含第三方库,如果您使用库中经过充分记录,经过良好测试的有用构建块,然后使用您自己的代码逐个替换它们,开发可能会快得多.(谁知道,你甚至可能找到一个开源库,其中包含一个比你自己的代码快得多的合适许可证,你最终会坚持使用它......)


ADD:如果你想重新考虑你的"无库"的位置:这是一个使用AForge的快速简单的实现(它具有比emgucv更宽松的库):

private static void ProcessImages()
{
    (* load images *)
    var img1 = AForge.Imaging.Image.FromFile(@"compare1.jpg");
    var img2 = AForge.Imaging.Image.FromFile(@"compare2.jpg");

    (* calculate absolute difference *)
    var difference = new AForge.Imaging.Filters.ThresholdedDifference(15)
        {OverlayImage = img1}
        .Apply(img2);

    (* create and initialize the blob counter *)
    var bc = new AForge.Imaging.BlobCounter();
    bc.FilterBlobs = true;
    bc.MinWidth = 5;
    bc.MinHeight = 5;

    (* find blobs *)
    bc.ProcessImage(difference);

    (* draw result *)
    BitmapData data = img2.LockBits(
       new Rectangle(0, 0, img2.Width, img2.Height),
          ImageLockMode.ReadWrite, img2.PixelFormat);

    foreach (var rc in bc.GetObjectsRectangles())
        AForge.Imaging.Drawing.FillRectangle(data, rc, Color.FromArgb(128,Color.Red));

    img2.UnlockBits(data);
    img2.Save(@"compareResult.jpg");
}
Run Code Online (Sandbox Code Playgroud)

对于第二次运行,实际差异+ blob检测部分(没有加载和结果显示)大约需要43ms(由于JITting,缓存等原因,这第一次需要更长的时间)

结果(由于jpeg工件,矩形更大):

在此输入图像描述