如何使用按位/布尔逻辑进行颜色或数字替换

时间:2011-11-28 20:54:26

标签: c# winforms image boolean-logic

如何在不使用if语句的情况下进行类似下面代码的颜色替换,而是使用布尔代数(或其他一些不会引入条件逻辑的魔法)

问题(借口代码):

private Image ReplaceRectangleColors(Bitmap b, 
                                     Rectangle rect, 
                                     Color oldColor, 
                                     Color newColor)
    {
        BitmapData bmData = b.LockBits(rect, 
                                       ImageLockMode.ReadWrite,
                                       PixelFormat.Format24bppRgb);
        int stride = bmData.Stride;
        IntPtr Scan0 = bmData.Scan0;

        byte red = 0;
        byte blue = 0;
        byte green = 0;

        unsafe
        {
            byte * p = (byte *)(void *)Scan0;
            int nOffset = stride - rect.Width *3; 

            for(int y=0; y < rect.Height; ++y)
            {
                for(int x=0; x < rect.Width; ++x )
                {
                    red = p[0];
                    blue = p[1];
                    green = p[2];

                    if (red == oldColor.R 
                        && blue == oldColor.B 
                        && green == oldColor.G)
                    {
                        p[0] = newColor.R;
                        p[1] = newColor.B;
                        p[2] = newColor.G;
                    }

                    p += 3;
                }
                p += nOffset;
            }
        }

        b.UnlockBits(bmData);

        return (Image)b;
    }

我遇到的问题是,如果图像很大,则此代码会被执行多次并且格式不佳。我知道必须有一种方法用更清洁/更快的东西替换颜色替换。有什么想法吗?

总结和简化,我想转向     if(红色== oldColor.R         &安培;&安培;蓝色== oldColor.B         &安培;&安培;绿色== oldColor.G)     {        red = newColor.R;        blue = newColor.B;        green = newColor.G;     }

进入不包含if语句的位操作。

2 个答案:

答案 0 :(得分:2)

没有任何按位操作可以替换一种颜色的像素。实际上,读取像素,应用按位操作并将结果写回每个像素可能比读取像素更慢,只做任何工作并将其写回如果它匹配你的目标颜色。

然而,随着复杂程度的提高,可以采取一些措施来加速代码:

1)您可以做的第一件事是在进行比较之前不要读取3个字节。如果只读取比较所需的每个字节,那么在红色字节不匹配的情况下,不需要读取或比较绿色/蓝色字节。 (优化者可能会代表您完成此工作)

2)通过访问存储在其中的地址顺序中的数据来使用缓存一致性。(您通过在内部循环中放置x来处理扫描线来实现此目的。)

3)使用多线程。将图像分成(例如)4个条带,然后并行处理它们,如果你有一个4+核心处理器,你应该可以获得“几倍”的加速。

4)通过使用32位或64位值而不是4或8位8位值,您可以更快地工作几倍。这是因为从内存中提取一个字节可能需要相同的时间(给出或采取一些高速缓存一致性等)来获取整个CPU寄存器(4或8个字节)。一旦在寄存器中获得了值,就可以进行单次比较(RGBA)而不是四次(分别为R,G,B,A字节),然后进行单次写回 - 可能会快4倍。这是一个简单的例子(对于32-bpp图像),因为它们方便地适合每像素一个像素,因此您可以使用32位整数在一次操作中读取/比较/写入整个RGBA像素。 / p>

但是对于其他图像深度,您将遇到更难的情况,因为每个像素中的字节数与32位int的大小不完全匹配。例如,对于24bpp图像,您需要读取三个32位双字(12字节),以便在循环的每次迭代中处理四个像素(3字节x 4 = 12)。你需要使用按位操作来剥离这3个整数并将它们与你的'oldcolour'进行比较(见下文)。另外一个复杂因素是,如果要在4像素跳转中处理它,必须注意不要在每条扫描线的末端运行。类似的过程适用于使用64位长度或处理较低的bpp图像 - 但是您必须开始执行更复杂的逐位操作以将数据干净地拉出来,并且它会变得非常复杂。

