简要:
我想学习摄像头,目前能做的是把V4L2(USB摄像头),瑞芯微,算能,全志的框架熟悉了,,学习一个抽象化接口,管理各种摄像头设备,但是我感觉几天不看代码就忘了,看的代码一愣一愣的,一方面有语言基础不大好的原因吧。所以写这篇文章,一是说明基础概念,二是写明路线,为之后的路指明方向,一方面是工作上,即抄即用,一方面希望能打下基础认知上。
基础篇:
摄像头接口:

秉持着最小最精的原则,还有其他接口不说了。
图像基础:
一个图像的要素:像素,分辨率,颜色深度,色彩空间,文件格式,压缩方式等等……
| 场景 | 标准描述 |
|---|---|
| 摄像头输出 | OV2640, 640×480, YUV422 (YUYV), 15 FPS |
| LCD 显示帧 | 320×240, RGB565, 60 Hz 刷新率 |
| 图像文件 | JPEG, 1920×1080, sRGB, 8-bit, Quality=85% |
| 视频流 | H.264, 1280×720, YUV420p, 30 FPS, Bitrate=2 Mbps |
摄像头就是获取“食材”(图像)的工具。
图像离不开视频,视频就是连续显示的图像。也离不开压缩,但不是我们的主要目的。我的目的是有个大概的认知,如何使用,再到程序上如何使用!
图像的表示:
图像由像素点表示,一般有黑白,灰度(由黑白混合),RGB像素点。
常见的
框图:
大致了解一下流程即可。
基础使用:
V4L2使用usb摄像头:
使用摄像头要注意的要点:
1.数据空间(buffer,通常需要mmap转到用户空间,因为V4L2是内核态下的,申请的buffer也是),2.如何获取查询设备的能力,设备支持的图片格式,3.获取图像数据的接口(API)以及如何优雅的获取一张图片。
上公式:
1.检测设备节点
int fd = open("/dev/video0", O_RDWR | O_NONBLOCK);
if (fd < 0) { printf("cant open video device\r\n");return -1; }
2.检查设备的能力(capture,streaming readwrite)
struct v4l2_capability cap;
ioctl(fd, VIDIOC_QUERYCAP, &cap);
if(! cap.capabilities & V4L2_CAP_VIDEO_CAPTURE){
printf("/dev/video0 is not a video device\r\n");return 0;}
if(! cap.capablility & V4L2_CAP_STREAMING){
printf("/dev/video0 is not a video device\r\n");return 0;}
}
if(! cap.capablility & V4L2_CAP_READWRITE){
printf("/dev/video0 is not a video device\r\n");return 0;}
}
3.枚举支持的格式,分辨率等
int flag = 0;
4.设置格式
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = width;
fmt.fmt.pix.height = height; fmt.fmt.pix.pixelformat = pixelFormat; fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) < 0) {
emit error(QString("VIDIOC_S_FMT failed: %1").arg(strerror(errno)));
return false;
}
// 保存实际设置的参数
width = fmt.fmt.pix.width;
height = fmt.fmt.pix.height;
pixelFormat = fmt.fmt.pix.pixelformat;
bytesPerLine = fmt.fmt.pix.bytesperline;
imageSize = fmt.fmt.pix.sizeimage;
qDebug() << "V4L2: Format set to" << width << "x" << height
<< "Format:" << (pixelFormat == V4L2_PIX_FMT_MJPEG ? "MJPEG" : "YUYV");
5.申请buffer
struct v4l2_requestbuffers req;
memset(&req, 0, sizeof(req));
req.count = 32;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &req) < 0) {
emit error(QString("VIDIOC_REQBUFS failed: %1").arg(strerror(errno)));
return false;
}
if (req.count < 2) {
emit error("Insufficient buffer memory");
return false;
}
buffers = new Buffer[req.count];
n_buffers = req.count;
for (unsigned int i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) {
emit error(QString("VIDIOC_QUERYBUF failed: %1").arg(strerror(errno)));
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) {
emit error(QString("mmap failed: %1").arg(strerror(errno)));
return false;
}
}
6.关闭摄像机
bool V4L2Camera::stopCapturing()
{
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMOFF, &type) < 0) {
emit error(QString("VIDIOC_STREAMOFF failed: %1").arg(strerror(errno)));
return false;
}
started = false;
return true;
}
void V4L2Camera::close()
{
stop();
if (buffers) {
for (unsigned int i = 0; i < n_buffers; ++i) {
if (buffers[i].start != MAP_FAILED) {
munmap(buffers[i].start, buffers[i].length);
}
}
delete[] buffers;
buffers = nullptr;
n_buffers = 0;
}
if (fd >= 0) {
::close(fd);
fd = -1;
}
}
7.打开摄像头使用
bool V4L2Camera::startCapturing()
{
for (unsigned int i = 0; i < n_buffers; ++i) {
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
emit error(QString("VIDIOC_QBUF failed: %1").arg(strerror(errno)));
return false;
}
}
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_STREAMON, &type) < 0) {
emit error(QString("VIDIOC_STREAMON failed: %1").arg(strerror(errno)));
return false;
}
started = true;
return true;
}
loop(循环使用并且转换buffer为image)

QImage V4L2Camera::readFrame()
{
if (!started || fd < 0) {
return QImage();
}
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_DQBUF, &buf) < 0) {
if (errno == EAGAIN) {
return QImage(); // 没有可用帧
}
emit error(QString("VIDIOC_DQBUF failed: %1").arg(strerror(errno)));
return QImage();
}
QImage image;
if (pixelFormat == V4L2_PIX_FMT_MJPEG) {
image = convertMJPEGToRGB((unsigned char*)buffers[buf.index].start, buf.bytesused);
} else if (pixelFormat == V4L2_PIX_FMT_YUYV) {
image = convertYUYVToRGB((unsigned char*)buffers[buf.index].start, width, height);
}
if (ioctl(fd, VIDIOC_QBUF, &buf) < 0) {
emit error(QString("VIDIOC_QBUF failed: %1").arg(strerror(errno)));
}
return image;
}
8.控制模块
至此完成了USB摄像头的基本使用。当然平常测试可以用一些工具来获取几张图片,可自行GPT搜索。
各个SOC厂商下的mpi库:
我只接触过瑞芯微,算能,sigmastar(这个接触比较少,当时实习刚学,而且sigmastar的模块比较多)

基本流程就是这也。
接下来探讨细节(可能不到位)。

对于VI模块(后续的模块都是如此):需要完成最上层的工作就是这样:
设置 DEV的属性,设置pipe的属性(这个没有属性),设置CHN的属性,然后绑定。

其中,各个模块在官方文档这样介绍:

当然实际用到,还是每个按照功能(单路,多路),去造一个模板,然后按照要求,去应用模板,至于内部,也没讲的很清楚,比如DEV对时序解析。这个得看摄像头的驱动,如何适配:如何设置回调函数,设置ISP,但是文档没说,没暴露接口,在其他文档。
单个摄像头,DEV初始化,用这个模板即可,
官方文档如是说,案例模板也是这样。

static bool init_vi_device(int dev_id, int pipe_id) { VI_DEV_ATTR_S dev_attr; VI_DEV_BIND_PIPE_S bind_pipe; memset(&dev_attr, 0, sizeof(dev_attr)); memset(&bind_pipe, 0, sizeof(bind_pipe)); RK_S32 ret = RK_MPI_VI_GetDevAttr(dev_id, &dev_attr); if (ret == RK_SUCCESS) { printf("VI_GetDevAttr OK\n"); } else if (ret == RK_ERR_VI_NOT_CONFIG) { ret = RK_MPI_VI_SetDevAttr(dev_id, &dev_attr); if (ret != RK_SUCCESS) { printf("VI_SetDevAttr fail %#x\n", ret); return false; } } else { printf("VI_GetDevAttr fail %#x\n", ret); return false; } ret = RK_MPI_VI_GetDevIsEnable(dev_id); if (ret != RK_SUCCESS) { ret = RK_MPI_VI_EnableDev(dev_id); if (ret != RK_SUCCESS) { printf("VI_EnableDev fail %#x\n", ret); return false; } bind_pipe.u32Num = 1; bind_pipe.PipeId[0] = pipe_id; ret = RK_MPI_VI_SetDevBindPipe(dev_id, &bind_pipe); if (ret != RK_SUCCESS) { printf("VI_SetDevBindPipe fail %#x\n", ret); return false; } } return true; }
重点在于CHN模块:

查看,通道是如何设置的,最大分辨率,缩放等等
如图查看VI_ISP_OPT_S,当然其他点也需要注意

如图,这是模板,其中bufcnt和VI_chn_attr.u32Depth的需要注意

