介绍 摄像头是一款很重要的传感器,它相当于计算机的眼睛,日常开发中也很常见,在工业控制、汽车电子、物联网等领域扮演着十分重要的角色,可以用来拍照、视频采集、监控、视觉识别、测距、SLAM建图……
流程 Linux下适配摄像头的步骤
graph TD
A[开始] --> B[查看摄像头参数]
B --> C{摄像头是否在位?}
C -->|是| D[根据编码格式和帧率初始化]
C -->|否| Z[结束]
D --> E[申请视频流缓冲]
E --> F[开始采集视频流]
F --> G[帧处理]
G --> H[视频流转封装格式]
H --> I[定时器空刷新图像]
I --> J{继续采集?}
J -->|是| F
J -->|否| Z
工具 安装v4l-utils,以Ubuntu20.04为例,arm linux需要交叉编译v4l-utils 源码
1 sudo apt install v4l-utils
调试 查看摄像头设备节点
1 2 dmesg | grep video ls /dev/video*
用v4l2命令查看摄像头节点
查看设备节点支持的视频流编码格式
1 v4l2-ctl --device=/dev/video0 --list-formats
可以看到video0支持MJPG和YUYV两种视频流格式
1 v4l2-ctl --device=/dev/video0 --list-formats-ext
这条命令详细列举了摄像头在不同格式和分辨率下对应的帧率情况
嵌入式平台,如rk3588上查看高清摄像头支持情况
确定设备节点支持的像素格式
1 v4l2-ctl -d /dev/video12 --get-fmt-video
使用 初始化 以MJPEG
格式为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 bool CameraCapture::initDevice () { fd = open (deviceName.toLocal8Bit ().constData (), O_RDWR | O_NONBLOCK, 0 ); if (fd == -1 ) { qWarning () << "Cannot open" << deviceName; return false ; } struct v4l2_capability cap; if (ioctl (fd, VIDIOC_QUERYCAP, &cap) == -1 ) { qWarning () << "Failed to query capabilities" ; return false ; } if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { qWarning () << "Device does not support video capture" ; return false ; } if (!(cap.capabilities & V4L2_CAP_STREAMING)) { qWarning () << "Device does not support streaming" ; return false ; } struct v4l2_format fmt = {}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 1280 ; fmt.fmt.pix.height = 720 ; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; fmt.fmt.pix.field = V4L2_FIELD_NONE; if (ioctl (fd, VIDIOC_S_FMT, &fmt) == -1 ) { qWarning () << "Failed to set format" ; return false ; } return true ; }
申请缓存 向内核态申请缓冲队列,用于缓存v4l2数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 bool CameraCapture::initMMap () { struct v4l2_requestbuffers req = {}; req.count = 4 ; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (ioctl (fd, VIDIOC_REQBUFS, &req) == -1 ) { qWarning () << "Failed to request buffers" ; return false ; } if (req.count < 2 ) { qWarning () << "Insufficient buffer memory" ; return false ; } buffers = new buffer[req.count]; nBuffers = req.count; for (unsigned int i = 0 ; i < req.count; ++i) { struct v4l2_buffer buf = {}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (ioctl (fd, VIDIOC_QUERYBUF, &buf) == -1 ) { qWarning () << "Failed to query buffer" ; return false ; } buffers[i].length = buf.length; buffers[i].start = mmap (NULL , buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (buffers[i].start == MAP_FAILED) { qWarning () << "Failed to map buffer" ; return false ; } if (ioctl (fd, VIDIOC_QBUF, &buf) == -1 ) { qWarning () << "Failed to queue buffer" ; return false ; } } return true ; }
采集指令 内核开始采集缓存v4l2视频流数据
1 2 3 4 5 6 7 // Start capturing enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (ioctl(fd, VIDIOC_STREAMON, &type) == -1) { emit error("Failed to start streaming"); uninitDevice(); return false; }
数据帧处理 拿到内核态mmap数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 void CameraCapture::captureFrame () { fd_set fds; FD_ZERO (&fds); FD_SET (fd, &fds); struct timeval tv = {}; tv.tv_sec = 2 ; tv.tv_usec = 0 ; int r = select (fd + 1 , &fds, nullptr , nullptr , &tv); if (r == -1 ) { emit errorOccurred ("select错误" ) ; } else if (r == 0 ) { emit errorOccurred ("采集超时" ); } struct v4l2_buffer buf = {}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (ioctl (fd, VIDIOC_DQBUF, &buf) == -1 ) { emit errorOccurred ("获取帧失败" ) ; } QByteArray frameData (static_cast <char *>(buffers[buf.index].start), buf.bytesused) ; if (ioctl (fd, VIDIOC_QBUF, &buf) == -1 ) { emit errorOccurred ("缓冲区重新入队失败" ) ; } if (!frameData.isEmpty ()) { QImage image = decoder->decode (frameData); if (!image.isNull ()) { emit newFrame (image) ; } } }
MJPEG转换 MJPEG数据转可视化图像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 QImage MJpegDecoder::decode (const QByteArray &mjpegData) { struct jpeg_decompress_struct cinfo; struct jpeg_error_mgr jerr; cinfo.err = jpeg_std_error (&jerr); jpeg_create_decompress (&cinfo); jpeg_mem_src (&cinfo, reinterpret_cast <const unsigned char *>(mjpegData.constData ()), mjpegData.size ()); if (jpeg_read_header (&cinfo, TRUE) != JPEG_HEADER_OK) { emit errorOccurred ("JPEG头解析失败" ) ; jpeg_destroy_decompress (&cinfo); return QImage (); } if (jpeg_start_decompress (&cinfo) != TRUE) { emit errorOccurred ("JPEG解码启动失败" ) ; jpeg_destroy_decompress (&cinfo); return QImage (); } QImage image (cinfo.output_width, cinfo.output_height, QImage::Format_RGB888) ; JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, cinfo.output_width * cinfo.output_components, 1 ); while (cinfo.output_scanline < cinfo.output_height) { jpeg_read_scanlines (&cinfo, buffer, 1 ); uchar *dest = image.scanLine (cinfo.output_scanline - 1 ); memcpy (dest, buffer[0 ], cinfo.output_width * cinfo.output_components); } jpeg_finish_decompress (&cinfo); jpeg_destroy_decompress (&cinfo); return image; }
在位信号 判断video节点是否具备采集视频信号的能力(这个大多数情况下都需要,尤其是汽车电子,开机自检硬件设备是否正常)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 int CameraCapture::is_video_capture_device (const char *device_path) { int fd = open (device_path, O_RDWR); if (fd == -1 ) { perror ("Failed to open device" ); return 0 ; } struct v4l2_capability cap = {0 }; if (ioctl (fd, VIDIOC_QUERYCAP, &cap) == -1 ) { perror ("Failed to query capabilities" ); close (fd); return 0 ; } close (fd); if (cap.capabilities & V4L2_CAP_VIDEO_CAPTURE_MPLANE) { printf ("%s 是一个视频捕获设备(如摄像头)\n" , device_path); return 1 ; } else { printf ("%s 不是视频捕获设备\n" , device_path); return 0 ; } }
图像显示 图像显示可以用QLabel直接显示或者通过QPainter自绘,这里不再详细展开
1 2 3 4 5 void MainWindow::updateFrame (const QImage &frame) { imageLabel->setPixmap (QPixmap::fromImage (frame).scaled ( imageLabel->width (), imageLabel->height (), Qt::KeepAspectRatio)); }
帧率控制 从上面的MJPG的帧率:30fps,我们需要一个定时器去轮询摄像头buffer数据
1 2 3 4 5 captureTimer->start(33); // ~30fps connect(captureTimer, &QTimer::timeout, this, &CameraCapture::captureFrame);
开源工程 https://github.com/hywing/v4l2-camera