javascripthtmlimagecanvashtml5-canvas

getImageData() does not return correct RGB values (Display P3 image)


I've written a website that extracts pixel colors from an image. I do this by using getImageData on a canvas holding the image. However, I do not manage to get the correct values for this sample image.

When using Pillow to extract, for example, the first pixel color (x=0, y=0) it returns (49, 97, 172, 255) for the RGBA color space.

from PIL import Image 
im = Image.open('image.png').convert('RGBA')
pix = im.load()
print(pix[0, 0])

Also Java gives me the same results:

BufferedImage bufferedImage = ImageIO.read(new File("image.png"));
        int rawRGB = bufferedImage.getRGB(0, 0);
        Color color = new Color(rawRGB);

However when I use an HTML canvas I get the following values by getImageData: (27,98,178,255).

let image = new Image();
        const reader = new FileReader();

        image.addEventListener('load', function () {
            let canvas = document.createElement('canvas');
            let ctx = canvas.getContext("2d");
            canvas.width = this.width;
            canvas.height = this.height;
            ctx.drawImage(this, 0, 0);
            let data = ctx.getImageData(0, 0, 1, 1).data;
            document.getElementById('imageData').innerHTML = data;
        });

        image.addEventListener('load', function () {
            let canvas = document.createElement('canvas');
            let gl = canvas.getContext("webgl2");
            gl.activeTexture(gl.TEXTURE0);
            let texture = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, texture);
            const framebuffer = gl.createFramebuffer();
            gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
            gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this);
            gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
            let data = new Uint8Array(1 * 1 * 4);
            gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, data);
            document.getElementById('webGl').innerHTML = data;
        });

        reader.addEventListener('load', (event) => {
            image.src = event.target.result;
        });

        var fileToRead = document.getElementById("yourFile");

        fileToRead.addEventListener("change", function (event) {
            var files = fileToRead.files;
            if (files.length) {
                reader.readAsDataURL(event.target.files[0]);
            }
        }, false);
<pre>Expected:  49,97,172,255<br>
ImageData: <span id="imageData"></span><br>
WebGL:     <span id="webGl"></span><br></pre>
<input type="file" id="yourFile" name="file">

I do not understand why this happens or how the browser comes up with these values. I've tried both Chrome and Firefox and they displayed the same incorrect values. I've read about premultiplied alphas but I don't how this should affect my results as the picture does not make use of the alpha channel. Any help would be appreciated!


Solution

  • This is because this image's color-profile is set to P3, while the default color-space for both the webGL and the 2D context is sRGB, meaning they do convert your image's colors.

    Now, there is a good news, the canvas 2D API recently got extended to support the P3 color-space, this is available in latests Chrome and Safari browsers (Firefox, and WebGL should follow soon).
    To enjoy this feature, you need to create your context with the colorSpace: "display-p3" option:

    (async () => {
      const canvas = document.createElement("canvas");
      const ctx = canvas.getContext("2d", {
        colorSpace: "display-p3"
      });
      if (!(ctx.getContextAttributes?.().colorSpace === "display-p3")) {
        console.warn("your browser doesn't support the colorSpace setting");
      }
      const image = new Image();
      image.src = "https://i.sstatic.net/Xncix.jpg";
      image.crossOrigin = "*";
      await image.decode();
      // not resizing the canvas to save memory since we're only interested in top-left pixel
      ctx.drawImage(image, 0, 0);
      const { data } = ctx.getImageData(0, 0, 1, 1);
      console.log(data);
    })();