那你如何比较像素?

第一个像素很容易。

int oldColour = 0x00112233;    // e.g. R=33, G=22, B=11
int newColour = 0x00445566;

int chunk1 = scanline[i];      // Treating scanline as an array of int, read 3 ints (12 bytes)
int chunk2 = scanline[i+1];    // We cache them in ints as we will read/write several times
int chunk3 = scanline[i+2];

if (chunk1 & 0x00ffffff == oldColour)              // read and check 3 bytes of pixel
    chunk2 = (chunk2 & 0xff000000) | newColour;    // Write back 3 bytes of pixel

下一个像素在第一个int中有一个字节,在下一个int中有2个字节:

if ((chunk1 >> 24) == (oldColour & 0xff))    // Does B byte match?
{
    if ((chunk2 & 0x0000ffff) == (oldColour >> 8))
    {
        chunk1 = (chunk1 & 0x00ffffff) | (newColour & 0xff);   // Replace B byte in chunk1
        chunk2 = (chunk2 & 0xffff0000) | (newColour >> 8);     // Replace G, B bytes in chunk2
    }
}

然后第三个像素在chunk2中有2个字节(RG),在chunk3中有1个字节(B):

if ((chunk2 >> 16) == (oldColour & 0xffff))
{
    if ((chunk3 & 0xff) == (oldColour >> 16))
    {
        chunk2 = (chunk2 & 0x0000ffff) | (newColour << 16);  // Replace RG bytes in chunk2
        chunk3 = (chunk3 & 0xffffff00) | (newColour >> 16);  // Replace B byte in chunk3
    }
}

最后,chunk3中的最后3个字节是最后一个像素

if ((chunk3 >> 8) == oldCOlour)
    chunk3 = (chunk3 & 0x000000ff) | (newColour << 8);

...然后将块写回扫描线缓冲区。

这是它的要点(我上面的屏蔽/组合可能有一些错误,因为我快速编写了示例代码并且可能混淆了一些像素!)。

当然,一旦它工作,你可以更多地优化它 - 例如,每当我将东西与oldColour的部分进行比较(例如oldColour >> 16)时,我可以在整个处理循环之外预先解决该常量,只需使用“oldColourShiftedRight16”变量,以避免在每次循环中重新计算它。对于所使用的newColour的所有位也是如此。也许你可以通过避免回写未被触及的值来获得一些收益,因为许多像素可能与你想要改变的像素不匹配。

所以这应该让你知道你要求的是什么。这不是特别简单,但它很有趣: - )

如果你已经完成所有书面和超级优化,那么最后一步就是把它扔掉,只需使用你的显卡就可以在硬件上做到几十倍的速度 - 但让我们面对它,那就是有趣吗? : - )

答案 1 :(得分:0)

我最近写了一个项目,我在每像素像素的基础上进行颜色处理。当你移动鼠标光标时,它必须快速运行。

我开始使用不安全的代码,但我不喜欢不安全的代码,因此更改为安全区域,当我这样做时,我遇到了速度问题,但解决方案并未改变条件逻辑。它正在为像素操作设计更好的算法。

我会概述一下我做了什么,我希望它可以让你到达你想去的地方,因为它非常接近。

首先:我有多种可能的输入像素格式。由于这个原因,我无法假设RGB字节处于特定偏移或甚至是静态宽度。因此,我从传入的图像中读取信息并返回表示每个字段大小的“颜色”:

    private System.Drawing.Color GetOffsets(System.Drawing.Imaging.PixelFormat PixelFormat)
    {
        //Alpha contains bytes per color,
        // R contains R offset in bytes
        // G contains G offset in bytes
        // B contains B offset in bytes
        switch(PixelFormat)
        {
            case System.Drawing.Imaging.PixelFormat.Format24bppRgb:
                return System.Drawing.Color.FromArgb(3, 0, 1, 2);
            case System.Drawing.Imaging.PixelFormat.Format32bppArgb:
            case System.Drawing.Imaging.PixelFormat.Format32bppPArgb:
                return System.Drawing.Color.FromArgb(4, 1, 2, 3);
            case System.Drawing.Imaging.PixelFormat.Format32bppRgb:
                return System.Drawing.Color.FromArgb(4, 0, 1, 2);
            case System.Drawing.Imaging.PixelFormat.Format8bppIndexed:
                return System.Drawing.Color.White;
            default:
                return System.Drawing.Color.White;
        }
    }

