在前几篇博客中,我们用ZED平台完成了对USB摄像头的视频采集,Qt的视频显示。在此基础上,我们更希望能把视频存储成视频文件,像DV机一样。MJPG是一种比较简单且易于实现的视频视频编码格式,而AVI封装则是非常流行的视频封装。本篇将介绍MJPG的编码格式和视频的AVI封装,并在ZedBoard上实现对视频的压缩和存储。
更多更新请关注我的博客:@超群天晴http://www.cnblogs.com/surpassal/
相关阅读:
(原创)基于ZedBoard的Webcam设计(一):USB摄像头(V4L2接口)的图片采集
(原创)基于ZedBoard的Webcam设计(二):USB摄像头图片采集+QT显示
(原创)基于ZedBoard的Webcam设计(三):视频的采集和动态显示
(原创)基于ZedBoard的Webcam设计(四):MJPG编码和AVI封装
(原创)基于ZedBoard的Webcam设计(五):x264编码在zedboard上的实现(软编码)
硬件平台:Digilent ZedBoard
开发环境:Windows XP 32 bit + Wmare 8.0 + Ubuntu 10.04 +Qt+ arm-linux-xilinx-gnueabi交叉编译环境
Zedboard linux:Digilent OOB Design
一、JPEG和MJPG编码介绍
1、JPEG编码
我个人简单的理解是,JPEG即是Joint Photographic Experts Group(联合图像专家组)的缩写,更是一种图像压缩编码算法。JPEG编码算法过程简单可以归结于下:其中DCT变换和量化是有损的,而熵编码(一般是哈夫曼编码)是无损的。量化和编码都可以通过量化表和编码表查询得到。
2、MJPG编码
Motion JPEG是一种基于静态图像JPEG 压缩标准的动态图像压缩标准,压缩时将连续图像的每一个帧视为一幅静止图像进行压缩,从而可以生成序列化运动图像。压缩时不对帧间的时间冗余进行压缩,也就是说没有MJPEG或者H264那样的帧间编码,阵内预测编码也没有,所以较于实现,只是压缩效率较低。一个MJPG帧序列可以看出是N帧JPEG编码后的数据流连接而成。
二、基于MJPG编码的AVI视频封装介绍
AVI是一种RIFF(Resource Interchange File Format)文件格式,多用于音视频捕捉、编辑、回放等应用程序。AVI包含三个部分:文件头、数据块和索引块。其中文件头包括文件的通用信息,定义数据格式,所用的压缩算法等参数。数据块包含实际数据流,即图像和声音序列数据。这是文件的主体,也是决定文件容量的主要部分。视频文件的大小等于该文件的数据率乘以该视频播放的时间长度。索引块包括数据块列表和它们在文件中的位置,以提供文件内数据随机存取能力。AVI文件的总体结构:
三、ZED上JPEG编码实现
整个编码过程比较繁琐,这里只做简单介绍。后续如果有时间,专门开辟一篇博客介绍JPEG编码过程。
1、主编码
1voidmjpg::jpeg_encode(unsignedchar**yuv_buffer_pointer)
2{
3unsignedintremnant;
4yuv_p = *yuv_buffer_pointer;
5
6bitstring fillbits;//filling bitstring for the bit alignment of the EOI marker
7
8//fp_jpeg_stream=fopen("000.jpg","wb");
9jpgsize =0;
10//505 bytes
11writeword(0xFFD8);//SOI 2
12write_APP0info();//JIFF
13//write_comment("Cris made this JPEG with his own encoder");
14write_DQTinfo();//= 0xFFDB
15write_SOF0info();//FFC0
16write_DHTinfo();
17write_SOSinfo();
18
19//jpgsize = 505;
20
21//init global variables
22bytenew =0;//current byte
23bytepos =7;//bit position in this byte
24main_encoder();
25
26//Do the bit alignment of the EOI marker
27if(bytepos >=0)
28{
29fillbits.length = bytepos +1;
30fillbits.value = (1<<(bytepos+1)) -1;
31writebits(fillbits);
32}
33writeword(0xFFD9);//EOI
34
35//remnant = (~(jpgsize&0x00000003))&0x00000003;//important
36remnant = (4-(jpgsize&0x00000003))&0x00000003;//important
37jpgsize = jpgsize +remnant;
38movisize = movisize +jpgsize;
39while(remnant >0)
40{
41fputc(0,avi_jpeg_stream);
42remnant--;
43}
44}
其中remnant是一个计算一帧图像大小的余数,因为后续AVI封装要求每帧图像大小都是4的整数倍。
2、获取8x8阵列数据
每次处理都是以8x8大小的数据矩阵进行处理的。由于USB摄像头采集到的图像数据是YUV422打包格式,而JPEG编码中比较多的是使用YUV411,所以优先考虑将其转换。其中yuv_p是原始YUV422图像数据指针,YDU1~YDU4是四个连续存储Y分量大小为64字节的数组,CbDU和CrDU分别为Cb和Cr分量。为了提高计算效率,乘法均由移位完成
1voidmjpg::load_data_units_from_YUV_buffer(WORD xpos, WORD ypos)
2{
3BYTE x, y;
4BYTE pos =0;
5DWORD location;
6//SBYTE Cr_temp,Cb_temp;
7
8//location = ypos * 640+ xpos;
9location = (ypos<<7) + (ypos<<9) +xpos;
10for(y=0; y<8; y++)
11{
12for(x=0; x<8; x++)
13{
14YDU1[pos] = *(yuv_p+((location)<<1)) -128;
15YDU2[pos] = *(yuv_p+((location+8)<<1)) -128;
16YDU3[pos] = *(yuv_p+((location+5120)<<1)) -128;//location = (ypos+8) * 640+ xpos+8;
17YDU4[pos] = *(yuv_p+((location+5128)<<1)) -128;//location = (ypos+8) * 640+ xpos+8;
18pos++;
19location++;
20}
21location +=632;//640 - 8;
22}
23
24pos =0;
25//location = ypos * 640+ xpos;
26location = (ypos<<7) + (ypos<<9) +xpos;
27for(y=0; y<8; y++)
28{
29for(x=0; x<8; x++)
30{
31CbDU[pos] = *(yuv_p+(location)*2+1)-128;
32CrDU[pos] = *(yuv_p+(location+1)*2+1)-128;
33pos++;
34location++;
35location++;
36}
37location +=1264;//640*2 - 16;
38}
39}
3、对每个8x8数据阵列进行JPEG处理
1voidmjpg::main_encoder()
2{
3SWORD DCY =0, DCCb =0, DCCr =0;//DC coefficients used for differential encoding
4WORD xpos, ypos;
5
6for(ypos=0; ypos16)
7{
8for(xpos=
0; xpos<(IMG_WIDTH); xpos+=
16)
9{
10load_data_units_from_YUV_buffer(xpos, ypos);
11process_DU(YDU1, fdtbl_Y, &
DCY, YDC_HT, YAC_HT);
12process_DU(YDU2, fdtbl_Y, &
DCY, YDC_HT, YAC_HT);
13process_DU(YDU3, fdtbl_Y, &
DCY, YDC_HT, YAC_HT);
14process_DU(YDU4, fdtbl_Y, &
DCY, YDC_HT, YAC_HT);
15process_DU(CbDU, fdtbl_Cb, &
DCCb, CbDC_HT, CbAC_HT);
16process_DU(CrDU, fdtbl_Cb, &
DCCr, CbDC_HT, CbAC_HT);
17}
18}
19}
四、ZED上MJPG的编码实现以及AVI封装
写avi文件第一步是写hdrl头信息,可是hdrl头信息需要确定文件的总帧数和文件大小,而在采集过程中这些都是不确定的(因为不知道什么时候采集结束),为此采用了一个“偷懒”方法:先写一个虚假的hdrl,然后每次对一帧图像进行JPEG编码后,将图像的数据量mjpgfile->movisize记录下来,并将数据帧数framecnt记录下来。停止采集后先结束avi文件的写入,再重新打开,然后对文件头进行修改;或者通过fseek寻找的头文件位置,同样修改hdrl信息。两种方法我都试过,感觉效率都差不多。
为了方便采集,添加按键来触发改变需要的状态,定义state为3个状态:
state--含义
0------idle,等待采集1------正在采集
2------结束采集
state为0时,标明需要准备写一个新的avi文件;state为1时,标明现在正在采集图像数据,并对每一帧进行jpeg编码;state为2时,标明采集已经结束,fseek往回修改头文件。新的paintEvent函数:
1voidWidget::paintEvent(QPaintEvent *)
2{
3rs = vd->get_frame(&yuv_buffer_pointer,&len);
4
5if(last_state==2&& state ==0)
6{
7//write hdrl
8hdrl.avih.width =640;//(width);
9hdrl.avih.height =480;//(height);
10hdrl.strl.strf.width =640;//(width);
11hdrl.strl.strf.height =480;//(height);
12hdrl.strl.strf.image_sz =640*480*3;//(width * height * 3);
13
14
15sizeofhdrl=sizeof(hdrl);
16
17mjpgfile->avi_jpeg_stream = fopen(avifilename,"wb");
18
19fputc('R', mjpgfile->avi_jpeg_stream);
20fputc('I', mjpgfile->avi_jpeg_stream);
21fputc('F', mjpgfile->avi_jpeg_stream);
22fputc('F', mjpgfile->avi_jpeg_stream);
23print_quartet(0/*riff_sz*/);//riff file size
24fputc('A', mjpgfile->avi_jpeg_stream);
25fputc('V', mjpgfile->avi_jpeg_stream);
26fputc('I', mjpgfile->avi_jpeg_stream);
27fputc('', mjpgfile->avi_jpeg_stream);
28
29fwrite(&hdrl, sizeofhdrl,1, mjpgfile->avi_jpeg_stream);//write head
30
31fputc('L', mjpgfile->avi_jpeg_stream);
32fputc('I', mjpgfile->avi_jpeg_stream);
33fputc('S', mjpgfile->avi_jpeg_stream);
34fputc('T', mjpgfile->avi_jpeg_stream);
35
36print_quartet(0/*jpg_sz + 8*TOTALFRAMES + 4*/);//size again
37fputc('m', mjpgfile->avi_jpeg_stream);
38fputc('o', mjpgfile->avi_jpeg_stream);
39fputc('v', mjpgfile->avi_jpeg_stream);
40fputc('i', mjpgfile->avi_jpeg_stream);
41
42avifilename[5]++;
43}
44
45if(state==1)
46{
47framecnt++;
48fputc('0', mjpgfile->avi_jpeg_stream);
49fputc('0', mjpgfile->avi_jpeg_stream);
50fputc('d', mjpgfile->avi_jpeg_stream);
51fputc('c', mjpgfile->avi_jpeg_stream);
52print_quartet(0);
53
54mjpgfile->jpeg_encode(&yuv_buffer_pointer);
55
56//printf("%ld\n",mjpgfile->jpgsize);
57
58fseek(mjpgfile->avi_jpeg_stream,-4-(long)mjpgfile->jpgsize,SEEK_CUR);
59print_quartet(mjpgfile->jpgsize);
60
61fseek(mjpgfile->avi_jpeg_stream,6,SEEK_CUR);
62fwrite("AVI1",4,1, mjpgfile->avi_jpeg_stream);
63
64fseek(mjpgfile->avi_jpeg_stream,mjpgfile->jpgsize-10,SEEK_CUR);
65
66
67}
68if(last_state==1&& state==2)
69{
70
71fseek(mjpgfile->avi_jpeg_stream,4,SEEK_SET);
72print_quartet(mjpgfile->movisize+sizeofhdrl);//riff file size
73fseek(mjpgfile->avi_jpeg_stream,4,SEEK_CUR);
74
75//overwrite hdrl
76hdrl.avih.us_per_frame =1000000/12;//(per_usec);
77hdrl.avih.max_bytes_per_sec = mjpgfile->movisize*12/framecnt;
78hdrl.avih.tot_frames =framecnt;
79hdrl.strl.list_odml.frames =framecnt;//(TOTALFRAMES);
80hdrl.strl.strh.scale =1;//
81hdrl.strl.strh.length =10;//
82hdrl.strl.strh.rate =12;
83
84fwrite(&hdrl, sizeofhdrl,1, mjpgfile->avi_jpeg_stream);//write head
85fseek(mjpgfile->avi_jpeg_stream,4,SEEK_CUR);
86
87print_quartet(mjpgfile->movisize);//size again
88fclose(mjpgfile->avi_jpeg_stream);
89}
90last_state=state;
91
92convert_yuv_to_rgb_buffer();
93
94frame->loadFromData(rgb_buffer,640*480*3);
95ui->label->setPixmap(QPixmap::fromImage(*frame,Qt::AutoColor));
96
97
98rs = vd->unget_frame();
99}
最开始定义了视频名字的数组,char avifilename[11] = {'r','c','q','0','0','0','.','a','v','i','\0'};
在42行:avifilename[5]++;
表示让名字由"rcq000.avi"依次计数增加。
五、测试效果
http://v.youku.com/v_show/id_XNTM0MTg2NTM2.html
============================================
可执行程序:zed_MJPG_camera.rar