2

I work with webcodecs and have found that when I manually convert an NV12 format to an RGB format, I get slightly different values than if I simply write the VideoFrame to a canvas and read the imageData.

The ImageData from the Canvas [87, 87, 39, 255, 86, 86, 39, 255, 85, 85, 39, 255, 83, 84, 39, 255, 81, 83, 39, 255, 79, 82, 38, 255, 77, 81, 37, 255, 76, 80, 36, 255, 79, 83, ... ]

The ImageData from manuel converting the NV12-Format to the RGB-Format: [94, 101, 62, 255, 94, 101, 62, 255, 95, 102, 63, 255, 97, 103, 64, 255, 97, 103, 64, 255, 98, 104, 66, 255, 100, 106, 68, 255, 101, 108, 69, 25, ...]

Can anyone tell me how these differences come about or how the Canvas converts a YUV format to an RGB format?

Here is the code for the two different options:

  1. draw the image on a canvas and get the ImageData from the Canvas
    this.context.drawImage(frame, 0,0, this.canvas.width, this.canvas.height);
    let imageData = this.context!.getImageData(0,0,this.canvas.width,this.canvas.height);
  1. Write the content of a VideoFrame into a Array and convert the Array to an RGB-Format.
  ...
  let imageData = convertToImageData(videoFrame);
  ...

  public async convertToImageData(frame) {
    let buffer : Uint8ClampedArray = new Uint8ClampedArray(frame.allocationSize());
    await frame.copyTo(buffer) 
    let imageData = await this.convertNV12ToRGB(buffer)
    return imageData;
  }

  private convertNV12ToRGB(buffer : Uint8ClampedArray) {
    // Y should be from 0-921599 (1280x720)
    // Cr & Cb should be from 921600 - 1382399 interleaved
    // NOTE: Solution is slow: 1280 * 720; 1 Minute -> 44 Seconds full convert.
    
    let imageData : ImageData = this.context!.getImageData(0, 0, 1280, 720);
        
    let pixels : Uint8ClampedArray = imageData.data;

    let row : number = 0;
    let col : number = 0;
    const plane2Start = 1280 * 720; //Hier starten die Chroma-Werte
    for (row = 0; row < 720; row++) {
      const p2row = (row % 2 === 0 ? row / 2 : row / 2 - 0.5) * 1280; //Hälfte der Row 
      let cr = 0;
      let cb = 0;
      const rowOffset : number = row * 1280;
      for (col = 0; col < 1280; col++) {
        const indexY = rowOffset + col;
        let y = buffer[indexY];
        if (col % 2 === 0) {
          const indexCr = plane2Start + p2row + (col);
          const indexCb = indexCr + 1;
          cr = buffer[indexCr];
          cb = buffer[indexCb];
        }
        this.yuvToRgb(y, cr, cb, row, col, pixels);
      }
    }
    return imageData;
  }

  private yuvToRgb(y : number, u : number, v : number, row : number, col : number, pixels : Uint8ClampedArray) {
    y -= 16;
    u -= 128;
    v -= 128;
    let r = 1.164 * y             + 1.596 * v;
    let g = 1.164 * y - 0.392 * u - 0.813 * v;
    let b = 1.164 * y + 2.017 * u;

    const index = (row * 1280 + col) * 4;
    pixels[index] = r;
    pixels[index + 1] = g;
    pixels[index + 2] = b;
    pixels[index + 3] = 255;
  } ```


  • Can you please convert the 4x4 image data from NV12 to RGBA, and post the 64 RGBA output elements? NV12 sample data: `[16,81,145,210,16,170,106,41,16,170,106,41,81,177,170,74,128,128,128,128,105,118,207,128]`. Since I don't know JavaScript, I don't know how to execute `this.context.drawImage(frame...)`. I may find the conversion formula by checking the converted result. – Rotem Oct 20 '22 at 18:57
  • @Rotem [here is a jsfiddle](https://jsfiddle.net/qupn26yo/) using your sample data as source + OP's result in comparison. – Kaiido Oct 21 '22 at 08:54

1 Answers1

1

The conversion formula used by Canvas applies BT.709 "limited range" conversion coefficients.

Conversion YUV to RGB matrix coefficients (after subtracting [16, 128, 128]):

1.1644   -0.0000    1.7927
1.1644   -0.2132   -0.5329
1.1644    2.1124    0.0000

It looks like Canvas conversion uses some kind of approximation, because I can't get the exact results.

JavaScript pixel conversion code:

function yuvToRgb(y, u, v, row, col, pixels) {
  y -= 16;
  u -= 128;
  v -= 128;

  //let r = 1.164 * y             + 1.596 * v;
  //let g = 1.164 * y - 0.392 * u - 0.813 * v;
  //let b = 1.164 * y + 2.017 * u;

  //Use BT.709 "limited range" conversion formula:
  //1.1644   -0.0000    1.7927
  //1.1644   -0.2132   -0.5329
  //1.1644    2.1124    0.0000
  let r = 1.1644 * y              + 1.7927 * v;
  let g = 1.1644 * y - 0.2132 * u - 0.5329 * v;
  let b = 1.1644 * y + 2.1124 * u;

  const index = (row * 4 + col) * 4;
  pixels[index] = Math.max(Math.min(Math.round(r), 255), 0); //Round and clip range to [0, 255]
  pixels[index + 1] = Math.max(Math.min(Math.round(g), 255), 0);
  pixels[index + 2] = Math.max(Math.min(Math.round(b), 255), 0);
  pixels[index + 3] = 255;
}

Notes:

  • Each element is rounded and clipped to range [0, 255].
  • Your code has a naming convention issue.
    The naming convention is: u applies cb, and v applies cr.

Testing code:

function yuvToRgb(y, u, v, row, col, pixels) {
  y -= 16;
  u -= 128;
  v -= 128;

  //let r = 1.164 * y             + 1.596 * v;
  //let g = 1.164 * y - 0.392 * u - 0.813 * v;
  //let b = 1.164 * y + 2.017 * u;

  //Use BT.709 "limited range" conversion formula:
  //1.1644   -0.0000    1.7927
  //1.1644   -0.2132   -0.5329
  //1.1644    2.1124    0.0000
  let r = 1.1644 * y              + 1.7927 * v;
  let g = 1.1644 * y - 0.2132 * u - 0.5329 * v;
  let b = 1.1644 * y + 2.1124 * u;

  const index = (row * 4 + col) * 4;
  pixels[index] = Math.max(Math.min(Math.round(r), 255), 0); //Round and clip range to [0, 255]
  pixels[index + 1] = Math.max(Math.min(Math.round(g), 255), 0);
  pixels[index + 2] = Math.max(Math.min(Math.round(b), 255), 0);
  pixels[index + 3] = 255;
}

//read image
function convertNV12ToRGBExample() {
  let NV12 = new Array(16, 81, 145, 210, 16, 170, 106, 41, 16, 170, 106, 41, 81, 177, 170, 74, 128, 128, 128, 128, 105, 118, 207, 128);
  let RGBA = new Array(64); //Output

  let row = 0;
  let col = 0;
  const plane2Start = 4 * 4;
  for (row = 0; row < 4; row++) {
    const p2row = (row % 2 === 0 ? row / 2 : (row-1) / 2) * 4;
    let cr = 0;
    let cb = 0;
    const rowOffset = row * 4;
    for (col = 0; col < 4; col++) {
      const indexY = rowOffset + col;
      let y = NV12[indexY];
      if (col % 2 === 0) {
        //const indexCr = plane2Start + p2row + col;
        //const indexCb = indexCr + 1;
        const indexCb = plane2Start + p2row + col;
        const indexCr = indexCb + 1;
        cb = NV12[indexCb];
        cr = NV12[indexCr];
      }

      yuvToRgb(y, cb, cr, row, col, RGBA);
    }
  }

  console.log(RGBA);
}

MATLAB testing code (code is incomplete):

NV12 = uint8([16,81,145,210,16,170,106,41,16,170,106,41,81,177,170,74,128,128,128,128,105,118,207,128]);
convertNV12ToRGB = uint8([0, 0, 0, 255, 75, 75, 75, 255, 150, 150, 150, 255, 225, 225, 225, 255, 241, 17, 210, 255, 163, 196, 132, 255, 104, 73, 8, 255, 29, 255, 188, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255]);
Canvas2D         = uint8([0, 0, 0, 255, 76, 76, 76, 255, 150, 150, 150, 255, 226, 226, 226, 255, 0, 0, 0, 255, 179, 179, 179, 255, 105, 105, 105, 255, 29, 29, 29, 255, 0, 10, 0, 255, 161, 190, 133, 255, 105, 87, 255, 255, 29, 12, 187, 255, 58, 86, 30, 255, 169, 198, 141, 255, 179, 162, 255, 255, 68, 50, 226, 255]);

Y = reshape(NV12(1:16), [4, 4])';
U = reshape(NV12(17:2:end), [2, 2])';
V = reshape(NV12(18:2:end), [2, 2])';
U = imresize(U, [4, 4], 'nearest');
V = imresize(V, [4, 4], 'nearest');
YUV = cat(3, Y, U, V);

T = [1.1644   -0.0000    1.7927
     1.1644   -0.2132   -0.5329
     1.1644    2.1124    0.0000];

yuv = double(YUV);
yuv(:,:,1) = yuv(:,:,1) - 16;
yuv(:,:,2) = yuv(:,:,2) - 128;
yuv(:,:,3) = yuv(:,:,3) - 128;

RGB = zeros(size(Y,1), size(Y,2), 3);
RGB(:,:,1) = T(1,1) * yuv(:,:,1) + T(1,2) * yuv(:,:,2) + T(1,3) * yuv(:,:,3);
RGB(:,:,2) = T(2,1) * yuv(:,:,1) + T(2,2) * yuv(:,:,2) + T(2,3) * yuv(:,:,3);
RGB(:,:,3) = T(3,1) * yuv(:,:,1) + T(3,2) * yuv(:,:,2) + T(3,3) * yuv(:,:,3);
RGB = uint8(round(RGB));

RGBA = cat(3, RGB(:,:,1), RGB(:,:,2), RGB(:,:,3), uint8(ones(4)*255)); % Convert to RGBA
rgba_data = reshape(permute(RGBA, [3, 2, 1]), [1, 64]);  % Convert to vector

C = permute(reshape(Canvas2D, [4, 4, 4]), [3, 2, 1]);
C = C(:, :, 1:3);
Rotem
  • 30,366
  • 4
  • 32
  • 65