如何扫描两张图片的差异?

19

我试图扫描两张32bppArgb格式的图片,识别出它们之间的差异,并将差异块的边界存储在一个矩形列表中。

假设这些是图片:enter image description hereenter image description here

我想要获取不同的矩形边界(在这种情况下是打开的目录窗口)。

这是我已经做过的:

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;
}

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

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");
}

但我没有得到精确的矩形...我只得到了一半甚至更差。我认为代码逻辑有问题。

@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;
    
}

当您发现第一个像素不匹配时,您不会进入if(found)块(因为它在检测的else分支中)。您难道不希望该块立即执行找到不匹配的像素吗? (如果您仍然在解决逻辑问题,请首先实现它而不使用整个步幅/扫描/像素字节,并使用GetPixel。获得最简单的可行解决方案,然后再进行性能优化。) - Chris Sinclair
除了Chris所说的,还要在调试器中跟踪它。看看会发生什么。 - D. Ben Knoble
@ChrisSinclair 好的..但是为什么它没有进入if块呢?我认为它确实进入了。因为在第一个不匹配时,我将found设置为True - Slashy
@Slashy:因为它在检测的else代码块中。当你找到第一个匹配项时,你必须等待下一轮迭代才能进入它。另外(第二次查看时),你只会在上一次迭代中检测到不匹配并且当前像素与原像素不匹配的情况下才进入它。如果像素恰好是相同的颜色,则不会进入它。检查你的if/else分支逻辑,并确保它对你的目的正确。 - Chris Sinclair
仍在寻找答案。@ChrisSinclair 我会尝试的。 - Slashy
显示剩余3条评论
4个回答

7
我看到你的代码存在几个问题。如果我理解正确,你需要:
  1. 找到两个图像之间不同的像素。
  2. 然后从这里向右扫描,直到找到两个图像再次相同的位置。
  3. 然后从上一个“不同”的像素向下扫描,直到找到两个图像再次相同的位置。
  4. 然后存储该矩形并从下面的下一行开始。

enter image description here

到目前为止,我是正确的吗?

这里有两个明显的问题:

  • 如果两个矩形有重叠的 y 范围,那么你就会遇到麻烦:你会找到第一个矩形,然后跳到底部的 Y 坐标,忽略矩形左侧或右侧的所有像素。
  • 即使只有一个矩形,你也假设矩形边框上的每个像素都不同,并且所有其他像素都相同。如果这个假设是无效的,你会停止搜索得太早,只找到矩形的一部分。

如果你的图像来自扫描仪或数码相机,或者它们包含有损压缩(JPEG)的伪像,那么第二个假设几乎肯定是错误的。为了说明这一点,下面是当我将你提供的两张 JPG 图像中每个相同的像素标记为黑色,每个不同的像素标记为白色时所得到的结果:

enter image description here

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

enter image description here

那是由于JPEG压缩伪像造成的。但即使您使用了无损源图像,由于抗锯齿或背景恰好在该区域具有类似的颜色,边框上的像素也可能不会形成完美的矩形。
您可以尝试改进算法,但如果您查看该边框,您将发现所有各种丑陋的反例都可以打破您所做出的任何几何假设。
最好按“正确的方式”实现。意思是:
要么实现flood fill算法来擦除不同的像素(例如通过将它们设置为相同或通过在单独的掩码中存储标志),然后递归检查4个相邻像素。
要么实现connected component labeling算法,使用聪明的数据结构将每个不同的像素标记为临时整数标签,并跟踪哪些临时标签连接。如果您只对边界框感兴趣,则甚至不必合并临时标签,只需合并相邻标记区域的边界框即可。

连通分量标记通常比泛洪填充快一些,但要正确实现却有点棘手。

最后一个建议:如果我是你,我会重新考虑“没有第三方库”的策略。即使你的最终产品不包含第三方库,如果你使用了文档齐全、经过充分测试和有用的构建库,开发速度可能会更快,然后再逐步用你自己的代码替换它们。(而且谁知道,你甚至可能找到一个具有合适许可证的开源库,比你自己的代码快得多,最终你会坚持使用它...)


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");
}

实际的差异加上斑点检测部分(不包括加载和结果显示)大约需要43毫秒,对于第二次运行(第一次因JIT编译、缓存等原因需要更长时间)。
结果(由于jpeg伪影,矩形较大):

enter image description here


