1

This is a short test for another problem, where I try to replace colors in an image using ImageAttributes.SetRemapTable(). I found that Graphics.DrawImage() changes other colors as well, that are not part of the color mapping.

Therefore I created a small test on Graphics.DrawImage().
I expect both saved images to be identical:

  var bitmap = new Bitmap (1, 1, PixelFormat.Format32bppArgb);
  bitmap.SetPixel(0,0,Color.FromArgb(10,10,10,10));
  bitmap.Save (@"bitmap1.png", ImageFormat.Png);

  var bitmap2 = new Bitmap (1, 1, PixelFormat.Format32bppArgb);
  var graphics = Graphics.FromImage(bitmap2);
  graphics.DrawImage(bitmap, Point.Empty);
  bitmap2.Save (@"bitmap2.png", ImageFormat.Png);

The ARGB of the pixel in bitmap1 is 10,10,10,10.
The ARGB of the pixel in bitmap2 is 10,0,0,0.

Why does DrawImage() not draw as I expect?
How can I get the expected result?

I don't want to create my own color replacement method; I want the color mapping of .net to work correctly.

EDIT
I made another test, same 1 pixel image, but instead of ARGB 10,10,10,10, I used ARGB 100,100,100,100. The output pixel now has ARGB 100,99,99,99.
This appears to me like a bug in GDI+ or .Net, but definitely not as an intended calculation output.

And, another improvement to ensure it's not caused by the 1 pixel width and height, I changed to

  var bitmap = new Bitmap (5, 5, PixelFormat.Format32bppArgb);
  bitmap.SetPixel(2,2,Color.FromArgb(100,100,100,100));

and again, the output pixel is ARGB 100,99,99,99.
This looks like rounding errors now, but the 10,0,0,0 from above definitly is no rounding error.

EDIT 2
It all looks like rounding errors now: The calculation of the pixel color appears to round e.g. after a division (maybe dividing into an integer), so the rounding error is multiplicated at another multiplication.
This test uses a 16x16 with RGB 20,100,255 and Alpha 0 (upper left) to 255 (lower right).
The comparison of bitmap1 (left) and bitmap2 (right) in Beyond Compare, with results at the bottom (contrast of result image increased), shows that the differences are high at low alpha, and decrease towards 1 or 0 in RGB at high alpha:
enter image description here
Example values:

Pixel 0|0: Left = ARGB 0,20,100,255; Right = ARGB 0,0,0,0.  
Pixel 1|0: Left = ARGB 1,20,100,255; Right = ARGB 1,0,0,255.  
Pixel 2|0: Left = ARGB 2,20,100,255; Right = ARGB 2,0,127,255.  
Pixel 3|0: Left = ARGB 3,20,100,255; Right = ARGB 3,0,85,255. (85 = 1/3*255 => obviously division by alpha 3)
Pixel 3|0: Left = ARGB 4,20,100,255; Right = ARGB 4,0,127,255. (127 = 2/4*255 => obviously division by alpha 4)
Pixel 5|0: Left = ARGB 5,20,100,255; Right = ARGB 5,0,102,255. (102 = 2/5*255 => obviously division by alpha 5)
Pixel 15|0: Left = ARGB 15,20,100,255; Right = ARGB 15,17,102,255. (17 = 1/15*255 => obviously division by alpha 15)
Pixel 14|15: Left = ARGB 254,20,100,255; Right = ARGB 254,20,100,254.
Pixel 15|15: Left = ARGB 255,20,100,255; Right = ARGB 255,20,100,255.

Is there an alternative way to draw partially transparent images?

