0

I need to perform some mathematical operations in photographs, and for that I need the floating point grayscale version of an image (which might come from JPG, PNG or BMP files with various colordepths).

I used to do that in Python using PIL and scipy.ndimage, and it was very straightforward to convert to grayscale with PIL and then to an array of floating-point numbers with numpy, but now I need to do something similar in C#, and I'm confused how to do so.

I have read this very nice tutorial, that seems to be a recurring reference, but that only covers the "convert to grayscale" part, I am not sure how to get an array of doubles from a Bitmap, and then (at some moment) to convert it back to System.Drawing.Bitmap for viewing.

heltonbiker
  • 26,657
  • 28
  • 137
  • 252
  • 3
    Committing to a double[,] just because a Python module works that way isn't particularly productive, it is not exactly a great data type for pixel data. But anything is possible, you'll drown in the examples by googling "c# lockbits". – Hans Passant Sep 12 '13 at 13:51
  • @HansPassant I am not trying to reproduce Python's way of working, but "real world" way of working. Saving pixel values as bytes is a necessity of digital file formats and display devices. If I apply a kernel at some position in an image and get a floating point as a result (gaussian blur comes to mind), I won't feel good to put it back as a quantized byte in another image, I would instead save this value in an array of floating point numbers, because it IS a floating point number. – heltonbiker Sep 12 '13 at 14:14
  • (just to suplement my rant, it is astonishing to notice how, still nowadays, most image-processing techniques are still being constrained by incidental implementation details of digital formats. Saving results as BYTES, really?? Come on...) – heltonbiker Sep 12 '13 at 14:20

2 Answers2

2

I'm sure there are loads of optimal ways to do this.

As @Groo points out perfectly in the comments section, one could use for instance the LockBits method to write and read pixel colors to and from a Bitmap instance. Going even further, one could use the graphics card of the computer to do the actual computations.

Furthermore, the method Color ToGrayscaleColor(Color color) which turns a color into its grayscale version is not optically correct. There is a set of ratios which actually need to be applied to the color component strengths. I just used 1, 1, 1 ratios. That's accceptable for me and probably horrible for an artist or a scientist.

In the comments section, @plinth was very nice to point out to this question you should look at, if you want to make an anatomically correct conversion: Converting RGB to grayscale/intensity

Just wanted to share this really easy to understand and implement solution:

First a little helper to turn a Color into it's grayscale version:

    public static Color ToGrayscaleColor(Color color) {
        var level = (byte)((color.R + color.G + color.B) / 3);
        var result = Color.FromArgb(level, level, level);
        return result;
    }

Then for the color bitmap to grayscale bitmap conversion:

    public static Bitmap ToGrayscale(Bitmap bitmap) {
        var result = new Bitmap(bitmap.Width, bitmap.Height);
        for (int x = 0; x < bitmap.Width; x++)
            for (int y = 0; y < bitmap.Height; y++) {
                var grayColor = ToGrayscaleColor(bitmap.GetPixel(x, y));
                result.SetPixel(x, y, grayColor);
            }
        return result;
    }

The doubles part is quite easy. The Bitmap object is a memory representation of the actual image which you can use in various operations. The colordepth and image format details are only the concern of loading and saving instances of Bitmap onto streams or files. We needn't care about those at this point:

    public static double[,] FromGrayscaleToDoubles(Bitmap bitmap) {
        var result = new double[bitmap.Width, bitmap.Height];
        for (int x = 0; x < bitmap.Width; x++)
            for (int y = 0; y < bitmap.Height; y++)
                result[x, y] = (double)bitmap.GetPixel(x, y).R / 255;
        return result;
    }

And turning a double array back into a grayscale image:

    public static Bitmap FromDoublesToGrayscal(double[,] doubles) {
        var result = new Bitmap(doubles.GetLength(0), doubles.GetLength(1));
        for (int x = 0; x < result.Width; x++)
            for (int y = 0; y < result.Height; y++) {
                int level = (int)Math.Round(doubles[x, y] * 255);
                if (level > 255) level = 255; // just to be sure
                if (level < 0) level = 0; // just to be sure
                result.SetPixel(x, y, Color.FromArgb(level, level, level));
            }
        return result;
    }

The following lines:

    if (level > 255) level = 255; // just to be sure
    level < 0) level = 0; // just to be sure

are really there in case you operate on the doubles and you want to allow room for little mistakes.

Community
  • 1
  • 1
Eduard Dumitru
  • 3,242
  • 17
  • 31
  • 1
    FYI, your conversion from rgb-> gray is mathematically correct, but perceptually incorrect. See here: http://stackoverflow.com/questions/687261/converting-rgb-to-grayscale-intensity – plinth Sep 12 '13 at 14:01
  • 1
    `GetPixel` and `SetPixel` are notoriously slow. When dealing with lots of pixels, a much better way is to use [`LockBits`](http://msdn.microsoft.com/en-us/library/5ey6h79d.aspx) (as @HansPassant mentioned above) and then operate on `BitmapData` directly. – vgru Sep 12 '13 at 14:02
  • @plinth You are correct. I was just writing an **EDIT** explaining that very problem. I will now also add the link you gave me along with the explanation for all to see. – Eduard Dumitru Sep 12 '13 at 14:05
  • @Groo You are correct. As I had already mentioned, I'm sure there are loads of optimal ways to do this, Just wanted to share this really easy to understand and implement solution. In my opinion, it is the essance of what needs to be accomplished. It is correct from a mathematical point of view. From an engineering point of view it is very very slow, indeed... In my opinion, anyone who knows almost nothing about graphics, should first learn how to perform these operations in the obviously simple way... So that was what I was trying to accomplish. – Eduard Dumitru Sep 12 '13 at 14:08
  • @plinth Thanks for reminding that. Fortunately for me, my algorithms are not that sensitive to perceptual variation, as long as the final grayscale preserves a "good" resemblance with the original image. – heltonbiker Sep 12 '13 at 14:09
  • For the record, good `LockBits` reading here: http://bobpowell.net/lockingbits.aspx – heltonbiker Sep 12 '13 at 14:23
1

The final code, based mostly in tips taken from the comments, specifically the LockBits part (blog post here) and the perceptual balancing between R, G and B values (not paramount here, but something to know about):

    private double[,] TransformaImagemEmArray(System.Drawing.Bitmap imagem) {
        // Transforma a imagem de entrada em um array de doubles
        // com os valores grayscale da imagem

        BitmapData bitmap_data = imagem.LockBits(new System.Drawing.Rectangle(0,0,_foto_franjas_original.Width,_foto_franjas_original.Height),
                                            ImageLockMode.ReadOnly, _foto_franjas_original.PixelFormat);

        int pixelsize = System.Drawing.Image.GetPixelFormatSize(bitmap_data.PixelFormat)/8;

        IntPtr pointer = bitmap_data.Scan0;
        int nbytes = bitmap_data.Height * bitmap_data.Stride;
        byte[] imagebytes = new byte[nbytes];
        System.Runtime.InteropServices.Marshal.Copy(pointer, imagebytes, 0, nbytes);

        double red;
        double green;
        double blue;
        double gray;

        var _grayscale_array = new Double[bitmap_data.Height, bitmap_data.Width];

        if (pixelsize >= 3 ) {
            for (int I = 0; I < bitmap_data.Height; I++) {
                for (int J = 0; J < bitmap_data.Width; J++ ) {
                    int position = (I * bitmap_data.Stride) + (J * pixelsize);
                    blue = imagebytes[position];
                    green = imagebytes[position + 1];
                    red = imagebytes[position + 2];
                    gray = 0.299 * red + 0.587 * green + 0.114 * blue;
                    _grayscale_array[I,J] = gray;
                }
            }
        }

        _foto_franjas_original.UnlockBits(bitmap_data);

        return _grayscale_array;
    }
Andrew Morton
  • 24,203
  • 9
  • 60
  • 84
heltonbiker
  • 26,657
  • 28
  • 137
  • 252
  • Why is `red`, `green`, and `blue` doubles? `imageBytes` returns a `byte`, I would change those 3 to `byte` and only have `grey` and `_greyscale_array` be the doubles. – Scott Chamberlain Oct 08 '13 at 13:56
  • @ScottChamberlain I don't think it really makes a difference. They will end up being cast soon after creation anyway, and since it is "three doubles vs three bytes" debate, it really doesn't make a difference in a PC, IMO. – heltonbiker Oct 08 '13 at 14:04
  • Fair enough, thinking more about it, the optimizer may just them in place on the calculation line anyway. – Scott Chamberlain Oct 08 '13 at 14:08
  • What is the _foto_franjas_original variable? – jkokorian Dec 10 '14 at 20:18