Grayscaling an image is a widely used transformation that consists of converting an image’s colors into shades of gray. This implies that the amount of color information required to encode a grayscale image is less than a colorful image1. Many consider Grayscale as a distinct color space. In this post, we will briefly go through some grayscale algorithms and their implementation in golang.

Grayscale Algorithms:

According to Helland(2011)2, there is more than half a dozen algorithms for grayscaling images. Not to mention all of them: the average method, the weighted method, desaturation, single color channel, decomposition,…and more. For the sake of simplicity, we are going to focus on the average, the weighted, and the single color channel algorithms as they seem to be the most widespread. One common thing between all the algorithms is that they replace the values of the color space components by a single computed value. The way this single value is computed is what distinguishes between the different algorithms.

The Average method:

The average method replaces the value of each color space component by the average value of the three component3. If our color space is RGB, then the algorithm (in pseudo code) can be outlined as follows:

For Each Pixel P in the Image Do:
    P.Red = (P.Red + P.Green + P.Blue) / 3
    P.Green = (P.Red + P.Green + P.Blue) / 3
    P.Blue = (P.Red + P.Green + P.Blue) / 3
The Weighted method:

The weighted method aims to compute a weighted value out of the color components values in which each component contributes with a different proportion to the overall value. The main motivation behind having a different factor for each color component is because humans do not perceive all colors equally (Helland, 2011)2. The algorithm assumes that the color components should not be treated equally. The weighted method gives the red component a factor of 0.3, the green component a factor of 0.59, and the blue component a factor of 0.11. Understanding the “why” behind each factor is beyond the scope of this post. These factors could be the result of applying some mathematical formulas like standard deviation on a sample of images. A wild guess. (anyone has a refrence to a paper ? please comment )

For Each Pixel P in the Image Do:
    P.Red = (P.Red * 0.3 + P.Green * 0.59 + P.Blue * 0.11)
    P.Green = (P.Red * 0.3 + P.Green * 0.59 + P.Blue * 0.11)
    P.Blue = (P.Red * 0.3 + P.Green * 0.59 + P.Blue * 0.11)
The Single color channel:

The single color channel is the simplest of all. It consists of replacing the value of each color component by a selected component value2. The selected component value should be used throughout the whole process. For example, if we select the value of the R component in an RGB color space, then every value of G and B in every pixel should have the value of R.

For Each Pixel P in the Image Do:
    P.Green = P.Red
    P.Blue =  P.Red

Let’s implement (in Golang):

One of Golang’s perks is its standard library. One can find a large set of available functionalities without the need to pull in external dependencies. Image processing is no exception. Golang provides basic data structures and functions for reading and manipualting mainstream images formats like jpeg and png through the image package. For less common image format like webp or bmp, the golang.org/x/image extension package can complement the basic functionality provided by the image package. As mentionned in the title of this post, we would like to implement the grayscale algorithms described in the previous section without any external tool or dependency.

The first step is reading or decoding the image:

    imgfile, err := os.Open("example.jpeg")
	if err != nil {
		//do something
	}

	defer imgfile.Close()

	img, _, err := image.Decode(imgfile)
	if err != nil {
		//do something
	}

Next, we create our empty destination image that has the same dimensions as the original image:

target := image.NewRGBA64(img.Bounds())
The Average method:
    i := 0
	for i < img.Bounds().Max.Y {
		j := 0
		for j < img.Bounds().Max.X {
			r, g, b, a := img.At(j, i).RGBA()
              average := (r + g + b) / 3
			target.Set(j, i, color.NRGBA64{R: uint16(average), G: uint16(average), B: uint16(average), A: uint16(a)})
			j++
		}
		i++
	}
The Weighted method:
    i := 0
	for i < img.Bounds().Max.Y {
		j := 0
		for j < img.Bounds().Max.X {
			r, g, b, a := img.At(j, i).RGBA()
               weightedAverage := (float64(r) * 0.3) + (float64(g) * 0.59) + (float64(b) * 0.11)
			target.Set(j, i, color.NRGBA64{R: uint16(weightedAverage), G: uint16(weightedAverage), B: uint16(weightedAverage), A: uint16(a)}))
			j++
		}
		i++
	}
The Single color channel:
    i := 0
	for i < img.Bounds().Max.Y {
		j := 0
		for j < img.Bounds().Max.X {
			r, _, _, a := img.At(j, i).RGBA()
			target.Set(j, i, color.NRGBA64{R: uint16(r), G: uint16(r), B: uint16(r), A: uint16(a)})
			j++
		}
		i++
	}

Note about the alpha channel:

The value of A defines the alpha channel value which is a pixel component that controls the opacity. We need to keep the value as-is in our case.

Quick note about the usage of color.Gray or color.Gray16:

Using color.Gray16{Y: uint16(r)} has the same effect as using color.NRGBA64{R: uint16(r), G: uint16(r), B: uint16(r), A: uint16(a)} because the RGBA() method returns the value of Y for r, g, and b. For the alpha channel value a, the returned value is the constant 65535 (or 0xffff) as you can see here in the source code: https://github.com/golang/go/blob/master/src/image/color/color.go#L137

We used the color.NRGBA64 to demonstrate that each color components needs to have the same value; however, for more conciseness color.Gray16{Y: uint16(r)} can be used.

Encoding back the image:

Once we are done with the target image, we can write it to disk to see the result:

  res, err := os.Create("result.jpeg")
  if err != nil {
    //handle error
  }

  if err := jpeg.Encode(res, target, &jpeg.Options{Quality: 100}); err != nil {
    //handle error
  }

Results comparison:

original average
weighted single channel

References

  1. Fisher, R., Perkins, S., Walker, A., & Wolfart, E. (2003). Grayscale Images. University Of Edinburgh. https://homepages.inf.ed.ac.uk/rbf/HIPR2/gryimage.htm 

  2. Helland, T. (2011, October). Seven grayscale conversion algorithms (with pseudocode and VB6 source code). Tannerhelland.Com. https://tannerhelland.com/2011/10/01/grayscale-image-algorithm-vb6.html  2 3

  3. Image Processing 101 Chapter 1.3: Color Space Conversion. (2019, May 24). Dynamsoft Blog. https://www.dynamsoft.com/blog/insights/image-processing/image-processing-101-color-space-conversion/