申明:該文檔只是記錄我的編寫和理解過程,代碼部分參考了較多的文章,如有意見請聯系我刪除,謝謝。
目標:
使用v4l2提供API,完成攝像頭視頻采集,並使用幀緩存顯示。
准備工作:
- USB攝像頭1個
- 編譯環境(我用的是PC+Ubuntu14.04)
- 了解大概情況,查看如下網址,基本情況應該沒問題了:http://baike.baidu.com/item/V4L2?sefr=enterbtn
框架理解:

關鍵點理解:
攝像頭采集的循環buf
必須使用循環buf,否則攝像頭采集圖像顯示不會連續。
攝像頭的分辨率和幀緩沖的分辨率
攝像頭的分辨率和幀緩沖的分辨率不相同,不能套用同一個寬高,需要區別對待,否則會導致顯示花屏或其他未知后果。
yuyv轉換rgb
yuyv:4個字節,表示2個像素(uv共用),也就是說一個像素2個字節。
rgb24:3個字節,表示1個像素
rgb32:4個字節,表示1個像素
YUV到RGB的轉換有如下公式:
R = 1.164*(Y-16) + 1.159*(V-128);
G = 1.164*(Y-16) - 0.380*(U-128)+ 0.813*(V-128);
B = 1.164*(Y-16) + 2.018*(U-128));
舉例:一個yuyv的轉換到rgb32如下:
1、先從yuyv中取出第一個像素點: y0 u v
2、可以用公式計算出 R0 G0 B0 的值。
3、組裝成RGB32的第一個像素點: rgb32_0[4]={R0,G0,B0,0}
4、再從yuyv中取出第二個像素點: y1 u v (注意這里uv共用)
5、可以用公式計算出 R1 G1 B1 的值。
6、組裝成RGB32的第二個像素點: rgb32_1[4]={R0,G0,B0,0}
搭建框架:
為了方便,全部代碼都放到一個文件中。最好的方式是按功能拆分成多個文件,這里為了理解方便,就不拆分了。
黑色是攝像頭模塊(v4l2) 藍色是幀緩沖模塊
1 void main() 2 { 3 //打開攝像頭設備 4 open_cameral(MY_CAMERA); 5 //初始化幀緩沖 6 init_FrameBuffer(); 7 //獲取當前攝像頭的格式信息 8 get_camInfo(); 9 //設置用戶需要的攝像頭格式信息(分辨率和圖形格式) 10 set_format(); 11 //獲取攝像頭采集圖片buf 12 get_buf(); 13 //映射buf到用戶空間 14 map_buf(); 15 //開始采集 16 startcon(); 17 while(1) //這里可以優化成select,就不會阻塞了 18 { 19 //獲取采集到的數據 20 get_picture(bmp); 21 //把采集數據寫入幀緩沖 22 write_data_to_fb(FrameBuffer, Frame_fd, bmp, cam_width, cam_hight, Framebpp); 23 } 24 25 //停止采集 26 stopcon(); 27 //解除映射 28 bufunmap(); 29 //關閉幀緩沖 30 exit_Framebuffer(); 31 //關閉攝像頭設備 32 close_cameral(); 33 }
具體實現:
攝像頭模塊
打開攝像頭設備
這里為了通用,把攝像頭設備路徑作為入參傳入。
1 //打開攝像頭設備 2 int open_cameral(char* path) 3 { 4 fd=open(path,O_RDWR); 5 if (fd < 0) { 6 printf("Open /dev/video0 failed\n"); 7 return -1; 8 } 9 }
一般usb攝像頭,插入后,會直接生成設備文件:/dev/videox,這里的x范圍[0~n],表示第幾個usb攝像頭設備,我的測試環境由於只有1個,所以是/dev/video0。
獲取當前攝像頭的格式信息
這里剛開始我也很困惑,這里獲取攝像頭信息有什么用。后面自己分析了下,主要原因是獲取當前攝像頭支持的圖片格式,防止后面設置的時候設置不正確。
有些老式的攝像頭,只有yuyv格式,后面采集就只能采集成yuyv格式,再轉換下。
1 //獲取攝像頭信息 2 void get_camInfo(void) 3 { 4 struct v4l2_format fmt; 5 fmt.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; 6 //獲取當前攝像頭的寬高 7 ioctl(fd, VIDIOC_G_FMT, &fmt); 8 printf("Current data format information:\n\twidth:%d\n\theight:%d\n",fmt.fmt.pix.width,fmt.fmt.pix.height); 9 10 struct v4l2_fmtdesc fmtdesc; 11 fmtdesc.index=0; 12 fmtdesc.type=V4L2_BUF_TYPE_VIDEO_CAPTURE; 13 //獲取當前攝像頭支持的格式 14 while(ioctl(fd,VIDIOC_ENUM_FMT,&fmtdesc)!=-1) 15 { 16 if(fmtdesc.pixelformat & fmt.fmt.pix.pixelformat) 17 { 18 printf("\tformat:%s\n",fmtdesc.description); 19 break; 20 } 21 fmtdesc.index++; 22 } 23 }
這里我只是通過打印來查看,沒有直接在代碼里面比較,可以在后期優化中添加。
設置用戶需要的攝像頭格式信息
這里需要注意,選擇格式的時候,一定要確認你的攝像頭是否支持。這里格式有2種yuyv和mjpeg,可以通過上面的接口 get_camInfo 來查看。我的設備只支持yuyv,所以這里選擇V4L2_PIX_FMT_YUYV。
如果攝像頭支持mjpeg,建議直接選擇V4L2_PIX_FMT_MJPEG,避免后面轉換。
1 //設置攝像頭具體格式 2 int set_format() 3 { 4 struct v4l2_format fmt; 5 fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //這里必須填這個 6 fmt.fmt.pix.width = cam_width; //用戶希望設置的寬 7 fmt.fmt.pix.height = cam_hight; //用戶希望設置的高 8 fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;//選擇格式:V4L2_PIX_FMT_YUYV或V4L2_PIX_FMT_MJPEG 9 fmt.fmt.pix.field = V4L2_FIELD_INTERLACED; 10 int ret = ioctl(fd, VIDIOC_S_FMT, &fmt); 11 if (ret < 0) { 12 printf("VIDIOC_S_FMT failed (%d)\n", ret); 13 return -1; 14 } 15 //如果用戶傳入超過了實際攝像頭支持大小,攝像頭會自動縮小成最大支持。這里把攝像頭當前支持的寬高情況反饋給用戶。 16 cam_width = fmt.fmt.pix.width; 17 cam_hight = fmt.fmt.pix.height; 18 19 printf("------------VIDIOC_S_FMT---------------\n"); 20 printf("Stream Format Informations:\n"); 21 printf(" type: %d\n", fmt.type); 22 printf(" width: %d\n", fmt.fmt.pix.width); 23 printf(" height: %d\n", fmt.fmt.pix.height); 24 return 0; 25 }
獲取攝像頭采集圖片buf
由於攝像頭采集數據是放在內部的buf中的,我們需要申請內部buf存放采集數據。一般選擇4個內部buf,構造成一個簡單的循環buf,方便圖片的循環顯示。
1 //獲取攝像頭圖片采集的緩存buf 2 int get_buf(void) 3 { 4 struct v4l2_requestbuffers req; 5 memset(&req, 0, sizeof (req)); 6 req.count = CBUF_NUM; //攝像頭圖片緩存buf個數,這里一般設置4個 7 req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 8 req.memory = V4L2_MEMORY_MMAP; 9 if (ioctl(fd,VIDIOC_REQBUFS,&req) <0) 10 { 11 perror("VIDIOC_REQBUFS error \n"); 12 return -1; 13 } 14 return 0; 15 }
這里不構造循環隊列,后面映射的時候再構造。
映射buf到用戶空間
將申請到的buf映射到用戶空間,這樣就可以直接操作采集到的幀了,而不必去復制,提升性能。
1 //映射buf到用戶空間 2 void map_buf(void) 3 { 4 int numBufs = 0; 5 struct v4l2_buffer tmp_buf ; //攝像頭緩沖buf臨時保存buf 6 for (numBufs = 0; numBufs < CBUF_NUM; numBufs++) 7 { 8 memset( &tmp_buf, 0, sizeof(tmp_buf) ); 9 tmp_buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE ; 10 tmp_buf.memory = V4L2_MEMORY_MMAP ; 11 tmp_buf.index = numBufs; 12 //獲取內部buf信息到tmp_buf 13 if (ioctl(fd, VIDIOC_QUERYBUF, &tmp_buf) < 0) 14 { 15 printf("VIDIOC_QUERYBUF (%d) error\n",numBufs); 16 return; 17 } 18 pic_buffers[numBufs].length = tmp_buf.length; 19 pic_buffers[numBufs].offset = (size_t) tmp_buf.m.offset; 20 //開始映射 21 pic_buffers[numBufs].start = mmap (NULL, tmp_buf.length,PROT_READ | PROT_WRITE, MAP_SHARED, fd, tmp_buf.m.offset); 22 if (pic_buffers[numBufs].start == MAP_FAILED) 23 { 24 perror("pic_buffers error\n"); 25 //return -1; 26 } 27 //把設置好的buf入隊列 28 if (ioctl (fd, VIDIOC_QBUF, &tmp_buf) < 0) 29 { 30 printf("VIDIOC_QBUF error\n"); 31 //return -1; 32 } 33 } 34 //初始化入隊出隊 35 enqueue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE ; 36 dequeue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE ; 37 enqueue.memory = V4L2_MEMORY_MMAP ; 38 dequeue.memory = V4L2_MEMORY_MMAP ; 39 }
開始采集
1 //開始采集 2 void startcon() 3 { 4 enum v4l2_buf_type type; 5 type = V4L2_BUF_TYPE_VIDEO_CAPTURE; 6 if (ioctl (fd, VIDIOC_STREAMON, &type) < 0) 7 { 8 printf("VIDIOC_STREAMON error\n"); 9 // return -1; 10 } 11 }
獲取采集到的數據
1 //獲取采集到的數據 2 int get_picture(char *buffer) 3 { 4 int ret ; 5 //把采集到圖片的緩沖出隊 6 ret = ioctl(fd , VIDIOC_DQBUF , &dequeue); 7 if(ret != 0) 8 { 9 perror("dequeue fail"); 10 return -1 ; 11 } 12 13 //把圖片數據放到buffer中 14 memcpy(buffer , pic_buffers[dequeue.index].start , pic_buffers[dequeue.index].length); 15 16 //由於當前出隊的緩沖數據已經拷貝到用戶buffer中,這里可以重新入隊用於后面的數據保存,構造起循環隊列。 17 enqueue.index = dequeue.index ; 18 ret = ioctl(fd , VIDIOC_QBUF , &enqueue); 19 if(ret != 0) 20 { 21 perror("enqueue fail"); 22 return -2 ; 23 } 24 return 0 ; 25 }
停止采集
1 int stopcon(void) 2 { 3 //停止攝像頭 4 int ret ; 5 int off= 1 ; 6 ret = ioctl(fd , VIDIOC_STREAMOFF, &off); 7 if(ret != 0) 8 { 9 perror("stop Cameral fail"); 10 return -1 ; 11 } 12 return 0 ; 13 }
解除映射
1 int bufunmap(void) 2 { 3 int i ; 4 for(i = 0 ; i < CBUF_NUM ; i++) 5 { 6 munmap(pic_buffers[i].start , pic_buffers[i].length); 7 } 8 return 0 ; 9 }
關閉攝像頭設備
1 //關閉攝像頭 2 void close_cameral(void) 3 { 4 close(fd); 5 }
幀緩沖模塊
初始化幀緩沖
1 //初始化framebuffer 2 int init_FrameBuffer(void) 3 { 4 5 struct fb_var_screeninfo vinfo; 6 struct fb_fix_screeninfo finfo; 7 Frame_fd = open("/dev/fb0" , O_RDWR); 8 if(-1 == Frame_fd) 9 { 10 perror("open frame buffer fail"); 11 return -1 ; 12 } 13 14 // Get fixed screen information 15 if (ioctl(Frame_fd, FBIOGET_FSCREENINFO, &finfo)) 16 { 17 printf("Error reading fixed information.\n"); 18 exit(0); 19 } 20 21 // Get variable screen information 22 if (ioctl(Frame_fd, FBIOGET_VSCREENINFO, &vinfo)) 23 { 24 printf("Error reading variable information.\n"); 25 exit(0); 26 } 27 //這里把整個顯存一起初始化(xres_virtual 表示顯存的x,比實際的xres大,bits_per_pixel位深) 28 screensize = vinfo.xres_virtual * vinfo.yres_virtual * vinfo.bits_per_pixel / 8; 29 //獲取實際的位色,這里很關鍵,后面轉換和填寫的時候需要 30 Framebpp = vinfo.bits_per_pixel; 31 printf("%dx%d, %dbpp screensize is %ld\n", vinfo.xres_virtual, vinfo.yres_virtual, vinfo.bits_per_pixel,screensize); 32 33 //映射出來,用戶直接操作 34 FrameBuffer = mmap(0, screensize, PROT_READ | PROT_WRITE , MAP_SHARED , Frame_fd ,0 ); 35 if(FrameBuffer == (void *)-1) 36 { 37 perror("memory map fail"); 38 return -2 ; 39 } 40 return 0 ; 41 }
填寫幀緩沖
1 //寫入framebuffer fbp:幀緩沖首地址 fbfd:幀緩沖fd img_buf:采集到的圖片首地址 width:用戶的寬 height:用戶的高 bits:幀緩沖的位深 2 int write_data_to_fb(void *fbp, int fbfd, void *img_buf, unsigned int img_width, unsigned int img_height, unsigned int img_bits) 3 { 4 int row, column; 5 int num = 0; //img_buf 中的某個像素點元素的下標 6 rgb32_frame *rgb32_fbp = (rgb32_frame *)fbp; 7 rgb32 *rgb32_img_buf = (rgb32 *)img_buf; 8 9 //防止攝像頭采集寬高比顯存大 10 if(screensize < img_width * img_height * img_bits / 8) 11 { 12 printf("the imgsize is too large\n"); 13 return -1; 14 } 15 16 /*不同的位深度圖片使用不同的顯示方案*/ 17 switch (img_bits) 18 { 19 case 32: 20 for(row = 0; row < img_height; row++) 21 { 22 for(column = 0; column < img_width; column++) 23 { 24 //由於攝像頭分辨率沒有幀緩沖大,完成顯示后,需要強制換行,幀緩沖是線性的,使用row * vinfo.xres_virtual換行 25 rgb32_fbp[row * Framex + column].r = rgb32_img_buf[num].r; 26 rgb32_fbp[row * Framex + column].g = rgb32_img_buf[num].g; 27 rgb32_fbp[row * Framex + column].b = rgb32_img_buf[num].b; 28 29 num++; 30 } 31 } 32 break; 33 default: 34 break; 35 } 36 37 return 0; 38 }
這里有個關鍵點:幀緩沖存放的格式是bgr,不能直接rgb拷貝,需要轉換下,否則顯示顏色不對。
由於我是是32位的,所以只寫了32位,具體的目標板上可能是24位的,需要修改對應的結構rgb32_frame和rgb32。 rgb24和rgb32的區別,請自行百度。
如下是32位的:
1 //rgb結構 2 typedef struct { 3 uint_8 r; // 紅色分量 4 uint_8 g; // 綠色分量 5 uint_8 b; // 藍色分量 6 uint_8 rgbReserved; // 保留字節(用作Alpha通道或忽略) 7 } rgb32; 8 9 //幀緩沖中的rgb結構 10 typedef struct { 11 uint_8 b; // 藍色分量 12 uint_8 g; // 綠色分量 13 uint_8 r; // 紅色分量 14 uint_8 rgbReserved; // 保留字節(用作Alpha通道或忽略) 15 } rgb32_frame;
如下是24位的:
1 //rgb結構 2 typedef struct { 3 uint_8 r; // 紅色分量 4 uint_8 g; // 綠色分量 5 uint_8 b; // 藍色分量 6 } rgb24; 7 8 //幀緩沖中的rgb結構 9 typedef struct { 10 uint_8 b; // 藍色分量 11 uint_8 g; // 綠色分量 12 uint_8 r; // 紅色分量 13 } rgb24_frame;
退出幀緩沖
1 //退出framebuffer 2 int exit_Framebuffer(void) 3 { 4 munmap(FrameBuffer , screensize); 5 close(Frame_fd); 6 return 0 ; 7 }
轉換模塊
yuyv轉rgb
//yuyv轉rgb32的算法實現 static int sign3 = 1; /* YUV到RGB的轉換有如下公式: R = 1.164*(Y-16) + 1.159*(V-128); G = 1.164*(Y-16) - 0.380*(U-128)+ 0.813*(V-128); B = 1.164*(Y-16) + 2.018*(U-128)); */ int yuvtorgb(int y, int u, int v) { unsigned int pixel32 = 0; unsigned char *pixel = (unsigned char *)&pixel32; int r, g, b; static long int ruv, guv, buv; if(1 == sign3) { sign3 = 0; ruv = 1159*(v-128); guv = -380*(u-128) + 813*(v-128); buv = 2018*(u-128); } r = (1164*(y-16) + ruv) / 1000; g = (1164*(y-16) - guv) / 1000; b = (1164*(y-16) + buv) / 1000; if(r > 255) r = 255; if(g > 255) g = 255; if(b > 255) b = 255; if(r < 0) r = 0; if(g < 0) g = 0; if(b < 0) b = 0; pixel[0] = r; pixel[1] = g; pixel[2] = b; return pixel32; } int yuyv2rgb32(unsigned char *yuv, unsigned char *rgb, unsigned int width, unsigned int height) { unsigned int in, out; int y0, u, y1, v; unsigned int pixel32; unsigned char *pixel = (unsigned char *)&pixel32; //分辨率描述像素點個數,而yuv2個像素點占有4個字符,所以這里計算總的字符個數,需要乘2 unsigned int size = width*height*2; for(in = 0, out = 0; in < size; in += 4, out += 8) { y0 = yuv[in+0]; u = yuv[in+1]; y1 = yuv[in+2]; v = yuv[in+3]; sign3 = 1; pixel32 = yuvtorgb(y0, u, v); rgb[out+0] = pixel[0]; rgb[out+1] = pixel[1]; rgb[out+2] = pixel[2]; rgb[out+3] = 0; //32位rgb多了一個保留位 pixel32 = yuvtorgb(y1, u, v); rgb[out+4] = pixel[0]; rgb[out+5] = pixel[1]; rgb[out+6] = pixel[2]; rgb[out+7] = 0; } return 0; }
