Converting RGBA image to Grayscale Golang

benjano picture benjano · Feb 28, 2017 · Viewed 10k times · Source

I'm currently working on a program to convert and RGBA image to grayscale.

I asked a question earlier and was directed to the following answer - Change color of a single pixel - Go lang image

Here is my original question - Program to convert RGBA to grayscale Golang

I have edited my code so it now successfully runs - however the image outputted is not what I want. It is converted to grayscale however the pixels are all messed up making it look like noise on an old TV.

package main

import (
"image"
"image/color"
"image/jpeg"
"log"
"os"
)

type ImageSet interface {
Set(x, y int, c color.Color)
}


func main() {
file, err := os.Open("flower.jpg")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

img, err := jpeg.Decode(file)
if err != nil {
    log.Fatal(os.Stderr, "%s: %v\n", "flower.jpg", err)
}

b := img.Bounds()

imgSet := image.NewRGBA(b)
for y := 0; y < b.Max.Y; y++ {
    for x := 0; x < b.Max.X; x++ {
      oldPixel := img.At(x, y)
       r, g, b, a:= oldPixel.RGBA()
       r = (r+g+b)/3
       pixel := color.RGBA{uint8(r), uint8(r), uint8(r), uint8(a)}
       imgSet.Set(x, y, pixel)
     }
    }

    outFile, err := os.Create("changed.jpg")
    if err != nil {
      log.Fatal(err)
    }
    defer outFile.Close()
    jpeg.Encode(outFile, imgSet, nil)

}

I know I haven't added in the if else statement for checking if the image can accept the Set() method, however the suggestion for simply making a new image seems to have solved this.

Any help much appreciated.

Edit:

I've added in some suggested code from the answer below:

    package main

import (
//"fmt"
"image"
"image/color"
"image/jpeg"
"log"
"os"
)

type ImageSet interface {
Set(x, y int, c color.Color)
}


func main() {
file, err := os.Open("flower.jpg")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

img, err := jpeg.Decode(file)
if err != nil {
    log.Fatal(os.Stderr, "%s: %v\n", "flower.jpg", err)
}

b := img.Bounds()
imgSet := image.NewRGBA(b)
for y := 0; y < b.Max.Y; y++ {
    for x := 0; x < b.Max.X; x++ {
      oldPixel := img.At(x, y)
      r, g, b, _ := oldPixel.RGBA()
      y := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
      pixel := color.Gray{uint8(y / 256)}
      imgSet.Set(x, y, pixel)
     }
    }

    outFile, err := os.Create("changed.jpg")
    if err != nil {
      log.Fatal(err)
    }
    defer outFile.Close()
    jpeg.Encode(outFile, imgSet, nil)

}

I currently get the following error.

.\rgbtogray.go:36: cannot use y (type uint32) as type int in argument to imgSet.Set

Am I missing something from the answer? Any tips appreciated.

Answer

icza picture icza · Feb 28, 2017

Color.RGBA() is a method that returns the alpha-premultiplied red, green, blue and alpha values, all being of type uint32, but only being in the range of [0, 0xffff] (using only 16 bits out of 32). This means you can add these components, they will not overflow (max value of each component fits into 16 bits, so their sum will fit into 32 bits).

One thing to note here: the result will also be alpha-premultiplied, and after dividing by 3, it will still be in the range of [0..0xffff]. So by doing a uint8(r) type conversion, you're just keeping the lowest 8 bits, which will be seemingly just a random value compared to the whole number. You should rather take the highest 8 bits.

But not so fast. What we're trying to do here is convert a color image to a grayscale image, which will lose the "color" information, and what we want is basically the luminosity of each pixel. Your proposed solution is called the average method, and it gives rather poor result, because it takes all the R, G and B components with equal weight, even though these colors have different wavelength and thus contribute in different measures to the luminosity of the overall pixel. Read more about it here: Grayscale to RGB Conversion.

For a realistic RGB -> grayscale conversion, the following weights have to be used:

Y = 0.299 * R +  0.587 * G + 0.114 * B

You can read more behind these weights (and variants) on wikipedia: Grayscale. This is called the luminosity method, and this will give the best grayscale images.

So far so good, we have the luminosity, how do we go to a color.Color value from here? One option is to use a color.RGBA color value, where you specify the same luminosity to all components (alpha may be kept). And if you intend to use an image.RGBA returned by image.NewRGBA(), probably this is the best way as no color conversion will be needed when setting the color (as it matches the image's color model).

Another tempting choice is to use the color.Gray which is a color (implements the color.Color interface), and models a color just the way we have it now: with Y, stored using an uint8. An alternative could be color.Gray16 which is basically the "same", but uses 16 bits to store Y as an uint16. For these, best would be to also use an image with the matching color model, such as image.Gray or image.Gray16 (although this is not a requirement).

So the conversion should be:

oldPixel := img.At(x, y)
r, g, b, _ := oldPixel.RGBA()
lum := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
pixel := color.Gray{uint8(lum / 256)}
imgSet.Set(x, y, pixel)

Note that we needed to convert the R, G, B components to float64 to be able to multiply by the weights. Since r, g, b are already of type uint32, we could substitute this with integer operations (without overflow).

Without going into details –and because the standard lib already has a solution for this–, here it is:

oldPixel := img.At(x, y)
r, g, b, _ := oldPixel.RGBA()
lum := (19595*r + 38470*g + 7471*b + 1<<15) >> 24
imgSet.Set(x, y, color.Gray{uint8(lum)})

Now without writing such "ugly" things, the recommended way is to simply use the color converters of the image/color package, called Models. The prepared color.GrayModel model is able to convert any colors to the model of color.Gray.

It's this simple:

oldPixel := img.At(x, y)
pixel := color.GrayModel.Convert(oldPixel)
imgSet.Set(x, y, pixel)

It does the same as our last luminosity weighted model, using integer-arithmetic. Or in one line:

imgSet.Set(x, y, color.GrayModel.Convert(img.At(x, y)))

To have a higher, 16-bit grayscale resolution:

imgSet.Set(x, y, color.Gray16Model.Convert(img.At(x, y)))

One last note: since you're drawing on an image returned by image.NewRGBA(), it is of type *image.RGBA. You don't need to check if it has a Set() method, because image.RGBA is a static type (not interface), and it does have a Set() method, it is checked at compile time. The case when you do need to check is if you have an image of the general image.Image type which is an interface, but this interface does not contain / "prescribe" the Set() method; but the dynamic type implementing this interface may provide that nonetheless.