目录

YUV格式图片如何转换为RGB格式并展示

问题现象

如何将YUV格式图片转换为RGB格式并展示?比如YUV422采样格式,NV16或者YUYV存储格式的YUV图片。

背景知识

一、YUV简介。

YUV是一种颜色编码方法,常使用在各个视频处理组件中。大多数视频采集芯片输出的原始图像数据通常为YUV格式(如YUV422、YUV420等),后续视频处理流程会基于该YUV数据进行预处理(如降噪/缩放)、视频编码(如H.264/H.265压缩),最终生成编码后的视频码流。

YUV编码采用了明亮度和色度表示每个像素的颜色。其中Y表示明亮度(Luminance、Luma),也就是灰阶值。U、V表示色度(Chrominance或Chroma),描述的是色调和饱和度。YCbCr其实是YUV经过缩放和偏移的翻版。其中Y与YUV中的Y含义一致,Cb,Cr同样都指色彩,只是在表示方法上不同而已。YCbCr其中Y是指亮度分量,Cb指蓝色色度分量,而Cr指红色色度分量。

二、YUV采样格式和存储格式。

以NV16和YUYV为例,存储顺序简介以及图例参考如下表格:

存储格式

存储顺序简介

存储顺序图例

NV16

NV16是YUV422Semi-Planar的一种,Y分量单独存放,UV分量交错存放。UV在排列的时候,从U开始,总长度为w*h*2。

YUYV

YUYV属于YUV422Interleaved的一种。事实上,Interleaved是属于Packed的,但是在422中,用Interleaved更加形象一些。在Packed内部,YUV的排列顺序是YUVY,两个Y共用一组UV。

三、YUV与RGB之间的转换。

在图像处理过程中,经常会遇到YUV与RGB之间的转换,以BT601 full range标准为基础,YUV和RGB转换的公式如下:

BT601 full range RGB=[0, 255], Y=[16, 235], UV=[16, 240]。

R = 1.164 * (Y - 16) + 1.596 * (V - 128)
G = 1.164 * (Y - 16) - 0.392 * (U - 128) - 0.812 * (V - 128)
B = 1.164 * (Y - 16) + 2.016 * (U - 128)

解决方案

展示YUV格式的图片,首先要确认YUV的采样格式,存储格式,以及YUV与RGB的转换公式。将YUV数据转为RGB数据,再将RGB数据编码为pixelMap。

