【问题标题】:How to find the difference between two images?如何找到两个图像之间的差异?
【发布时间】:2014-10-06 20:11:20
【问题描述】:

我正在开发一个屏幕共享应用程序。在这个项目中,我需要通过互联网传输图像。显然,我不能每隔几秒钟就通过互联网发送一张新图片,这会非常慢。 我想将服务器屏幕的一张图像发送到客户端,然后再发送一张新图片,而不是发送一张新图片,仅发送自上一张图像(客户端已经拥有的图像)以来已更改的像素。

我已经写了这段代码:

private List<Color> CompareBitmaps(Image old, Image _new)
{
    List<Color> returnList = new List<Color>();

    for(int i = 0; i < old.Width; i++)
        for (int j = 0; j < old.Height; j++)
        {
            if (((Bitmap)old).GetPixel(i, j) != ((Bitmap)_new).GetPixel(i, j))
            {
                returnList.Add(((Bitmap)_new).GetPixel(i, j));
            }
        }

return returnList;
}

但是,它的运行速度太慢了。

我正在寻找一种更快、更复杂的算法。

注意:我不想要这样做的内置库。我需要一个算法。

【问题讨论】:

  • 一个想法是首先检查每个 [10th, 5th, 2nd] 等像素,如果确定更改,则优化您检查的区域。
  • GetPixel 也是一种超级慢的方式。尝试UnlockBits 方法。如果你查一下,有很多教程,而且速度要快得多。
  • 为什么要从头开始实现它?使用Aforge.Imaging。我以前用它在 c# 中进行图像处理,而且速度快得惊人。您可能想要使用Difference 过滤器。
  • 为了它的价值,here's how Aforge implements it
  • 另外,添加到List&lt;&gt; 对性能也不是很好(尽管不是问题的根源)。你应该返回IEnumerable&lt;&gt;yield return

标签: c#


【解决方案1】:

此例程通过将其他所有内容设置为几乎黑色和几乎透明的方式找到两个位图之间的差异并在第一个位图中返回它们。它还可以通过将结果添加回之前的图像来恢复原始的第二个文件..

我缩小了 800MB 1o 12k 的屏幕截图 - 但时钟指针确实只是一个非常小的变化 ;-) 如果您的图像在许多像素上有所不同,压缩将不会那么壮观..但我相信它会b 对于传输来说已经足够好了,我怀疑任何逐个像素的东西都可以与 png 或 jpg 文件格式的压缩例程进行比较。(我希望你不传输 bmps!)

该例程使用 LockBits,速度非常快。

bool 参数决定是创建差异位图还是恢复更改的位图。

public static Bitmap Difference(Bitmap bmp0, Bitmap bmp1, bool restore)
{
    int Bpp = 4;  // assuming an effective pixelformat of 32bpp
    var bmpData0 = bmp0.LockBits(
                    new Rectangle(0, 0, bmp0.Width, bmp0.Height),
                    ImageLockMode.ReadWrite, bmp0.PixelFormat);
    var bmpData1 = bmp1.LockBits(
                    new Rectangle(0, 0, bmp1.Width, bmp1.Height),
                    ImageLockMode.ReadOnly, bmp1.PixelFormat);

    int len = bmpData0.Height * bmpData0.Stride;
    byte[] data0 = new byte[len];
    byte[] data1 = new byte[len];
    Marshal.Copy(bmpData0.Scan0, data0, 0, len);
    Marshal.Copy(bmpData1.Scan0, data1, 0, len);

    for (int i = 0; i < len; i += Bpp)
    {
        if (restore)
        {
            bool toberestored = (data1[i  ] != 2 && data1[i+1] != 3 && 
                                 data1[i+2] != 7 && data1[i+2] != 42);
            if (toberestored)
            {
                data0[i  ] = data1[i];    // Blue
                data0[i+1] = data1[i+1];  // Green 
                data0[i+2] = data1[i+2];  // Red
                data0[i+3] = data1[i+3];  // Alpha
            }
        }
        else
        {
            bool changed = ((data0[i  ] != data1[i  ]) ||  
                            (data0[i+1] != data1[i+1]) || (data0[i+2] != data1[i+2]) );
            data0[i  ] = changed ? data1[i  ] : (byte)2;   // special markers
            data0[i+1] = changed ? data1[i+1] : (byte)3;   // special markers
            data0[i+2] = changed ? data1[i+2] : (byte)7;   // special markers
            data0[i+3] = changed ? (byte)255  : (byte)42;  // special markers
        }
    }

    Marshal.Copy(data0, 0, bmpData0.Scan0, len);
    bmp0.UnlockBits(bmpData0);
    bmp1.UnlockBits(bmpData1);
    return bmp0;
}

注意事项: - 我选择了一种特殊的颜色来标记那些需要在收件人处恢复的像素。在这里,我选择了alpha=42R=7; G=3; B=2;.. 不是 100% 安全,但几乎是;不会错过很多像素;也许你无论如何都没有透明度..?

我附加了两个较小的图像,都是 PNG,大约 400kB:

这是差异图像(3kB):

恢复的图像与第二张图像相同。

【讨论】:

    【解决方案2】:

    您需要返回所有更改的像素,因此复杂度必须为 m*n。

    1. (Bitmap)_new).GetPixel(i, j) 被调用了两次,使用临时值存储可能会好一些。

    2. 像素应该有几个值吧?您可以尝试创建一个名为 comprareTwoPixel(color A, color B) 的函数吗?并一一比较所有值,如果其中一个为假,则不需要比较其余的,只需返回假即可。 (不确定这是否会使其更快。)

    喜欢:

    bool comprareTwoPixel(color A, color B)
    {
        if(A.a!=B.b)
            return false;
        if(A.b!=B.b)
            return false;
        if(A.c!=B.c)
            return false;
    
        return true;
    }
    

    【讨论】:

      【解决方案3】:

      我不确定您要对这段代码做什么,因为您没有保存已更改的索引...

      您可能希望为您的 List 使用其他构造函数,因为 List() haves a default capacity of 0

      这意味着如果许多像素发生了变化,您可能会多次重新分配 List 的内部缓冲区

      也许记录已更改的平均像素数并将列表的初始容量设置为该数字可以加速您的代码。至少,你可以这么说,例如每帧有 10% 的像素变化:

      List<Color> returnList = new List<Color>( (int)( 0.10 * numberOfPixel ) );
      

      【讨论】:

        【解决方案4】:

        这可能会或可能不会完美地工作,但这里就是这样。如果您认为合适,可以通过添加 AsParallel 将其并行化。

        我还在此处复制并粘贴了几行,如果我有任何拼写错误或不匹配的变量,请随时告诉我或编辑。但这就是它的要点。这应该很快,在合理的范围内。

        本质上,由于这可能有点难以按原样理解,因此想法是“锁定”这些位,然后使用该指针将它们复制到byte[]。这有效地复制了所有 RGB(A) 值,然后您可以非常轻松地访问它们。当您阅读时,这比 GetPixel 快很多,超过一两个像素,因为抓取像素需要一点时间,但它只是简单地读取内存。

        一旦我将它们放入byte[]s,就可以很容易地比较每个像素坐标的它们。我选择使用 LINQ 以便在需要时很容易并行化,但您可能会或可能不会选择实际实现它。我不确定你是否需要。

        我在这里做了一些我认为是公平的假设,因为听起来您的实现使所有图像都来自单一来源。也就是说,我假设图像的大小和格式相同。因此,如果实际情况并非如此,您需要在此处使用一些额外的代码来解决这个问题,但这仍然很容易。

        private byte[] UnlockBits(Bitmap bmp, out int stride)
        {
            BitmapData bmpData = bmp.LockBits(new Rectangle(Point.Empty, bmp.Size), System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp.PixelFormat);
        
            IntPtr ptr = bmpData.Scan0;
        
            stride = bmpData.Stride;
        
            int bytes = Math.Abs(bmpData.Stride) * bmp.Height;
        
            byte[] ret = new byte[bytes];
        
            System.Runtime.InteropServices.Marshal.Copy(ptr, ret, 0, bytes);
        
            bmp.UnlockBits(bmpData);
        
            return ret;
        }
        
        private bool AreArraysEqual(byte[] a, byte[] b, int offset, int length)
        {
            for (int v = 0; v < length; v++)
            {
                int c = v + offset;
        
                if (a[c] != b[c])
                {
                    return false;
                }
            }
            return true;
        }
        
        private IEnumerable<KeyValuePair<Point, Tuple<Color, Color>>> GetDifferences(Bitmap a, Bitmap b)
        {
            if (a.PixelFormat != b.PixelFormat)
                throw new ArgumentException("Unmatched formats!");
        
            if (a.Size != b.Size)
                throw new ArgumentException("Unmatched length!");
        
            int stride;
            byte[] rgbValuesA = UnlockBits(a, out stride);
            byte[] rgbValuesB = UnlockBits(b, out stride);
        
            if (rgbValuesA.Length != rgbValuesB.Length)
                throw new ArgumentException("Unmatched array lengths (unexpected error)!");
        
            int bytesPerPixel = Image.GetPixelFormatSize(a.PixelFormat) / 8;
        
            return Enumerable.Range(0, a.Height).SelectMany(y =>
                   Enumerable.Range(0, a.Width)
                             .Where(x => !AreArraysEqual(rgbValuesA,
                                                         rgbValuesB,
                                                         (y * stride) + (x * bytesPerPixel),
                                                         bytesPerPixel))
                             .Select(x =>
                                     {
                                         Point pt = new Point(x, y);
        
                                         int pixelIndex = (y * stride) + (x * bytesPerPixel);
        
                                         Color colorA = ReadPixel(rgbValuesA, pixelIndex, bytesPerPixel);
                                         Color colorB = ReadPixel(rgbValuesB, pixelIndex, bytesPerPixel);
        
                                         return new KeyValuePair<Point, Tuple<Color, Color>>(pt, colorA, colorB);
                                     }
        }
        
        private Color ReadPixel(byte[] bytes, int offset, int bytesPerPixel)
        {
            int argb = BitConverter.ToInt32(pixelBytes, offset);
        
            if (bytesPerPixel == 3) // no alpha
                argb |= (255 << 24);
        
            return Color.FromArgb(argb);
        }
        
        public IEnumerable<KeyValuePair<Point, Color>> GetNewColors(Bitmap _new, Bitmap old)
        {
            return GetDifferences(_new, old).Select(c => new KeyValuePair<Point, Color>(c.Key, c.Value.Item1));
        }
        

        在真正的实现中,您可能需要比我更彻底地考虑字节顺序和像素格式,但这应该或多或少地作为概念证明,我相信应该可以处理大多数实际案例。

        正如@TaW 在评论中所说,您还可以尝试清除(可能将 alpha 设置为零)任何未更改的内容。您也可以从像素解锁中受益。同样,可能有教程会告诉您如何操作。但其中大部分可能保持不变。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-01-02
          • 2021-12-05
          相关资源
          最近更新 更多