深入浅出:理解YUV图像合成原理
大家好,又见面了,我是你们的朋友全栈君。
YUV图像合成原理
引言:在视频监控中最常用的就是图像拼接和字符叠加,25FPS的视频流,如果每隔40MS就从各个通道中取一幅图像来合成,则可以看到一个实时的合成视频。合成的过程也就是原始图像的拼接、缩放的过程,本文主要阐述UV分开存储的YUV420图像拼接的过程,实现下图的效果。
一、原图图像格式
1、图像常用的格式有两种RGB和YUV
(1)YUV是被欧洲电视系统所采用的一种颜色编码方法(属于PAL),是PAL和SECAM模拟彩色电视制式采用的颜色空间。在现代彩色电视系统中,通常采用三管彩色摄影机或彩色CCD摄影机进行取像,然后把取得的彩色图像信号经分色、分别放大校正后得到RGB,再经过矩阵变换电路得到亮度信号Y和两个色差信号R-Y(即U)、B-Y(即V),最后发送端将亮度和色差三个信号分别进行编码,用同一信道发送出去。这种色彩的表示方法就是所谓的YUV色彩空间表示。采用YUV色彩空间的重要性是它的亮度信号Y和色度信号U、V是分离的。
(2)RGB色彩模式是工业界的一种颜色标准,是通过对红(R)、绿(G)、蓝(B)三个颜色通道的变化以及它们相互之间的叠加来得到各式各样的颜色的,RGB即是代表红、绿、蓝三个通道的颜色,这个标准几乎包括了人类视力所能感知的所有颜色,是目前运用最广的颜色系统之一。
2、两者在存储上的区别
(1)YUV按照内存消耗量总体上分为YUV420、YUV422两种
YUV420—–其Y:U:V或者Y:UV或者Y:V:U的总量为4:2:0
YUV422—–其Y:U:V比例为4:2:2
(2)RGB内存比例为1:1:1
则可以看出 显示一个像素点 需要的内存字节数
YUV420=(4+2+0)/4=3/2BYTE
YUV422=(4+2+2)/4=2 BYTE
RGB=(1+1+1)/1=3 BYTE
现在有一副图像,宽W高H,那么显示一副图像所需内存
YUV420=W*H*3/2 BYTE
YUV422=W*H*2 BYTE
RGB=W*H*3 BYTE
3、YUV数据格式YUV420-UV分开存储
X代表Y分量 0代表U/V分量 420的常规打包格式如下:
也就是说在水平方向和锤子4个像素点Y分量公用一组U/V分量
在内存结构上U/V分量水平和垂直分量均为1/2 U=w/2*h/2 V=w/2*h/2
用表格数据得出来就是
二、图像合成过程
合成前的图像和需要合成到的目的图像如下图所示
需要进行采样缩放、贴图后就能实现图像合成
1、采样
这里不做详细介绍 只看采样后效果
2、贴图
贴图原理-采样后图像的Y分量直接memcpy到合成图像的对应区域,Y/V分量则需要注意下
U/V水平和垂直均1/2采样
隔行拷贝,每次拷贝采样后w/2长度。由于合成图像的U/V数据已经隔行-存储的时候是连续的,所以UV拷贝的时候连续拷贝采样后长度/2然后进入合成图像下一行的U/V然后再拷贝,拷贝的高度为采样后图像高度h/2
上代码
//图像类型枚举
typedef enum IMAGE_TYPE
{
IMAGE_YUV420,
IMAGE_YUV422
};
//YUV图像结构体
typedef struct YUV_IMAGE
{
YUV_IMAGE(){ y = NULL; u = NULL; v = NULL; w = 0; h = 0; }
~YUV_IMAGE(){ y = NULL; u = NULL; v = NULL; w = 0; h = 0; }
UCHAR *y;
UCHAR *u;
UCHAR *v;
UINT w;
UINT h;
IMAGE_TYPE t;
};
/*从各个通道获取图像并缩放到合成图像上*/
BOOL CDigitalCourt::GetMergeImg()
{
DWORD dwTimeBegin = GetTickCount();
BOOL bRet = FALSE;
YUV_IMAGE *pImg = NULL;
for (int i = 0; i < MAX_CHANNEL_CNT; i++)
{
if (m_pChannel[i])
{
//将YUV图像进行拷贝
if (m_pChannel[i]->GetYUVImage(&pImg))
{
//将拷贝后的YUV图像缩放到合成图像上
MergeImage(pImg, &m_MergeImg, i);
bRet = TRUE;
}
}
}
//memcpy(m_MergeImg.y, src->y, 1920 * 1080 * 3 / 2);
/*memset(m_MergeImg.y, m_nMergeFrmCnt % 255, m_MergeImg.w*m_MergeImg.h);
memset(m_MergeImg.u, m_nMergeFrmCnt % 255 + 100, m_MergeImg.w*m_MergeImg.h / 2);
*/
return bRet;
}
//创建采样器
SwsContext * CDigitalCourt::CreateSws(YUV_IMAGE *src, UINT nOrder, UINT w, UINT h)
{
if (NULL == src || nOrder >= MAX_CHANNEL_CNT || 0 == w || 0 == h)
return NULL;
if (NULL == m_pSubSws[nOrder])
{
m_pSubSws[nOrder] = sws_getContext(src->w,
src->h,
AV_PIX_FMT_YUV420P,
w,
h,
AV_PIX_FMT_YUV420P,
SWS_BICUBIC,
NULL,
NULL,
NULL);
if (NULL == m_pSubSws)
{
LOG(LOG_ERROR, "CDigitalCourt::Create sws failed index=%d,w=%d,h=%d,mergeW=%d,mergeH=%d!", nOrder, src->w, src->h, m_nMergeW, m_nMergeH);
return FALSE;
}
}
return m_pSubSws[nOrder];
}
//图像缩放--从源图像缩放到另外一个图像空间中去
void CDigitalCourt::Scale(YUV_IMAGE *src, YUV_IMAGE *dst, SwsContext *pSws)
{
AVPicture pictsrc;
AVPicture pictdst;
if (NULL == src || NULL == dst || NULL == pSws) return;
pictsrc.data[0] = src->y;
pictsrc.data[1] = src->u;
pictsrc.data[2] = src->v;
pictsrc.linesize[0] = src->w;
pictsrc.linesize[1] = src->w / 2;
pictsrc.linesize[2] = src->w / 2;
pictdst.data[0] = dst->y;
pictdst.data[1] = dst->u;
pictdst.data[2] = dst->v;
pictdst.linesize[0] = dst->w;
pictdst.linesize[1] = dst->w / 2;
pictdst.linesize[2] = dst->w / 2;
sws_scale(pSws, pictsrc.data, pictsrc.linesize, 0, src->h, pictdst.data, pictdst.linesize);
}
//将图像缩放到合成图像上 nOrder--图像位置0~5 对应于合成图像s1-s6
//这里用了两个变量来存储缩放后的图像 m_mainMerge--位置0 m_subMerge--其它位置
void CDigitalCourt::MergeImage(YUV_IMAGE *src, YUV_IMAGE *dst, UINT nOrder)
{
UINT nOffY = 0, nOffx = 0,w=0,h=0;
SwsContext *pSws = NULL;
YUV_IMAGE *pImgSmall = NULL;
if (NULL == src || NULL == dst || nOrder < 0 || nOrder >= MAX_CHANNEL_CNT)
{
return;
}
//根据位置找出图像Y偏移量
switch (nOrder)
{
case 0:
nOffx = 0;
nOffY = 0;
w = dst->w * 2 / 3;
h = dst->h * 2 / 3;
pImgSmall = &m_mainMerge;
break;
case 1:
nOffx = dst->w * 2 / 3;
nOffY = 0;
w = dst->w / 3;
h = dst->h / 3;
pImgSmall = &m_subMerge;
break;
case 2:
nOffx = dst->w * 2 / 3;
nOffY = dst->h / 3;
w = dst->w / 3;
h = dst->h / 3;
pImgSmall = &m_subMerge;
break;
case 3:
nOffx = dst->w * 2 / 3;
nOffY = dst->h * 2 / 3;
w = dst->w / 3;
h = dst->h / 3;
pImgSmall = &m_subMerge;
break;
case 4:
nOffx = dst->w / 3;
nOffY = dst->h * 2 / 3;
w = dst->w / 3;
h = dst->h / 3;
pImgSmall = &m_subMerge;
break;
case 5:
nOffx = 0;
nOffY = dst->h * 2 / 3;
w = dst->w / 3;
h = dst->h / 3;
pImgSmall = &m_subMerge;
break;
default:
LOG(LOG_ERROR, "CDigitalCourt::MergeImage failed of image order error!");
return;
}
//创建缩放器
pSws = CreateSws(src, nOrder, w, h);
//缩放
Scale(src, pImgSmall, pSws);
//图像粘贴
MapImage(pImgSmall, &m_MergeImg, nOffx, nOffY);
}
//图像黏贴
void MapImage(YUV_IMAGE *src, YUV_IMAGE *dst, UINT nOffX, UINT nOffY)
{
if (NULL == src || NULL == dst) return;
if (src->w > dst->w || src->h > dst->h) return;
if (NULL == src->y || NULL == src->u || NULL == src->v) return;
if (NULL == dst->y || NULL == dst->u || NULL == dst->v) return;
UINT nOff = 0;
for (int i = 0; i < src->h; i++)
{
nOff = dst->w*(nOffY + i) + nOffX;
//逐行拷贝
memcpy(dst->y + nOff, src->y + src->w*i, src->w);
}
UINT nUVOffX = nOffX / 2, nUVOffY = nOffY / 2;
UINT nUVSrcW = src->w / 2, nUVSrcH = src->h / 2;
UINT nUVDstW = dst->w / 2, nUVDstH = dst->h / 2;
for (int j = 0; j < nUVSrcH; j++)
{
nOff = nUVDstW*(nUVOffY + j) + nUVOffX;
memcpy(dst->u + nOff, src->u + nUVSrcW*j, nUVSrcW);
memcpy(dst->v + nOff, src->v + nUVSrcW*j, nUVSrcW);
}
}
引用文献:
【1】http://baike.baidu.com/view/189685.htm?fr=aladdin
【2】http://blog.****.net/searchsun/article/details/2443867
推荐阅读
-
深入浅出:理解YUV图像合成原理
-
Camera2录制视频(音视频合成)及其YUV数据提取(二)- 图像转化和YUV数据提取
-
理解相机工作原理:图像形成背后的机制
-
详解常见图像滤波算法的实践操作与原理解析
-
图像处理:Halcon滤波算法的工作机制与原理解析
-
常用图像滤波算法的实现与原理解析
-
理解不同图像色彩编码格式:NV21、NV12、YV12、RGB、YUV、RGBA和RGBX8888的区别
-
深入理解Retinex图像增强算法的原理和实践方法
-
[Halcon&拟合] 拟合直线边缘并计算距离-图像预处理: 一般是去噪或抠图(blob分析抠图或手绘ROI区域抠图)两方面 轮廓提取: 1)boundary:区域轮廓提取 2)edges_sub_pix:图像轮廓提取 3)threshold_sub_pix:图像轮廓提取 使用算子edges_sub_pix进行亚像素的边缘提取最为普遍。其用到的滤波器有Deriche, Lanser, Shen, or Canny filters。 关于这几个滤波器的对比,帮助文档有如下介绍: Deriche, Lanser, Shen为递归滤波器,Canny 为掩膜滤波器; 递归滤波器的执行时间不依赖滤波器的大小,Canny的执行时间与滤波器大小成正相关。 参数alpha数值越大,Deriche, Lanser, Shen滤波器宽度越小,平滑越差,细节越突出,而Canny效果相反。 分割、联合(根据情况而定) 分割算子: segment_contours_xld:可分割’lines’,‘lines_circles’,‘lines_ellipses’,原理是多边形逼近,逼近程度通过算子中后两个阀值参数控制。 联合算子: 临近:union_adjacent_contours_xld (Operator) 共线:union_collinear_contours_xld (Operator) 共圆:union_cocircular_contours_xld (Operator) 拟合 fit_line_contour_xld:拟合直线 fit_line_contour_xld:拟合圆 fit_ellipse_contour_xld:拟合椭圆 fit_rectangle2_contour_xld:拟合矩形 注:有时候在拟合轮廓之前需要判断一下轮廓属性,以确定应拟合成直线还是还是圆,可通过算子:get_contour_global_attrib_xld (SingleSegment,‘cont_approx’,)名字:获取轮廓属性描述:用于确定应拟合成直线还是还是圆参数:SingleSegment:输入轮廓(input_object)cont_approx:属性名称,即采用什么方式去计算 ,一般用这个参数就可以了(input_control)Attrib:属性值: Attrib>0:拟合圆,否则拟合直线(output_control) ) 求距离 二、示例:
-
计算机视觉中,究竟有哪些好用的目标跟踪算法(下)-快速变形主要因为CF是模板类方法。容易跟丢这个比较好理解,前面分析了相关滤波是模板类方法,如果目标快速变形,那基于HOG的梯度模板肯定就跟不上了,如果快速变色,那基于CN的颜色模板肯定也就跟不上了。这个还和模型更新策略与更新速度有关,固定学习率的线性加权更新,如果学习率太大,部分或短暂遮挡和任何检测不准确,模型就会学习到背景信息,积累到一定程度模型跟着背景私奔了,一去不复返。如果学习率太小,目标已经变形了而模板还是那个模板,就会变得不认识目标。(举个例子,多年不见的同学,你很可能就认不出了,而经常见面的同学,即使变化很大你也认识,因为常见的同学在你大脑里面的模型在持续更新,而多年不见就是很久不更新) 快速运动主要是边界效应(Boundary Effets),而且边界效应产生的错误样本会造成分类器判别力不够强,下面分训练阶段和检测阶段分别讨论。 训练阶段,合成样本降低了判别能力。如果不加余弦窗,那么移位样本是长这样的: 除了那个最原始样本,其他样本都是“合成”的,100*100的图像块,只有1/10000的样本是真实的,这样的样本集根本不能拿来训练。如果加了余弦窗,由于图像边缘像素值都是0,循环移位过程中只要目标保持完整那这个样本就是合理的,只有目标中心接近边缘时,目标跨越边界的那些样本是错误的,这样虽不真实但合理的样本数量增加到了大约2/3(padding= 1),即使这样仍然有1/3(3000/10000)的样本是不合理的,这些样本会降低分类器的判别能力。再者,加余弦窗也不是“免费的”,余弦窗将图像块的边缘区域像素全部变成0,大量过滤掉分类器本来非常需要学习的背景信息,原本训练时判别器能看到的背景信息就非常有限,我们还加了个余弦窗挡住了背景,这样进一步降低了分类器的判别力(是不是上帝在我前遮住了帘。不是上帝,是余弦窗)。 检测阶段,相关滤波对快速运动的目标检测比较乏力。相关滤波训练的图像块和检测的图像块大小必须是一样的,这就是说你训练了一个100*100的滤波器,那你也只能检测100*100的区域,如果打算通过加更大的padding来扩展检测区域,那样除了扩展了复杂度,并不会有什么好处。目标运动可能是目标自身移动,或摄像机移动,按照目标在检测区域的位置分四种情况来看: 如果目标在中心附近,检测准确且成功。 如果目标移动到了边界附近但还没有出边界,加了余弦窗以后,部分目标像素会被过滤掉,这时候就没法保证这里的响应是全局最大的,而且,这时候的检测样本和训练过程中的那些不合理样本很像,所以很可能会失败。 如果目标的一部分已经移出了这个区域,而我们还要加余弦窗,很可能就过滤掉了仅存的目标像素,检测失败。 如果整个目标已经位移出了这个区域,那肯定就检测失败了。 以上就是边界效应(Boundary Effets),推荐两个主流的解决边界效应的方法,但速度比较慢,并不推荐用于实时场合。