攝像頭v4l2編寫,實現視頻在幀緩沖顯示


申明:該文檔只是記錄我的編寫和理解過程,代碼部分參考了較多的文章,如有意見請聯系我刪除,謝謝。

目標:

  使用v4l2提供API,完成攝像頭視頻采集,並使用幀緩存顯示。

准備工作:

  1. USB攝像頭1個
  2. 編譯環境(我用的是PC+Ubuntu14.04)
  3. 了解大概情況,查看如下網址,基本情況應該沒問題了: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;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM