4
votes

I'm trying to create a heat map from eye-tracker results (showing where on the screen was the user looking most frequently).

For pixels that the user looked frequently onto, I want to set non-transparent red color, for a bit less-frequent orange color, etc.. I'm using red to green color-scale. But for the pixels with the least frequency, I want them to not only show green color, I want them to be transparent.

imgfile, err := os.Open("unchanged.jpeg")
defer imgfile.Close()
if err != nil {
    fmt.Println(err.Error())
}

decodedImg, err := jpeg.Decode(imgfile)
img := image.NewRGBA(decodedImg.Bounds())

size := img.Bounds().Size()
for x := 0; x < size.X; x++ {
    for y := 0; y < size.Y; y++ {
        img.Set(x, y, decodedImg.At(x, y))
    }
}

// I change some pixels here with img.Set(...)

outFile, _ := os.Create("changed.png")
defer outFile.Close()
png.Encode(outFile, img)

The problem is that when I try to change color for example with img.Set(x, y, color.RGBA{85, 165, 34, 50}) it doesn't actually color the pixel as averaged value depending on old and new color, it just shows some new, strange-looking color.

Image with alpha RGBA value set to 50: (transparent)

enter image description here

Image with alpha RGBA value set to 255: (opaque)

enter image description here

I'm using png image that as far as I know does support transparency. Why does this happen? Any ideas that can solve this problem and make the least-frequently seen pixels appear transparent?

Thanks for every answer.

1

1 Answers

3
votes

It's because the Set() method sets (overwrites) the color of the pixel at the specified location. This is not what you want.

You want to combine the original color of the pixel with a color of your choosing, and you want this to be the final color.

For this to achieve, first you may query the original color with Image.At(), then combine the color with whatever you want to (you may do this by red, green, blue and alpha channels separately), and set the final color with the Set() method.

Combining:

Note that the R, G, B, A fields of the color.RGBA struct are alpha-premultiplied values, which means the R field for example holds the value of the color's red component multiplied by the alpha channel.

You may use the RGBA.RGBA() method which returns all the fields as uint32 values, and all will be in the range of 0..0xffff so you may do operations on them without having to worry about overflow (max value of uint8 is 255, so even by adding 2 of them could easily overflow).

So when you combine 2 colors, you may combine by components. But note that type of these fields in color.RGBA is uint8, so after combining you have to convert the result to fit into 8 bits, so you have to shift right by 8.

A very simple combining: calculating average:

// Query:
x, y := 1, 1
r, g, b, a := img.At(x, y).RGBA()

// Combine:
col := color.RGBA{85, 165, 34, 50}
r2, g2, b2, a2 := col.RGBA()

col.R = uint8((r + r2) >> 9) // div by 2 followed by ">> 8"  is ">> 9"
col.G = uint8((g + g2) >> 9)
col.B = uint8((b + b2) >> 9)
col.A = uint8((a + a2) >> 9)

// Set new pixel:
img.Set(x, y, col)

You may incorporate this into a helper function:

func combine(c1, c2 color.Color) color.Color {
    r, g, b, a := c1.RGBA()
    r2, g2, b2, a2 := c2.RGBA()

    return color.RGBA{
        uint8((r + r2) >> 9), // div by 2 followed by ">> 8"  is ">> 9"
        uint8((g + g2) >> 9),
        uint8((b + b2) >> 9),
        uint8((a + a2) >> 9),
    }
}

And using it:

x, y := 1, 1
img.Set(x, y, combine(img.At(x, y), color.RGBA{85, 165, 34, 50}))

Please check out the Alpha compositing page if you need realistic color compositing.