一、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%')
}
}