YUV介绍
https://zh.wikipedia.org/wiki/YUV
YUV是一种颜色空间,基于YUV的颜色编码是流媒体的常用编码方式。Y表示流明,U、V表示色度、浓度,这种表达方式起初是为了彩色电视与黑白电视之间的信号兼容。 对于图像每一点,Y确定其亮度,UV确认其彩度。
YUV编码是image/video pipeline的重要组成。比如常用的I420相对于RGB24(RGB三个分量各8个字节)的编码格式,只需要一半的存储容量。在流数据传输时降低了带宽压力。
为节省带宽起见,大多数YUV格式平均使用的每像素位数都少于24位。主要的抽样(subsample)格式有YCbCr 4:2:0、YCbCr 4:2:2、YCbCr 4:1:1和YCbCr 4:4:4。YUV的表示法称为A:B:C表示法:
最常用Y:UV记录的比重通常1:1或2:1,DVD-Video是以YUV 4:2:0的方式记录,也就是我们俗称的I420,YUV4:2:0并不是说只有U(即Cb), V(即Cr)一定为0,而是指U:V互相援引,时见时隐,也就是说对于每一个行,只有一个U或者V分量,如果一行是4:2:0的话,下一行就是4:0:2,再下一行是4:2:0...以此类推。至于其他常见的YUV格式有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420等。
I420 的单帧结构示意图如下(Planar 方式):
这幅图的上面一幅可以看出 Y1、Y2、Y7、Y8 共用 U1 和 V1。后面的线性数组为其存储顺序,可以看出 Y、U 和 V 都是顺序存储的,往外写的时候,先按顺序将 Y 分量写出,然后再根据 U、V 分别将它们依次写出即可。
0.颜色赋值
// 如果是设置灰色, 只需要 Y=灰度值比如54 U=V=128 也就是0x80 即可
//
//
// Y = 0.299R + 0.587G + 0.114B
// U = -0.147R - 0.289G + 0.436B+128
// V = 0.615R - 0.515G - 0.100B+128
// R = Y + 1.14V
// G = Y - 0.39U - 0.58V
// B = Y + 2.03U
//
//OR
// Y = 0.257R + 0.504G + 0.098B + 16
// U = 0.148R - 0.291G + 0.439B + 128
// V = 0.439R - 0.368G - 0.071B + 128
// B = 1.164(Y - 16) + 2.018(U - 128)
// G = 1.164(Y - 16) - 0.813(V - 128) - 0.391(U - 128)
// R = 1.164(Y - 16) + 1.596(V - 128)
//
//
// 用桌面拾取工具测试时 设置的灰度值 54 拾取的是43 估计Y最后面那个参数是可变的
详细参考前面文章
1 缩放
// Scales a YUV 4:2:0 image from the src width and height to the
// dst width and height.
// If filtering is kFilterNone, a simple nearest-neighbor algorithm is
// used. This produces basic (blocky) quality at the fastest speed.
// If filtering is kFilterBilinear, interpolation is used to produce a better
// quality image, at the expense of speed.
// If filtering is kFilterBox, averaging is used to produce ever better
// quality image, at further expense of speed.
// Returns 0 if successful.
int I420Scale(const uint8* src_y, int src_stride_y,
const uint8* src_u, int src_stride_u,
const uint8* src_v, int src_stride_v,
int src_width, int src_height,
uint8* dst_y, int dst_stride_y,
uint8* dst_u, int dst_stride_u,
uint8* dst_v, int dst_stride_v,
int dst_width, int dst_height,
FilterMode filtering);
YUV420 空间分布: Y占4 U 占1 V占1
1)示例1
ret_data.append(width * height * 3 / 2, (char)0); //des目的空间 目的宽高 width height
uint8_t* src_y = (uint8_t*)pic_info->pdata_; //soucre内存首地址
uint8_t* src_u = src_y + src_w * src_h; //U 首地址
uint8_t* src_v = src_u + src_w * src_h / 4; //V 首地址
uint8_t* des_y = (uint8_t*)ret_data.c_str(); //des 内存首地址
uint8_t* des_u = des_y + width * height;
uint8_t* des_v = des_u + width * height / 4;
libyuv::FilterMode filter_mode = libyuv::kFilterNone;
libyuv::I420Scale(src_y, src_w,
src_u, src_w / 2,
src_v, src_w / 2,
src_w, src_h,
des_y, width,
des_u, width / 2,
des_v, width / 2,
width, height,
filter_mode);
2)示例2 将画面缩放到画布右侧, 左侧1/4留白
if (nim_comp::VideoManager::GetInstance()->video_frame_mng_.GetVideoFrame("", time, (char*)data_temp.c_str(), width, height, false, false))
{
//CopyI420Data(capture_type_ == kCaptureTypeCamera ? SIZEALL : SIZENE,
// data_ret, data_temp, width, height);
frame_show = true;
int src_w = width;
int src_h = height;
int desWidth = width_ - width_ / 4;
std::string data_mid; //要生成的视频帧数据
int size_Y = desWidth * height;
int size_U = desWidth * height / 4;
int size_V = desWidth * height / 4;
data_mid.append(size_Y, (char)0x36);
data_mid.append(size_U, (char)clr_temp);
data_mid.append(size_V, (char)clr_temp);
bool bCut = false;
if (!bCut) //缩放形式
{
uint8_t* src_y = (uint8_t*)data_temp.c_str();
uint8_t* src_u = src_y + src_w * src_h;
uint8_t* src_v = src_u + src_w * src_h / 4;
uint8_t* des_y = (uint8_t*)data_mid.c_str();
uint8_t* des_u = des_y + desWidth * height;
uint8_t* des_v = des_u + desWidth * height / 4;
libyuv::FilterMode filter_mode = libyuv::kFilterNone;
libyuv::I420Scale(
src_y, src_w,
src_u, src_w / 2,
src_v, src_w / 2,
src_w, src_h,
des_y, desWidth,
des_u, desWidth / 2,
des_v, desWidth / 2,
desWidth, height,
filter_mode);
CopyI420Data(SIZENW,
data_ret, data_mid, desWidth, height);
}
}
3) 等比例缩放
原图等比例缩放内接于画布中
假设:原图宽 src_w 原图高 src_h
画布宽 nCtrlW 画布高 nCtrlH
假设 调整后的图宽width 高height
等比例内接于画面
width==nCtrlW 或height==nCtrlH
根据比例调整令一边,使得全部画面都在控件内,且顶满控件
调整方式
//等比
if (src_h * nCtrlW > src_w * nCtrlH)
{
width = src_w * nCtrlH / src_h; //高顶满 左右留白
}
else
{
height= src_h * nCtrlW / src_w; //宽顶满 上下留白
}
GetVideoFrame(char* out_data, int& width, int& height)
{
int src_w = pic_info->width_;
int src_h = pic_info->height_;
//等比
if (src_h * nCtrlW > src_w * nCtrlH)
{
width = src_w * nCtrlH / src_h; //高顶满 左右留白
}
else
{
height= src_h * nCtrlW / src_w; //宽顶满 上下留白
}
width -= width % 2;
height -= height % 2;
std::string ret_data;
if (width != src_w || height != src_h)
{
//缩放
ret_data.append(width * height * 3 / 2, (char)0); //des目的空间 目的宽高 width height
uint8_t* src_y = (uint8_t*)pic_info->pdata_; //soucre内存首地址
uint8_t* src_u = src_y + src_w * src_h; //U 首地址
uint8_t* src_v = src_u + src_w * src_h / 4; //V 首地址
uint8_t* des_y = (uint8_t*)ret_data.c_str(); //des 内存首地址
uint8_t* des_u = des_y + width * height;
uint8_t* des_v = des_u + width * height / 4;
libyuv::FilterMode filter_mode = libyuv::kFilterNone;
libyuv::I420Scale(src_y, src_w,
src_u, src_w / 2,
src_v, src_w / 2,
src_w, src_h,
des_y, width,
des_u, width / 2,
des_v, width / 2,
width, height,
filter_mode);
}else
{
ret_data.append(pic_info->pdata_, pic_info->size_);
}
//将数据拷贝到out_data中
memcpy(out_data, ret_data.c_str(), ret_data.size());
}
如果要将数据传递出去
memcpy(out_data, ret_data.c_str(), ret_data.size());
示例:
static char clr_temp = 0x80;// ;
std::string data_temp; //临时数据
int size_Y = width_ * height_;
int size_U = width_ * height_/4;
int size_V = width_ * height_ / 4;
data_temp.append(size_Y, (char)0x36);
data_temp.append(size_U, (char)clr_temp);
data_temp.append(size_V, (char)clr_temp);
//将原图缩放 到data_temp中
GetVideoFrame((char*)data_temp.c_str(), width, height)
缩放封装:
图片缩放进一步封装下
ScaleYUV(const std::string& src, int src_width, int src_height,
std::string& dest, int dst_width, int dst_height)
{
uint8_t* src_i420_data = (uint8_t*)src.c_str();
uint8_t* dst_i420_data = (uint8_t*)dest.c_str();
int src_i420_y_size = src_width * src_height;
int src_i420_u_size = (src_width >> 1) * (src_height >> 1);
uint8 *src_i420_y_data = src_i420_data;
uint8 *src_i420_u_data = src_i420_data + src_i420_y_size;
uint8 *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;
int dst_i420_y_size = dst_width * dst_height;
int dst_i420_u_size = (dst_width >> 1) * (dst_height >> 1);
uint8 *dst_i420_y_data = dst_i420_data;
uint8 *dst_i420_u_data = dst_i420_data + dst_i420_y_size;
uint8 *dst_i420_v_data = dst_i420_data + dst_i420_y_size + dst_i420_u_size;
libyuv::I420Scale((const uint8 *)src_i420_y_data, src_width,
(const uint8 *)src_i420_u_data, src_width >> 1,
(const uint8 *)src_i420_v_data, src_width >> 1,
src_width, src_height,
(uint8 *)dst_i420_y_data, dst_width,
(uint8 *)dst_i420_u_data, dst_width >> 1,
(uint8 *)dst_i420_v_data, dst_width >> 1,
dst_width, dst_height,
(libyuv::FilterMode) libyuv::kFilterNone);
}
等比例缩放优化为:
if (width==nMainScreenWidth&&height<=nMainScreenHeight||height==nMainScreenHeight&&width<=nMainScreenWidth)
{
//居中
CopyI420Data(SIZECENTER_Main, data_ret, data_temp, width, height);
}
else
{
//等比例放缩
float fMainRatio = (nMainScreenWidth*1.0) / nMainScreenHeight; //控件比例
float fRadio = (width*1.0) / height; //实际视频图像比例
if (fRadio>fMainRatio)
{
//实际视频比较宽, 则以宽度为准,求等比例后的高度
std::string data_scale;
int nScaleWidth = nMainScreenWidth; //宽度为控件宽度
int nScaleHeight = nScaleWidth / fRadio; //计算等比例缩放后的 高度
nScaleWidth -= nScaleWidth % 2;
nScaleHeight -= nScaleHeight % 2;
int nScaleSize = nScaleWidth * nScaleHeight * 3 / 2;
data_scale.append(nScaleSize, (char)clr_temp); //保存目的图像数据
ScaleYUV(data_temp, width, height,
data_scale, nScaleWidth, nScaleHeight);
CopyI420Data(SIZECENTER_Main, data_ret, data_scale, nScaleWidth, nScaleHeight);
}
else{
//实际视频比较窄,以高度为准,求等比例后的宽度
std::string data_scale;
int nScaleHeight = nMainScreenHeight; //高度为控件高度
int nScaleWidth = nScaleHeight / fRadio; //等比例调整后的宽度
nScaleWidth -= nScaleWidth % 2;
nScaleHeight -= nScaleHeight % 2;
int nScaleSize = nScaleWidth * nScaleHeight * 3 / 2;
data_scale.append(nScaleSize, (char)clr_temp); //保存目的图像数据
ScaleYUV(data_temp, width, height,
data_scale, nScaleWidth, nScaleHeight);
CopyI420Data(SIZECENTER_Main, data_ret, data_scale, nScaleWidth, nScaleHeight);
}
}
4) 裁剪充满
充满控件, 使用的方法是: 最大化选择控件同比例视频内容, 然后,对这部分视频再做缩放操作,充满控件
假设画布的宽高比为2:1
那就是在图像上选择2:1的最大区域, 然后将其放缩在画布中,即可实现充满效果
int src_width_ = src_w; //图像宽高
int src_height_ = src_h;
int dst_width_ = width; //画布宽高
int dst_height_ = height;
// Making sure that destination frame is of sufficient size.
// We want to preserve aspect ratio instead of stretching the frame.
// Therefore, we need to crop the source frame. Calculate the largest center
// aligned region of the source frame that can be used.
const int cropped_src_width =
min(src_width_, dst_width_ * src_height_ / dst_height_);
const int cropped_src_height =
min(src_height_, dst_height_ * src_width_ / dst_width_);
// Make sure the offsets are even to avoid rounding errors for the U/V planes.
const int src_offset_x = ((src_width_ - cropped_src_width) / 2) & ~1;
const int src_offset_y = ((src_height_ - cropped_src_height) / 2) & ~1;
假设图像比例和画布比例相同
实际上,大部分是不同的,但选择的区域比例是相同的
基本值
//YUV420 image size
int I420_Y_Size = src_width_ * src_height_;
int I420_U_Size = src_width_ * src_height_ / 4;
// int I420_V_Size = I420_U_Size;
unsigned char *Y_data_src = (unsigned char *)pic_info->pdata_;
unsigned char *U_data_src = Y_data_src + I420_Y_Size;
unsigned char *V_data_src = Y_data_src + I420_Y_Size + I420_U_Size;
int src_stride_Y = src_width_;
int src_stride_U = (src_width_ + 1) >> 1;
int src_stride_V = src_stride_U;
//最终写入目标
int dest_I420_Y_Size = dst_width_ * dst_height_;
int dest_I420_U_Size = dst_width_ * dst_height_ / 4;
unsigned char *Y_data_dest = (unsigned char *)ret_data.c_str();
unsigned char *U_data_dest = Y_data_dest + dest_I420_Y_Size;
unsigned char *V_data_dest = Y_data_dest + dest_I420_Y_Size + dest_I420_U_Size;
int dest_stride_Y = dst_width_;
int dest_stride_U = (dst_width_ + 1) >> 1;
int dest_stride_V = dest_stride_U;
因为我们选择的是原图像部分区域进行缩放,所以需要计算源图像选中区域的相关参数
const uint8_t* y_ptr =
Y_data_src +
src_offset_y * src_stride_Y +
src_offset_x;
const uint8_t* u_ptr =
U_data_src +
src_offset_y / 2 * src_stride_U +
src_offset_x / 2;
const uint8_t* v_ptr =
V_data_src +
src_offset_y / 2 * src_stride_V +
src_offset_x / 2;
libyuv::I420Scale(
y_ptr,
src_stride_Y,
u_ptr,
src_stride_U,
v_ptr,
src_stride_V,
cropped_src_width, cropped_src_height,
Y_data_dest,
dest_stride_Y,
U_data_dest,
dest_stride_U,
V_data_dest,
dest_stride_V,
dst_width_, dst_height_,
libyuv::kFilterNone);
5)拷贝
// Copy I420 to I420.
int I420Copy(const uint8* src_y, int src_stride_y,
const uint8* src_u, int src_stride_u,
const uint8* src_v, int src_stride_v,
uint8* dst_y, int dst_stride_y,
uint8* dst_u, int dst_stride_u,
uint8* dst_v, int dst_stride_v,
int width, int height);
void I420CopyEx(const uint8* src_y, int src_stride_y, const uint8* src_ya, int src_stride_ya,
const uint8* src_u, int src_stride_u, const uint8* src_ua, int src_stride_ua,
const uint8* src_v, int src_stride_v, const uint8* src_va, int src_stride_va,
uint8* dst_y, int dst_stride_y,
uint8* dst_u, int dst_stride_u,
uint8* dst_v, int dst_stride_v,
int width, int height)
{
if (src_stride_ya == 0 && src_stride_ua == 0 && src_stride_va == 0)
{
libyuv::I420Copy(src_y, src_stride_y,
src_u, src_stride_u,
src_v, src_stride_v,
dst_y, dst_stride_y,
dst_u, dst_stride_u,
dst_v, dst_stride_v,
width, height);
}
else
{
//ARGBTO420 自己做的转换
for (int ih = 0; ih < height; ++ih)
{
bool uv = ih % 2 == 0;
for (int iw = 0; iw < width / 2; ++iw)
{
MixClr(src_y++, dst_y++, *src_ya++);
MixClr(src_y++, dst_y++, *src_ya++);
if (uv)
{
MixClr(src_u++, dst_u++, *src_ua++);
MixClr(src_v++, dst_v++, *src_va++);
}
}
src_y += (src_stride_y - width);
src_ya += (src_stride_ya - width);
dst_y += (dst_stride_y - width);
if (uv)
{
src_u += (src_stride_u - width / 2);
src_v += (src_stride_v - width / 2);
src_ua += (src_stride_ua - width / 2);
src_va += (src_stride_va - width / 2);
dst_u += (dst_stride_u - width / 2);
dst_v += (dst_stride_v - width / 2);
}
}
}
}
CopyI420Data(int type, std::string& dest, const std::string& src, int width, int height, ImageYuvDataType image_type, POINT pt)
{
int nCtrlH = height_; //目的控件画布的高度
int nCtrlW = width_; //目的控件画布的宽度
int left = (nCtrlW - width) / 2;
left -= left % 2;
int top = (nCtrlH - height) / 2;
top -= top % 2;
if (left < 0)
left = 0;
if (top < 0)
top = 0;
int stride_y = width;
int stride_u = width / 2;
int stride_v = width / 2;
int stride_ya = 0;
int stride_ua = 0;
int stride_va = 0;
if (image_type == kYuvDataTypeImage || image_type == kYuvDataTypeImageAlpha)
{
stride_y *= 2; //ARGBTO420转换的 跨度不一样
stride_u *= 2;
stride_v *= 2;
if (image_type == kYuvDataTypeImageAlpha)
{
stride_ya = stride_y;
stride_ua = stride_u;
stride_va = stride_v;
}
}
uint8_t* src_y = (uint8_t*)src.c_str();
uint8_t* src_u = src_y + stride_y * height;
uint8_t* src_v = src_u + stride_u * height / 2;
uint8_t* des_y = (uint8_t*)dest.c_str();
uint8_t* des_u = des_y + nCtrlW * nCtrlH;
uint8_t* des_v = des_u + nCtrlW * nCtrlH / 4;
des_y += left + top * nCtrlW;
des_u += left / 2 + top * nCtrlW / 4;
des_v += left / 2 + top * nCtrlW / 4;
I420CopyEx(src_y, stride_y, src_y + width, stride_ya,
src_u, stride_u, src_u + width / 2, stride_ua,
src_v, stride_v, src_v + width / 2, stride_va,
des_y, nCtrlW,
des_u, nCtrlW / 2,
des_v, nCtrlW / 2,
width, height);
}
6) 区域拷贝
由原图的某位置 拷贝到目标图指定位置 指定大小
void ScreenCaptureTool::CutCopyI420Data(std::string& dest, int dest_xPos, int dest_yPos, const std::string& src, int src_xPos, int src_yPos, int srcWidth, int srcHeight, int cutWidth, int cutHeight, ImageYuvDataType image_type/* = kYuvDataTypeNormal*/)
{
int nCtrlH = height_; //目的控件画布的高度
int nCtrlW = width_; //目的控件画布的宽度
int stride_y = srcWidth;
int stride_u = srcWidth / 2;
int stride_v = srcWidth / 2;
int stride_ya = 0;
int stride_ua = 0;
int stride_va = 0;
if (image_type == kYuvDataTypeImage || image_type == kYuvDataTypeImageAlpha)
{
stride_y *= 2; //ARGBTO420转换的 跨度不一样
stride_u *= 2;
stride_v *= 2;
if (image_type == kYuvDataTypeImageAlpha)
{
stride_ya = stride_y;
stride_ua = stride_u;
stride_va = stride_v;
}
}
uint8_t* src_y = (uint8_t*)src.c_str();
uint8_t* src_u = src_y + stride_y * srcHeight;
uint8_t* src_v = src_u + stride_u * srcHeight / 2;
uint8_t* des_y = (uint8_t*)dest.c_str();
uint8_t* des_u = des_y + nCtrlW * nCtrlH;
uint8_t* des_v = des_u + nCtrlW * nCtrlH / 4;
src_y += src_xPos + src_yPos * srcWidth;
src_u += src_xPos / 2 + src_yPos * srcWidth / 4;
src_v += src_xPos / 2 + src_yPos * srcWidth / 4;
des_y += dest_xPos + dest_yPos * nCtrlW;
des_u += dest_xPos / 2 + dest_yPos * nCtrlW / 4;
des_v += dest_xPos / 2 + dest_yPos * nCtrlW / 4;
I420CopyEx(src_y, stride_y, src_y + srcWidth, stride_ya,
src_u, stride_u, src_u + srcWidth / 2, stride_ua,
src_v, stride_v, src_v + srcWidth / 2, stride_va,
des_y, nCtrlW,
des_u, nCtrlW / 2,
des_v, nCtrlW / 2,
cutWidth, cutHeight);
}
示例:
if (bMain)
{
//裁剪绘制
CutCopyI420Data(data_ret, 0, 0, data_temp, 0, 0, 1280, 720, 1040, 720);
}
if (bSec)
{
//裁剪绘制
CutCopyI420Data(data_ret, 1040, 0, data_temp, 1040, 0, 1280, 720, 240, 180);
}