4、图像采集基础:使用v4l2采集单帧图像、OpenCV读取摄像头、帧率控制
好,咱们正式开始搞图像采集。
这一章是实战的敲门砖。你想想看,后面所有的算法、模型、显示,都得先拿到图像数据。如果这一步没搞对,后面全是白搭。我个人习惯把图像采集分成两个层面:一个是底层驱动层面,用v4l2直接跟硬件打交道;另一个是应用层面,用OpenCV快速上手。今天咱们两个都讲透。
4.1 先聊聊v4l2——摄像头驱动的灵魂
v4l2,全称Video for Linux 2。说白了,它就是Linux系统下摄像头设备的统一接口标准。不管你用的是USB摄像头,还是CSI接口的摄像头,只要驱动写好了,v4l2都能管。
我在项目中遇到过一个问题:同一款摄像头,在x86的Ubuntu上跑得好好的,换到Jetson上就死活打不开。查了半天,发现是v4l2的设备节点路径不一样。x86上通常是/dev/video0,但Jetson上可能是/dev/video1甚至/dev/video2。嗯,这里要注意,先ls /dev/video*看一眼。
4.2 用v4l2采集单帧图像
为什么要学v4l2?因为有时候OpenCV不够灵活。比如你想设置某个特定的曝光时间,或者想用YUV格式直接处理,OpenCV的封装就不够用了。这时候就得自己写v4l2代码。
下面这个例子,是我常用的一个单帧采集函数。它做的事情很简单:打开设备、设置格式、申请缓冲区、开始采集、取一帧、保存。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
int capture_one_frame(const char *dev_path, const char *output_file) {
int fd = open(dev_path, O_RDWR);
if (fd < 0) {
perror("open device failed");
return -1;
}
// 设置格式:640x480,YUV420
struct v4l2_format fmt = {0};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
perror("set format failed");
close(fd);
return -1;
}
// 申请缓冲区
struct v4l2_requestbuffers req = {0};
req.count = 1;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
perror("request buffers failed");
close(fd);
return -1;
}
// 映射缓冲区
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
perror("query buffer failed");
close(fd);
return -1;
}
unsigned char *buffer = (unsigned char *)mmap(
NULL, buf.length, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset
);
// 入队并开始采集
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("queue buffer failed");
close(fd);
return -1;
}
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
perror("stream on failed");
close(fd);
return -1;
}
// 等待一帧
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd, &fds);
struct timeval tv = {2, 0}; // 2秒超时
int r = select(fd + 1, &fds, NULL, NULL, &tv);
if (r <= 0) {
perror("select timeout or error");
close(fd);
return -1;
}
// 出队获取数据
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
perror("dequeue buffer failed");
close(fd);
return -1;
}
// 保存为原始YUV文件
FILE *fp = fopen(output_file, "wb");
if (fp) {
fwrite(buffer, 1, buf.bytesused, fp);
fclose(fp);
printf("Saved %d bytes to %s\n", buf.bytesused, output_file);
}
// 清理
munmap(buffer, buf.length);
close(fd);
return 0;
}
v4l2-ctl --list-formats看看摄像头支持哪些格式。我曾经在Jetson Nano上踩过坑,摄像头明明支持MJPEG,但用YUYV格式去打开,结果帧率只有5fps。后来改成MJPEG格式,直接飙到30fps。
4.3 OpenCV读取摄像头——更省心的方式
说实话,大部分时候我们不需要自己写v4l2代码。OpenCV的VideoCapture封装得很好,几行代码就能搞定。
import cv2
# 打开摄像头,0表示第一个摄像头设备
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("摄像头打开失败")
exit()
# 设置分辨率
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
# 读取一帧
ret, frame = cap.read()
if ret:
cv2.imwrite("frame.jpg", frame)
print("图像已保存")
cap.release()
你看,是不是简单很多?但这里有个坑——OpenCV默认用的是v4l2后端,但在Jetson上,有时候它会自动选择GStreamer后端。为什么会这样?因为Jetson的硬件加速编码器需要GStreamer来驱动。
cap.read()返回的图像是空的或者花屏,试试强制指定后端:cap = cv2.VideoCapture(0, cv2.CAP_V4L2)。我曾经在Jetson Xavier上调试了整整一天,最后发现是后端选择的问题。
4.4 帧率控制——别让摄像头跑飞了
帧率控制是个容易被忽视的问题。你想想看,如果你的算法处理一帧需要50ms,但摄像头以30fps(约33ms一帧)的速度往缓冲区塞数据,缓冲区很快就会满,然后丢帧。
我常用的帧率控制方法有三种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 硬件帧率设置 | 通过v4l2或OpenCV直接设置摄像头帧率 | 摄像头支持固定帧率输出 |
| 软件帧率限制 | 在读取循环中加入延时 | 算法处理时间不稳定 |
| 时间戳同步 | 根据上一帧的处理时间动态调整 | 需要精确控制处理节奏 |
先看硬件帧率设置。用OpenCV的话,很简单:
cap.set(cv2.CAP_PROP_FPS, 15) # 设置为15fps
但说实话,这个方法不是所有摄像头都支持。我遇到过一些USB摄像头,你设了15fps,它还是按30fps跑。这时候就得用软件方法了。
软件帧率限制,说白了就是加个延时:
import cv2
import time
cap = cv2.VideoCapture(0)
target_fps = 15
frame_time = 1.0 / target_fps
while True:
start_time = time.time()
ret, frame = cap.read()
if not ret:
break
# 处理图像...
cv2.imshow("frame", frame)
# 计算需要等待的时间
elapsed = time.time() - start_time
wait_time = frame_time - elapsed
if wait_time > 0:
time.sleep(wait_time)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
time.sleep()的精度大约在10ms左右。如果你需要更精确的控制,可以考虑用nanosleep()或者硬件定时器。
4.5 实战中的避坑指南
最后,分享几个我踩过的坑:
- 设备节点冲突: Jetson上可能有多个
/dev/video*节点,有的是摄像头,有的是ISP(图像信号处理器)。我曾经误用了ISP节点,结果读出来的图像全是黑的。用v4l2-ctl --list-devices可以查看每个节点的具体信息。 - 缓冲区大小: 用v4l2采集高分辨率图像时,默认的缓冲区可能不够。我遇到过1920x1080的YUV图像,一帧大约3MB,但缓冲区只分配了1MB,结果图像被截断了。记得根据分辨率计算缓冲区大小。
- OpenCV的imshow延迟: 在Jetson上,
cv2.imshow()的显示延迟可能高达100ms。如果你在做实时处理,建议用cv2.imwrite()保存到文件,或者用GStreamer直接推流显示。
嗯,这一章的内容就到这里。图像采集看起来简单,但里面的细节不少。下一章我们会讲图像预处理,到时候会用到今天采集的图像数据。记得把代码跑一遍,有问题随时翻回来看看。