bool init_vi_channel(int pipe_id, int channel_id, int width, int height)
{
RK_S32 ret;
int buf_cnt = 2;
VI_CHN_ATTR_S vi_chn_attr;
memset(&vi_chn_attr, 0, sizeof(vi_chn_attr));
vi_chn_attr.stIspOpt.u32BufCount = buf_cnt;
vi_chn_attr.stIspOpt.enMemoryType = VI_V4L2_MEMORY_TYPE_DMABUF;
vi_chn_attr.stSize.u32Width = width;
vi_chn_attr.stSize.u32Height = height;
vi_chn_attr.enPixelFormat = RK_FMT_YUV420SP;
vi_chn_attr.enCompressMode = COMPRESS_MODE_NONE;
vi_chn_attr.u32Depth = 0;
ret = RK_MPI_VI_SetChnAttr(pipe_id, channel_id, &vi_chn_attr);
if (ret != RK_SUCCESS) {
printf("RK_MPI_VI_SetChnAttr fail, ret = %#x\n", ret);
return false;
}
ret = RK_MPI_VI_EnableChn(pipe_id, channel_id);
if (ret) {
printf("RK_MPI_VI_EnableChn fail, ret = %#x\n", ret);
return false;
}
return true;
}
到此为止,VI基本的设置也讲完了。
接下里讲,通常的设置,从VI模块获取图像数据,
通常就是这样的一个模板:(后续的VPSS,VENC获取如H264都是这样的)
开一个线程
1.获取一帧数据,转化为虚拟地址,
2.复制
3.释放掉
if RK_MPI_VI_GetChnFrame(ViPipe ,Vi chn,pstFrameInfo,延时时间)
int *data = malloc(sizeof(RK_U64)*H*W);
memcpy(data,pstFrameInfo-->pViraddr,len);
RK_S32 RK_MPI_VI_ReleaseChnFrame( *pstFrameInfo);

这样来说,VI模块也讲完了。
接着到Venc:
模块流程是这样。

