2

I'm trying to build a function to convert a color image to monochrome. There's a lot of confusion around terminology, but I'm talking about true black-and-white, where we only have 2 colors: black and white - no grays.

Here is an example of what I'm trying to achieve:

Original Image --> Monochrome Image

I also wanted a way to control the "threshold" of the conversion (i.e. how "bright" does a pixel have to be to produce a white pixel).

Here is what I have right now.

private Bitmap bitmap2Monochrome(Bitmap srcImg, int brightness)
{
    Bitmap monoImg = new Bitmap(srcImg.Width, srcImg.Height);

    // Loop through all the pixels in the image
    for (int y = 0; y < srcImg.Height; y++)
    {
        for (int x = 0; x < srcImg.Width; x++)
        {
            // Get pixel color
            Color existingColor = srcImg.GetPixel(x, y);
            
            // Average R, G, and B to determine "brightness" of this pixel
            int gray = (int)((existingColor.R + existingColor.G + existingColor.G) / 3);

            // If the "brightness" is greater than the threshold, this is a white pixel
            int color = 0;
            if (gray > brightness)
            {
                color = 255;
            }

            // Update the output image
            Color newColor = Color.FromArgb(color, color, color);
            monoImg.SetPixel(x, y, newColor);
        }
    }

    return monoImg;
}

This method works exactly the way I want, but it is (understandably) very slow. This is due to the fact that the GetPixel() and SetPixel() functions are very slow.

I would love to use a ColorMatrix because they are so fast, but I'm not very good at matrices, and I can't seem to come up with a matrix that would achieve this result.

Here's an example of how the logic works:

  1. Source color = (200, 150, 250)
  2. So, grayscale equivalent = (200, 200, 200)
  3. So, gray value = 200
  4. So, 200 > threshold
  5. So, monochrome value = 255
  6. So, monochrome color = (255, 255, 255)

Is there any way I could achieve this sort of functionality using a ColorMatrix?

I'm also open to other ideas!

