V4L2:Video for Linux two,縮寫 Video4Linux2,是 Linux 核心中的一個框架,提供了一套用於影片裝置驅動程式開發的 API。
它是一個開放的、通用的、模組化的影片裝置驅動程式框架,允許 Linux 作業系統和應用程式與各種影片裝置(如攝像頭、影片採集卡等)進行互動。
V4L2 提供了通用的 API,使應用程式能夠訪問和控制影片裝置,包括獲取裝置資訊、設定裝置引數、採集影片資料、控制裝置狀態等。V4L2 還提供了一個統一的影片資料格式,允許應用程式在處理影片資料時無需考慮裝置的具體格式。
V4L2 是 V4L 的改進版。V4L2 支援三種方式來採集影像:記憶體對映方式(mmap)、直接讀取方式(read)和使用者指標。記憶體對映的方式採集速度較快,一般用於連續影片資料的採集,實際工作中的應用多;直接讀取的方式相對速度慢一些,常用於靜態圖片資料的採集;使用者指標使用較少。
V4L2 的主要特性
-
模組化的架構:V4L2 是一個模組化的架構,允許多個裝置驅動程式同時存在並共享同一個 API。每個裝置驅動程式都是一個獨立的核心模組,可以在執行時載入和解除安裝。這種架構可以使開發人員更容易地開發新的影片裝置驅動程式,並允許多個驅動程式同時使用相同的 API。
-
統一的裝置節點:V4L2 提供了統一的裝置節點,使應用程式可以使用相同的方式訪問不同型別的影片裝置。這種節點通常是 /dev/videoX,其中 X 是一個數字,表示裝置的編號。應用程式可以透過開啟這個節點來訪問裝置,並使用 V4L2 API 進行資料採集和控制。
-
統一的影片資料格式:V4L2 提供了一個統一的影片資料格式,稱為 V4L2_PIX_FMT,允許應用程式在處理影片資料時無需考慮裝置的具體格式。V4L2_PIX_FMT 包括了許多常見的影片格式,如 RGB、YUV 等。應用程式可以使用 V4L2 API 來查詢裝置支援的資料格式,並選擇適當的格式進行資料採集和處理。
-
支援多種影片裝置:V4L2 支援許多不同型別的影片裝置,包括攝像頭、影片採集卡、TV 卡等。每個裝置都有自己的驅動程式,提供了相應的 V4L2 API。這些驅動程式可以根據裝置的不同特性,提供不同的採集模式、資料格式、控制引數等。
-
支援流式 I/O:V4L2 支援流式 I/O,即透過記憶體對映的方式將影片資料從裝置直接傳輸到應用程式中。這種方式可以減少資料複製的次數,提高資料傳輸的效率。
-
支援控制引數:V4L2 允許應用程式透過 API 來控制影片裝置的引數,包括亮度、對比度、色彩飽和度、曝光時間等。應用程式可以使用 V4L2 API 來查詢裝置支援的引數,並設定適當的值。
-
支援事件通知:V4L2 支援事件通知,當影片裝置狀態發生變化時,如影片訊號丟失、幀率變化等,V4L2 驅動程式可以嚮應用程式傳送通知,以便應用程式做出相應的處理。
從上面的特徵可以看出,V4L2 提供了一套通用、靈活、可擴充套件的影片裝置驅動程式框架,使得 Linux 作業系統和應用程式可以方便地與各種影片裝置進行互動,並且不需要關心裝置的具體實現細節。從而讓開發人員能夠更加專注於應用程式的開發。
V4L2 影片採集步驟
簡單例子
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include <getopt.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <malloc.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/time.h> #include <sys/mman.h> #include <sys/ioctl.h> #include <asm/types.h> #include <stdint.h> #include <stdbool.h> #include <string.h> #include <signal.h> #include <linux/videodev2.h> #include <linux/fb.h> #define FILE_NAME "./my.yuyv" #define FILE_YUV420 "test.yuv420" #define FILE_YUV422 "test.yuv422" #define FILE_RGB "test.rgb32" #define WIDTH 640 // 圖片的寬度 #define HEIGHT 480 // 圖片的高度 #define COUNT 5 // 緩衝區個數 #define FMT V4L2_PIX_FMT_YUYV // 圖片格式 #define LEN_YUV422 WIDTH * HEIGHT * 2 //YUV422長度 #define VIDEO_FILE "/dev/video1" #define FB_FILE "/dev/fb0" //檔案控制代碼 int fd = 0; int m_fb = 0; //frameBuffer 控制代碼 FILE *fl = nullptr; FILE *f420 = nullptr; FILE *f422 = nullptr; FILE *frgb32 = nullptr; //緩衝區地址 uint8_t *pYUV420Buff = NULL; //yuv420資料緩衝區(輸出) unsigned char *pYUV422 = NULL; //yuv422資料緩衝區(輸出) unsigned char *pRGBBuff = NULL; //rgb資料緩衝區(輸出) unsigned char *datas[COUNT]; // 緩衝區資料地址 unsigned char yuv422Buf[LEN_YUV422] = {0}; /** YUV422 長度大小: width * height * 2 2 byte * YUV444 長度大小: width * height * 3 3 byte * YUV420 長度大小: 3 * width * height / 2 1.5 byte * RGB24 長度大小: 3 * width * height 3 byte */ //YUYV422轉YUV420 int yuyv_to_yuv420p(const unsigned char *in, unsigned char *out, unsigned int width, unsigned int height) { unsigned char *y = out; unsigned char *u = out + width*height; unsigned char *v = out + width*height + width*height/4; unsigned int i,j; unsigned int base_h; unsigned int is_y = 1, is_u = 1; unsigned int y_index = 0, u_index = 0, v_index = 0; unsigned long yuv422_length = 2 * width * height; //序列為YU YV YU YV,一個yuv422幀的長度 width * height * 2 個位元組 //丟棄偶數行 u v for(i=0; i<yuv422_length; i+=2) { *(y+y_index) = *(in+i); y_index++; } for(i=0; i<height; i+=2) { base_h = i*width*2; for(j=base_h+1; j<base_h+width*2; j+=2) { if(is_u) { *(u+u_index) = *(in+j); u_index++; is_u = 0; } else { *(v+v_index) = *(in+j); v_index++; is_u = 1; } } } return 1; } //yuyv422轉rgb24 void yuyv422_to_rgb24(unsigned char *yuv_buffer,unsigned char *rgb_buffer,int iWidth,int iHeight) { int x; int z=0; unsigned char *ptr = rgb_buffer; unsigned char *yuyv= yuv_buffer; for (x = 0; x < iWidth*iHeight; x++) { int r, g, b; int y, u, v; if (!z) y = yuyv[0] << 8; else y = yuyv[2] << 8; u = yuyv[1] - 128; v = yuyv[3] - 128; r = (y + (359 * v)) >> 8; g = (y - (88 * u) - (183 * v)) >> 8; b = (y + (454 * u)) >> 8; *(ptr++) = (r > 255) ? 255 : ((r < 0) ? 0 : r); *(ptr++) = (g > 255) ? 255 : ((g < 0) ? 0 : g); *(ptr++) = (b > 255) ? 255 : ((b < 0) ? 0 : b); if(z++) { z = 0; yuyv += 4; } } } void yuv422sp_to_yuv422p( unsigned char* yuv422sp, unsigned char* yuv422p, int width, int height) { int i, j; int y_size; int uv_size; unsigned char* p_y1; unsigned char* p_uv; unsigned char* p_y2; unsigned char* p_u; unsigned char* p_v; y_size = uv_size = width * height; p_y1 = yuv422sp; p_uv = yuv422sp + y_size; p_y2 = yuv422p; p_u = yuv422p + y_size; p_v = p_u + width * height / 2; memcpy(p_y2, p_y1, y_size); for (j = 0, i = 0; j < uv_size; j+= 2, i++) { p_u[i] = p_uv[j]; p_v[i] = p_uv[j+ 1]; } } //YUV420轉RGB24 void YUV420P_TO_RGB24(unsigned char *yuv420p, unsigned char *rgb24, int width, int height) { int index = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int indexY = y * width + x; int indexU = width * height + y / 2 * width / 2 + x / 2; int indexV = width * height + width * height / 4 + y / 2 * width / 2 + x / 2; unsigned char Y = yuv420p[indexY]; unsigned char U = yuv420p[indexU]; unsigned char V = yuv420p[indexV]; rgb24[index++] = Y + 1.402 * (V - 128); //R rgb24[index++] = Y - 0.34413 * (U - 128) - 0.71414 * (V - 128); //G rgb24[index++] = Y + 1.772 * (U - 128); //B } } } //yuyv422轉yuv422 void YUYV422ToYUV422(char* in, unsigned char* out, unsigned int width, unsigned int height) { unsigned int total(width * height); char* in_y(in); char* in_u(in + 1); char* in_v(in + 3); char* out_y((char*)out); char* out_u((char*)out + width * height); char* out_v((char*)out + width * height + 1); for (unsigned int i(0); i < total; i += 10) { *out_y = *in_y; out_y++; in_y += 2; *out_y = *in_y; out_y++; in_y += 2; *out_u = *in_u; out_u += 2; in_u += 4; *out_v = *in_v; out_v += 2; in_v += 4; *out_y = *in_y; out_y++; in_y += 2; *out_y = *in_y; out_y++; in_y += 2; *out_u = *in_u; out_u += 2; in_u += 4; *out_v = *in_v; out_v += 2; in_v += 4; *out_y = *in_y; out_y++; in_y += 2; *out_y = *in_y; out_y++; in_y += 2; *out_u = *in_u; out_u += 2; in_u += 4; *out_v = *in_v; out_v += 2; in_v += 4; *out_y = *in_y; out_y++; in_y += 2; *out_y = *in_y; out_y++; in_y += 2; *out_u = *in_u; out_u += 2; in_u += 4; *out_v = *in_v; out_v += 2; in_v += 4; *out_y = *in_y; out_y++; in_y += 2; *out_y = *in_y; out_y++; in_y += 2; *out_u = *in_u; out_u += 2; in_u += 4; *out_v = *in_v; out_v += 2; in_v += 4; } } //寫檔案 int writeFile(FILE *fp, void *data, int len){ if ( fp == nullptr || !data ) return -1; return fwrite(data, len, 1, fp); } //讀取一幀 int readFrame(int count) { // printf("readFrame - %d\n", count); struct v4l2_buffer buff; memset(&buff, 0, sizeof (buff)); buff.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buff.memory = V4L2_MEMORY_MMAP; //第三個引數存放地址, 否則沒辦法拿到幀資料 if ( 0 == ioctl(fd, VIDIOC_DQBUF, &buff) ) //從緩衝區取出一個緩衝幀 { memset(pYUV420Buff, 0, 3 * WIDTH * HEIGHT / 2); printf("%p - frame Len: %d\n", datas[buff.index], buff.length); //yuyv轉yuv420 yuyv_to_yuv420p(datas[buff.index], pYUV420Buff, WIDTH, HEIGHT); // int wLen = writeFile(f420, pYUV420Buff, 3 * WIDTH * HEIGHT / 2); // printf("yuv420 writelen: %d\n", wLen); //顯示一幀到fb輸出 }else{ perror("get frame fail.\n"); } //將應用層的將幀的緩衝區重新放入影片採集佇列 if ( -1 == ioctl(fd, VIDIOC_QBUF, &buff) ) return 0; return 1; } //迴圈讀取 void mainLoop() { //先獲取100幀資料, 並寫檔案 // int count = 50; // while ( count-- > 0 ) while ( 1 ) { for(;;){ //等待獲取幀 fd_set fds; struct timeval tv; //超時時間設定 tv.tv_sec = 10; tv.tv_usec = 0; FD_ZERO(&fds); FD_SET(fd, &fds); //監聽裝置套接字 int res = select(fd + 1, &fds, NULL, NULL, &tv); //失敗 if (-1 == res) { if (EINTR == errno) continue; printf("error.\n"); exit(0); } //超時 if (0 == res) { printf("TimeOut.\n"); exit(0); } //正常讀取一幀 // if ( readFrame(count) ) if ( readFrame(0) ) { // printf("count - %d\n", count); break; } } } } void stop_capturing(void) { enum v4l2_buf_type type; } void fun_sig(int sig) { printf("\nsig = %d\n",sig); //停止資料流 enum v4l2_buf_type type; type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if ( -1 == ioctl(fd, VIDIOC_STREAMOFF &type) ){ perror("stop Capture fail.\n"); } //關閉控制代碼 fclose(fl); fclose(f420); fclose(frgb32); fclose(f422); close(fd); exit(0); } int main(int argc, char **argv) { //捕捉系統Ctrl+C訊號 signal(SIGINT, fun_sig); //捕捉段錯誤訊號 signal(SIGSEGV, fun_sig); /* 第一步:開啟攝像頭裝置檔案 */ int ret, i; fd = open(VIDEO_FILE, O_RDWR); if (-1 == fd){ perror("open /dev/video0 fail.\n"); return -1; } //獲取裝置屬性, 包括影像的格式 struct v4l2_fmtdesc fmtdesc; memset(&fmtdesc, 0, sizeof (fmtdesc)); fmtdesc.index = 0; fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //操作型別為獲取圖片 while( ioctl(fd, VIDIOC_ENUM_FMT, &fmtdesc) == 0 ) { printf("index: %d, description: %s\n", fmtdesc.index+1, fmtdesc.description); fmtdesc.index++; } printf("VIDIOC_ENUM_FMT Finish...\n"); /* 第二步:設定捕捉圖片幀格式 */ struct v4l2_format format; format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 操作型別為獲取圖片 format.fmt.pix.width = WIDTH; // 圖片的寬度 format.fmt.pix.height = HEIGHT; // 圖片的高度 format.fmt.pix.pixelformat = FMT; // 圖片格式 ret = ioctl(fd, VIDIOC_S_FMT, &format); // 進行設定(Set) if (-1 == ret) { perror("ioctl VIDIOC_S_FMT"); close(fd); return -2; } printf("VIDIOC_S_FMT Finish...\n"); /* 第三步:檢查是否設定成功 */ ret = ioctl(fd, VIDIOC_G_FMT, &format); if (-1 == ret) { perror("ioctl VIDIOC_G_FMT"); close(fd); return -3; } if (format.fmt.pix.pixelformat == FMT) { printf("ioctl VIDIOC_S_FMT sucessful\n"); } else{ printf("ioctl VIDIOC_S_FMT failed\n"); } printf("VIDIOC_G_FMT Finish...\n"); /* 第四步:讓攝像頭驅動申請存放影像資料的緩衝區 */ struct v4l2_requestbuffers reqbuf; reqbuf.count = COUNT; // 緩衝區個數 reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 緩衝區型別 reqbuf.memory = V4L2_MEMORY_MMAP; // 緩衝區的用途:用於記憶體對映 ret = ioctl(fd, VIDIOC_REQBUFS, &reqbuf); if (-1 == ret) { perror("ioctl VIDIOC_REQBUFS"); close(fd); return -4; } printf("VIDIOC_REQBUFS Finish...\n"); /* 第五步:查詢每個緩衝區的資訊,同時進行記憶體對映 */ struct v4l2_buffer buff; buff.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buff.memory = V4L2_MEMORY_MMAP; for (i = 0; i < COUNT; i++) { buff.index = i; ret = ioctl(fd, VIDIOC_QUERYBUF, &buff); if (-1 == ret) break; /* 列印緩衝區的長度和偏移量 */ printf("buf[%d]: len = %d offset: %d\n", i, buff.length, buff.m.offset); /* 把每塊緩衝區對映到當前程序來 */ datas[i] = (unsigned char*)mmap(NULL, buff.length, PROT_READ, MAP_SHARED, fd, buff.m.offset); if (MAP_FAILED == datas[i]) // 對映失敗 { perror("mmap failed"); return -5; } /* 把對映成功的緩衝區加入到攝像頭驅動的影像資料採集佇列裡 */ ret = ioctl(fd, VIDIOC_QBUF, &buff); // Queue if (-1 == ret) { perror("VIDIOC_QBUF"); return -6; } } printf("VIDIOC_QUERYBUF Finish...\n"); /* 第六步:啟動採集 */ int on = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 設定啟動標誌位 ret = ioctl(fd, VIDIOC_STREAMON, &on); // 開啟攝像頭流 if (-1 == ret) { perror("ioctl VIDIOC_STREAMON"); close(fd); return -7; } printf("VIDIOC_STREAMON Finish...\n"); //申請YUV420影像記憶體(空間大小為 3 * width * height / 2 ) //申請RGB32影像記憶體(空間大小為3 * width * height ) pYUV420Buff = new uint8_t[3 * WIDTH * HEIGHT / 2]; pRGBBuff = new unsigned char [3 * WIDTH * HEIGHT]; pYUV422 = new unsigned char [2 * LEN_YUV422]; #if 0 fl = fopen(FILE_NAME, "w"); if (NULL == fl) { fprintf(stderr, "open %s failed.\n", FILE_NAME); close(fd); return -1; } f420 = fopen(FILE_YUV420, "w"); if ( NULL == f420 ) { fprintf(stderr, "open %s failed.\n", FILE_YUV420); close(fd); return -1; } f422 = fopen(FILE_YUV422, "w"); if ( f422 == nullptr ) { fprintf(stderr, "open %s failed.\n", FILE_YUV422); close(fd); return -1; } frgb32 = fopen(FILE_RGB, "w"); if ( NULL == frgb32 ) { fprintf(stderr, "open %s failed.\n", FILE_RGB); close(fd); return -1; } #endif /* 第七步: 迴圈獲取影片幀資料 */ mainLoop(); /* 第八步: 關閉檔案控制代碼 */ #if 0 fclose(fl); fclose(f420); fclose(frgb32); fclose(f422); #endif close(fd); return 0; }
參考: https://mp.weixin.qq.com/s/sG_8_tzQUI7hOkBV7s51Zw