学无止境呀呀呀 的学生作业:
camera.c
#include "camera.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* camera.c 封装了本示例的完整采集链路:
* 1. 打开 /dev/videoX 并确认它支持 V4L2 视频采集
* 2. 把采集格式固定为 640x480 的 YUYV
* 3. 申请内核采集缓冲区,并通过 mmap 映射到用户空间
* 4. 启动 streaming,让驱动持续向缓冲区写入图像
* 5. 取出一帧,分别保存成 picture.yuv、picture.rgb,以及一组不同质量的 JPEG
* 6. 停止采集并释放所有资源
*
* 这个示例选择 mmap + streaming 模式,是因为它最能体现 V4L2
* 采集的标准工作流:应用程序不自己 malloc 大块帧缓冲,而是让驱动
* 分配 buffer,再映射到用户空间直接访问。
*/
/* 保存驱动分配并映射到用户空间的所有缓冲区信息。 */
static struct camera_t *camera_data;
/* 记录驱动实际分配到的缓冲区数量。 */
static unsigned int n_buffer;
/* 标记是否已经对设备执行过 STREAMON。 */
static int camera_streaming;
/*
* libjpeg 默认的错误处理方式比较“激烈”:
* 一旦内部发现严重错误,可能直接结束当前流程。
*
* 为了让在出错时仍然能回到自己的清理代码里释放资源,
* 这里额外包了一层错误上下文:
* 1. pub 是 libjpeg 规定的标准错误管理结构
* 2. setjmp_buffer 用来保存“返回点”
* 3. message 用来保存可读的错误文本
*/
struct jpeg_error_context {
struct jpeg_error_mgr pub;
jmp_buf setjmp_buffer;
char message[JMSG_LENGTH_MAX];
};
/*
* 当 libjpeg 内部出错时,会回调到这里。
* 这里不直接退出程序,而是:
* 1. 先把错误信息格式化成字符串
* 2. 再通过 longjmp 跳回 yuyv_to_jpeg() 里的 setjmp 检查点
*
* 这样调用者就还能执行 fclose/free/jpeg_destroy_compress 等清理动作。
*/
static void jpeg_error_exit(j_common_ptr cinfo)
{
struct jpeg_error_context *err = (struct jpeg_error_context *)cinfo->err;
(*cinfo->err->format_message)(cinfo, err->message);
longjmp(err->setjmp_buffer, 1);
}
/*
* 对 ioctl 做一层简单封装:
* 如果系统调用被信号中断(EINTR),就自动重试一次,
* 避免上层到处重复写相同的错误处理。
*/
static int xioctl(int fd, unsigned long request, void *arg)
{
int ret;
do {
ret = ioctl(fd, request, arg);
} while (ret < 0 && errno == EINTR);
return ret;
}
/*
* RGB 每个颜色分量都只占 1 个字节,因此合法范围只能是 0~255。
* YUV -> RGB 的公式是浮点运算,算出来的结果可能会小于 0 或大于 255,
* 所以最后必须做一次“截断”,避免写出非法颜色值。
*/
static unsigned char clip_color(int value)
{
if (value < 0) {
return 0;
}
if (value > 255) {
return 255;
}
return (unsigned char)value;
}
/*
* 把一行 YUYV 数据转换成一行 RGB888 数据。
*
* 先把几个概念说清楚:
* 1. Y 表示亮度,可以简单理解为“明暗”
* 2. U/V 表示色度,可以简单理解为“颜色往哪边偏”
* 3. RGB 则是更熟悉的红、绿、蓝三种颜色强度
*
* 当前代码处理的是 YUYV 格式。
* 会把它和 “YUV4”“YVU4” 之类的名字混着叫,
* 但这里真正的字节排列是:
* Y0 U Y1 V
*
* 这 4 个字节描述的是两个相邻像素,而不是一个像素:
* 1. 第 1 个像素使用自己的亮度 Y0,共享色度 U/V
* 2. 第 2 个像素使用自己的亮度 Y1,也共享同一组 U/V
*
* 为什么两个像素共用 U/V?
* 因为人眼对亮度变化更敏感,对颜色细节没那么敏感。
* 所以这类格式会让两个相邻像素共用一组颜色信息,从而节省带宽和存储空间。
*
* 输入一行占 width * 2 字节:
* 每 2 个像素只需要 4 个字节,所以平均每个像素占 2 字节。
*
* 输出一行占 width * 3 字节:
* RGB888 中每个像素都要单独保存 R/G/B 三个分量,因此每个像素占 3 字节。
*/
static void convert_yuyv_row_to_rgb(const unsigned char *src, unsigned char *dst, int width)
{
for (int col = 0; col < width; col += 2) {
/*
* 每次循环处理 2 个像素,所以 col 一次加 2。
* 这 2 个像素刚好对应 src 当前指向的 4 个字节:
* src[0] -> 第 1 个像素的亮度 Y0
* src[1] -> 两个像素共享的色度 U
* src[2] -> 第 2 个像素的亮度 Y1
* src[3] -> 两个像素共享的色度 V
*/
int y0 = src[0];
int u = src[1];
int y1 = src[2];
int v = src[3];
/*
* 下面三行是在计算“第 1 个像素”的 R/G/B。
*
* 为什么公式里有 (u - 128)、(v - 128)?
* 因为 U/V 在字节里通常是以 128 为中心存储的:
* 1. 128 左右可理解为“颜色偏移不大”
* 2. 小于 128 表示往一个方向偏
* 3. 大于 128 表示往另一个方向偏
*
* 所以在代入公式前,先减 128,才能把它还原成“以 0 为中心”的偏移量。
*
* 公式本身可以先不用死记,只要知道:
* 1. R 主要受 Y 和 V 影响
* 2. G 同时受 Y、U、V 影响
* 3. B 主要受 Y 和 U 影响
*/
dst[0] = clip_color((int)(y0 + 1.402 * (v - 128)));
dst[1] = clip_color((int)(y0 - 0.344 * (u - 128) - 0.714 * (v - 128)));
dst[2] = clip_color((int)(y0 + 1.772 * (u - 128)));
/*
* 下面三行计算“第 2 个像素”的 R/G/B。
* 注意它和第 1 个像素唯一的区别,是亮度换成了 y1;
* U/V 仍然是同一组共享色度。
*/
dst[3] = clip_color((int)(y1 + 1.402 * (v - 128)));
dst[4] = clip_color((int)(y1 - 0.344 * (u - 128) - 0.714 * (v - 128)));
dst[5] = clip_color((int)(y1 + 1.772 * (u - 128)));
/*
* 当前这一轮已经把 4 字节 YUYV 变成了 6 字节 RGB:
* 1. src 前进 4,去读下一组 YUYV
* 2. dst 前进 6,去写下一组 RGB
*/
src += 4;
dst += 6;
}
}
/*
* 按二进制方式把一段“裸数据”原样写入文件。
* 这里不附加任何文件头,因此输出文件只是纯字节流:
* picture.yuv 是原始 YUYV 数据,picture.rgb 是原始 RGB888 数据。
*/
static int write_binary_file(const char *file_name, const void *data, size_t length)
{
FILE *fp = fopen(file_name, "wb");
if (fp == NULL) {
perror(file_name);
return -1;
}
if (fwrite(data, 1, length, fp) != length) {
perror(file_name);
fclose(fp);
return -1;
}
if (fclose(fp) != 0) {
perror(file_name);
return -1;
}
return 0;
}
/*
* 打印一张 JPEG 文件的体积信息,便于观察质量和文件大小的权衡。
*/
static int print_jpeg_size_summary(const char *file_name, int quality)
{
struct stat st = {0};
if (stat(file_name, &st) < 0) {
perror(file_name);
return -1;
}
printf("%-8d %-18s %-12lld %.2f\n",
quality,
file_name,
(long long)st.st_size,
(double)st.st_size / 1024.0);
return 0;
}
/*
* 用同一帧 YUYV 数据导出一组不同压缩质量的 JPEG,
* 并打印每张图对应的文件大小。
*/
static int export_jpeg_quality_series(const unsigned char *yuv, int width, int height)
{
static const int jpeg_qualities[] = {10, 20, 30, 40, 50, 60, 70, 80, 90};
char file_name[32];
if (yuv == NULL || width > 8) & 0xFF,
(fmt.pixelformat >> 16) & 0xFF,
(fmt.pixelformat >> 24) & 0xFF,
fmt.description);
++fmt.index;
}
/*
* 向驱动申请示例想使用的采集格式。
* 这里固定请求 YUYV,是因为后续 read_camera() 会把拿到的数据
* 直接按 YUYV 的内存布局解释并转换为 RGB。
*/
format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
format.fmt.pix.width = IMG_WIDTH;
format.fmt.pix.height = IMG_HEIGHT;
format.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
format.fmt.pix.field = V4L2_FIELD_INTERLACED;
/* 如果驱动不接受这个格式,后面按固定长度解释图像数据就会出错。 */
if (xioctl(fd, VIDIOC_S_FMT, &format) < 0) {
perror("VIDIOC_S_FMT failure");
close(fd);
return -1;
}
/*
* S_FMT 并不保证驱动一定完全按请求值接受。
* 某些设备会悄悄改成别的分辨率或像素格式,所以这里必须二次核对。
* 一旦驱动实际给出的不是 640x480 YUYV,后面按固定长度写文件、
* 按 YUYV 公式转 RGB 就都会出错。
*/
if (format.fmt.pix.width != IMG_WIDTH ||
format.fmt.pix.height != IMG_HEIGHT ||
format.fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV) {
fprintf(stderr, "camera did not accept %dx%d YUYV format\n", IMG_WIDTH, IMG_HEIGHT);
close(fd);
return -1;
}
return fd;
}
/*
* 申请一组 V4L2 采集缓冲区,并映射到用户空间。
*
* 这一阶段对应 streaming 模式中最关键的一段准备工作:
* REQBUFS -> QUERYBUF -> mmap -> QBUF
*
* 完成后,camera_data[] 中保存了每块缓冲区的地址和长度,
* 而且这些空 buffer 都已经回送给驱动,驱动随时可以往里写入图像。
*
* 返回值:
* 成功返回 0;失败返回 -1。
* 失败时若前面已经申请了部分资源,调用者仍应执行 cleanup_camera()。
*/
int init_mmap(int fd)
{
/* 用于向驱动申请一组采集缓冲区。 */
struct v4l2_requestbuffers reqbuf = {0};
/*
* 一次申请 4 块 buffer。
* 对这个只抓一帧的示例来说,数量并不需要很大,但多块缓冲区是
* streaming 模式的常见做法,可以让驱动和应用程序之间更顺畅地轮转。
*/
reqbuf.count = 4;
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
/* 让驱动在内核空间分配缓冲区。 */
if (xioctl(fd, VIDIOC_REQBUFS, &reqbuf) < 0) {
perror("VIDIOC_REQBUFS failure");
return -1;
}
/* 驱动可能返回比请求值更少的 buffer,0 表示申请失败。 */
if (reqbuf.count == 0) {
fprintf(stderr, "driver did not allocate capture buffers\n");
return -1;
}
/*
* 在用户空间准备一个数组,记录每块 mmap buffer 的元信息。
* 注意:真正的大块图像内存仍然由驱动分配,这里只保存映射地址和长度。
*/
camera_data = calloc(reqbuf.count, sizeof(*camera_data));
if (camera_data == NULL) {
perror("calloc failure");
return -1;
}
n_buffer = reqbuf.count;
/*
* QUERYBUF 用来询问“第 i 块 buffer 在驱动里长什么样”:
* 它会告诉这块缓冲区的长度,以及它在设备内存中的偏移位置。
* 有了这些信息,用户进程才能用 mmap 把它映射进自己的地址空间。
*/
for (unsigned int i = 0; i < n_buffer; ++i) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (xioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
perror("VIDIOC_QUERYBUF failure");
return -1;
}
camera_data[i].length = buf.length;
/*
* 把第 i 个内核 buffer 映射到当前进程的地址空间。
* 映射成功后,camera_data[i].start 就是这块缓冲区在用户态看到的起始地址,
* 之后驱动把图像写进 buffer,应用程序就能直接从这个地址读取像素数据。
*/
camera_data[i].start = mmap(NULL,
buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED,
fd,
buf.m.offset);
if (camera_data[i].start == MAP_FAILED) {
camera_data[i].start = NULL;
perror("mmap failure");
return -1;
}
}
/*
* 映射完成后,还要做一次“预入队”:
* 把每块空 buffer 放回驱动输入队列。
* 只有已经 QBUF 的 buffer,驱动才会把采集到的图像填进去。
* 如果忘了这一步,即使后面 STREAMON 成功,驱动也没有可写入的目标 buffer。
*/
for (unsigned int i = 0; i < n_buffer; ++i) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (xioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("VIDIOC_QBUF failure");
return -1;
}
}
return 0;
}
/*
* 通知驱动正式开始采集视频流。
*
* 调用前提:
* init_mmap() 已成功,且至少有一块 buffer 已经 QBUF 入队。
*
* 返回值:
* 成功返回 0;失败返回 -1。
*/
int start_camera(int fd)
{
/* STREAMON 之后,驱动才真正开始往已入队的 buffer 写数据。 */
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (xioctl(fd, VIDIOC_STREAMON, &type) < 0) {
perror("VIDIOC_STREAMON failure");
return -1;
}
camera_streaming = 1;
return 0;
}
/*
* 读取一帧已经采集完成的图像,并写出一组文件:
* 1. picture.yuv: 驱动给出的原始 YUYV 帧数据
* 2. picture.rgb: 把同一帧转换成 RGB888 后的裸数据
* 3. picture_q10.jpg ~ picture_q90.jpg: 把同一帧压缩成不同质量的 JPEG 图片
*
* 这个函数体现了 streaming 模式下 buffer 的典型生命周期:
* 1. DQBUF: 从驱动取出一块“已经装满数据”的 buffer
* 2. 处理这块 buffer 里的内容
* 3. QBUF: 处理完成后把 buffer 再次放回驱动队列
*
* 注意:
* 一旦 DQBUF 成功,哪怕后续写文件或转换失败,也应该尽量把 buffer 回队,
* 否则驱动手里的可用 buffer 会越来越少。
*
* 返回值:
* 成功返回 0;失败返回 -1。
*/
int read_camera(int fd)
{
/* 用于从输出队列中取出一块已经装满图像数据的 buffer。 */
struct v4l2_buffer buf = {0};
const unsigned char *frame_ptr;
size_t frame_len;
/* 640x480 的 YUYV 一帧占用 width * height * 2 字节。 */
const size_t expected_frame_len = (size_t)IMG_WIDTH * (size_t)IMG_HEIGHT * 2U;
int status = -1;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
/*
* DQBUF 表示“出队”:
* 从驱动的完成队列里取出一块已经写好图像的 buffer。
* 调用成功后,buf.index 会指出是哪一块 buffer 已经准备完毕。
*/
if (xioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
perror("VIDIOC_DQBUF failure");
return -1;
}
/* 防御检查:驱动返回的索引必须能在本地数组中找到。 */
if (buf.index >= n_buffer || camera_data[buf.index].start == NULL) {
fprintf(stderr, "invalid capture buffer index %u\n", buf.index);
goto requeue_buffer;
}
frame_ptr = (const unsigned char *)camera_data[buf.index].start;
/*
* bytesused 是“这一帧真正有效的数据长度”。
* 它和 buffer 总长度不是一个概念:
* 1. buf.length / camera_data[i].length 是整块缓冲区容量
* 2. bytesused 是这次采集实际写进来的字节数
*
* 写 picture.yuv 时应该优先使用 bytesused,避免把缓冲区里未使用的尾部空间
* 也一起写到文件里。
*/
frame_len = buf.bytesused;
if (frame_len == 0) {
fprintf(stderr, "captured frame is empty\n");
goto requeue_buffer;
}
if (frame_len > camera_data[buf.index].length) {
frame_len = camera_data[buf.index].length;
}
/* 先保存驱动给出的原始 YUYV 数据,方便后续分析。 */
if (write_binary_file("picture.yuv", frame_ptr, frame_len) < 0) {
goto requeue_buffer;
}
/*
* 640x480 YUYV 的理论帧长 = width * height * 2。
* YUYV 每个像素平均占 2 字节,因此长度不足时不能继续按整帧去做
* YUV->RGB 转换,否则会越界读取或得到残缺图像。
*/
if (frame_len < expected_frame_len) {
fprintf(stderr, "captured frame is too short for %dx%d YUYV data\n", IMG_WIDTH, IMG_HEIGHT);
goto requeue_buffer;
}
/* 再把这帧原始数据转换成 RGB 字节流。 */
if (yuyv_to_rgb(frame_ptr, "picture.rgb", IMG_WIDTH, IMG_HEIGHT) < 0) {
goto requeue_buffer;
}
/*
* 用同一帧图像导出多张不同质量的 JPEG,
* 便于直接观察 10~90 质量档位在体积和视觉效果上的差别。
*/
if (export_jpeg_quality_series(frame_ptr, IMG_WIDTH, IMG_HEIGHT) < 0) {
goto requeue_buffer;
}
status = 0;
requeue_buffer:
/*
* 不管处理成功还是失败,都要把这块 buffer 再次 QBUF 回输入队列。
* 这是 streaming 模式的核心规则之一:
* 应用程序并不“拥有”这些 buffer,只是在 DQBUF 和下一次 QBUF 之间
* 暂时借用它们处理数据。
*/
if (xioctl(fd, VIDIOC_QBUF, &buf) < 0) {
perror("VIDIOC_QBUF failure");
return -1;
}
return status;
}
/*
* 统一释放采集过程中可能持有的所有资源。
*
* 设计目标是“可以安全地处理部分初始化成功的情况”:
* 即使某一步中途失败,调用者也可以直接跳到这里统一收尾。
*/
void cleanup_camera(int fd)
{
/*
* 清理顺序之所以这样安排,是为了符合资源依赖关系:
* 1. 先 STREAMOFF,停止驱动继续使用这些 buffer
* 2. 再 munmap,解除用户态到内核 buffer 的映射
* 3. 释放保存映射信息的用户态数组
* 4. 最后 close 设备 fd
*/
if (fd >= 0 && camera_streaming) {
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (xioctl(fd, VIDIOC_STREAMOFF, &type) < 0) {
perror("VIDIOC_STREAMOFF failure");
}
camera_streaming = 0;
}
if (camera_data != NULL) {
for (unsigned int i = 0; i < n_buffer; ++i) {
if (camera_data[i].start != NULL) {
munmap(camera_data[i].start, camera_data[i].length);
}
}
free(camera_data);
camera_data = NULL;
}
n_buffer = 0;
if (fd >= 0) {
close(fd);
}
}
/*
* 把一帧 YUYV 数据转换成 RGB888 原始字节流并写入文件。
*
* 参数约束:
* 1. yuv 必须指向完整的一帧 YUYV 数据
* 2. width 必须为偶数,因为 YUYV 每 4 字节描述 2 个像素
* 3. 输出文件是裸 RGB 数据,不带 BMP/PNG 等文件头
*
* 返回值:
* 成功返回 0;失败返回 -1。
*/
int yuyv_to_rgb(const unsigned char *yuv, const char *fileName, int width, int height)
{
FILE *fp = NULL;
/*
* line_buf 用来保存“当前一整行”的 RGB 数据。
* 这里不一次性申请整帧 RGB 图像,而是每次只处理一行:
* 1. 内存更省
* 2. 处理流程更直观
* 3. 后面写 JPEG 时也能复用这种“逐行处理”的思路
*/
unsigned char *line_buf = NULL;
/*
* src 始终指向“还没处理到的 YUYV 数据起点”。
* 每完成一行转换,就往后移动一整行的 YUYV 字节数。
*/
const unsigned char *src = yuv;
size_t line_len;
int status = -1;
if (yuv == NULL || fileName == NULL) {
return -1;
}
if (width QBUF 的完整流程。
*/
if (read_camera(cam_fd) < 0) {
goto cleanup;
}
status = EXIT_SUCCESS;
cleanup:
/*
* 第六步:统一收尾。
* 不论前面是初始化失败、等待超时还是成功拿到一帧,最后都走这里。
* cleanup_camera() 会按“streamoff -> munmap -> free -> close”的顺序释放资源,
* 因此主流程可以保持直线结构,不需要在每个失败分支重复写清理代码。
*/
cleanup_camera(cam_fd);
return status;
}
输出
quality file bytes KB
10 picture_q10.jpg 6333 6.18
20 picture_q20.jpg 7304 7.13
30 picture_q30.jpg 8239 8.05
40 picture_q40.jpg 9305 9.09
50 picture_q50.jpg 10669 10.42
60 picture_q60.jpg 12473 12.18
70 picture_q70.jpg 15653 15.29
80 picture_q80.jpg 21587 21.08
90 picture_q90.jpg 36616 35.76