文档管理中心
您当前正在浏览新版开发者文档中心,目录分类和层级有所调整。点击左侧当前文档分类名称前的“☰”图标,可切换文档分类。 了解新版目录
问题处理与运维开发与测试问题媒体开发拍照和图片图片处理(Image)YUV格式图片如何转换为RGB格式并展示

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]。

收起
自动换行
深色代码主题
复制
  1. R = 1.164 * (Y - 16) + 1.596 * (V - 128)
  2. G = 1.164 * (Y - 16) - 0.392 * (U - 128) - 0.812 * (V - 128)
  3. 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数据,示例代码如下:
    收起
    自动换行
    深色代码主题
    复制
    1. // nv16数据转为rgba数据
    2. convertNv16ToRgba(nv16Data: Uint8Array, width: number, height: number): Uint8Array {
    3. const rgbaData = new Uint8Array(width * height * 4);
    4. const yPlaneSize = width * height;
    5. for (let i = 0; i < width * height; i++) {
    6. const row = Math.floor(i / width);
    7. const col = i % width;
    8. // Y分量
    9. const y = nv16Data[i];
    10. // UV分量 (NV16水平2:1下采样)
    11. const uvRow = row;
    12. const uvCol = Math.floor(col / 2) * 2;
    13. const uvIndex = yPlaneSize + (uvRow * width) + uvCol;
    14. let u = 0, v = 0;
    15. if (col % 2 === 0) {
    16. u = nv16Data[uvIndex];
    17. v = nv16Data[uvIndex + 1];
    18. } else {
    19. u = nv16Data[uvIndex - 2];
    20. v = nv16Data[uvIndex - 1];
    21. }
    22. // 使用查找表快速转换
    23. const rgb = this.fastYuvToRgb(y, u, v);
    24. // 填充RGBA
    25. const rgbaIndex = i * 4;
    26. rgbaData[rgbaIndex] = rgb.b;
    27. rgbaData[rgbaIndex + 1] = rgb.g;
    28. rgbaData[rgbaIndex + 2] = rgb.r;
    29. rgbaData[rgbaIndex + 3] = 255;
    30. }
    31. return rgbaData;
    32. }
    33. // yuyv数据转为rgba数据
    34. convertYuyvToRgba(yuyvArray: Uint8Array, width: number, height: number): Uint8Array {
    35. const evenWidth = width % 2 === 0 ? width : width + 1;
    36. // 创建RGBA缓冲区
    37. const rgbaBuffer = new ArrayBuffer(evenWidth * height * 4);
    38. const rgbaArray = new Uint8Array(rgbaBuffer);
    39. for (let y = 0; y < height; y++) {
    40. for (let x = 0; x < evenWidth; x += 2) {
    41. const yuyvIdx = (y * evenWidth + x) * 2;
    42. // 获取YUYV分量
    43. const y1 = yuyvArray[yuyvIdx]; // Y1
    44. const u = yuyvArray[yuyvIdx + 1]; // U
    45. const y2 = yuyvArray[yuyvIdx + 2]; // Y2
    46. const v = yuyvArray[yuyvIdx + 3]; // V
    47. // 转换为RGB
    48. const rgb1 = this.fastYuvToRgb(y1, u, v);
    49. const rgb2 = this.fastYuvToRgb(y2, u, v);
    50. // 填入RGBA数组
    51. const idx1 = (y * evenWidth + x) * 4;
    52. const idx2 = (y * evenWidth + x + 1) * 4;
    53. if (x < width) {
    54. rgbaArray[idx1] = rgb1.b; // B
    55. rgbaArray[idx1 + 1] = rgb1.g; // G
    56. rgbaArray[idx1 + 2] = rgb1.r; // R
    57. rgbaArray[idx1 + 3] = 255; // A(完全不透明)
    58. }
    59. if ((x + 1) < width) {
    60. rgbaArray[idx2] = rgb2.b;
    61. rgbaArray[idx2 + 1] = rgb2.g;
    62. rgbaArray[idx2 + 2] = rgb2.r;
    63. rgbaArray[idx2 + 3] = 255;
    64. }
    65. }
    66. }
    67. return rgbaArray;
    68. }
    69. /**
    70. * 快速YUV转RGB (整数运算)
    71. */
    72. fastYuvToRgb(y: number, u: number, v: number): rgbInfo {
    73. // 使用整数运算的近似公式
    74. const c = y - 16;
    75. const d = u - 128;
    76. const e = v - 128;
    77. // 使用整数运算 (乘以1000避免浮点数运算)
    78. let r = (1164 * c + 1596 * e) / 1000;
    79. let g = (1164 * c - 392 * d - 812 * e) / 1000;
    80. let b = (1164 * c + 2016 * d) / 1000;
    81. // 限制范围
    82. r = Math.min(255, Math.max(0, Math.round(r)));
    83. g = Math.min(255, Math.max(0, Math.round(g)));
    84. b = Math.min(255, Math.max(0, Math.round(b)));
    85. return { r, g, b };
    86. }
  2. 完整示例代码如下:
    收起
    自动换行
    深色代码主题
    复制
    1. import { resourceManager } from '@kit.LocalizationKit';
    2. import { image } from '@kit.ImageKit';
    3. export class rgbInfo {
    4. r: number = 0;
    5. g: number = 0;
    6. b: number = 0;
    7. }
    8. @Entry
    9. @Component
    10. struct YuvTogrba {
    11. @State pixelMapNv16: PixelMap | undefined = undefined;
    12. @State pixelMapYuyv: PixelMap | undefined = undefined;
    13. private context: Context = this.getUIContext().getHostContext() as Context;
    14. // nv16数据转为rgba数据
    15. convertNv16ToRgba(nv16Data: Uint8Array, width: number, height: number): Uint8Array {
    16. const rgbaData = new Uint8Array(width * height * 4);
    17. const yPlaneSize = width * height;
    18. for (let i = 0; i < width * height; i++) {
    19. const row = Math.floor(i / width);
    20. const col = i % width;
    21. // Y分量
    22. const y = nv16Data[i];
    23. // UV分量 (NV16水平2:1下采样)
    24. const uvRow = row;
    25. const uvCol = Math.floor(col / 2) * 2;
    26. const uvIndex = yPlaneSize + (uvRow * width) + uvCol;
    27. let u = 0, v = 0;
    28. if (col % 2 === 0) {
    29. u = nv16Data[uvIndex];
    30. v = nv16Data[uvIndex + 1];
    31. } else {
    32. u = nv16Data[uvIndex - 2];
    33. v = nv16Data[uvIndex - 1];
    34. }
    35. // 使用查找表快速转换
    36. const rgb = this.fastYuvToRgb(y, u, v);
    37. // 填充RGBA
    38. const rgbaIndex = i * 4;
    39. rgbaData[rgbaIndex] = rgb.b;
    40. rgbaData[rgbaIndex + 1] = rgb.g;
    41. rgbaData[rgbaIndex + 2] = rgb.r;
    42. rgbaData[rgbaIndex + 3] = 255;
    43. }
    44. return rgbaData;
    45. }
    46. // yuyv数据转为rgba数据
    47. convertYuyvToRgba(yuyvArray: Uint8Array, width: number, height: number): Uint8Array {
    48. const evenWidth = width % 2 === 0 ? width : width + 1;
    49. // 创建RGBA缓冲区
    50. const rgbaBuffer = new ArrayBuffer(evenWidth * height * 4);
    51. const rgbaArray = new Uint8Array(rgbaBuffer);
    52. for (let y = 0; y < height; y++) {
    53. for (let x = 0; x < evenWidth; x += 2) {
    54. const yuyvIdx = (y * evenWidth + x) * 2;
    55. // 获取YUYV分量
    56. const y1 = yuyvArray[yuyvIdx]; // Y1
    57. const u = yuyvArray[yuyvIdx + 1]; // U
    58. const y2 = yuyvArray[yuyvIdx + 2]; // Y2
    59. const v = yuyvArray[yuyvIdx + 3]; // V
    60. // 转换为RGB
    61. const rgb1 = this.fastYuvToRgb(y1, u, v);
    62. const rgb2 = this.fastYuvToRgb(y2, u, v);
    63. // 填入RGBA数组
    64. const idx1 = (y * evenWidth + x) * 4;
    65. const idx2 = (y * evenWidth + x + 1) * 4;
    66. if (x < width) {
    67. rgbaArray[idx1] = rgb1.b; // B
    68. rgbaArray[idx1 + 1] = rgb1.g; // G
    69. rgbaArray[idx1 + 2] = rgb1.r; // R
    70. rgbaArray[idx1 + 3] = 255; // A(完全不透明)
    71. }
    72. if ((x + 1) < width) {
    73. rgbaArray[idx2] = rgb2.b;
    74. rgbaArray[idx2 + 1] = rgb2.g;
    75. rgbaArray[idx2 + 2] = rgb2.r;
    76. rgbaArray[idx2 + 3] = 255;
    77. }
    78. }
    79. }
    80. return rgbaArray;
    81. }
    82. /**
    83. * 快速YUV转RGB (整数运算)
    84. */
    85. fastYuvToRgb(y: number, u: number, v: number): rgbInfo {
    86. // 使用整数运算的近似公式
    87. const c = y - 16;
    88. const d = u - 128;
    89. const e = v - 128;
    90. // 使用整数运算 (乘以1000避免浮点数运算)
    91. let r = (1164 * c + 1596 * e) / 1000;
    92. let g = (1164 * c - 392 * d - 812 * e) / 1000;
    93. let b = (1164 * c + 2016 * d) / 1000;
    94. // 限制范围
    95. r = Math.min(255, Math.max(0, Math.round(r)));
    96. g = Math.min(255, Math.max(0, Math.round(g)));
    97. b = Math.min(255, Math.max(0, Math.round(b)));
    98. return { r, g, b };
    99. }
    100. build() {
    101. Row() {
    102. Column() {
    103. Row() {
    104. Column() {
    105. Image(this.pixelMapNv16)
    106. .margin({ bottom: 10 })
    107. .width('45%')
    108. .height(140)
    109. Button('nv16转rgba')
    110. .onClick(() => {
    111. const resourceMgr: resourceManager.ResourceManager = this.context.resourceManager;
    112. let rawBuf = resourceMgr.getRawFileContentSync('test_nv16.yuv'); // 需要替换为rawfile目录下实际nv16存储格式的yuv数据
    113. let readBuffer = this.convertNv16ToRgba(rawBuf, 550, 400); // 像素宽高要根据实际的yuv数据填写
    114. let opts: image.InitializationOptions =
    115. { editable: true, pixelFormat: 3, size: { height: 400, width: 550 } }; // 像素宽高要根据实际的yuv数据填写
    116. this.pixelMapNv16 = image.createPixelMapSync(readBuffer.buffer, opts);
    117. })
    118. }.margin({ right: 5 })
    119. Column() {
    120. Image(this.pixelMapYuyv)
    121. .margin({ bottom: 10 })
    122. .width('45%')
    123. .height(140)
    124. Button('yuyv转rgba')
    125. .onClick(() => {
    126. const resourceMgr: resourceManager.ResourceManager = this.context.resourceManager;
    127. let rawBuf = resourceMgr.getRawFileContentSync('test_yuyv.yuv'); // 需要替换为rawfile目录下实际yuyv存储格式的yuv数据
    128. let readBuffer = this.convertYuyvToRgba(rawBuf, 550, 400); // 像素宽高要根据实际的yuv数据填写
    129. let opts: image.InitializationOptions =
    130. { editable: true, pixelFormat: 3, size: { height: 400, width: 550 } }; // 像素宽高要根据实际的yuv数据填写
    131. this.pixelMapYuyv = image.createPixelMapSync(readBuffer.buffer, opts);
    132. })
    133. }.margin({ left: 5 })
    134. }
    135. }.width('100%')
    136. }.height('100%')
    137. }
    138. }