介绍

摄像头是一款很重要的传感器,它相当于计算机的眼睛,日常开发中也很常见,在工业控制、汽车电子、物联网等领域扮演着十分重要的角色,可以用来拍照、视频采集、监控、视觉识别、测距、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 --list-devices

img

查看设备节点支持的视频流编码格式

1
v4l2-ctl --device=/dev/video0 --list-formats

可以看到video0支持MJPG和YUYV两种视频流格式

img

1
v4l2-ctl --device=/dev/video0 --list-formats-ext

这条命令详细列举了摄像头在不同格式和分辨率下对应的帧率情况

img

嵌入式平台,如rk3588上查看高清摄像头支持情况

img

确定设备节点支持的像素格式

1
v4l2-ctl -d /dev/video12 --get-fmt-video

img

使用

初始化

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;
}

// Check capabilities
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;
}

// Set format
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;
}

// Queue the buffer
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


『 下里巴人 』
海纳百川,文以载道
hywing技术自留地
总访问 113701 次 | 本页访问 326