例如,假设24位RGB图像是源。我不想改变alpha值,因为我要将颜色混合到它中。

因此,R在偏移0处,B在偏移1处并且G在偏移2处并且每个像素是3位宽。这个我用这个数据创建一个临时颜色。

接下来,因为这是一个自定义控件,我不想闪烁所以我覆盖了OnPaintBackground并将其关闭:

    protected override void OnPaintBackground(System.Windows.Forms.PaintEventArgs pevent)
    {
        //base.OnPaintBackground(pevent);
    }

最后,这里是你正在做的事情的关键部分,我在每个OnPaint上绘制一个新图像(当鼠标移动因为我在鼠标移动事件处理程序中“无效”时,它会被触发)

完整代码 - 在我将某些部分调出之前......

    protected override void OnPaint(System.Windows.Forms.PaintEventArgs pe)
    {
        base.OnPaint(pe);
        pe.Graphics.FillRectangle(new System.Drawing.SolidBrush(this.BackColor), pe.ClipRectangle);
        System.Drawing.Rectangle DestinationRect = GetDestinationRectangle(pe.ClipRectangle);
        if(DestinationRect != System.Drawing.Rectangle.Empty)
        {
            System.Drawing.Image BlendedImage = (System.Drawing.Image) this.Image.Clone();
            if(HighlightRegion != System.Drawing.Rectangle.Empty && this.Image != null)
            {
                System.Drawing.Rectangle OffsetHighlightRegion = 
                    new System.Drawing.Rectangle(
                        new System.Drawing.Point(
                            Math.Min(Math.Max(HighlightRegion.X + OffsetX, 0), BlendedImage.Width - HighlightRegion.Width -1), 
                            Math.Min(Math.Max(HighlightRegion.Y + OffsetY, 0), BlendedImage.Height - HighlightRegion.Height -1)
                            )
                            , HighlightRegion.Size
                            );
                System.Drawing.Bitmap BlendedBitmap = (System.Drawing.Bitmap) BlendedImage;
                System.Drawing.Color OffsetRGB = GetOffsets(BlendedImage.PixelFormat);
                byte BlendR = SelectionColor.R;
                byte BlendG = SelectionColor.G;
                byte BlendB = SelectionColor.B;
                byte BlendBorderR = SelectionBorderColor.R;
                byte BlendBorderG = SelectionBorderColor.G;
                byte BlendBorderB = SelectionBorderColor.B;
                if(OffsetRGB != System.Drawing.Color.White) //White means not supported
                {
                    int BitWidth = OffsetRGB.G - OffsetRGB.R;
                    System.Drawing.Imaging.BitmapData BlendedData = BlendedBitmap.LockBits(new System.Drawing.Rectangle(0, 0, BlendedBitmap.Width, BlendedBitmap.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, BlendedBitmap.PixelFormat);
                    int StrideWidth = BlendedData.Stride;
                    int BytesPerColor = OffsetRGB.A;
                    int ROffset = BytesPerColor - (OffsetRGB.R + 1);
                    int GOffset = BytesPerColor - (OffsetRGB.G + 1);
                    int BOffset = BytesPerColor - (OffsetRGB.B + 1);
                    byte[] BlendedBytes = new byte[Math.Abs(StrideWidth) * BlendedData.Height];
                    System.Runtime.InteropServices.Marshal.Copy(BlendedData.Scan0, BlendedBytes, 0, BlendedBytes.Length);

                    //Create Highlighted Region
                    for(int Row = OffsetHighlightRegion.Top ; Row <= OffsetHighlightRegion.Bottom ; Row++)
                    {
                        for(int Column = OffsetHighlightRegion.Left ; Column <= OffsetHighlightRegion.Right ; Column++)
                        {
                            int Offset = Row * StrideWidth + Column * BytesPerColor;
                            if(Row == OffsetHighlightRegion.Top || Row == OffsetHighlightRegion.Bottom || Column == OffsetHighlightRegion.Left || Column == OffsetHighlightRegion.Right)
                            {
                                BlendedBytes[Offset + ROffset] = BlendBorderR;
                                BlendedBytes[Offset + GOffset] = BlendBorderG;
                                BlendedBytes[Offset + BOffset] = BlendBorderB;
                            }
                            else
                            {
                                BlendedBytes[Offset + ROffset] = (byte) ((BlendedBytes[Offset + ROffset] + BlendR) >> 1);
                                BlendedBytes[Offset + GOffset] = (byte) ((BlendedBytes[Offset + GOffset] + BlendG) >> 1);
                                BlendedBytes[Offset + BOffset] = (byte) ((BlendedBytes[Offset + BOffset] + BlendB) >> 1);
                            }
                        }
                    }
                    System.Runtime.InteropServices.Marshal.Copy(BlendedBytes, 0, BlendedData.Scan0, BlendedBytes.Length);
                    BlendedBitmap.UnlockBits(BlendedData);

                    //base.Image = (System.Drawing.Image) BlendedBitmap;
                }
            }
            pe.Graphics.DrawImage(BlendedImage, 0, 0, DestinationRect, System.Drawing.GraphicsUnit.Pixel);
        }
    }

在这里查看代码是一些解释......

System.Drawing.Image BlendedImage = (System.Drawing.Image) this.Image.Clone();

绘制到屏幕外图像非常重要 - 这会创建一个这样的图像。否则,绘图会慢得多。

if(HighlightRegion != System.Drawing.Rectangle.Empty && this.Image != null)

HighlightRegion是一个RECT,用于保存源图像上“标记”的区域。我用它来标记400万像素的图像区域,它仍然运行得足够快,可以“实时”

使用下面的一些代码是因为用户可能会在图像上向上或向下滚动,因此我会按其滚动量修改目的地。

在此之下,我将IMAGE转换为BITMAP并获取之前提到的颜色信息,我现在需要开始使用它。根据您正在做的事情,您可能希望缓存而不是每次都获取它。

System.Drawing.Bitmap BlendedBitmap = (System.Drawing.Bitmap) BlendedImage;

在我的控制下,我暴露了两个Color属性 - SelectionColor和SelectionBorderColor - 这样我的区域仍然有一个很好的边框。我的速度优化的一部分是将这些预先转换为字节,因为我将在稍后进行按位操作。

您会在代码“White not supported”中看到注释 - 在这种情况下,“White”是我们用来存储位宽的“Fake Color”。我使用“白色”来表示“我无法对此数据进行操作”

下一行确定每种颜色确实是一位,因为它们可能不依赖于我们的目标颜色格式,而是减去R和G偏移。请注意,如果您无法确认您的G跟随您的R,那么您将需要使用其他内容。在我的情况下,它是garaunteed。

现在你正在寻找的部分开始了。我使用LockBits来获取位数据。之后,我使用数据来完成一些预循环变量的设置。

然后,我将数据复制到一个字节数组。我将循环遍历此字节数组,更改值,然后将其数据复制回BITMAP。我之前一直在研究BITMAP,因为它在屏幕外,它与使用原生数组一样快。

我错了。性能分析证明了这一点。将所有内容复制到字节数组并在其中工作会更快。

现在循环开始了。它逐行逐列。偏移量是一个数字,告诉我们字节数组在“当前像素”方面的位置。

然后,我混合了50%或者我画了一个边框。请注意,对于每个像素,我不仅有IF语句,还有OR检查。

它仍然像火焰一样快。

最后,我复制并解锁这些位。然后将图像复制到屏幕表面。