什麼是yuv
YUV 颜色编码采用的是 明亮度 和 色度 来指定像素的颜色。
其中,Y 表示明亮度(Luminance、Luma),而 U 和 V 表示色度、浓度(Chrominance、Chroma)。
和 RGB 表示图像类似,每个像素点都包含 Y、U、V 分量。但是它的 Y 和 UV 分量是可以分离的,如果没有 UV 分量一样可以显示完整的图像,只不过是黑白的,所以yuv图像可以兼容於黑白影像和彩色影像。
为什么yuv更省空间
RGB像素表示法很简单,如果你没做过数字图像和视频的开发,可能很少听说过YUV。但在数字图像和视频编码里领域,YUV像素表示法非常流行,有几个原因造成。首先,人眼对亮度更敏感,对颜色的敏感度稍弱,所以使用YUV来表示图像可以节省存储资源。其次由于数字摄像机传感器不能直接采样三原色,所以RGB也不适合硬件处理。因此YUV才如此应用广泛。
用RGB表示像素需要用3个字节。但YUV表示一个像素,可能是3个字节,也可能是2个字节(丢掉U或者丢掉V),还可能只有1个字节(丢掉U和V)。占用字节大小的不同因为采用不同的采样方式。
YUV采样格式
YUV 图像的主流采样方式有如下三种:
- YUV 4:4:4采样
- YUV 4:2:2采样
- YUV 4:2:0采样
YUV 4:4:4 采样
YUV 4:4:4 采样,意味着 Y、U、V 三个分量的采样比例相同,一个像素点,都是(Y、U、V)3个字节组成
举个例子 :
假如图像像素为:[Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]
那么采样的码流为:Y0 U0 V0 Y1 U1 V1 Y2 U2 V2 Y3 U3 V3
最后映射出的像素点依旧为 [Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]
YUV 4:2:2采样
YUV 4:2:2 采样,意味着 UV 分量是 Y 分量采样的一半,Y 分量和 UV 分量按照 2 : 1 的比例采样。如果水平方向有 10 个像素点,那么采样了 10 个 Y 分量,而只采样了 5 个 UV 分量。
举个例子 :
假如图像像素为:[Y0 U0 V0]、[Y1 U1 V1]、[Y2 U2 V2]、[Y3 U3 V3]
那么采样的码流为:Y0 U0 Y1 V1 Y2 U2 Y3 V3
其中,每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一个采集一个。
最后映射出的像素点为 [Y0 U0 V1]、[Y1 U0 V1]、[Y2 U2 V3]、[Y3 U2 V3]
YUV 4:2:0 采样
举个例子 :
假设图像像素为:
[Y0 U0 V0]、[Y1 U1 V1]、 [Y2 U2 V2]、 [Y3 U3 V3]
[Y5 U5 V5]、[Y6 U6 V6]、 [Y7 U7 V7] 、[Y8 U8 V8]
那么采样的码流为:Y0 U0 Y1 Y2 U2 Y3 Y5 V5 Y6 Y7 V7 Y8
其中,每采样过一个像素点,都会采样其 Y 分量,而 U、V 分量就会间隔一行按照 2 : 1 进行采样。
最后映射出的像素点为:
[Y0 U0 V5]、[Y1 U0 V5]、[Y2 U2 V7]、[Y3 U2 V7]
[Y5 U0 V5]、[Y6 U0 V5]、[Y7 U2 V7]、[Y8 U2 V7]
YUV存储格式
采样之后,就是YUV的存储,往往我们操作的都是存储格式的二进制,所以要理解YUV存储的排列方式。才能正常解析YUV图像。
YUV 的存储格式,有两种:
- planar 平面格式
- 指先连续存储所有像素点的 Y 分量,然后存储 U 分量,最后是 V 分量。
- packed 打包模式
- 指每个像素点的 Y、U、V 分量是连续交替存储的。
这里篇幅关系,只介绍YUV420的存储格式(比较常用)
YUV 420P 和 YUV 420SP 都是基于 Planar 平面模式 进行存储的,先存储所有的 Y 分量后, YUV420P 类型就会先存储所有的 U 分量或者 V 分量,而 YUV420SP 则是按照 UV 或者 VU 的交替顺序进行存储了,具体查看看下图:
YUV420SP 的格式:
YUV420P 的格式:
為什麼yuv要轉換成rgb?
浏览器不能直接识别输出yuv图像,需要转成rgb格式。
YUV与RBG的转换
为了实现格式转换,我们首先要明确待转换格式和目标格式的特点和相互转换关系,这是编程实现转换的核心。
YUV转RGB的公式,我查阅到的不少于3个,但我不明白原理,需要查阅下:
Y = 0.298R + 0.612G + 0.117B;
U = -0.168R - 0.330G + 0.498B + 128;
V = 0.449R - 0.435G - 0.083B + 128;
R = Y + 1.4075( V - 128);
G = Y - 0.3455( U - 128) - 0.7169( V - 128);
B = Y + 1.779( U - 128);
比如YUV图像转RGB图像,需要逐个遍历像素所需的YUV字节,通过公式转换为每个像素对应的RGB字节即可
YUV中的stride
stride可以翻译为:跨距、步长
stride指在内存中每行像素所占的空间。如下图所示,为了实现内存对齐,每行像素在内存中所占的空间并不是图像的宽度。
一般stride都是等于图像的width,除非有内存对齐的需要,不过我还没遇到这种情况。
JS实现YUV转RGB渲染图像
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style></style>
</head>
<body>
<img id="img" src="./cat-color.jpg" alt="" />
<input type="file" />
<canvas id="canvas"></canvas>
<script>
var clamp = function(val) {
if(val < 0) return 0
if(val > 255) return 255
return val
}
var image = document.getElementById("img");
image.onload = function () {
var canvas = document.createElement("canvas");
(canvas.width = image.naturalWidth),
(canvas.height = image.naturalHeight);
var context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
var data = context.getImageData(0, 0, canvas.width, canvas.height);
console.log(data);
};
const input = document.querySelector("input");
input.addEventListener("change", function (e) {
var file = e.target.files[0];
var fr = new FileReader();
fr.readAsArrayBuffer(file);
fr.addEventListener(
"loadend",
(e) => {
var buf = e.target.result;
var data = new Uint8ClampedArray(buf);
// 这里是没问题的
const width = 640;
const height = 480;
const bytesY = data.slice(0, width * height);
const bytesCb = data.slice(width * height, width * height * 1.25);
const bytesCr = data.slice(
width * height * 1.25,
width * height * 1.5
);
const strideY = 640;
const strideCb = strideY / 2;
const strideCr = strideY / 2;
const hdec = 1;
const vdec = 1;
let output = new Uint8ClampedArray(width * height * 4);
let xdec = 0;
let outPtr = 0;
let pos = 0;
for (y = 0; y < height; y++) {
xdec = 0;
ydec = y >> vdec;
YPtr = (ydec * strideY) | 0;
CbPtr = (ydec * strideCb) | 0;
CrPtr = (ydec * strideCr) | 0;
// 遍历
for (x = 0; x < width; x++) {
xdec = x >> hdec;
colorCb = bytesCb[CbPtr + xdec] | 0;
colorCr = bytesCr[CrPtr + xdec] | 0;
if(pos == 1280) {
console.log(CbPtr + xdec)
console.log(CrPtr + xdec)
}
let Y = bytesY[pos];
let U = colorCb;
let V = colorCr;
let C = Y - 16;
let D = U - 128;
let E = V - 128;
output[outPtr] = clamp((298*C + 409 * E +128)>>8)
output[outPtr + 1] = clamp((298*C - 100* D - 208* E+ 128)>>8)
output[outPtr + 2] = clamp((298*C + 516* D- 128)>>8)
output[outPtr + 3] = 255;
outPtr += 4;
pos++;
}
}
console.log(output);
var canvas = document.getElementById("canvas");
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext("2d");
var imgData = ctx.createImageData(width, height);
for (var i = 0; i < output.length; i++) {
imgData.data[i] = output[i];
}
console.log(imgData);
ctx.putImageData(imgData, 0, 0, 0, 0, width, height);
},
false
);
});
</script>
</body>
</html>
webgl显示YUV
上文我们知道canvas渲染需要将YUV转换为RGB渲染,但是这需要消耗CPU,而使用WebGL转换可以调用GPU加速,从而减轻CPU的压力。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style></style>
</head>
<body>
<img id="img" src="./cat-color.jpg" alt="" />
<input type="file" />
<canvas id="canvas"></canvas>
<script>
function Texture(gl) {
this.gl = gl;
this.texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}
Texture.prototype.bind = function (n, program, name) {
var gl = this.gl;
gl.activeTexture([gl.TEXTURE0, gl.TEXTURE1, gl.TEXTURE2][n]);
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.uniform1i(gl.getUniformLocation(program, name), n);
};
Texture.prototype.fill = function (width, height, data) {
var gl = this.gl;
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.LUMINANCE,
width,
height,
0,
gl.LUMINANCE,
gl.UNSIGNED_BYTE,
data
);
};
function setupCanvas(canvas) {
var gl =
canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
if (!gl) return gl;
// 创建着色器
var program = gl.createProgram();
var vertexShaderSource = [
"attribute highp vec4 aVertexPosition;",
"attribute vec2 aTextureCoord;",
"varying highp vec2 vTextureCoord;",
"void main(void) {",
" gl_Position = aVertexPosition;",
" vTextureCoord = aTextureCoord;",
"}",
].join("\n");
var vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);
var fragmentShaderSource = [
"precision highp float;",
"varying lowp vec2 vTextureCoord;",
"uniform sampler2D YTexture;",
"uniform sampler2D UTexture;",
"uniform sampler2D VTexture;",
"const mat4 YUV2RGB = mat4",
"(",
" 1.1643828125, 0, 1.59602734375, -.87078515625,",
" 1.1643828125, -.39176171875, -.81296875, .52959375,",
" 1.1643828125, 2.017234375, 0, -1.081390625,",
" 0, 0, 0, 1",
");",
"void main(void) {",
" gl_FragColor = vec4( texture2D(YTexture, vTextureCoord).x, texture2D(UTexture, vTextureCoord).x, texture2D(VTexture, vTextureCoord).x, 1) * YUV2RGB;",
"}",
].join("\n");
var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSource);
gl.compileShader(fragmentShader);
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.log("Shader link failed.");
}
var vertexPositionAttribute = gl.getAttribLocation(
program,
"aVertexPosition"
);
gl.enableVertexAttribArray(vertexPositionAttribute);
var textureCoordAttribute = gl.getAttribLocation(
program,
"aTextureCoord"
);
gl.enableVertexAttribArray(textureCoordAttribute);
var verticesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
1.0, 1.0, 0.0, -1.0, 1.0, 0.0, 1.0, -1.0, 0.0, -1.0, -1.0, 0.0,
]),
gl.STATIC_DRAW
);
gl.vertexAttribPointer(
vertexPositionAttribute,
3,
gl.FLOAT,
false,
0,
0
);
var texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0]),
gl.STATIC_DRAW
);
gl.vertexAttribPointer(textureCoordAttribute, 2, gl.FLOAT, false, 0, 0);
gl.y = new Texture(gl);
gl.u = new Texture(gl);
gl.v = new Texture(gl);
gl.y.bind(0, program, "YTexture");
gl.u.bind(1, program, "UTexture");
gl.v.bind(2, program, "VTexture");
return gl;
}
function renderFrame(gl, videoFrame, width, height, uOffset, vOffset) {
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.y.fill(width, height, videoFrame.subarray(0, uOffset));
gl.u.fill(
width >> 1,
height >> 1,
videoFrame.subarray(uOffset, uOffset + vOffset)
);
gl.v.fill(
width >> 1,
height >> 1,
videoFrame.subarray(uOffset + vOffset, videoFrame.length)
);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
var clamp = function(val) {
if(val < 0) return 0
if(val > 255) return 255
return val
}
const input = document.querySelector("input");
input.addEventListener("change", function (e) {
var file = e.target.files[0];
var fr = new FileReader();
fr.readAsArrayBuffer(file);
fr.addEventListener(
"loadend",
(e) => {
var buf = e.target.result;
// 这里是没问题的
const width = 640;
const height = 480;
const ylen = width * height;
const uvlen = (width / 2) * (height / 2);
var canvas = document.getElementById("canvas");
canvas.width = width;
canvas.height = height;
var renderContext = setupCanvas(canvas, {
preserveDrawingBuffer: false,
});
var data = new Uint8ClampedArray(buf);
renderFrame(
renderContext,
data,
width,
height,
ylen,
uvlen
);
},
false
);
});
</script>
</body>
</html>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。