Linux V4L2 視頻采集 + JPEG 解碼 + LCD 顯示實踐
本文記錄一個完整的嵌入式視頻處理項目:使用 V4L2 接口從攝像頭采集 MJPEG 圖像,使用 libjpeg 解碼為 RGB 格式,并通過 framebuffer 顯示在 LCD 屏幕上。適用于使用 ARM Cortex-A 系列開發板進行嵌入式 Linux 多媒體開發的學習和實踐。
開發環境
- 操作系統:Linux(支持 V4L2 和 framebuffer)
- 攝像頭:支持 MJPEG 輸出格式,分辨率 640×480
- 顯示屏:支持 framebuffer 顯示,分辨率 800×480,RGB565 格式
- 編程語言:C
- 編譯依賴:
libjpeg
解碼庫
實現功能
- 打開攝像頭
/dev/video1
,設置 MJPEG 格式采集 - 申請并映射視頻緩沖區
- 解碼采集到的 JPEG 數據為 RGB 圖像
- 將 RGB 圖像轉換為 RGB565 并顯示在 LCD(
/dev/fb0
)上
關鍵流程
1. 打開 LCD 設備并映射 framebuffer
int lcdfd = open("/dev/fb0", O_RDWR);
lcdptr = (unsigned int *)mmap(NULL, 800*480*4, PROT_READ | PROT_WRITE, MAP_SHARED, lcdfd, 0);
2.打開攝像頭設備并設置采集格式
int fd = open("/dev/video1", O_RDWR);
struct v4l2_format v4formt;
v4formt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
v4formt.fmt.pix.width = 640;
v4formt.fmt.pix.height = 480;
ioctl(fd, VIDIOC_S_FMT, &v4formt);
3.申請緩沖區并映射到用戶空間
struct v4l2_requestbuffers v4rqbuffer;
v4rqbuffer.count = 4;
v4rqbuffer.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &v4rqbuffer);for (int i = 0; i < 4; i++) {ioctl(fd, VIDIOC_QUERYBUF, &v4buffer);mptr[i] = (unsigned char *)mmap(NULL, v4buffer.length, ...);ioctl(fd, VIDIOC_QBUF, &v4buffer); // 放回隊列
}
4.啟動采集并循環抓圖
ioctl(fd, VIDIOC_STREAMON, &type);
while (1) {ioctl(fd, VIDIOC_DQBUF, &readbuffer); // 取幀read_JPEG_file(mptr[readbuffer.index], rgbdata, readbuffer.length);lcd_show_rgb(rgbdata, 640, 480); // 顯示圖像ioctl(fd, VIDIOC_QBUF, &readbuffer); // 放回隊列
}
圖像解碼與顯示
攝像頭輸出的是 MJPEG 格式(實質是一幀幀 JPEG 圖像),我們使用 libjpeg
將其解碼為 RGB888 格式(三通道,每像素 3 字節):
jpeg_mem_src(&cinfo, jpegData, size); // 將 JPEG 數據源指向內存
jpeg_read_header(&cinfo, TRUE); // 讀取頭部信息
jpeg_start_decompress(&cinfo); // 啟動解壓
jpeg_read_scanlines(&cinfo, &buffer, 1); // 逐行讀取 RGB 數據
RGB → RGB565 顯示
LCD framebuffer 使用的是 RGB565 格式(每像素 2 字節),我們將 RGB888 的三通道數據壓縮為 RGB565,并寫入 /dev/fb0
:
unsigned char r = rgbdata[j*3 + 0];
unsigned char g = rgbdata[j*3 + 1];
unsigned char b = rgbdata[j*3 + 2];
unsigned short color = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);
ptr[j] = color;
編譯方法
需要下載libjpeg源碼,然后把一些庫文件一直到imx6u里面。交叉編譯使用命令
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi
${CC} -o video_show video_show.c -I/home/zwl/linux/tool/jpeg/include -L/home/zwl/linux/tool/jpeg/lib -ljpeg -Wl,-rpath,/home/zwl/linux/tool/jpeg/lib
運行效果
使用win系統下的obs打開攝像頭,對比發現拍攝畫質基本相似。
源碼附錄
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/videodev2.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/mman.h>
#include <stdio.h>
#include <jpeglib.h>
#include <linux/fb.h>int read_JPEG_file (const char *jpegData, char *rgbdata, int size)
{struct jpeg_error_mgr jerr;struct jpeg_decompress_struct cinfo;cinfo.err = jpeg_std_error(&jerr);//1創建解碼對象并且初始化jpeg_create_decompress(&cinfo);//2.裝備解碼的數據//jpeg_stdio_src(&cinfo, infile);jpeg_mem_src(&cinfo,jpegData, size);//3.獲取jpeg圖片文件的參數(void) jpeg_read_header(&cinfo, TRUE);/* Step 4: set parameters for decompression *///5.開始解碼(void) jpeg_start_decompress(&cinfo);//6.申請存儲一行數據的內存空間int row_stride = cinfo.output_width * cinfo.output_components;unsigned char *buffer = malloc(row_stride);int i=0;while (cinfo.output_scanline < cinfo.output_height) {//printf("****%d\n",i);(void) jpeg_read_scanlines(&cinfo, &buffer, 1); memcpy(rgbdata+i*640*3, buffer, row_stride );i++;}//7.解碼完成(void) jpeg_finish_decompress(&cinfo);//8.釋放解碼對象jpeg_destroy_decompress(&cinfo);return 1;
}int lcdfd = 0;
unsigned int *lcdptr = NULL;void lcd_show_rgb(unsigned char *rgbdata, int w, int h)
{ unsigned short *ptr = (unsigned short *)lcdptr; // 重要!!16位屏幕要用short指針!!for (int i = 0; i < h; i++){for (int j = 0; j < w; j++){unsigned char r = rgbdata[j*3 + 0];unsigned char g = rgbdata[j*3 + 1];unsigned char b = rgbdata[j*3 + 2];unsigned short color = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3);ptr[j] = color;}ptr += 800; // 每行跳800列rgbdata += w * 3; // 每行跳 w 個像素 * 3字節}
}int main(void)
{lcdfd = open("/dev/fb0", O_RDWR);if (lcdfd < 0){perror("/dev/fb0打開失敗\n");return -1;}lcdptr = (unsigned int *)mmap(NULL, 800*480*4, PROT_READ | PROT_WRITE, MAP_SHARED, lcdfd, 0);if(lcdptr < 0){perror("lcd內存映射失敗\n");return -1;}//1.打開設備int fd = open("/dev/video1",O_RDWR);if (fd < 0){perror("video0 打開失敗");return -1;}//2.獲取攝像頭支持的格式struct v4l2_fmtdesc v4fmtdesc;v4fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;for (int i = 0; i < 3; i++) {v4fmtdesc.index = i;int ret = ioctl(fd, VIDIOC_ENUM_FMT, &v4fmtdesc);if (ret < 0){perror("VIDIOC_ENUM_FMT獲取結束!");break;}printf("index=%d\n",v4fmtdesc.index);printf("flags=%d\n",v4fmtdesc.flags);printf("description=%s\n",v4fmtdesc.description);unsigned char *p = (unsigned char *)&v4fmtdesc.pixelformat;printf("pixelformat=%C%C%C%C\n",p[0],p[1],p[2],p[3]);printf("reserved[0]=%d\n",v4fmtdesc.reserved[0]); }printf("---------------設置采集格式--------------\n");//3.設置采集格式struct v4l2_format v4formt;v4formt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //攝像頭采集v4formt.fmt.pix.width = 640; //設置寬 不能任意大小v4formt.fmt.pix.height = 480; //設置高v4formt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; //設置視頻采集格式int ret = ioctl(fd, VIDIOC_S_FMT, &v4formt);if(ret < 0){perror("VIDIOC_S_FMT:設置格式失敗");}//驗證memset(&v4formt, 0, sizeof(v4formt)); //清空v4formt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;ret = ioctl(fd, VIDIOC_G_FMT, &v4formt);if(ret < 0){perror("獲取格式失敗");}else{printf("v4formt.fmt.pix.width = %d\n",v4formt.fmt.pix.width);printf("v4formt.fmt.pix.height = %d\n",v4formt.fmt.pix.height);unsigned char *p = (unsigned char *)&v4formt.fmt.pix.pixelformat;printf("v4formt.fmt.pix.pixelformat = %C%C%C%C\n",p[0],p[1],p[2],p[3]);printf("設置成功\n");}printf("---------------4.申請內核緩沖隊列--------------\n");//4.申請內核緩沖區隊列struct v4l2_requestbuffers v4rqbuffer;v4rqbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;v4rqbuffer.count = 4; //申請4個緩沖區v4rqbuffer.memory = V4L2_MEMORY_MMAP; //映射方式ret = ioctl(fd, VIDIOC_REQBUFS, &v4rqbuffer);if (ret < 0){perror("申請隊列空間失敗");}printf("---------------5.映射隊列空間到用戶空間--------------\n");
//5.映射隊列空間到用戶空間unsigned char *mptr[4]; //保存映射后空間的首地址 重要!!!unsigned int size[4];struct v4l2_buffer v4buffer;v4buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;for (int i = 0; i < 4; i++){v4buffer.index = i;ret = ioctl(fd, VIDIOC_QUERYBUF, &v4buffer); //從內核空間中查詢一個空間做映射if (ret < 0){perror("查詢內核空間隊列失敗");}mptr[i] = (unsigned char *)mmap(NULL,v4buffer.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, v4buffer.m.offset);size[i] = v4buffer.length;//通知使用完畢--‘放回去’ret = ioctl(fd, VIDIOC_QBUF, &v4buffer);if(ret < 0){perror("返回失敗");}}/* VIDIOC_STREAMON(開始采集寫數據到隊列中)VIDIOC_DQBUF(告訴內核我要某一個數據,內核不可以修改)VIDIOC_QBUF(告訴內核我已經使用完畢)VIDIOC_STREAMOFF(停止采集-不在向隊列中寫數據)*/printf("---------------6.開始采集--------------\n");
//6.開始采集int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;ret = ioctl(fd, VIDIOC_STREAMON, &type);if (ret < 0){perror("開啟失敗");}printf("---------------7.采集數據 從隊列中提取一幀數據--------------\n");
//7.采集數據 從隊列中提取一幀數據unsigned char rgbdata[640*480*3]; //定義一個空間存儲解碼后的RGB數據struct v4l2_buffer readbuffer;readbuffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;while (1){ ret = ioctl(fd, VIDIOC_DQBUF, &readbuffer);if (ret < 0){perror("讀取數據失敗");}//顯示在lcd上read_JPEG_file(mptr[readbuffer.index], rgbdata, readbuffer.length);//把jpeg數據解碼為RGB數據lcd_show_rgb(rgbdata, 640, 480);//通知內核已經使用完畢ret = ioctl(fd, VIDIOC_QBUF, &readbuffer);if (ret < 0){perror("放回隊列失敗");}}printf("---------------8.停止采集--------------\n");
//8.停止采集ret = ioctl(fd, VIDIOC_STREAMOFF, &type);printf("---------------9.釋放映射--------------\n");
//9.釋放映射for (int i = 0; i < 4; i++){printf("size[%d]: %d\n",i,size[i]);munmap(mptr[i],size[i]);}printf("---------------10.關閉設備--------------\n");
//10.關閉設備close(fd);printf("all end\n");return 0;
}