我同意前两点,但实际上它是一张PNG图片。为什么我无法获取边缘? - Slashy
即使我想使用第三方库,比如emgucv,但迄今为止我还没有看到能够满足我的需求的东西... - Slashy
@Slashy:老实说,我对C#图像处理库不是很了解,但我相对确定它们大多数都会有计算两个图像之间像素差的绝对值、将结果二值化以及在其中查找连通组件(及其边界框)的函数。 - Niki
牛逼!但是你认为先应用diff,然后再计算块是不是有些多余了?难道我们不能直接找到不同的块吗?我知道我在要求太高了......但是我正在为此苦苦搜寻算法,从将图像分成块到使用不安全指针扫描,哈哈,所以我真的希望能够取得好的结果:)非常感谢您的回答! - Slashy
@Slashy:小更新:我刚刚意识到有一个“ThresholdedDifference”滤镜,它返回一个8位图像。使用它可以让整个过程运行大约快两倍。 - Niki
显示剩余4条评论

2
这里是基于泛洪填充的代码版本。它检查每个像素的差异。如果发现不同的像素,它会运行一个探索来找到整个不同的区域。
该代码仅作为示例。肯定有一些可以改进的地方。
unsafe bool ArePixelsEqual(byte* p1, byte* p2, int bytesPerPixel)
{
    for (int i = 0; i < bytesPerPixel; ++i)
        if (p1[i] != p2[i])
            return false;
    return true;
}

private static unsafe List<Rectangle> CodeImage(Bitmap bmp, Bitmap bmp2)
{
    if (bmp.PixelFormat != bmp2.PixelFormat || bmp.Width != bmp2.Width || bmp.Height != bmp2.Height)
        throw new ArgumentException();

    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 = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;        

    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++)
        {
            byte* p1 = base1;
            byte* p2 = base2;

            for (int x = 0; x < nWidth; ++x)
            {
                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); }
                        }
                    }

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

                p1 += bytesPerPixel;
                p2 += bytesPerPixel;
            }

            base1 += stride1;
            base2 += stride2;
        }


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

    return rec;
}

是的,我刚试了一下...整个方法只需要5秒钟...我正在寻找非常快速的东西...你认为我们可以改善这个性能吗?尝试使用“Parallel”循环,但它在堆栈方面可能会有一些问题。 - Slashy
如果您可以假设一个恒定的 bytesPerPixel 并硬编码像素比较,那么您甚至可以获得更快的速度(约50%)。并行化对于这个短时间内没有太大帮助。在我的测试中,它甚至会降低性能。 - Nico Schertler
好奇怪...找不到原因..我猜可能是哪里有个小错字 :) - Slashy
是的,这是一个打字错误(多了一个“!”)。正如其他人已经建议的那样,检查每个其他像素可以显著加快整个过程的速度。 - Nico Schertler
在帖子中查找,我只增加了 xy,但我不明白在哪里增加 base1...请参见帖子。 - Slashy
显示剩余9条评论

1
你可以使用泛洪填充分割算法轻松实现此操作。
首先,需要一个实用程序类来更轻松地访问位图。这将有助于封装复杂的指针逻辑并使代码更易读:
class BitmapWithAccess
{
    public Bitmap Bitmap { get; private set; }
    public System.Drawing.Imaging.BitmapData BitmapData { get; private set; }

    public BitmapWithAccess(Bitmap bitmap, System.Drawing.Imaging.ImageLockMode lockMode)
    {
        Bitmap = bitmap;
        BitmapData = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), lockMode, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
    }

    public Color GetPixel(int x, int y)
    {
        unsafe
        {
            byte* dataPointer = MovePointer((byte*)BitmapData.Scan0, x, y);

            return Color.FromArgb(dataPointer[3], dataPointer[2], dataPointer[1], dataPointer[0]);
        }
    }

    public void SetPixel(int x, int y, Color color)
    {
        unsafe
        {
            byte* dataPointer = MovePointer((byte*)BitmapData.Scan0, x, y);

            dataPointer[3] = color.A;
            dataPointer[2] = color.R;
            dataPointer[1] = color.G;
            dataPointer[0] = color.B;
        }
    }

    public void Release()
    {
        Bitmap.UnlockBits(BitmapData);
        BitmapData = null;
    }

    private unsafe byte* MovePointer(byte* pointer, int x, int y)
    {
        return pointer + x * 4 + y * BitmapData.Stride;
    }
}

然后创建一个代表矩形的类,其中包含不同像素,以在结果图像中标记它们。一般来说,此类还可以包含Point实例列表(或byte[,]映射),以便在结果图像中指示单个像素:

class Segment
{
    public int Left { get; set; }
    public int Top { get; set; }
    public int Right { get; set; }
    public int Bottom { get; set; }
    public Bitmap Bitmap { get; set; }

    public Segment()
    {
        Left = int.MaxValue;
        Right = int.MinValue;
        Top = int.MaxValue;
        Bottom = int.MinValue;
    }
};

