Search code examples
c#imagehashgrayscale

coverting Image to grayscale (Perceptual image hashes)


I am working on Perceptual image hashes. Firstly, I reduce image size to remove high frequencies. Then, I shrink image to (8*8) so that there are 64 total pixels. I use the following lines of code.

private void button1_Click(object sender, EventArgs e)
{
    Image img = pictureBox1.Image;
    img = resizeImage(img, new Size(8,8));
    pictureBox2.Image = img;            
}

public static Image resizeImage(Image imgToResize, Size size)
{
    return (Image)(new Bitmap(imgToResize, size));
}

Now I want to reduce color so the tiny (8x8) picture is converted to a gray scale and want to changes the hash from 64 pixels (64 red, 64 green, and 64 blue) to 64 total colors. Here, I'm stuck.


Solution

  • If you're going for 'perceptual' hashes, and only need to process 64 values, it may be interesting to use a more advanced algorithm. The theory behind it is roughly explained here.

    Gray = (0.2126×Red2.2 + 0.7152×Green2.2 + 0.0722×Blue2.2)1/2.2

    As code, this would become

    public static Byte GetGreyValue(Byte red, Byte green, Byte blue)
    {
        Double redFactor = 0.2126d * Math.Pow(red, 2.2d);
        Double grnFactor = 0.7152d * Math.Pow(green, 2.2d);
        Double bluFactor = 0.0722d * Math.Pow(blue, 2.2d);
        Double grey = Math.Pow(redFactor + grnFactor + bluFactor, 1d / 2.2);
        return (Byte)Math.Max(0, Math.Min(255, Math.Round(grey, MidpointRounding.AwayFromZero)));
    }
    

    Now you can just go over all the bytes in your resized image and convert them to gray. Using LockBits, that's not too hard. You just copy the bytes out, iterate over them per four (per ARGB bytes quad), get the RGB components out, throw them into the 'make grey' function, put the result in the RGB spots you took them from, and write the byte array back into your image when you're done.

    public static Bitmap GetGreyImage(Image img, Int32 width, Int32 height)
    {
        // get image data
        Bitmap b = new Bitmap(img, width, height);
        BitmapData sourceData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
        Int32 stride = sourceData.Stride;
        Byte[] data = new Byte[stride * b.Height];
        // Copy bytes from image into data array
        Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
        // iterate over array and convert to gray
        for (Int32 y = 0; y < height; y++)
        {
            Int32 offset = y * stride;
            for (Int32 x = 0; x < width; x++)
            {
                // "ARGB" is little-endian Int32, so the actual byte order is B,G,R,A
                Byte colB = data[offset + 0]; // B
                Byte colG = data[offset + 1]; // G
                Byte colR = data[offset + 2]; // R
                Int32 colA = data[offset + 3]; // A
                // Optional improvement: set pixels to black if color
                // is considered too transparent to matter.
                Byte grayValue = colA < 128 ? 0 : GetGreyValue(colR, colG, colB);
                data[offset + 0] = grayValue; // B
                data[offset + 1] = grayValue; // G
                data[offset + 2] = grayValue; // R
                data[offset + 3] = 0xFF; // A
                offset += 4;
            }
        }
        // Copy bytes from data array back into image
        Marshal.Copy(data, 0, sourceData.Scan0, data.Length);
        b.UnlockBits(sourceData);
        return b;
    }
    

    If with "change from 64 red, 64 green, and 64 blue to 64 total colors" you mean you want an 8-bit (paletted) image where each byte is one pixel, then you're going to have to simply save the values you generate there in a new byte array instead of writing them back, and then create a new 8x8 Bitmap with the Format8bppIndexed format, open it in a second BitmapData object, and write it in there.

    Do note that 8-bit images need a colour palette, so you'll need to go over the standard generated 8-bit palette of the image and change it to a fade from (0,0,0) to (255,255,255) with a for loop.