Tobias Knauss
  • 3,361
  • 1
  • 21
  • 45
  • 3
    According to [this post](https://www.codeproject.com/Articles/14884/BorderBug) using a 1x1 bitmap will reduce the whole image to its border and hence mess up all 1 pixels. Whether the solution (using a RectangleF with half pixels) will help with your goal will depend on the details.. – TaW Feb 08 '21 at 16:13
  • 1
    @TaW: See my edit – Tobias Knauss Feb 08 '21 at 16:47
  • 2
    Not that it changed matters here, you never set `CompositingMode.SourceCopy` as one would need to.. – TaW Feb 08 '21 at 18:25
  • @TaW: Thanks, that may solve another problem. But as you already noted, it does not solve this one. – Tobias Knauss Feb 08 '21 at 21:52
  • _"Is there an alternative way to draw partially transparent images?"_ -- yes, of course. What other alternatives have you tried? Which ones have you found so far but haven't tried? What specifically do you need help with? Obviously for pixel-to-pixel exact fidelity, you could `LockBits()` and perform the copy yourself. Alternatively: https://stackoverflow.com/a/4597984. I will point out that I've experimented with your scenario, and I have found that when the two bitmaps (original and copy) are drawn to the screen, the actual pixel value that winds up on the screen is identical, in spite ... – Peter Duniho Feb 08 '21 at 23:44
  • ... of the difference in the image pixel value. I.e. while the pixels aren't exactly the same in one bitmap and the other, in practical terms there's no meaningful difference (due of course to the alpha channel). – Peter Duniho Feb 08 '21 at 23:44
  • @PeterDuniho: So far I haven't tried alternatives, because I don't know any that perform a color remapping during drawing, which is my original intention. I have used `LockBits()` in the past for copying and editing of image data already, but I didn't want to start with that again. I need to check whether the images still look nice when low transparency colors are replaced. Right now, I test ColorMatrix instead of ColorMap, but that one has the same problems. – Tobias Knauss Feb 09 '21 at 00:00
  • _Is there an alternative way to draw partially transparent images?_ Sure, you can do the drawing using e.g. Lockbits and an alpha algorithm you like. I did that once using a large variety of 'photoshop-like blending modes'. I had to define my own rules for how to blend alpha, which is all but obvious; well, except that with a compositing mode of copy one would expect that the pixels inc. alpha all would be copied. Lockbits is really fast (and simple without scaling) and you can define more than just two compositing modes. Example: What should drawing 50% on 50% result in? 100? 50? 25? – TaW Feb 09 '21 at 01:29

1 Answers1

0

The purpose behind the question was exchanging colors in an image. The exchanged colors include such with transparency (alpha < 255), and the built-in methods of .Net (or GDI+) turned out to be inaccurate, see the edits on the question.
Therefore, finally, I created my own method:

/// ------------------------------------------------------------------
/// <summary>
/// Replace colors in a bitmap.<br/>
/// If <paramref name="i_expandOnAllAlpha"/> is true, the replacement
/// will not only be performed with the given colors, but it will be
/// expanded to colors that are built from the given colors with all
/// possible alpha values.
/// </summary>
/// <param name="i_bitmap">The bitmap in which colors will be replaced.</param>
/// <param name="i_oldAndNewColors">The collection of old and new colors.</param>
/// <param name="i_expandOnAllAlpha">A flag that specifies whether all alpha values (0-255) are included in the color replacement. E.g.: 1 given old color A[255]R[255]G[0]B[0] (red), 256 used old colors A[0-255]R[255]G[0]B[0].</param>
/// ------------------------------------------------------------------
public static Bitmap ReplaceColors (Bitmap i_bitmap,
                                    (Color OldColor, Color NewColor)[] i_oldAndNewColors,
                                    bool i_expandOnAllAlpha = false)
{
  if (i_bitmap == null)
    throw new ArgumentNullException (nameof (i_bitmap));

  var imageSize    = i_bitmap.Size;
  var pixelFormat  = i_bitmap.PixelFormat;
  int bitsPerPixel = Image.GetPixelFormatSize (pixelFormat);
  var bitmap       = (Bitmap)i_bitmap.Clone ();

  var colorMapping = new Dictionary<Color, Color> ();
  foreach (var oldAndNewColor in i_oldAndNewColors)
  {
    var oldColor = oldAndNewColor.OldColor;
    var newColor = oldAndNewColor.NewColor;

    if (i_expandOnAllAlpha)
    {
      for (int alpha = 0; alpha < 256; alpha++)
      {
        colorMapping.Add (Color.FromArgb (alpha, oldColor.R, oldColor.G, oldColor.B),
                          Color.FromArgb (alpha, newColor.R, newColor.G, newColor.B));
      }
    }
    else
    {
      colorMapping.Add (oldColor,newColor);
    }
  }

  BitmapData bitmapData = null;
  try
  {
    bitmapData = bitmap.LockBits (new Rectangle (new Point (), imageSize),
                                  ImageLockMode.ReadWrite,
                                  pixelFormat);
    var ptrBitmap = bitmapData.Scan0;

    int    rowByteCount    = CalcImageRowByteCount (new Size (bitmapData.Width, 0), bitsPerPixel);
    byte[] rowImageData    = new byte[rowByteCount];
    byte[] rowImageDataNew = new byte[rowByteCount];

    for (int rowIndex = 0; rowIndex < bitmapData.Height; rowIndex++)
    {
      var ptrRow = IntPtr.Add (ptrBitmap, rowIndex * bitmapData.Stride);
      Marshal.Copy (ptrRow, rowImageData, 0, rowByteCount);
      Array.Copy (rowImageData, rowImageDataNew, rowImageData.Length);

      int byteIndex = 0;
      switch (bitsPerPixel)
      {
      case 24:
        while (byteIndex < rowImageData.Length)
        {
          byte blueValue  = rowImageData[byteIndex];
          byte greenValue = rowImageData[byteIndex+1];
          byte redValue   = rowImageData[byteIndex+2];
          var  color      = Color.FromArgb (255, redValue, greenValue, blueValue);

          if (colorMapping.TryGetValue (color, out var newColor))
          {
            rowImageDataNew[byteIndex]     = newColor.B;
            rowImageDataNew[byteIndex + 1] = newColor.G;
            rowImageDataNew[byteIndex + 2] = newColor.R;
          }

          byteIndex += 3;
        }
        break;

      case 32:
        while (byteIndex < rowImageData.Length)
        {
          byte blueValue  = rowImageData[byteIndex];
          byte greenValue = rowImageData[byteIndex + 1];
          byte redValue   = rowImageData[byteIndex + 2];
          byte alphaValue = rowImageData[byteIndex + 3];
          var  color      = Color.FromArgb (alphaValue, redValue, greenValue, blueValue);

          if (colorMapping.TryGetValue (color, out var newColor))
          {
            rowImageDataNew[byteIndex]     = newColor.B;
            rowImageDataNew[byteIndex + 1] = newColor.G;
            rowImageDataNew[byteIndex + 2] = newColor.R;
            rowImageDataNew[byteIndex + 3] = newColor.A;
          }

          byteIndex += 4;
        }
        break;

      default:
        throw new NotImplementedException ();
      }

      Marshal.Copy (rowImageDataNew, 0, ptrRow, rowByteCount);
    }
  }
  finally
  {
    bitmap.UnlockBits (bitmapData);
  }

  return bitmap;
}
Tobias Knauss
  • 3,361
  • 1
  • 21
  • 45