Jyclop
  • 469
  • 1
  • 6
  • 15
  • 1
    [Replacing Colour Of An Image](https://stackoverflow.com/a/47529000/7444103). B&W and B&W inverted - For OCR -- There's `ImageAttributes.SetThreshold()` there, but you also need to control the Gamma (a little). It's in the `ImageAttributes`, anyway. -- GrayScale conversion here: [How can I gray-out a disabled PictureBox used as Button?](https://stackoverflow.com/a/61584671/7444103), you can use it as an intermediate step, eventually. – Jimi Jun 08 '21 at 23:43
  • 1
    ...because you may get better results (it's semi-common practice, actually). – Jimi Jun 08 '21 at 23:57
  • 1
    I'm not sure how `ColorMatrix` is implemented, but if it clamps the minimum value at 0.0d and maximum value of 1.0d, you could use an identity `ColorMatrix` with the fifth row of `{ -threshold, -threshold, -threshold, 0, 1 }` which would shift all values less than your threshold to 0. Then you could use a second color matrix to multiply all RGB so they are larger than 1.0d. That would create the effect you want(assuming the implementation of `ColorMatrix` prevents negative values) – DekuDesu Jun 09 '21 at 05:51
  • @DekuDesu That works perfectly and such a simple solution too! I'm going to post my code as an answer. I'll mention you in it. :) – Jyclop Jun 09 '21 at 17:22

2 Answers2

2

Consider using an identity ColorMatrix with the fifth row of

{ -threshold, -threshold, -threshold, 0, 1 }

This will allow you to shift all colors below your threshold to 0

Then you could use a second ColorMatrix to multiply all RGB so they are larger than 1.0d. That would create the effect you want(assuming the implementation of ColorMatrix prevents negative values and larger values than 1.0d)

DekuDesu
  • 2,224
  • 1
  • 5
  • 19
1

Just expanding on DekuDesu's answer, here's a coded solution to this.

private Bitmap bitmap2Monochrome(Bitmap img, int threshold)
{
    // Convert the picture to grayscale (so we have "lightness values")
    Bitmap grayImg = bitmap2Grayscale(img, 0);

    // Adjust threshold to be in range 0.0 - 1.0
    float thresh = -1 * ((float)threshold / 255);

    // Subtract threshold value from all pixels to change dark pixels to black
    // Note: Values are automatically bound in range 0 - 255
    float[][] colorTransMatrix =
    {
        new float[] {1.000F, 0.000F, 0.000F, 0.000F, 0.000F},
        new float[] {0.000F, 1.000F, 0.000F, 0.000F, 0.000F},
        new float[] {0.000F, 0.000F, 1.000F, 0.000F, 0.000F},
        new float[] {0.000F, 0.000F, 0.000F, 1.000F, 0.000F},
        new float[] {thresh, thresh, thresh, 0.000F, 1.000F}
    };
    Bitmap temp = translateBitmap(img, colorTransMatrix);

    // Multiply remaining pixels by large value to change light pixels to white
    // Note: Values are automatically bound in range 0 - 255
    float[][] colorTransMatrix2 =
    {
        new float[] {100.000F, 100.000F, 100.000F, 0.000F, 0.000F},
        new float[] {100.000F, 100.000F, 100.000F, 0.000F, 0.000F},
        new float[] {100.000F, 100.000F, 100.000F, 0.000F, 0.000F},
        new float[] {0.000F, 0.000F, 0.000F, 1.000F, 0.000F},
        new float[] {0.000F, 0.000F, 0.000F, 0.000F, 1.000F}
    };
    Bitmap monoImg = translateBitmap(temp, colorTransMatrix2);

    // Return the monochrome image
    return monoImg;
}


private Bitmap bitmap2Grayscale(Bitmap img, int brightness)
{
    // Adjust brightness to be in range 0.0 - 1.0
    float bright = -1 * ((float)brightness / 255);

    // Average R, G, B values of all pixels
    float[][] colorTransMatrix =
    {
        new float[] {0.333F, 0.333F, 0.333F, 0.000F, 0.000F},
        new float[] {0.333F, 0.333F, 0.333F, 0.000F, 0.000F},
        new float[] {0.333F, 0.333F, 0.333F, 0.000F, 0.000F},
        new float[] {0.000F, 0.000F, 0.000F, 1.000F, 0.000F},
        new float[] {bright, bright, bright, 0.000F, 1.000F},
    };
    Bitmap grayImg = translateBitmap(img, colorTransMatrix);

    // Return the grayscale image
    return grayImg;
}


private Bitmap translateBitmap(Bitmap img, float[][] colorTranslationMatrix)
{
    // Setup color translation
    ColorMatrix colorMatrix = new ColorMatrix(colorTranslationMatrix);
    ImageAttributes imgAttr = new ImageAttributes();
    imgAttr.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

    // Draw the image with translated colors
    Bitmap trImg = new Bitmap(img.Width, img.Height);
    Graphics g = Graphics.FromImage(trImg);
    g.DrawImage(img, new Rectangle(0, 0, trImg.Width, trImg.Height), 0, 0, img.Width, img.Height, GraphicsUnit.Pixel, imgAttr);

    // Return the translated image
    return trImg;
}
Jyclop
  • 469
  • 1
  • 6
  • 15
  • Did this end up being faster as you wanted? If so about how much? – DekuDesu Jun 09 '21 at 18:21
  • 1
    @DekuDesu Yes, it is way faster. Using the same picture, the original code was taking around 425ms to execute, and this new code only takes about 30ms. So like 14 times faster! :) – Jyclop Jun 09 '21 at 18:51
  • 1
    Though to be clear, it's only faster because you were using `SetPixel` and `GetPixel`, both incredibly slow. If you rewrite your algorithm to use `LockBits` and direct memory manipulation through pointers, your algorithm will be hundreds of times faster than this needless re-drawing of the image over and over. – Blindy Oct 05 '22 at 01:50