1. H.264编码的介绍
H.264是一种 视频高压缩技术,全称是MPEG-4 AVC,用中文说是“活动图像专家组-4的高等视频编码”,或称为MPEG-4 Part10。它是由国际电信标准化部门ITU-T和规定MPEG的国际标准化组织ISO/国际电工协会IEC共同制订的一种活动图像编码方式的国际标准 格式。由于H.264在制定时就充分考虑了多媒体通信对视频编解码的各种要求,并借鉴了H系列和MPEG系列视频标准的研究成果,因而具有明显的优势。 H.264作为最新的国际建议标准,在IP视频监控系统中有着重要的意义。它与目前的Mpeg4和H.263编码相比较,优势表现在以下几个方面:
1) 压缩率和图像质量方面
H.264通过对传统的帧内预测、帧间预测、变换编码和熵编码等算法的改进来进一步提高编码效率和图像质量。在相同的重建图像质量下,H.264比H.263节约50%左右的码率,比Mpeg4节约35%左右。
2) 网络适应性方面
H.264支持不同网络资源下的分级编码传输,从而获得平稳的图像质量。H.264能适应于不同网络中的视频传 输,网络亲和性好。H.264的基本系统无需使用版权,具有开放的性质,能很好地适应IP和无线网络的使用,这对目前的因特网传输多媒体信息、移动网中传 输宽带信息等都具有重要的意义。
3) 抗丢包和抗误码方面
H.264具有较强的抗误码特性,可适应丢包率高、干扰严重的信道中的视频传输。
实际应用中,实时性和较好的图像质量,较低的网络带宽占用以及带宽适应能力是监控系统的主要考虑因素。H.264 相比较以前的视频编码标准,主要在网络接口友好性和高的压缩性能上有了很大的提高。综合以上因素在本系统中采用H.264作为视频数据的编码方式。
2. H.264编码的实现原理
1) 量化
H. 264为了提高码率控制的能力, 量化步长的变化的幅度控制在12.5 %左右, 而不是以不变的增幅变化。变换系数幅度的归一化被放在反量化过程中处理以减少计算的复杂性。为了强调彩色的逼真性,对色度系数采用了较小量化步长。
2) 运动补偿
宏块划分方式: H. 264 支持形状不等的宏块划分, 其划分方法有: 16×16, 16×8, 8×16, 8×8, 8×4, 4×8, 4×4. 这种更小的、更多形状的的宏块划分,更切合图形中实际运动物体的形状, 改善运动补偿的精度, 更好的实现运动隔离, 提高图像质量和编码效率. 搜索精度: H. 264 支持1/4 和1/8 精度的运动补偿, 使用一个6 抽头滤波器从整像素样本的到1/2 像素样本, 用线性插值获得1/4 像素样本, 用8 抽头滤波器实现1/8 像素精度。 多参考帧模式: H. 264 在对周期性的运动或背景切换进行预测时, 多参考帧可以提供更好的预测效果。
3) 帧内预测
4) 变换编码
5) 熵编码
3 H.264编码算法的实现
在H.264编码具体实现过程中,采用了目前国际上应用最广泛的开源编码器X.264作为实现的基础。X.264和JM系列编码器、T.264编码器相比 有着优秀的性能和出色效果。由于X.264没有提供直接的开发API,所以在本系统中的编码部分重新封装了X.264的编码API,便于软件系统的设计和 使用。以下是本系统中H.264编码的具体实现过程:
1) RGB和YUV颜色空间的转换
在系统中通过Logitech摄像头获得的视频数据为RGB24格式,但是X.264的输入流为标准的YUV(4:2:0)的图像子采样格式。因此,在编 码前需要将RGB颜色空间转换为YUV的颜色空间。实现的函数调用有InitLookupTable()用于初始化色彩空间转换;
RGB2YUV420(int x_dim, int y_dim, unsigned char *bmp, unsigned char *yuv, int flip);用于实际的转换。由于人眼的生理特性,经过图像子采样后,实际的图像大小已经减小为采样前的1.5个样本点,即减小了一半的数据量。
2) 设置H.264编码参数
使用x264_param_default(x264_param_t *param)对当前需要编码的图像参数进行设置。包括数据帧数量(param .i_frame_total)、采样图像的长宽度和高度(param .i_width,param .i_height)、视频数据比特率(param .rc.i_bitrate) 、视频数据帧率(param .i_fps_num)等参数进行设置,以完成编码前预设置。
3) 初始化编码器
将上步中的设置作为编码器初始化的参数,x264_t *x264_encoder_open ( x264_param_t *param )。如果初始化失败将返回NULL,在这里需要对编码器初始化结果进行处理。
4) 分配编码空间
如果编码器初始化成功,则需要为本次处理分配内存空间
Void x264_picture_alloc(x264_picture_t *pic, int i_csp, int i_width, int i_height)。
5) 图像编码
将以上步骤初始化后的数据作为编码输入,使用下面的方法进行编码:
int x264_encoder_encode( x264_t *h,x264_nal_t **pp_nal, int *pi_nal,x264_picture_t *pic_in,x264_picture_t *pic_out );
6) 资源回收
编码完成后,需要回收系统资源和关闭编码器,使用以下函数调用实现回收。
void x264_picture_clean( x264_picture_t *pic );
void x264_encoder_close( x264_t *h );
至此,完成了H.264编码,编码后的数据量将大大减小。我们可以对编码后的数据做相关的进一步处理。
4 H.264编码算法的完整源代码
文件:VideoEncoderX264.h
class CVideoEncoderX264 :
{
public:
CVideoEncoderX264(void);
~CVideoEncoderX264(void);
virtual bool Connect(CVideoEnDecodeNotify* pNotify, const CVideoEnDecodeItem& Item);
virtual void Release(void);
virtual void Encode(BYTE* pInData, int nLen, BYTE* pOutBuf, int& nOutLen, int& nKeyFrame);
private:
x264_picture_t m_Pic;
x264_t *h;
x264_param_t param;
void Flush(void);
};
文件:VideoEncoderX264.cpp
bool CVideoEncoderX264::Connect(CVideoEnDecodeNotify* pNotify, const CVideoEnDecodeItem& Item)
{
CBase::Connect(pNotify, Item);
ParseSize(Item.m_stSize);
x264_param_default( ¶m );
param.i_threads = 1;
param.i_frame_total = 0;
param.i_width = m_nWidth;
param.i_height = m_nHeight;
param.i_keyint_min = Item.m_nKeyInterval;
param.i_keyint_max = Item.m_nKeyInterval * 10;
param.i_fps_num = Item.m_nFps;*/
param.i_log_level = X264_LOG_NONE;
if( ( h = x264_encoder_open( ¶m ) ) == NULL )
{
return false;
}
/* Create a new pic */
x264_picture_alloc( &m_Pic, X264_CSP_I420, param.i_width, param.i_height );
return true;
}
void CVideoEncoderX264::Release(void)
{
Flush();
x264_picture_clean( &m_Pic );
x264_encoder_close( h );
CBase::Release();
}
void CVideoEncoderX264::Encode(BYTE* pInData, int nLen, BYTE* pOutBuf, int& nOutLen, int& nKeyFrame)
{
if(nLen != param.i_width * param.i_height * 3)
return;
param.i_frame_total ++;
memcpy(m_Pic.img.plane[0], pInData, param.i_width * param.i_height);
memcpy(m_Pic.img.plane[1], pInData + param.i_width * param.i_height, param.i_width * param.i_height / 4);
memcpy(m_Pic.img.plane[2], pInData + param.i_width * param.i_height * 5 / 4, param.i_width * param.i_height / 4);
m_Pic.i_pts = (int64_t)param.i_frame_total * param.i_fps_den;
static x264_picture_t pic_out;
x264_nal_t *nal = NULL;
int i_nal, i;
if( &m_Pic )
{
m_Pic.i_type = X264_TYPE_AUTO;
m_Pic.i_qpplus1 = 0;
}
//TraceTime("x264_encoder_encode begin");
if( x264_encoder_encode( h, &nal, &i_nal, &m_Pic, &pic_out ) < 0 ) {
return;
}
//TraceTime("x264_encoder_encode end");
int nOutCanUse = nOutLen;
nOutLen = 0;
for( i = 0; i < i_nal; i++ )
{
int i_size = 0;
if( ( i_size = x264_nal_encode( pOutBuf + nOutLen, &nOutCanUse, 1, &nal[i] ) ) > 0 )
{
nOutLen += i_size;
nOutCanUse -= i_size;
}
}
nKeyFrame = pic_out.i_type==X264_TYPE_IDR;// || (pic_out.i_type==X264_TYPE_I && coCfg->x264_max_ref_frames==1);
}
void CVideoEncoderX264::Flush(void)
{
x264_picture_t pic_out;
x264_nal_t *nal;
int i_nal, i;
int i_file = 0;
if( x264_encoder_encode( h, &nal, &i_nal, NULL, &pic_out ) < 0 ){
}
}
很长一段时间没有写博客了,原因很多。SVC相关的研究和代码推进都暂时停止了, 因为有太多的其它学习和开发。SVC在视频的灵活传送和错误隐藏当面是有很大潜力的,但并非意味着它能适合于真正的网络工程应用。最近随着“阿凡达”的热播,很多人对3D视频的兴趣都热情起来,立体视频时代由这样的错觉3D开始,也将打开它的大幕了。
3D视频的制作超出了编解码范畴,更重要的利用视差进行渲染和视频特技制作,希望内行的朋友去看“阿发达”的时候,不仅仅是看热闹。
不要紧跟这些热闹的话题,今年还是踏踏实实的去探索一些基础知识的研究。打算继续对X264和FFMPEG相关内容的深入探讨,打算重新开展MVC 和机器视觉结合学习,打算把基础图像处理的算法重新复习,并且写作小结博客。等待一切的浮华被拂去,视频技术还将艰难的像更高级的目标,全息视频迈进。
这一次的文章将分析X264的多线程过程,也可以说是并行编码过程。
1. 编译并行编码的x264
从X264的帮助命令行可以看到,添加--threads项可以调整运行的线程数,可是当我完成X264编译,视图对手头的YUV进行编码的时候, 发现在自己的双核计算机上,只能发挥50%的效率,即使使用--threads n 也无济于事,提示就是没有打开pthread支持。Pthreads定义了一套 C程序语言类型、函数与常量,它以 pthread.h 头文件和一个线程库实现。【1】
下面就把我在windows上实现pthread版本的X264编译过程写作如下:
2009年3月的66版本
1. 从http://sourceware.org/pthreads-win32/ 下载pthread的win32版本,把其中的include和lib加入到VC++的引用目录中去。
2. 在项目属性的“C/C++ -> 预处理器 ->预处理器”中加入HAVE_PTHREAD。
3. 在osdep.h文件,紧接着#ifdef USE_REAL_PTHREAD加入
#pragma comment(lib, "pthreadVC2.lib")
引用pthreadVC2.lib,重新编译。
2009年10月的77版本
4. 在项目属性的“C/C++ -> 预处理器 ->预处理器”中加入SYS_MINGW。
其它版本请自己根据可能的编译错误随机应变。调整项目属性意味着同时调整libx264和x264两处的属性。
经过如上调整编译出的X264就可以在--threads n //n>=2的时候用完CPU的潜力了。
2. X264的编码基本流程
(1)接口变更
以前曾经写过文章介绍X264的编程架构并且分析了它的接口,现在进一步看看x264是怎么把YUV图像编程H.264编码的。在代码分析中,最 容易让人头疼的是X264代码随处充斥着的多线程处理和码率控制两方面的代码,所以,这里将先简化过程,忽略掉这些非主体代码。需要说明的是,本文分析的 是版本77,2009年10月的版本。
这里的API比版本66少了x264_nal_encode(...),该函数是将码率封装成NAL,现在它被放到static int x264_encoder_encapsulate_nals( x264_t *h )中,不再作为单独API出现。而x264_encoder_encapsulate_nals(...)分别被 x264_encoder_headers(...)和x264_encoder_frame_end(...)所调用,分别用于封装参数 (sps,pps)和其它数据的码流。
(2)main函数
从代码的main()函数开始, 这个函数很简单,就是读取参数,然后编码。到了版本77,相对于66版本而已,增加了参数--preset,用于定义一些预设的参数,究竟是哪个版本引入 的可自行考证。在调试程序的时候,可以根据需要选择预设参数值,如果采用默认状态,编码的FPS会比较慢。
现在重点考察编码函数static int Encode( x264_param_t *param, cli_opt_t *opt ), 在这个函数里,将会使用到X264的API,从代码带注释直接装贴过来,就不解释了。
首先,代码通过x264_encoder_open( param ) 和 x264_picture_alloc( )来初始化编码器和分配内存功输入YUV图像使用。接下来可以看到由两个注释隔开的代码块,它们的功能如下
/* Encode frames */
while(输入图像中的正常编码帧){
编码正常的码流
}
/* Flush delayed frames */
编码因为B帧而残余的码流(在B帧编码中,需要参考最后一个P帧的那些B帧,这时,输入帧已经结束,而编码帧尚未结束)
Encode()最后的代码是进行编码器关闭和内存謇恚⑼臣票嗦胫∈亢虵PS等。
(3)帧编码函数Encode_frame()
在上面的两个编码代码块中,主体函数是
static int Encode_frame( x264_t *h, hnd_t hout, x264_picture_t *pic )
这个函数将输入每帧的YUV数据,然后谕涑鰊al包。编码码流的具体工作交由API
int x264_encoder_encode( x264_t *h,x264_nal_t **pp_nal, int *pi_nal,x264_picture_t *pic_in,
x264_picture_t *pic_out )
来完成,它应该是X264中最重要的函数了。
(4)分析x264_encoder_encode()
首先遇到参考帧调整好书如下,
static inline int x264_reference_update( x264_t *h )
它会在h->frames.reference 保留需要的参考帧,然后根据参考帧队列的大小限制,移除不使用的参考帧。
然后根据注释把代码块逐个往下分析:
/* ------------------- Setup new frame from picture -------------------- */
/* 1: Copy the picture to a frame and move it to a buffer */
把帧输入的YUV数据传入 x264_frame_t *fenc中,然后进行一些码率控制方式的初始化。
/* 2: Place the frame into the queue for its slice type decision */
把fenc放到slice决定队列中,也输入码率控制的一部分
/* 3: The picture is analyzed in the lookahead */
分析slice类型,具体的类型决定工作将在函数void x264_slicetype_decide( x264_t *h )中处理。
后面做码率控制分析的时候再详述。
/* ------------------- Get frame to be encoded ------------------------- */
/* 4: get picture to encode */
去处编码帧,放置在h->fenc中,并重新设置编码参数。
/* ------------------- Setup frame context ----------------------------- */
/* 5: Init data dependent of frame type */
根据帧类型设置i_nal_type,i_nal_ref_idc,h->sh.i_type ,如果是IDR帧,重置参考帧队列。
/* ------------------- Init ----------------------------- */
根据当前帧建立参考帧队列,当前参考帧按编码帧类型分别写在h->fref0和h->fref1中。并整理好他们的排列顺序,h->fref0按poc从高到低,h->fref1反之。
/* ---------------------- Write the bitstream -------------------------- */
写NAL码流
/* Write SPS and PPS */
写参数集
/* ------------------------ Create slice header ----------------------- */
初始化slice header参数
/* Write frame */
输出slice header和slice data
函数最后调用
static int x264_encoder_frame_end( x264_t *h, x264_t *thread_current,x264_nal_t **pp_nal, int *pi_nal, x264_picture_t *pic_out )
来做NAL装,并且调整编码器状态和输出本帧编码的统计数据。
(5)static void *x264_slices_write( x264_t *h )
这个函数被x264_encoder_encode()调用作为处理slice header和slice data的编码,这个函数主要是分出slice group中的一个slice,具体做slice编码则在
static int x264_slice_write( x264_t *h )
这个函数的代码块划分如下:
step1. 初始化NAL,调用x264_slice_header_write()根据前面的参数设置输出slice header码流,
step2. 如果是用CABAC,则初始化其上下文。
step3. 进入宏块,逐个宏块编码:
宏块编码重要的是以下两个函数:
x264_macroblock_analyse( h );
x264_macroblock_encode( h );
其之前的代码是做宏块数据的导入,其后的代码是对编码数据进行熵编码,根据slicedata协议写入码流,更新coded_block_pattern,处理码率控制状态和更新CABAC上下文数据等。代码分析到宏块级了,就看看这个基本的编码单位是怎么被处理的吧。
(6)x264_macroblock_analyse( h )
这个函数就是分析宏块以确定其宏块分区模式,对I帧进行帧内预测和对P/B帧进行运动估计就发生在此函数,首先进行亮度编码,紧接着是色度。同样来一步步分析其实现。
step1.
进行码率控制准备,x264_mb_analyse_init()函数的功能包括:初始化码率控制的模型参数(码率控制依然基于Lagrangian率失
真优化算法,所以初始化lambda系数),把各宏块分类的Cost设为COST_MAX,计算MV范围,快速决定Intra宏块。
step2. 根据h->sh.i_type的类型(I,P,B)来分别计算宏块模式的率失真代价,代价计算使用SATD方法,【2】中有相关介绍。通过计算SATD可以大致估计编码码流,作为宏块选择的依据。
随机取h->mb.i_type == I_8x8的情况来分析,
if( h->mb.b_lossless )
x264_predict_lossless_8x8( h, p_dst, i, i_mode, edge );
else
h->predict_8x8[i_mode]( p_dst, edge );
x264_mb_encode_i8x8( h, i, i_qp );
step3. 根据i_mbrd的不同,做一些后续运算。
(7)x264_macroblock_encode( h )
在确定了宏块分区模式后,在本函数将对I帧剩余的宏块分区进行预测和编码,而对P/B帧的运动补偿和残差编码主要发生在这里。
基本流程分析到这里已经算结束了,在代码中,会发现宏块的预测和编码会散布在不同的函数发生,原因是对率失真优化的要求(对P/B帧)。所以,在X264中参考帧管理,码率控制,帧间预测和多线程编码都是比较有趣的探索对象。
3. 多线程代码分析
(1)文档解读
分析完X264的基本架构,来看看多线程发挥力量的地方。X264自带的多线程介绍文档是本课题的必读文档,它存放在X264的DOC文件夹下。本 文描述的大意是:当前的X264多线程模式已经放弃基于slice的并行编码,转而采用帧级和宏块级的并行,原因是slice并行需要采用slice group,会引入而外冗余降低编码效率。摘抄一段原文如下:
New threading method: frame-based
application calls x264
x264 runs B-adapt and ratecontrol (serial to the application, but parallel to the other x264 threads)
spawn a thread for this frame
thread runs encode in 1 slice, deblock, hpel filter
meanwhile x264 waits for the oldest thread to finish
return to application, but the rest of the threads continue running in the background
No
additional threads are needed to decode the input, unless
decoding+B-adapt is slower than slice+deblock+hpel, in which case an
additional input thread would allow decoding in parallel to B-adapt.【3】
以上的说明意味着,X264采用B帧在编码时不作为参考帧,所以适宜对其进行并行。
(2)运行状况分析
先来看看x264_pthread_create被调用的地方,只有这些地方才实实在在的创建了线程。
x264_pthread_create( &h->thread_handle, NULL, (void*)x264_slices_write, h )
x264_pthread_create( &look_h->thread_handle, NULL, (void *)x264_lookahead_thread, look_h )
x264_pthread_create( &h->tid, NULL, (void*)read_frame_thread_int, h->next_args )
由上图的运行可以看出,在开启了--threads 4后。x264_slices_write()可以开启4个线程同时编码,而同时存在一个主线程和一个x264_lookahead_thread()线 程。x264_slices_write()的优先级为低,原因是调用了
if( h->param.i_sync_lookahead )
x264_lower_thread_priority( 10 );
调低本线程的优先级。read_frame_thread_int()是读磁盘上的流数据信息,因为I/O和内存的不同步,所以应该分开线程处理。
在x264_encoder_open()中可以找到一下代码,可以看到对于x264_slices_write()和x264_lookahead_thread()都有被分配了专有的上下文变量,供单一线程使用。
for( i = 1; i < h->param.i_threads + !!h->param.i_sync_lookahead; i++ )
CHECKED_MALLOC( h->thread[i], sizeof(x264_t) );
(3)如何确保按指定线程数来开启线程编码?
按打印实验可以看到,假设使用--threads 4的参数选项,代码会同时开启4个x264_slices_write()线程,然后每编完一个帧(前面的一个线程返回后),一个新的被产生出来,使得 x264_slices_write()线程总数保持在4个,这一过程的相关代码如下:
int x264_encoder_encode( x264_t *h,x264_nal_t **pp_nal, int *pi_nal,x264_picture_t *pic_in,
x264_picture_t *pic_out )
{
...
if( h->param.i_threads > 1)
{
int i = ++h->i_thread_phase;
int t = h->param.i_threads;
thread_current = h->thread[ i%t ];
thread_prev = h->thread[ (i-1)%t ];
thread_oldest = h->thread[ (i+1)%t ];
x264_thread_sync_context( thread_current, thread_prev );
x264_thread_sync_ratecontrol( thread_current, thread_prev, thread_oldest );
h = thread_current;
}
...
/* Write frame */
if( h->param.i_threads > 1 )
{
printf("x264_pthread_create\n");
if( x264_pthread_create( &h->thread_handle, NULL, (void*)x264_slices_write, h ) )
return -1;
h->b_thread_active = 1;
}
else
if( (intptr_t)x264_slices_write( h ) )
return -1;
return x264_encoder_frame_end( thread_oldest, thread_current, pp_nal, pi_nal, pic_out );
...
}
static int x264_encoder_frame_end( x264_t *h, x264_t *thread_current,x264_nal_t **pp_nal, int *pi_nal, x264_picture_t *pic_out )
{
...
if( h->b_thread_active )
{
void *ret = NULL;
x264_pthread_join( h->thread_handle, &ret );
if( (intptr_t)ret )
return (intptr_t)ret;
h->b_thread_active = 0;
}
...
}
从以上两个函数的代码段可以看到,h上下文中保持的线程不会多于4个, x264_pthread_create()根据主线程的调用,创建出x264_slices_write线程,然后thread_oldest被指定并被率控函数判断重设,当前的线程数还不足4的时候,thread_oldest指向新线程,h->b_thread_active为0,不能进入x264_encoder_frame_end()的相关代码,主线程继续循环创建x264_slices_write线程,当线程总数为4,这时thread_oldest指向4个线程中被判断最快返回的那个,这时h->b_thread_active=1将进入x264_pthread_join(),那样,该线程就将主线至于阻塞状态,直至thread_oldest完成,才能重现创建新线程,以此机制,保持指定数码的编码线程数。
(4)x264_lookahead_thread()线程的作用
在分析这个线程之前,来看看两个重要的线程控制函数:
//唤醒等待该条件变量的所有线程。如果没有等待的线程,则什么也不做。
#define x264_pthread_cond_broadcast pthread_cond_broadcast
//自动解锁互斥量(如同执行了 pthread_unlock_mutex),并等待条件变量触发。这时线程挂起,不占用 CPU
时间,直到条件变量被触发。在调用 pthread_cond_wait 之前,应用程序必须加锁互斥量。pthread_cond_wait 函数返回前,自动重新对互斥量加锁(如同执行了 pthread_lock_mutex)。
#define x264_pthread_cond_wait pthread_cond_wait
以下的代码是X264中x264_lookahead_thread代码经常阻塞的地方,
**************************代码段A********************************************
if( h->lookahead->next.i_size <= h->lookahead->i_slicetype_length )
{
while( !h->lookahead->ifbuf.i_size && !h->lookahead->b_exit_thread )
x264_pthread_cond_wait( &h->lookahead->ifbuf.cv_fill, &h->lookahead->ifbuf.mutex );
x264_pthread_mutex_unlock( &h->lookahead->ifbuf.mutex );
}
else
{
x264_pthread_mutex_unlock( &h->lookahead->ifbuf.mutex );
x264_lookahead_slicetype_decide( h );
}
这里是等待满足!h->lookahead->ifbuf.i_size && !h->lookahead->b_exit_thread 的条件,后一条件在正常编码过程是TRUE,因为不会无故退出线程。那么这里等待的其实是ifbuf.i_size为非0.查找相关代码,
这里的 ifbuf.i_size条件是在x264_synch_frame_list_push()得到满足的,这里在得到一个输入的新编码帧后将发出信号。
slist->list[ slist->i_size++ ] = frame;
x264_pthread_cond_broadcast( &slist->cv_fill );
在 代码段A中,if( h->lookahead->next.i_size <= h->lookahead->i_slicetype_length )条件中,i_slicetype_length表示为了进行slice type的判断而缓存的帧,它的值有取决于h->frames.i_delay,由代码的初始化设定值决定(默认为40)。也就是说预存40帧的数 值,进行slice type决定用。暂时不详细分析slice type判断的具体实现,它的大概思想是根据码率,GOP和失真状况的权衡,来进行帧类型选择,在类似实时通信场合,不允许B帧的使用,也不可能预存那么多帧,这样的处理没有意义。
回头看这里的处理意义,是阻塞线程,等待后续的输入帧,然后利用处理规则来决定其slice type,为slice编码准备帧。
(5)宏块级别的并行
在数据结构x264_frame_t中,有变量x264_pthread_cond_t cv; 该变量分别在下面的两个函数里被封装了阻塞和唤醒:
void x264_frame_cond_broadcast( x264_frame_t *frame, int i_lines_completed );
void x264_frame_cond_wait( x264_frame_t *frame, int i_lines_completed );
考查它们被调用的地方,
************代码B****************from x264_macroblock_analyse( )->x264_mb_analyse_init()
int thresh = pix_y + h->param.analyse.i_mv_range_thread;
for( i = (h->sh.i_type == SLICE_TYPE_B); i >= 0; i-- )
{
x264_frame_t **fref = i ? h->fref1 : h->fref0;
int i_ref = i ? h->i_ref1 : h->i_ref0;
for( j=0; j<i_ref; j++ )
{
x264_frame_cond_wait( fref[j], thresh );
thread_mvy_range = X264_MIN( thread_mvy_range, fref[j]->i_lines_completed - pix_y );
}
}
**************************代码C************************************from x264_fdec_filter_row()
if( h->param.i_threads > 1 && h->fdec->b_kept_as_ref )
{
x264_frame_cond_broadcast( h->fdec, mb_y*16 + (b_end ? 10000 : -(X264_THREAD_HEIGHT <<h->sh.b_mbaff)) );
}
从
上面的代码段可以看到没完成图像一行的编码,便会使用mb_y*16
-X264_THREAD_HEIGH的值来尝试唤醒x264_pthread_cond_wait( &frame->cv,
&frame->mutex ),要判断的条件是
mb_y*16 -X264_THREAD_HEIGH < thresh = pix_y + h->param.analyse.i_mv_range_thread;
后者作为一个设想的阈值,用于确保依赖于本帧的后续帧在编码时,本帧已经编码出若干行宏块,以后续编码帧的基础,那样可以设想的情形如下图,不过X264是以编码完整行为单位的。
本文的分析道这里告一段落,对于帧间多线程分析和宏块的并行优化,或按自己的应用做代码裁剪,可以通过改正上面的(4)(5)代码段来实现,在当前(四核CPU)的X264测试中,已有代码确实能够很好的利用多核资源,并行编码的话题会随硬件的升级而不断探索下去。voidRGB2YUV420(int x_dim, int y_dim, unsigned char *bmp, unsigned char *yuv, int flip)
rgb2yuv420 (uint32_t *data_rgb, int width, int height,
uint8_t *data_y, uint8_t *data_u, uint8_t *data_v)
原来的是这样
我是Gem,今天做个简单的视频,我的耳机坏了,不好意思,没有声音,请原谅~~
时间很仓促,我把作业全部做完了,完整的过程我演示你看看,有什么问题请你指出来~~
详细的代码说明和友好的界面(用MFC做),即将给出,如果你感兴趣,请时刻关注我的Blog: http://hexun.com/ctfysj
我们开始,在C++ 里面我建立了两个工程,一个是RGB2YUV,另一个是yuv2rgb的,我们先看看RGB2YUV
看我操作~~~
RGB2YUV420 演示:
用的图片是你给我的test.bmp,把test.bmp文件拷到工程目录下 E:\网站文件夹\YUV420和RGB相互转换\homework\RGB2YUV
我转换成YUV420时候,存储在test.cif
看我生成这种文件啊~~
编译: 0 error(s), 0 warning(s)
这种效果可以吗? 应该还行吧~~
现在再看 RGB2YUV420 演示
RGB2YUV420 演示:
把上面生成的test.cif文件拷到工程目录下 E:\网站文件夹\YUV420和RGB相互转换\homework\yuv2rgb
还原成test.bmp文件
编译:0 error(s), 0 warning(s)
还原后,差不多吧,反正我没有看出来有什么区别~~
完整的代码 : 在 RGBYUV420代码完整版.txt 记事本里面
慕容听雨
个人主页:http://hexun.com/ctfysj
QQ:308463776
说明:下面的代码用C\C++执行都可以,用C的时候请把#include<iostream> 删除。
代码的详细说明视频即将推出,请时刻关注我的Blog: http://hexun.com/ctfysj
RGB to YUV420 原代码: RGB2YUV.CPP文件
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include<iostream>
//转换矩阵
#define MY(a,b,c) (( a* 0.2989 + b* 0.5866 + c* 0.1145))
#define MU(a,b,c) (( a*(-0.1688) + b*(-0.3312) + c* 0.5000 + 128))
#define MV(a,b,c) (( a* 0.5000 + b*(-0.4184) + c*(-0.0816) + 128))
//大小判断
#define DY(a,b,c) (MY(a,b,c) > 255 ? 255 : (MY(a,b,c) < 0 ? 0 : MY(a,b,c)))
#define DU(a,b,c) (MU(a,b,c) > 255 ? 255 : (MU(a,b,c) < 0 ? 0 : MU(a,b,c)))
#define DV(a,b,c) (MV(a,b,c) > 255 ? 255 : (MV(a,b,c) < 0 ? 0 : MV(a,b,c)))
//只处理352*288文件
#define WIDTH 352
#define HEIGHT 288
//读BMP
void ReadBmp(unsigned char *RGB,FILE *fp);
//转换函数
void Convert(unsigned char *RGB, unsigned char *YUV);
//入口
int main()
{
int i=1;
char file[255];
FILE *fp;
FILE *fp2;
unsigned char *YUV = NULL;
unsigned char *RGB = NULL;
unsigned int imgSize = WIDTH*HEIGHT;
if((fp2 = fopen("test.cif", "wb")) == NULL)//生成文件名
{
return 0;
}
RGB = (unsigned char*)malloc(imgSize*6);
YUV = (unsigned char*)malloc(imgSize + (imgSize>>1));
for(i=1; i<2; i++)
{
sprintf(file, "test.bmp", i);//读取文件
if((fp = fopen(file, "rb")) == NULL)
continue;
printf("打开文件%s\n", file);
ReadBmp(RGB, fp);
Convert(RGB, YUV);
fwrite(YUV, 1, imgSize+(imgSize>>1), fp2);//写入文件
fclose(fp);
}
fclose(fp2);
if(RGB)
free(RGB);
if(YUV)
free(YUV);
printf("完成\n");
system("pause");
return 1;
}
//读BMP
void ReadBmp(unsigned char *RGB,FILE *fp)
{
int i,j;
unsigned char temp;
fseek(fp,54, SEEK_SET);
fread(RGB+WIDTH*HEIGHT*3, 1, WIDTH*HEIGHT*3, fp);//读取
for(i=HEIGHT-1,j=0; i>=0; i--,j++)//调整顺序
{
memcpy(RGB+j*WIDTH*3,RGB+WIDTH*HEIGHT*3+i*WIDTH*3,WIDTH*3);
}
//顺序调整
for(i=0; (unsigned int)i < WIDTH*HEIGHT*3; i+=3)
{
temp = RGB[i];
RGB[i] = RGB[i+2];
RGB[i+2] = temp;
}
}
void Convert(unsigned char *RGB, unsigned char *YUV)
{
//变量声明
unsigned int i,x,y,j;
unsigned char *Y = NULL;
unsigned char *U = NULL;
unsigned char *V = NULL;
Y = YUV;
U = YUV + WIDTH*HEIGHT;
V = U + ((WIDTH*HEIGHT)>>2);
for(y=0; y < HEIGHT; y++)
for(x=0; x < WIDTH; x++)
{
j = y*WIDTH + x;
i = j*3;
Y[j] = (unsigned char)(DY(RGB[i], RGB[i+1], RGB[i+2]));
if(x%2 == 1 && y%2 == 1)
{
j = (WIDTH>>1) * (y>>1) + (x>>1);
//上面i仍有效
U[j] = (unsigned char)
((DU(RGB[i ], RGB[i+1], RGB[i+2]) +
DU(RGB[i-3], RGB[i-2], RGB[i-1]) +
DU(RGB[i -WIDTH*3], RGB[i+1-WIDTH*3], RGB[i+2-WIDTH*3]) +
DU(RGB[i-3-WIDTH*3], RGB[i-2-WIDTH*3], RGB[i-1-WIDTH*3]))/4);
V[j] = (unsigned char)
((DV(RGB[i ], RGB[i+1], RGB[i+2]) +
DV(RGB[i-3], RGB[i-2], RGB[i-1]) +
DV(RGB[i -WIDTH*3], RGB[i+1-WIDTH*3], RGB[i+2-WIDTH*3]) +
DV(RGB[i-3-WIDTH*3], RGB[i-2-WIDTH*3], RGB[i-1-WIDTH*3]))/4);
}
}
}
YUV420 to RGB 原代码: yuv2rgb.cpp文件
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#define WIDTH 352
#define HEIGHT 288
//转换矩阵
double YuvToRgb[3][3] = {1, 0, 1.4022,
1, -0.3456, -0.7145,
1, 1.771, 0};
//根据RGB三分量写BMP,不必关注
int WriteBmp(int width, int height, unsigned char *R,unsigned char *G,unsigned char *B, char *BmpFileName);
//转换函数
int Convert(char *file, int width, int height, int n)
{
//变量声明
int i = 0;
int temp = 0;
int x = 0;
int y = 0;
int fReadSize = 0;
int ImgSize = width*height;
FILE *fp = NULL;
unsigned char* yuv = NULL;
unsigned char* rgb = NULL;
unsigned char* cTemp[6];
char BmpFileName[256];
//申请空间
int FrameSize = ImgSize + (ImgSize >> 1);
yuv = (unsigned char *)malloc(FrameSize);
rgb = (unsigned char *)malloc(ImgSize*3);
//读取指定文件中的指定帧
if((fp = fopen(file, "rb")) == NULL)
return 0;
fseek(fp, FrameSize*(n-1), SEEK_CUR);
fReadSize = fread(yuv, 1, FrameSize, fp);
fclose(fp);
if(fReadSize < FrameSize)
return 0;
//转换指定帧 如果你不是处理文件 主要看这里
cTemp[0] = yuv; //y分量地址
cTemp[1] = yuv + ImgSize; //u分量地址
cTemp[2] = cTemp[1] + (ImgSize>>2); //v分量地址
cTemp[3] = rgb; //r分量地址
cTemp[4] = rgb + ImgSize; //g分量地址
cTemp[5] = cTemp[4] + ImgSize; //b分量地址
for(y=0; y < height; y++)
for(x=0; x < width; x++)
{
//r分量
temp = cTemp[0][y*width+x] + (cTemp[2][(y/2)*(width/2)+x/2]-128) * YuvToRgb[0][2];
cTemp[3][y*width+x] = temp<0 ? 0 : (temp>255 ? 255 : temp);
//g分量
temp = cTemp[0][y*width+x] + (cTemp[1][(y/2)*(width/2)+x/2]-128) * YuvToRgb[1][1]
+ (cTemp[2][(y/2)*(width/2)+x/2]-128) * YuvToRgb[1][2];
cTemp[4][y*width+x] = temp<0 ? 0 : (temp>255 ? 255 : temp);
//b分量
temp = cTemp[0][y*width+x] + (cTemp[1][(y/2)*(width/2)+x/2]-128) * YuvToRgb[2][1];
cTemp[5][y*width+x] = temp<0 ? 0 : (temp>255 ? 255 : temp);
}
//写到BMP文件中
sprintf(BmpFileName, "test.bmp", file, n);
WriteBmp(width, height, cTemp[3], cTemp[4], cTemp[5], BmpFileName);
free(yuv);
free(rgb);
return n;
}
//入口 没啥东西
void main()
{
int i=1;
// for( i=0; i<260; i++)
Convert("test.cif", WIDTH, HEIGHT, i);//调用上面的Convert,获取文件的第i帧
}
//写BMP 不必关注
int WriteBmp(int width, int height, unsigned char *R,unsigned char *G,unsigned char *B, char *BmpFileName)
{
int x=0;
int y=0;
int i=0;
int j=0;
FILE *fp;
unsigned char *WRGB;
unsigned char *WRGB_Start;
int yu = width*3%4;
int BytePerLine = 0;
yu = yu!=0 ? 4-yu : yu;
BytePerLine = width*3+yu;
if((fp = fopen(BmpFileName, "wb")) == NULL)
return 0;
WRGB = (unsigned char*)malloc(BytePerLine*height+54);
memset(WRGB, 0, BytePerLine*height+54);
//BMP头
WRGB[0] = 'B';
WRGB[1] = 'M';
*((unsigned int*)(WRGB+2)) = BytePerLine*height+54;
*((unsigned int*)(WRGB+10)) = 54;
*((unsigned int*)(WRGB+14)) = 40;
*((unsigned int*)(WRGB+18)) = width;
*((unsigned int*)(WRGB+22)) = height;
*((unsigned short*)(WRGB+26)) = 1;
*((unsigned short*)(WRGB+28)) = 24;
*((unsigned short*)(WRGB+34)) = BytePerLine*height;
WRGB_Start = WRGB + 54;
for(y=height-1,j=0; y >= 0; y--,j++)
{
for(x=0,i=0; x<width; x++)
{
WRGB_Start[y*BytePerLine+i++] = B[j*width+x];
WRGB_Start[y*BytePerLine+i++] = G[j*width+x];
WRGB_Start[y*BytePerLine+i++] = R[j*width+x];
}
}
fwrite(WRGB, 1, BytePerLine*height+54, fp);
free(WRGB);
fclose(fp);
return 1;
}
视频和代码下载地址:
ftp://ctfysj.kongjian.in:***@ctfysj.kongjian.in/视频教学文件/homework.rar
***是密码,为了隐私,不能告诉别人,如你需要,请回复邮箱
密码:
**** 本内容跟帖回复才可浏览 *****
文件名: homework.rar
下载链接: http://cachefile5.fs2you.com/zh-cn/download/d643ef1aee1a19187dd0850c5259e48d/homework.rar
/*
* This file is from:
* http://mri.beckman.uiuc.edu/~pan/software.html
*
* It doesn't have any copyright notice that I could find.
* This YUV conversion routine has been modified to read the input
* video from the bottom up, to be compatible with glReadPixels.
*/
#include <stdlib.h>
static int RGB2YUV_YR[256], RGB2YUV_YG[256], RGB2YUV_YB[256];
static int RGB2YUV_UR[256], RGB2YUV_UG[256], RGB2YUV_UBVR[256];
static int RGB2YUV_VG[256], RGB2YUV_VB[256];
void InitLookupTable();
/************************************************************************
*
* int RGB2YUV420 (int x_dim, int y_dim,
* unsigned char *bmp,
* unsigned char *yuv, int flip)
*
* Purpose : It takes a 24-bit RGB bitmap and convert it into
* YUV (4:2:0) format
*
* Input : x_dim the x dimension of the bitmap
* y_dim the y dimension of the bitmap
* bmp pointer to the buffer of the bitmap
* yuv pointer to the YUV structure
*
************************************************************************/
int RGB2YUV420 (int x_dim, int y_dim,
unsigned char *bmp,
unsigned char *yuv,
unsigned char *tmp1, unsigned char *tmp2)
{
int i, j;
unsigned char *r, *g, *b, *rgb_line;
unsigned char *y, *u, *v;
unsigned char *uu, *vv;
unsigned char *pu1, *pu2,*pu3,*pu4;
unsigned char *pv1, *pv2,*pv3,*pv4;
int pitch;
pitch = x_dim * 3;
y=yuv;
uu=tmp1;
vv=tmp2;
u=uu;
v=vv;
rgb_line = bmp + pitch * (y_dim-1);
for (i=0;i<y_dim;i++){
r=rgb_line;
g=rgb_line+1;
b=rgb_line+2;
rgb_line -= pitch;
for (j=0;j<x_dim;j++){
*y++=( RGB2YUV_YR[*r] +RGB2YUV_YG[*g]+RGB2YUV_YB[*b]+1048576)>>16;
*u++=(-RGB2YUV_UR[*r] -RGB2YUV_UG[*g]+RGB2YUV_UBVR[*b]+8388608)>>16;
*v++=( RGB2YUV_UBVR[*r]-RGB2YUV_VG[*g]-RGB2YUV_VB[*b]+8388608)>>16;
r+=3;
g+=3;
b+=3;
}
}
//dimension reduction for U and V components
u=yuv+x_dim*y_dim;
v=u+x_dim*y_dim/4;
pu1=uu;
pu2=pu1+1;
pu3=pu1+x_dim;
pu4=pu3+1;
pv1=vv;
pv2=pv1+1;
pv3=pv1+x_dim;
pv4=pv3+1;
for(i=0;i<y_dim;i+=2){
for(j=0;j<x_dim;j+=2){
*u++=int(*pu1+*pu2+*pu3+*pu4)>>2;
*v++=int(*pv1+*pv2+*pv3+*pv4)>>2;
pu1+=2;
pu2+=2;
pu3+=2;
pu4+=2;
pv1+=2;
pv2+=2;
pv3+=2;
pv4+=2;
}
pu1+=x_dim;
pu2+=x_dim;
pu3+=x_dim;
pu4+=x_dim;
pv1+=x_dim;
pv2+=x_dim;
pv3+=x_dim;
pv4+=x_dim;
}
return 0;
}
void InitLookupTable()
{
int i;
for (i = 0; i < 256; i++) RGB2YUV_YR[i] = (int) ((float)65.481 * (i<<8));
for (i = 0; i < 256; i++) RGB2YUV_YG[i] = (int) ((float)128.553 * (i<<8));
for (i = 0; i < 256; i++) RGB2YUV_YB[i] = (int) ((float)24.966 * (i<<8));
for (i = 0; i < 256; i++) RGB2YUV_UR[i] = (int) ((float)37.797 * (i<<8));
for (i = 0; i < 256; i++) RGB2YUV_UG[i] = (int) ((float)74.203 * (i<<8));
for (i = 0; i < 256; i++) RGB2YUV_VG[i] = (int) ((float)93.786 * (i<<8));
for (i = 0; i < 256; i++) RGB2YUV_VB[i] = (int) ((float)18.214 * (i<<8));
for (i = 0; i < 256; i++) RGB2YUV_UBVR[i] = (int) ((float)112 * (i<<8));
}
Parent Directory | Revision Log
fixed handling of odd-shaped windows (non-4bit-aligned ones). since teaching the rgb2yuv conversion about these seems to be non-trivial, just use a properly sized imlib image.
/*
* Copyright (c) 2005 Vincent Torri (vtorri at univ-evry fr)
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdint.h>
/*
* Colour conversion from
* http://www.poynton.com/notes/colour_and_gamma/ColorFAQ.html#RTFToC30
*
* YCbCr in Rec. 601 format
* RGB values are in the range [0..255]
*
* [ Y ] [ 16 ] 1 [ 65.738 129.057 25.064 ] [ R ]
* [ Cb ] = [ 128 ] + --- * [ -37.945 -74.494 112.439 ] * [ G ]
* [ Cr ] [ 128 ] 256 [ 112.439 -94.154 -18.285 ] [ B ]
*/
void
rgb2yuv420 (uint32_t *data_rgb, int width, int height,
uint8_t *data_y, uint8_t *data_u, uint8_t *data_v)
{
int x, y, row_stride = width * 4;
uint8_t *rgb, *Y = data_y, *U = data_u, *V = data_v;
uint8_t u00, u01, u10, u11;
uint8_t v00, v01, v10, v11;
int32_t RtoYCoeff = (int32_t) (65.738 * 256 + 0.5);
int32_t GtoYCoeff = (int32_t) (129.057 * 256 + 0.5);
int32_t BtoYCoeff = (int32_t) (25.064 * 256 + 0.5);
int32_t RtoUCoeff = (int32_t) (-37.945 * 256 + 0.5);
int32_t GtoUCoeff = (int32_t) (-74.494 * 256 + 0.5);
int32_t BtoUCoeff = (int32_t) (112.439 * 256 + 0.5);
int32_t RtoVCoeff = (int32_t) (112.439 * 256 + 0.5);
int32_t GtoVCoeff = (int32_t) (-94.154 * 256 + 0.5);
int32_t BtoVCoeff = (int32_t) (-18.285 * 256 + 0.5);
/* Y plane */
rgb = (uint8_t *) data_rgb;
for (y = height; y-- > 0; ) {
for (x = width; x-- > 0; ) {
/* No need to saturate between 16 and 235 */
*Y = 16 + ((32768 +
BtoYCoeff * *(rgb) +
GtoYCoeff * *(rgb + 1) +
RtoYCoeff * *(rgb + 2)) >> 16);
Y++;
rgb += 4;
}
}
/* U and V planes */
rgb = (uint8_t *) data_rgb;
for (y = height / 2; y-- > 0; ) {
for (x = width / 2; x-- > 0; ) {
/* No need to saturate between 16 and 240 */
u00 = 128 + ((32768 +
BtoUCoeff * *(rgb) +
GtoUCoeff * *(rgb + 1) +
RtoUCoeff * *(rgb + 2)) >> 16);
u01 = 128 + ((32768 +
BtoUCoeff * *(rgb + 4) +
GtoUCoeff * *(rgb + 5) +
RtoUCoeff * *(rgb + 6)) >> 16);
u10 = 128 + ((32768 +
BtoUCoeff * *(rgb + row_stride) +
GtoUCoeff * *(rgb + row_stride + 1) +
RtoUCoeff * *(rgb + row_stride + 2)) >> 16);
u11 = 128 + ((32768 +
BtoUCoeff * *(rgb + row_stride + 4) +
GtoUCoeff * *(rgb + row_stride + 5) +
RtoUCoeff * *(rgb + row_stride + 6)) >> 16);
*U++ = (2 + u00 + u01 + u10 + u11) >> 2;
v00 = 128 + ((32768 +
BtoVCoeff * *(rgb) +
GtoVCoeff * *(rgb + 1) +
RtoVCoeff * *(rgb + 2)) >> 16);
v01 = 128 + ((32768 +
BtoVCoeff * *(rgb + 4) +
GtoVCoeff * *(rgb + 5) +
RtoVCoeff * *(rgb + 6)) >> 16);
v10 = 128 + ((32768 +
BtoVCoeff * *(rgb + row_stride) +
GtoVCoeff * *(rgb + row_stride + 1) +
RtoVCoeff * *(rgb + row_stride + 2)) >> 16);
v11 = 128 + ((32768 +
BtoVCoeff * *(rgb + row_stride + 4) +
GtoVCoeff * *(rgb + row_stride + 5) +
RtoVCoeff * *(rgb + row_stride + 6)) >> 16);
*V++ = (2 + v00 + v01 + v10 + v11) >> 2;
rgb += 8;
}
rgb += row_stride;
}
}
admin | ViewVC Help |
Powered by ViewVC 1.0.0 |
rgb数据如何转换YUV显示出来? - [ | 102 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 3,843 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 498 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 536 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 205 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 546 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 46 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 70 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 63 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 87 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 161 byte(s)]
Re: rgb数据如何转换YUV显示出来? - [ | 63 byte(s)]
|