我i们要记得流程就是:
创建编码通道,设置venc的属性,设置编码器属性,一般都要获取编码的数据:用一个线程,循环周期的获取,保存或者传输。
需要了解一些编码方面的概念:
编码:
H264/H265、MJPEG、VP9,这些就压缩率和清晰度的区别。
还有这个。
| 应用场景 | 推荐模式 | 原因 |
|---|---|---|
| 直播推流(RTMP/RTSP) | CBR | 网络带宽恒定,避免卡顿 |
| 视频会议(WebRTC) | CBR + 低延迟配置 | 实时性 > 画质,需抗网络抖动 |
| NVR 安防录像存储 | VBR 或 ABR | 节省硬盘空间,夜间静态画面码率极低 |
| 短视频上传(抖音/快手) | VBR(上限约束) | 平衡画质与文件大小 |
| 车载 DVR(事故录像) | CBR(高码率) | 保证关键时刻画质,不因 VBR 降码 |
| AI 分析源(人脸识别) | CBR(高码率 + 高 I 帧频率) | 避免 P/B 帧错误传播影响识别 |
venc看这个模板就行。
关注码率,GOP。图像编码类型,输入类型。
bool init_venc(int venc_channel_id, int width, int height, RK_CODEC_ID_E venc_type, VENC_RC_MODE_E rc_mode) { RK_S32 ret; VENC_RECV_PIC_PARAM_S recv_param; VENC_CHN_ATTR_S venc_chn_attr; //编码通道参数结构体 memset(&venc_chn_attr, 0, sizeof(VENC_CHN_ATTR_S)); if (venc_type == RK_VIDEO_ID_AVC) { venc_chn_attr.stVencAttr.enType = RK_VIDEO_ID_AVC; //H264 venc_chn_attr.stVencAttr.u32Profile = H264E_PROFILE_HIGH; if (rc_mode == VENC_RC_MODE_H264CBR) { venc_chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR; //固定码率,适用于直播,实时流 venc_chn_attr.stRcAttr.stH264Cbr.u32Gop = VIDEO_FRAME_RATE; //20 GOP venc_chn_attr.stRcAttr.stH264Cbr.u32BitRate = 2000; //码率,试试不同码率对QP值和画质的影响,250kbps 1000kbps 2000kbps venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1; venc_chn_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = VIDEO_FRAME_RATE; //帧率,20fps venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1; venc_chn_attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = VIDEO_FRAME_RATE; } else { venc_chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264VBR; //可编码率,适用于点播,存储 venc_chn_attr.stRcAttr.stH264Vbr.u32Gop = VIDEO_FRAME_RATE; venc_chn_attr.stRcAttr.stH264Vbr.u32BitRate = VIDEO_FRAME_RATE; venc_chn_attr.stRcAttr.stH264Vbr.u32MaxBitRate = 8000; venc_chn_attr.stRcAttr.stH264Vbr.u32MinBitRate = 0; venc_chn_attr.stRcAttr.stH264Vbr.fr32DstFrameRateDen = 1; venc_chn_attr.stRcAttr.stH264Vbr.fr32DstFrameRateNum = VIDEO_FRAME_RATE; venc_chn_attr.stRcAttr.stH264Vbr.u32SrcFrameRateDen = 1; venc_chn_attr.stRcAttr.stH264Vbr.u32SrcFrameRateNum = VIDEO_FRAME_RATE; } } else if (venc_type == RK_VIDEO_ID_HEVC) { venc_chn_attr.stVencAttr.enType = RK_VIDEO_ID_HEVC; venc_chn_attr.stVencAttr.u32Profile = H265E_PROFILE_MAIN; if (rc_mode == VENC_RC_MODE_H265CBR) { venc_chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H265CBR; venc_chn_attr.stRcAttr.stH265Cbr.u32Gop = VIDEO_FRAME_RATE; venc_chn_attr.stRcAttr.stH265Cbr.u32BitRate = 2000; venc_chn_attr.stRcAttr.stH265Cbr.fr32DstFrameRateDen = 1; venc_chn_attr.stRcAttr.stH265Cbr.fr32DstFrameRateNum = VIDEO_FRAME_RATE; venc_chn_attr.stRcAttr.stH265Cbr.u32SrcFrameRateDen = 1; venc_chn_attr.stRcAttr.stH265Cbr.u32SrcFrameRateNum = VIDEO_FRAME_RATE; } else { venc_chn_attr.stRcAttr.enRcMode = VENC_RC_MODE_H265VBR; venc_chn_attr.stRcAttr.stH265Vbr.u32Gop = VIDEO_FRAME_RATE; venc_chn_attr.stRcAttr.stH265Vbr.u32BitRate = 2000; venc_chn_attr.stRcAttr.stH265Vbr.u32MaxBitRate = 4000; venc_chn_attr.stRcAttr.stH265Vbr.u32MinBitRate = 0; venc_chn_attr.stRcAttr.stH265Vbr.fr32DstFrameRateDen = 1; venc_chn_attr.stRcAttr.stH265Vbr.fr32DstFrameRateNum = VIDEO_FRAME_RATE; venc_chn_attr.stRcAttr.stH265Vbr.u32SrcFrameRateDen = 1; venc_chn_attr.stRcAttr.stH265Vbr.u32SrcFrameRateNum = VIDEO_FRAME_RATE; } } else { printf("venc_type is %d, not support\n", venc_type); goto fail; } venc_chn_attr.stGopAttr.enGopMode = VENC_GOPMODE_NORMALP; // venc_chn_attr.stGopAttr.u32GopSize = rk_param_get_int("video.0:gop", -1); venc_chn_attr.stVencAttr.enPixelFormat = RK_FMT_YUV420SP; //原始图像的像素格式NV12 venc_chn_attr.stVencAttr.u32PicWidth = width; //原始图像的宽高 venc_chn_attr.stVencAttr.u32PicHeight = height; venc_chn_attr.stVencAttr.u32VirWidth = width; venc_chn_attr.stVencAttr.u32VirHeight = height; venc_chn_attr.stVencAttr.u32StreamBufCnt = 2; venc_chn_attr.stVencAttr.u32BufSize = width * height / 2; //编码缓冲区大小。对于 H264/H265:一幅图像大小的 1/2。 ret = RK_MPI_VENC_CreateChn(venc_channel_id, &venc_chn_attr); //根据编码通道参数,创建编码通道 if (ret) { printf("RK_MPI_VENC_CreateChn, ret=%#x\n", ret); return -1; } // stRecvParam.s32RecvPicNum = 100; //recv 100 slice // RK_MPI_VENC_StartRecvFrame(venc_channel_id, &stRecvParam); memset(&recv_param, 0, sizeof(VENC_RECV_PIC_PARAM_S)); recv_param.s32RecvPicNum = -1; //不限制接受帧的数量 RK_MPI_VENC_StartRecvFrame(venc_channel_id, &recv_param); //开始从指定的通道接受编码数据 return true; fail: return false; }
然后就是存储和推流(MP4/FLV)
1.添加流,从VENC获取IDR(第一帧),获取PPS,SPS参数等
2.开启线程循环填充后续的数据
3.接下来就是
主要讲下MPI的一些要点把,内存分配,工作模式(不同功能下需要设置的一些东西)
1.内存缓存池
私有模式:
模块内部创建MB,自己管理。
共有模式:
用户注入,行为一致的模块可以共享MB
独立模式:
用户注入MB,用户管理
2.系统绑定:
这个比较简单

