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.
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 Model
s. 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.