比如YUV图片是YUV422采样格式,NV16或者YUYV的存储格式,BT601 full range的转换公式。

  1. 将YUV数据转为RGB数据,示例代码如下:
    // nv16数据转为rgba数据
    convertNv16ToRgba(nv16Data: Uint8Array, width: number, height: number): Uint8Array {
      const rgbaData = new Uint8Array(width * height * 4);
      const yPlaneSize = width * height;
    
      for (let i = 0; i < width * height; i++) {
        const row = Math.floor(i / width);
        const col = i % width;
        // Y分量
        const y = nv16Data[i];
        // UV分量 (NV16水平2:1下采样)
        const uvRow = row;
        const uvCol = Math.floor(col / 2) * 2;
        const uvIndex = yPlaneSize + (uvRow * width) + uvCol;
    
        let u = 0, v = 0;
        if (col % 2 === 0) {
          u = nv16Data[uvIndex];
          v = nv16Data[uvIndex + 1];
        } else {
          u = nv16Data[uvIndex - 2];
          v = nv16Data[uvIndex - 1];
        }
        // 使用查找表快速转换
        const rgb = this.fastYuvToRgb(y, u, v);
        // 填充RGBA
        const rgbaIndex = i * 4;
        rgbaData[rgbaIndex] = rgb.b;
        rgbaData[rgbaIndex + 1] = rgb.g;
        rgbaData[rgbaIndex + 2] = rgb.r;
        rgbaData[rgbaIndex + 3] = 255;
      }
      return rgbaData;
    }
    
    // yuyv数据转为rgba数据
    convertYuyvToRgba(yuyvArray: Uint8Array, width: number, height: number): Uint8Array {
      const evenWidth = width % 2 === 0 ? width : width + 1;
      // 创建RGBA缓冲区
      const rgbaBuffer = new ArrayBuffer(evenWidth * height * 4);
      const rgbaArray = new Uint8Array(rgbaBuffer);
    
      for (let y = 0; y < height; y++) {
        for (let x = 0; x < evenWidth; x += 2) {
          const yuyvIdx = (y * evenWidth + x) * 2;
          // 获取YUYV分量
          const y1 = yuyvArray[yuyvIdx]; // Y1
          const u = yuyvArray[yuyvIdx + 1]; // U
          const y2 = yuyvArray[yuyvIdx + 2]; // Y2
          const v = yuyvArray[yuyvIdx + 3]; // V
          // 转换为RGB
          const rgb1 = this.fastYuvToRgb(y1, u, v);
          const rgb2 = this.fastYuvToRgb(y2, u, v);
          // 填入RGBA数组
          const idx1 = (y * evenWidth + x) * 4;
          const idx2 = (y * evenWidth + x + 1) * 4;
          if (x < width) {
            rgbaArray[idx1] = rgb1.b; // B
            rgbaArray[idx1 + 1] = rgb1.g; // G
            rgbaArray[idx1 + 2] = rgb1.r; // R
            rgbaArray[idx1 + 3] = 255; // A(完全不透明)
          }
          if ((x + 1) < width) {
            rgbaArray[idx2] = rgb2.b;
            rgbaArray[idx2 + 1] = rgb2.g;
            rgbaArray[idx2 + 2] = rgb2.r;
            rgbaArray[idx2 + 3] = 255;
          }
        }
      }
      return rgbaArray;
    }
    
    /**
     * 快速YUV转RGB (整数运算)
     */
    fastYuvToRgb(y: number, u: number, v: number): rgbInfo {
      // 使用整数运算的近似公式
      const c = y - 16;
      const d = u - 128;
      const e = v - 128;
    
      // 使用整数运算 (乘以1000避免浮点数运算)
      let r = (1164 * c + 1596 * e) / 1000;
      let g = (1164 * c - 392 * d - 812 * e) / 1000;
      let b = (1164 * c + 2016 * d) / 1000;
    
      // 限制范围
      r = Math.min(255, Math.max(0, Math.round(r)));
      g = Math.min(255, Math.max(0, Math.round(g)));
      b = Math.min(255, Math.max(0, Math.round(b)));
    
      return { r, g, b };
    }
  2. 完整示例代码如下:
    import { resourceManager } from '@kit.LocalizationKit';
    import { image } from '@kit.ImageKit';
    
    export class rgbInfo {
      r: number = 0;
      g: number = 0;
      b: number = 0;
    }
    
    @Entry
    @Component
    struct YuvTogrba {
      @State pixelMapNv16: PixelMap | undefined = undefined;
      @State pixelMapYuyv: PixelMap | undefined = undefined;
      private context: Context = this.getUIContext().getHostContext() as Context;
    
      // nv16数据转为rgba数据
      convertNv16ToRgba(nv16Data: Uint8Array, width: number, height: number): Uint8Array {
        const rgbaData = new Uint8Array(width * height * 4);
        const yPlaneSize = width * height;
    
        for (let i = 0; i < width * height; i++) {
          const row = Math.floor(i / width);
          const col = i % width;
          // Y分量
          const y = nv16Data[i];
          // UV分量 (NV16水平2:1下采样)
          const uvRow = row;
          const uvCol = Math.floor(col / 2) * 2;
          const uvIndex = yPlaneSize + (uvRow * width) + uvCol;
    
          let u = 0, v = 0;
          if (col % 2 === 0) {
            u = nv16Data[uvIndex];
            v = nv16Data[uvIndex + 1];
          } else {
            u = nv16Data[uvIndex - 2];
            v = nv16Data[uvIndex - 1];
          }
          // 使用查找表快速转换
          const rgb = this.fastYuvToRgb(y, u, v);
          // 填充RGBA
          const rgbaIndex = i * 4;
          rgbaData[rgbaIndex] = rgb.b;
          rgbaData[rgbaIndex + 1] = rgb.g;
          rgbaData[rgbaIndex + 2] = rgb.r;
          rgbaData[rgbaIndex + 3] = 255;
        }
        return rgbaData;
      }
    
      // yuyv数据转为rgba数据
      convertYuyvToRgba(yuyvArray: Uint8Array, width: number, height: number): Uint8Array {
        const evenWidth = width % 2 === 0 ? width : width + 1;
        // 创建RGBA缓冲区
        const rgbaBuffer = new ArrayBuffer(evenWidth * height * 4);
        const rgbaArray = new Uint8Array(rgbaBuffer);
    
        for (let y = 0; y < height; y++) {
          for (let x = 0; x < evenWidth; x += 2) {
            const yuyvIdx = (y * evenWidth + x) * 2;
            // 获取YUYV分量
            const y1 = yuyvArray[yuyvIdx]; // Y1
            const u = yuyvArray[yuyvIdx + 1]; // U
            const y2 = yuyvArray[yuyvIdx + 2]; // Y2
            const v = yuyvArray[yuyvIdx + 3]; // V
            // 转换为RGB
            const rgb1 = this.fastYuvToRgb(y1, u, v);
            const rgb2 = this.fastYuvToRgb(y2, u, v);
            // 填入RGBA数组
            const idx1 = (y * evenWidth + x) * 4;
            const idx2 = (y * evenWidth + x + 1) * 4;
            if (x < width) {
              rgbaArray[idx1] = rgb1.b; // B
              rgbaArray[idx1 + 1] = rgb1.g; // G
              rgbaArray[idx1 + 2] = rgb1.r; // R
              rgbaArray[idx1 + 3] = 255; // A(完全不透明)
            }
            if ((x + 1) < width) {
              rgbaArray[idx2] = rgb2.b;
              rgbaArray[idx2 + 1] = rgb2.g;
              rgbaArray[idx2 + 2] = rgb2.r;
              rgbaArray[idx2 + 3] = 255;
            }
          }
        }
        return rgbaArray;
      }
    
      /**
       * 快速YUV转RGB (整数运算)
       */
      fastYuvToRgb(y: number, u: number, v: number): rgbInfo {
        // 使用整数运算的近似公式
        const c = y - 16;
        const d = u - 128;
        const e = v - 128;
    
        // 使用整数运算 (乘以1000避免浮点数运算)
        let r = (1164 * c + 1596 * e) / 1000;
        let g = (1164 * c - 392 * d - 812 * e) / 1000;
        let b = (1164 * c + 2016 * d) / 1000;
    
        // 限制范围
        r = Math.min(255, Math.max(0, Math.round(r)));
        g = Math.min(255, Math.max(0, Math.round(g)));
        b = Math.min(255, Math.max(0, Math.round(b)));
    
        return { r, g, b };
      }
    
      build() {
        Row() {
          Column() {
            Row() {
              Column() {
                Image(this.pixelMapNv16)
                  .margin({ bottom: 10 })
                  .width('45%')
                  .height(140)
                Button('nv16转rgba')
                  .onClick(() => {
                    const resourceMgr: resourceManager.ResourceManager = this.context.resourceManager;
                    let rawBuf = resourceMgr.getRawFileContentSync('test_nv16.yuv'); // 需要替换为rawfile目录下实际nv16存储格式的yuv数据
                    let readBuffer = this.convertNv16ToRgba(rawBuf, 550, 400); // 像素宽高要根据实际的yuv数据填写
                    let opts: image.InitializationOptions =
                      { editable: true, pixelFormat: 3, size: { height: 400, width: 550 } }; // 像素宽高要根据实际的yuv数据填写
                    this.pixelMapNv16 = image.createPixelMapSync(readBuffer.buffer, opts);
                  })
              }.margin({ right: 5 })
    
              Column() {
                Image(this.pixelMapYuyv)
                  .margin({ bottom: 10 })
                  .width('45%')
                  .height(140)
                Button('yuyv转rgba')
                  .onClick(() => {
                    const resourceMgr: resourceManager.ResourceManager = this.context.resourceManager;
                    let rawBuf = resourceMgr.getRawFileContentSync('test_yuyv.yuv'); // 需要替换为rawfile目录下实际yuyv存储格式的yuv数据
                    let readBuffer = this.convertYuyvToRgba(rawBuf, 550, 400); // 像素宽高要根据实际的yuv数据填写
                    let opts: image.InitializationOptions =
                      { editable: true, pixelFormat: 3, size: { height: 400, width: 550 } }; // 像素宽高要根据实际的yuv数据填写
                    this.pixelMapYuyv = image.createPixelMapSync(readBuffer.buffer, opts);
                  })
              }.margin({ left: 5 })
            }
          }.width('100%')
        }.height('100%')
      }
    }