最近在研究mjpg_streamer,发现这是个好东西,关于mjpg_streamer就不做具体介绍了,总之它是在Linux上运行的视频服务器,可以将摄像头采集到的视频数据通过网络传输到客户端,实现视频监控,mjpg_streamer是开源项目。
首先简要的分析一下mjpg_streamer的源码及其工作过程。我主要参考了这里:Mjpeg-streamer源码分析,另外可以参考这里:http://blog.csdn.net/wavemcu/article/details/7795561。在“Mjpeg-streamer源码分析”一文中已经对mjpg_streamer的源代码做了比较详细的分析,这里只做简要概括。mjpg_streamer主要由三部分构成,主函数mjpg_streamer.c和输入、输出组件,其中输入、输出组件通常是input_uvc.so和output_http.so,他们是以动态链接库的形式在主函数中调用的。
主函数的主要功能有:1.解析命令行的各个输入参数。2.判断程序是否需要成为守护进程,如果需要,则执行相应的操作。3.初始化global全局变量。4.打开并配置输入、输出插件。5.运行输入、输出插件。
输入插件将采集到的视频送到全局缓存中,输出插件则从全局缓存中读取数据并发送。输出插件的实现是一个http服务器,这里就不介绍了。输入插件的视频采集功能主要是通过Linux的V4L2接口实现的,主要是4个函数input_init()、 input_stop()、 input_run()和 input_cmd()。其中iniput_run()函数创建了线程cma_thread(),这个线程很重要,该函数的作用是抓取一帧的图像,并复制到全局缓冲区。具体的代码说明请参考链接中的分析。
mjpg_streamer的功能的确强大,但是由于我的主要研究方向是图像处理,因此也想到了一些问题。现在的网络视频监控系统除了传统的视频传输功能外,大多还有视频分析的能力,即在图像捕获和图像传输之间加上了图像处理的能力,例如能够检测移动目标,这样视频监控服务器的功能就大大增强了。mjpg_streamer是一个很好的视频服务器框架,那么它否能够通过修改而拥有视频图像处理能力呢?为此我也查阅了很多资料,同时发现了另一个开源项目motion。关于motion的详细信息可以参考这里:http://www.lavrsen.dk/foswiki/bin/view/Motion。motion的移植可以参考这里:http://blog.csdn.net/kangear/article/details/8763790。
motion实际上功能类似于mjpg_streamer,但是正如它的名字,除了能够捕获和传输视频外,它还拥有运动检测的功能,能够将检测到的运动物体标识出来,并且检测到移动物体后可以执行用户脚本,实现报警等功能,非常强大。但是motion也有他的局限性,它只能单纯的检测移动物体,如果想要做更复杂的或者有针对性的算法就没有办法了,比如车辆检测、火灾监测、人脸识别等等。鉴于mjpg_streamer的源代码比较易读,思路清晰,我准备修改它的源码来支持图像处理功能。通过前面mjpg_streamer的源码分析可知要实现图像处理功能,应该从输入组件input_uvc.so下手。
在input_uvc.c文件中有一个线程是cam_thread()前面已经提到过了,其作用是抓取一帧的图像,并复制到全局缓冲区。如果在抓取一帧图像后先进行处理,在复制到全局缓冲区就能实现目标。下面是cam_thread()的代码:
1 void *cam_thread( void *arg ) { 2 /* set cleanup handler to cleanup allocated ressources */ 3 pthread_cleanup_push(cam_cleanup, NULL); 4 while( !pglobal->stop ) { 5 /* grab a frame */ 6 if( uvcGrab(videoIn) < 0 ) { 7 IPRINT("Error grabbing frames\n"); 8 exit(EXIT_FAILURE); 9 } 10 DBG("received frame of size: %d\n", videoIn->buf.bytesused); 11 /* 12 * Workaround for broken, corrupted frames: 13 * Under low light conditions corrupted frames may get captured. 14 * The good thing is such frames are quite small compared to the regular pictures. 15 * For example a VGA (640x480) webcam picture is normally >= 8kByte large, 16 * corrupted frames are smaller. 17 */ 18 if ( videoIn->buf.bytesused < minimum_size ) { 19 DBG("dropping too small frame, assuming it as broken\n"); 20 continue; 21 } 22 /* copy JPG picture to global buffer */ 23 pthread_mutex_lock( &pglobal->db ); 24 /* 25 * If capturing in YUV mode convert to JPEG now. 26 * This compression requires many CPU cycles, so try to avoid YUV format. 27 * Getting JPEGs straight from the webcam, is one of the major advantages of 28 * Linux-UVC compatible devices. 29 */ 30 if (videoIn->formatIn == V4L2_PIX_FMT_YUYV) { 31 DBG("compressing frame\n"); 32 pglobal->size = compress_yuyv_to_jpeg(videoIn, pglobal->buf, videoIn->framesizeIn, gquality); 33 } 34 else { 35 DBG("copying frame\n"); 36 pglobal->size = memcpy_picture(pglobal->buf, videoIn->tmpbuffer, videoIn->buf.bytesused); 37 } 38 #if 0 39 /* motion detection can be done just by comparing the picture size, but it is not very accurate!! */ 40 if ( (prev_size - global->size)*(prev_size - global->size) > 4*1024*1024 ) { 41 DBG("motion detected (delta: %d kB)\n", (prev_size - global->size) / 1024); 42 } 43 prev_size = global->size; 44 #endif 45 /* signal fresh_frame */ 46 pthread_cond_broadcast(&pglobal->db_update); 47 pthread_mutex_unlock( &pglobal->db ); 48 DBG("waiting for next frame\n"); 49 /* only use usleep if the fps is below 5, otherwise the overhead is too long */ 50 if ( videoIn->fps < 5 ) { 51 usleep(1000*1000/videoIn->fps); 52 } 53 } 54 DBG("leaving input thread, calling cleanup function now\n"); 55 pthread_cleanup_pop(1); 56 return NULL; 57 }
22行——37行的代码实现了将图像数据拷贝到全局缓冲区,if语句针对的是输出YUYV格式的摄像头,在拷贝数据之前还要进行数据转换,将YUYV格式利用libjpeg库转换成jpg格式再进行拷贝。关于libjpeg库的移植方法可以参考这里:http://blog.chinaunix.net/uid-11765716-id-172491.html。else语句针对输出格式为jpg的摄像头,它直接将数据拷贝到全局缓冲区,无需转换。
由于我使用的摄像头是中星微zc301,直接输出jpg格式,因此这里只讨论这种情况的修改方法。从上面代码中我们可以看出,我们只需要在21行与22行之间增加处理算法就能实现相应的功能。要进行图像处理,首先要将采集到的jpg图像转换为RGB或YUV,然后再由RGB或YUV转换为灰度,对灰度图像进行处理,完成之后再转换为jpg格式,最后拷贝到全局缓冲。而jpg与RGB或YUV格式的转换还要靠libjpeg库来实现。但是libjpeg库有个缺陷:它只能对文件进行转换,而我们的数据在内存中,因此需要对libjpeg库进行一定的修改。幸运的是我在motion项目中发现了jpg与YUV转换的代码,经过简单修改后可以直接使用。相应的文件是jpegutils.h和jpegutils.c,将他们放入mjpg-streamer/plugins/input_uvc/中即可。这是经过修改的文件:jpegutils。要用到的两个函数如下,分别是将jpg数据解码为YUV和将YUV编码成jpg。
1 int decode_jpeg_raw(unsigned char *jpeg_data, int len, 2 int itype, int ctype, unsigned int width, 3 unsigned int height, unsigned char *raw0, 4 unsigned char *raw1, unsigned char *raw2); 5 6 int encode_jpeg_raw(unsigned char *jpeg_data, int len, int quality, 7 int itype, int ctype, unsigned int width, 8 unsigned int height, unsigned char *raw0, 9 unsigned char *raw1, unsigned char *raw2);
将jpg数据解码为YUV数据后,由于Y代表亮度,即灰度。我们只需要对Y分量做处理即可,最后再编码为jpg数据。下面附上修改后的input_uvc.c文件:
1 #include2 #include 3 #include 4 #include <string.h> 5 #include 6 #include 7 #include 8 #include 9 #include 10 #include 11 #include 12 #include 13 #include 14 #include 15 #include 16 #include 17 18 #include "../../utils.h" 19 #include "../../mjpg_streamer.h" 20 #include "v4l2uvc.h" 21 #include "huffman.h" 22 #include "jpeg_utils.h" 23 #include "jpegutils.h" 24 #include "dynctrl.h" 25 26 #define INPUT_PLUGIN_NAME "UVC webcam grabber" 27 #define MAX_ARGUMENTS 32 28 29 /* 30 * UVC resolutions mentioned at: (at least for some webcams) 31 * http://www.quickcamteam.net/hcl/frame-format-matrix/ 32 */ 33 static const struct { 34 const char *string; 35 const int width, height; 36 } resolutions[] = { 37 { "QSIF", 160, 120 }, 38 { "QCIF", 176, 144 }, 39 { "CGA", 320, 200 }, 40 { "QVGA", 320, 240 }, 41 { "CIF", 352, 288 }, 42 { "VGA", 640, 480 }, 43 { "SVGA", 800, 600 }, 44 { "XGA", 1024, 768 }, 45 { "SXGA", 1280, 1024 } 46 }; 47 48 /* private functions and variables to this plugin */ 49 pthread_t cam; 50 pthread_mutex_t controls_mutex; 51 struct vdIn *videoIn; 52 static globals *pglobal; 53 static int gquality = 80; 54 static unsigned int minimum_size = 0; 55 static int dynctrls = 1; 56 57 //jpeg压缩、解压缩 58 unsigned char *y,*u,*v,*processed_jpeg_data; 59 60 void *cam_thread( void *); 61 void cam_cleanup(void *); 62 void help(void); 63 int input_cmd(in_cmd_type, int); 64 65 int input_init(input_parameter *param) { 66 67 /////////////////此处省略n行///////////// 68 void *cam_thread( void *arg ) { 69 /*********************利用libjpeg库实现jpeg图像段压缩、解压缩*********************/ 70 int i,j; 71 int length=sizeof(unsigned char)*videoIn->width*videoIn->height; 72 y=(unsigned char *)malloc(length); 73 u=(unsigned char *)malloc(length/4); 74 v=(unsigned char *)malloc(length/4); 75 processed_jpeg_data=(unsigned char *)malloc(length); 76 memset(y,0,length); 77 memset(u,0,length/4); 78 memset(v,0,length/4); 79 memset(processed_jpeg_data,0,length); 80 /* set cleanup handler to cleanup allocated ressources */ 81 pthread_cleanup_push(cam_cleanup, NULL); 82 while( !pglobal->stop ) { 83 /* grab a frame */ 84 if( uvcGrab(videoIn) < 0 ) { 85 IPRINT("Error grabbing frames\n"); 86 exit(EXIT_FAILURE); 87 } 88 DBG("received frame of size: %d\n", videoIn->buf.bytesused); 89 /* 90 * Workaround for broken, corrupted frames: 91 * Under low light conditions corrupted frames may get captured. 92 * The good thing is such frames are quite small compared to the regular pictures. 93 * For example a VGA (640x480) webcam picture is normally >= 8kByte large, 94 * corrupted frames are smaller. 95 */ 96 if ( videoIn->buf.bytesused < minimum_size ) { 97 DBG("dropping too small frame, assuming it as broken\n"); 98 continue; 99 } 100 //将输入的jpeg图像解压成YUV分量 101 decode_jpeg_raw(videoIn->tmpbuffer,videoIn->buf.bytesused,0,420,videoIn->width,videoIn->height,y,u,v); 102 //这里可以对Y分量进行更改加入图像处理算法 103 /*二值化*/ 104 /*for(i=0;i height;i++) 105 for(j=0;jwidth;j++) { 106 if (*(y+i*videoIn->width+j)>128) 107 *(y+i*videoIn->width+j)=250; 108 else 109 *(y+i*videoIn->width+j)=0;}*/ 110 /*负片*/ 111 for(i=0;iheight;i++) 112 for(j=0;j width;j++) 113 *(y+i*videoIn->width+j)=255-*(y+i*videoIn->width+j); 114 /*sobel滤波*/ 115 /* 116 int m,n,edge;//m,n为x,y方向上的梯度 117 for(i=1;i height-1;i++) 118 for(j=1;jwidth;j++) 119 { 120 m=*(y+(i-1)*videoIn->width+(j+1))+*(y+i*videoIn->width+(j+1))*2+*(y+(i+1)*videoIn->width+(j+1))\ 121 -*(y+(i-1)*videoIn->width+(j-1))-*(y+i*videoIn->width+(j-1))*2-*(y+(i+1)*videoIn->width+(j-1)); 122 n=*(y+(i-1)*videoIn->width+(j-1))+*(y+(i-1)*videoIn->width+j)*2+*(y+(i-1)*videoIn->width+(j+1))\ 123 -*(y+(i+1)*videoIn->width+(j-1))-*(y+(i+1)*videoIn->width+j)*2-*(y+(i+1)*videoIn->width+(j+1)); 124 edge=(int)sqrt((float)m*m+(float)n*n)+0.5; 125 //if(edge>255) 126 //edge=255; 127 *(y+i*videoIn->width+j)=edge> 450?250 : 20; 128 129 }*/ 130 //将UV分量设置为128,压缩后为灰度图像 131 memset(u,128,length/4); 132 memset(v,128,length/4); 133 //将YUV分量压缩成jpeg 134 encode_jpeg_raw(processed_jpeg_data,length,80,0,420,videoIn->width,videoIn->height,y,u,v); 135 136 //free()在后面不要忘了! 137 138 /* copy JPG picture to global buffer */ 139 pthread_mutex_lock( &pglobal->db ); 140 141 /* 142 * If capturing in YUV mode convert to JPEG now. 143 * This compression requires many CPU cycles, so try to avoid YUV format. 144 * Getting JPEGs straight from the webcam, is one of the major advantages of 145 * Linux-UVC compatible devices. 146 */ 147 if (videoIn->formatIn == V4L2_PIX_FMT_YUYV) { 148 DBG("compressing frame\n"); 149 pglobal->size = compress_yuyv_to_jpeg(videoIn, pglobal->buf, videoIn->framesizeIn, gquality); 150 } 151 else { 152 DBG("copying frame\n"); 153 //pglobal->size = memcpy_picture(pglobal->buf, videoIn->tmpbuffer, videoIn->buf.bytesused); 154 //将新压缩的jpeg图像复制到全局缓冲区 155 pglobal->size = memcpy_picture(pglobal->buf,processed_jpeg_data, videoIn->width*videoIn->height); 156 } 157 #if 0 158 /* motion detection can be done just by comparing the picture size, but it is not very accurate!! */ 159 if ( (prev_size - global->size)*(prev_size - global->size) > 4*1024*1024 ) { 160 DBG("motion detected (delta: %d kB)\n", (prev_size - global->size) / 1024); 161 } 162 prev_size = global->size; 163 #endif 164 /* signal fresh_frame */ 165 pthread_cond_broadcast(&pglobal->db_update); 166 pthread_mutex_unlock( &pglobal->db ); 167 DBG("waiting for next frame\n"); 168 /* only use usleep if the fps is below 5, otherwise the overhead is too long */ 169 if ( videoIn->fps < 5 ) { 170 usleep(1000*1000/videoIn->fps); 171 } 172 } 173 DBG("leaving input thread, calling cleanup function now\n"); 174 pthread_cleanup_pop(1); 175 return NULL; 176 } 177 178 void cam_cleanup(void *arg) { 179 static unsigned char first_run=1; 180 if ( !first_run ) { 181 DBG("already cleaned up ressources\n"); 182 return; 183 } 184 first_run = 0; 185 IPRINT("cleaning up ressources allocated by input thread\n"); 186 /* restore behaviour of the LED to auto */ 187 input_cmd(IN_CMD_LED_AUTO, 0); 188 close_v4l2(videoIn); 189 if (videoIn->tmpbuffer != NULL) free(videoIn->tmpbuffer); 190 if (videoIn != NULL) free(videoIn); 191 if (pglobal->buf != NULL) free(pglobal->buf); 192 //释放jpeg压缩、解压缩缓存 193 if (y != NULL) free(y); 194 if (u != NULL) free(u); 195 if (v != NULL) free(v); 196 if (processed_jpeg_data != NULL) free(processed_jpeg_data); 197 }
程序中实现了二值化、负片和sobel滤波效果。
除了修改源码外,还需要修改input_uvc文件夹内的Makefile,修改如下:
############################################################### # # Purpose: Makefile for "M-JPEG Streamer" # Author.: Tom Stoeveken (TST) # Version: 0.3 # License: GPL # ############################################################### CC = arm-linux-gcc OTHER_HEADERS = ../../mjpg_streamer.h ../../utils.h ../output.h ../input.h CFLAGS += -O2 -DLINUX -D_GNU_SOURCE -Wall -shared -fPIC #CFLAGS += -DDEBUG LFLAGS += -ljpeg -lm #for math.h all: input_uvc.so clean: rm -f *.a *.o core *~ *.so *.lo input_uvc.so: $(OTHER_HEADERS) input_uvc.c v4l2uvc.lo dynctrl.lo jpeg_utils.lo jpegutils.lo $(CC) $(CFLAGS) $(LFLAGS) -o $@ input_uvc.c v4l2uvc.lo dynctrl.lo jpeg_utils.lo jpegutils.lo v4l2uvc.lo: huffman.h uvc_compat.h uvcvideo.h v4l2uvc.c v4l2uvc.h $(CC) -c $(CFLAGS) -o $@ v4l2uvc.c jpeg_utils.lo: jpeg_utils.c jpeg_utils.h $(CC) -c $(CFLAGS) -o $@ jpeg_utils.c dynctrl.lo: dynctrl.c dynctrl.h uvcvideo.h $(CC) -c $(CFLAGS) -o $@ dynctrl.c jpegutils.lo: jpegutils.c jpegutils.h $(CC) -c $(CFLAGS) -o $@ jpegutils.c
编译完成后通过TQ2440进行测试,在终端中启动mjpg_streamer,客户端输出结果如下:
至此修改终于成功了。客户端软件看这里:http://www.armbbs.net/forum.php?mod=viewthread&tid=17042。除了使用客户端软件,还可以直接使用浏览器查看。
但是修改还没有结束,以上使用了自己编写代码的方法实现了图像处理功能,Opencv是图像处理利器,如果能进一步将Opencv应用到这里,能够大大简化我们的代码量,同时能够实现更复杂的算法。Opencv在x86 Linux和arm_linux上的移植我已经成功实现了,看我的另一篇博文:Opencv2.3.1在ubuntu10.04和TQ2440 arm-linux上的移植与测试。
这里我们进行背景差分算法的实现。主要参考了Opencv官网中的读视频文件和运动检测范例,此外涉及到IplImage类型与unsigned char*类型数据转换的问题,参考了BMP与IplImage相互转换范例。
使用Opencv需要加入头文件:
#include "cv.h" #include "highgui.h"
主要算法如下:
1 //将输入的jpeg图像解压成YUV分量 2 decode_jpeg_raw(videoIn->tmpbuffer,videoIn->buf.bytesused,0,420,videoIn->width,videoIn->height,y,u,v); 3 //这里可以对Y分量进行更改加入图像处理算法 4 framenumber++; 5 pFrame->imageData=(char *)y; 6 //Canny算子 7 //cvCanny(pImg, pCannyImg, 50, 150, 3); 8 if(framenumber == 1) 9 { 10 pBkImg = cvCreateImage(cvSize(videoIn->width,videoIn->height), IPL_DEPTH_8U,1); 11 pFrImg = cvCreateImage(cvSize(videoIn->width,videoIn->height), IPL_DEPTH_8U,1); 12 13 pBkMat = cvCreateMat(videoIn->height, videoIn->width, CV_32FC1); 14 pFrMat = cvCreateMat(videoIn->height, videoIn->width, CV_32FC1); 15 pFrameMat = cvCreateMat(videoIn->height, videoIn->width, CV_32FC1); 16 17 //转化成单通道图像再处理 18 pBkImg=pFrame;//当前帧为背景 19 pFrImg=pFrame;//当前帧转为灰度并作为前景 20 21 cvConvert(pFrImg, pFrameMat);//图像转为矩阵,以便计算 22 cvConvert(pFrImg, pFrMat); 23 cvConvert(pFrImg, pBkMat); 24 } 25 else //从第2帧开始 26 { 27 pFrImg=pFrame;//将当前帧作为前景并转灰度 28 cvConvert(pFrImg, pFrameMat); 29 //高斯滤波先,以平滑图像 30 cvSmooth(pFrameMat, pFrameMat, CV_GAUSSIAN, 3, 0, 0,0); 31 32 //当前帧跟背景图相减 33 cvAbsDiff(pFrameMat, pBkMat, pFrMat);//计算当前帧与背景的差的绝对值,作为前景 34 35 //二值化前景图 36 cvThreshold(pFrMat, pFrImg, 60, 255.0, CV_THRESH_BINARY); 37 38 //进行形态学滤波,去掉噪音 39 cvErode(pFrImg, pFrImg, 0, 1); 40 cvDilate(pFrImg, pFrImg, 0, 1); 41 42 //更新背景,背景自动更新,权值0.003 43 cvRunningAvg(pFrameMat, pBkMat, 0.03, 0); 44 45 //将UV分量设置为128,压缩后为灰度图像 46 memset(u,128,length/4); 47 memset(v,128,length/4); 48 //将YUV分量压缩成jpeg 49 encode_jpeg_raw(processed_jpeg_data,length,80,0,420,videoIn->width,videoIn->height,(unsigned char *)pFrImg->imageData,u,v); 50 }
附上使用Opencv的Makefile:
############################################################### # # Purpose: Makefile for "M-JPEG Streamer" # Author.: Tom Stoeveken (TST) # Version: 0.3 # License: GPL # ############################################################### CC = arm-linux-gcc OTHER_HEADERS = ../../mjpg_streamer.h ../../utils.h ../output.h ../input.h #################OpenCV Package Information for pkg-config############## prefix=/opt/EmbedSky/opencv_arm exec_prefix=${prefix} libdir=${exec_prefix}/lib includedir_old=${prefix}/include/opencv includedir_new=${prefix}/include # #Name: OpenCV #Description: Open Source Computer Vision Library #Version: 2.3.1 #Libs: -L${libdir} -lopencv_core -lopencv_imgproc -lopencv_highgui -lopencv_ml -lopencv_video -lopencv_features2d -lopencv_calib3d -lopencv_objdetect -#lopencv_contrib -lopencv_legacy -lopencv_flann -lpthread -lrt #Cflags: -I${includedir_old} -I${includedir_new} CV_LFLAGS += -ljpeg -L${libdir} -lopencv_core -lopencv_imgproc -lopencv_highgui -lopencv_ml -lopencv_video -lopencv_features2d -lopencv_calib3d -lopencv_objdetect -lopencv_contrib -lopencv_legacy -lopencv_flann -lpthread -lrt CV_CFLAGS += -O2 -DLINUX -D_GNU_SOURCE -Wall -shared -fPIC -I${includedir_old} -I${includedir_new} ##########end############################################# CFLAGS += -O2 -DLINUX -D_GNU_SOURCE -Wall -shared -fPIC #CFLAGS += -DDEBUG LFLAGS += -ljpeg -lm #for math.h all: input_uvc.so clean: rm -f *.a *.o core *~ *.so *.lo input_uvc.so: $(OTHER_HEADERS) input_uvc.c v4l2uvc.lo dynctrl.lo jpeg_utils.lo jpegutils.lo $(CC) $(CV_CFLAGS) $(CV_LFLAGS) -o $@ input_uvc.c v4l2uvc.lo dynctrl.lo jpeg_utils.lo jpegutils.lo v4l2uvc.lo: huffman.h uvc_compat.h uvcvideo.h v4l2uvc.c v4l2uvc.h $(CC) -c $(CFLAGS) -o $@ v4l2uvc.c jpeg_utils.lo: jpeg_utils.c jpeg_utils.h $(CC) -c $(CFLAGS) -o $@ jpeg_utils.c dynctrl.lo: dynctrl.c dynctrl.h uvcvideo.h $(CC) -c $(CFLAGS) -o $@ dynctrl.c jpegutils.lo: jpegutils.c jpegutils.h $(CC) -c $(CFLAGS) -o $@ jpegutils.c
编译成功后放到开发板验证,结果如下:
前两张图是建立背景,第三张是背景差分结果,从图中可以看出背景差分效果很好。
至此,我们已经能够成功将Opencv应用于mjpg_streamer项目中,为实现更复杂的算法打下了基础。值得一提的是,此次我使用的验证平台是TQ2440,虽然修改后的mjpg_streamer能够成功运行,但是速度实在不敢恭维。如果能够使用更高端的芯片验证,相信效果会更流畅。
最后附上修改后的项目源码:my_mjpg_streamer