如何将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的转换公式。
- // 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 };
- }
- 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%')
- }
- }
合作咨询
我们的专家服务团队将竭诚为您提供专业的合作咨询服务
解决方案
精准高效的一站式服务支持,助力开发者商业成功