一个简单算法的步骤如下:

  • 找到不同的像素
  • 使用泛洪算法在差异图像上找到段落
  • 为找到的段落绘制边界矩形

第一步是最容易的:

static Bitmap FindDifferentPixels(Bitmap i1, Bitmap i2)
{
    var result = new Bitmap(i1.Width, i2.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
    var ia1 = new BitmapWithAccess(i1, System.Drawing.Imaging.ImageLockMode.ReadOnly);
    var ia2 = new BitmapWithAccess(i2, System.Drawing.Imaging.ImageLockMode.ReadOnly);
    var ra = new BitmapWithAccess(result, System.Drawing.Imaging.ImageLockMode.ReadWrite);

    for (int x = 0; x < i1.Width; ++x)
        for (int y = 0; y < i1.Height; ++y)
        {
            var different = ia1.GetPixel(x, y) != ia2.GetPixel(x, y);

            ra.SetPixel(x, y, different ? Color.White : Color.FromArgb(0, 0, 0, 0));
        }

    ia1.Release();
    ia2.Release();
    ra.Release();

    return result;
}

第二步和第三步由以下三个函数完成:

static List<Segment> Segmentize(Bitmap blackAndWhite)
{
    var bawa = new BitmapWithAccess(blackAndWhite, System.Drawing.Imaging.ImageLockMode.ReadOnly);
    var result = new List<Segment>();

    HashSet<Point> queue = new HashSet<Point>();
    bool[,] visitedPoints = new bool[blackAndWhite.Width, blackAndWhite.Height];

    for (int x = 0;x < blackAndWhite.Width;++x)
        for (int y = 0;y < blackAndWhite.Height;++y)
        {
            if (bawa.GetPixel(x, y).A != 0
                && !visitedPoints[x, y])
            {
                result.Add(BuildSegment(new Point(x, y), bawa, visitedPoints));
            }
        }

    bawa.Release();

    return result;
}

static Segment BuildSegment(Point startingPoint, BitmapWithAccess bawa, bool[,] visitedPoints)
{
    var result = new Segment();

    List<Point> toProcess = new List<Point>();

    toProcess.Add(startingPoint);

    while (toProcess.Count > 0)
    {
        Point p = toProcess.First();
        toProcess.RemoveAt(0);

        ProcessPoint(result, p, bawa, toProcess, visitedPoints);
    }

    return result;
}

static void ProcessPoint(Segment segment, Point point, BitmapWithAccess bawa, List<Point> toProcess, bool[,] visitedPoints)
{
    for (int i = -1; i <= 1; ++i)
    {
        for (int j = -1; j <= 1; ++j)
        {
            int x = point.X + i;
            int y = point.Y + j;

            if (x < 0 || y < 0 || x >= bawa.Bitmap.Width || y >= bawa.Bitmap.Height)
                continue;

            if (bawa.GetPixel(x, y).A != 0 && !visitedPoints[x, y])
            {
                segment.Left = Math.Min(segment.Left, x);
                segment.Right = Math.Max(segment.Right, x);
                segment.Top = Math.Min(segment.Top, y);
                segment.Bottom = Math.Max(segment.Bottom, y);

                toProcess.Add(new Point(x, y));
                visitedPoints[x, y] = true;
            }
        }
    }
}

如果给定两个图像作为参数,以下程序将会执行:

static void Main(string[] args)
{
    Image ai1 = Image.FromFile(args[0]);
    Image ai2 = Image.FromFile(args[1]);

    Bitmap i1 = new Bitmap(ai1.Width, ai1.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
    Bitmap i2 = new Bitmap(ai2.Width, ai2.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

    using (var g1 = Graphics.FromImage(i1))
    using (var g2 = Graphics.FromImage(i2))
    {
        g1.DrawImage(ai1, Point.Empty);
        g2.DrawImage(ai2, Point.Empty);
    }

    var difference = FindDifferentPixels(i1, i2);
    var segments = Segmentize(difference);

    using (var g1 = Graphics.FromImage(i1))
    {
        foreach (var segment in segments)
        {
            g1.DrawRectangle(Pens.Red, new Rectangle(segment.Left, segment.Top, segment.Right - segment.Left, segment.Bottom - segment.Top));
        }
    }

    i1.Save("result.png");

    Console.WriteLine("Done.");
    Console.ReadKey();
}

产生以下结果:

enter image description here

如您所见,给定的图像之间存在更多差异。例如,您可以根据其大小过滤结果段以删除小的工件。当然,在错误检查、设计和性能方面还有很多工作要做。
一种思路是按以下方式进行:
1)将图像重新缩放为较小的尺寸(下采样)
2)在较小的图像上运行上述算法
3)在原始图像上运行上述算法,但仅限于步骤2中发现的矩形
当然,这可以扩展到多级分层方法(使用更多不同的图像尺寸,每个步骤增加精度)。

0

啊,一个算法挑战。喜欢!:-)

这里有其他答案使用了floodfill等方法,也可以很好地解决问题。我只是注意到你想要快速的东西,所以让我提出一个不同的想法。与其他人不同的是,我没有测试过它;它应该不会太难,而且应该相当快,但我目前没有时间测试它自己。如果你测试了,请分享结果。另外,请注意它不是标准算法,所以我的解释中可能有一些错误和没有专利。

我的想法源于均值自适应阈值的想法,但有很多重要的区别。我无法再从维基百科或我的代码中找到链接,所以我将从我的记忆中开始。基本上,你为两个图像创建一个新的(64位)缓冲区,并用以下内容填充:

f(x,y) = colorvalue + f(x-1, y) + f(x, y-1) - f(x-1, y-1)
f(x,0) = colorvalue + f(x-1, 0)
f(0,y) = colorvalue + f(0, y-1)

主要的技巧是你可以快速地计算图像部分的总和,即通过:
g(x1,y1,x2,y2) = f(x2,y2)-f(x1-1,y2)-f(x2,y1-1)+f(x1-1,y1-1)

换句话说,这将会得到与以下代码相同的结果:
result = 0;
for (x=x1; x<=x2; ++x) 
  for (y=y1; y<=y2; ++y)    
    result += f(x,y)

在我们的情况下,这意味着只需进行4个整数操作即可获得所需块的某个唯一编号。我认为这非常棒。
现在,在我们的情况下,我们并不真正关心平均值;我们只关心某种独特的数字。如果图像发生变化,它应该随之改变-就这么简单。至于颜色值,通常使用一些灰度数字进行阈值处理-相反,我们将使用完整的24位RGB值。因为只有很少的比较,所以我们可以简单地扫描直到找到一个不匹配的块。
我提出的基本算法如下:
for (y=0; y<height;++y)
    for (x=0; x<width; ++x)
       if (src[x,y] != dst[x,y])
          if (!IntersectsWith(x, y, foundBlocks))
              FindBlock(foundBlocks);

现在,IntersectsWith可以像四叉树一样,如果只有几个块,你可以简单地迭代这些块并检查它们是否在块的范围内。您还可以相应地更新x变量(我会这样做)。如果您有太多块(更精确地说:将找到的块从dst合并回src,然后重建缓冲区),甚至可以通过重新构建f(x,y)的缓冲区来平衡事物。

FindBlocks是有趣的地方。使用现在非常容易的g公式:

int x1 = x-1; int y1 = y-1; int x2 = x; int y2 = y; 
while (changes)
{
    while (g(srcimage,x1-1,y1,x1,y2) == g(dstimage,x1-1,y1,x1,y2)) { --x1; }
    while (g(srcimage,x1,y1-1,x1,y2) == g(dstimage,x1,y1-1,x1,y2)) { --y1; }
    while (g(srcimage,x1,y1,x1+1,y2) == g(dstimage,x1,y1,x1+1,y2)) { ++x1; }
    while (g(srcimage,x1,y1,x1,y2+1) == g(dstimage,x1,y1,x1,y2+1)) { ++y1; }
}

就是这样。请注意,FindBlocks算法的复杂度为O(x + y),对于查找2D块来说非常棒,我个人认为。:-)

正如我所说,请告诉我它的结果。


哈哈其实不是什么挑战..只是帮助一位小朋友 ;) 谢谢你的回答,是的我确实需要一个快速高效的方法来处理基本上我需要不断处理多达2m像素!如果你有空的话,我希望你能添加更多解释。我觉得我开始明白了,但还没有100%的把握。我只学习了几个月的图像处理课程..提前感谢! - Slashy
@Slashy 基本上 g(...) 函数计算一个矩形的和。我可能在计算中犯了一些错误,我记得应该使用 -1 作为 x 和 y 的值。只需在随机数字的小网格中尝试在 Excel 中实现 fg 函数,就会看到神奇的效果。 - atlaste
@Slashy 再次提醒,它们代表的是像素值的总和。这有点独特。你试过 Excel 的技巧了吗? - atlaste
我还没回家。度假中 XD。明天回来。这个总和是指a+b+g+r吗? - Slashy
1
很棒的想法。我认为这被称为“积分图像”。一个建议:不要计算两个积分图像,而是计算两个图像的绝对差的积分图像。 - Niki
显示剩余11条评论

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