树莓派学习专题<15>:树莓派学习专题<15>:树莓派5:使用libcamera驱动获取摄像头图像
0. 项目代码
可以从下面获取到项目中的代码。
https://github.com/cdsmakc/h264_codec_base_rpi4b_rpi5_rv1106_visual_studio.git
1. 背景
之前的系列文章,都是在树莓派4上运行的。这两天又买了个树莓派5来测试。满以为重新编译一下就可以运行,结果发现存在很多问题:
- 树莓派5已经不支持硬件编码了,所以只能使用x264编码。
- V4L2在树莓派5上支持不好,之前的代码,VIDIOC_S_PARM命令和VIDIOC_G_PARM命令,在树莓派5上都报错。更关键的是,VIDIOC_STREAMON命令也无法启动流,报告参数错误。翻了树莓派的论坛,官方的回答是,强烈建议使用libcamera驱动替代V4L2驱动。
- 树莓派官方提供了rpicam-xxx系列工具及源码(源码可以github下载)。rpicam-vid中包含了我想要的所有功能,但是代码时C++的(libcamera驱动也是C++的),而且基本使用的都是C++11之后的特性写的,自己又不会C++,折腾了几天,才勉强弄出来一个可以运行的代码。
2. 参考
主要参考libcamera官方的驱动的说明。
一个是驱动的API:
API
另一个是官方的介绍文档,这些文档介绍了libcamera的特性、框架等,最重要的是里面提供了一个示例程序的步骤:
官方文档
基本上,按照上面这篇文章的内容一步一步小心地复制进去,就可以得到一个可运行的代码。
3. 代码
3.1 类定义
class RCE_CAM
{
private :
std::unique_ptr<CameraManager> pobjCameraManager ;
std::shared_ptr<Camera> pobjCamera ;
std::unique_ptr<CameraConfiguration> pobjCameraConfig ;
std::unique_ptr<ControlList> pobjCameraControls ;
FrameBufferAllocator *pobjAllocator ;
std::vector<std::unique_ptr<Request>> vecpobjRequests ;
public :
RCE_CAM(VOID) ;
public :
// camera manager
INT RCE_CAM_StartCameraManager(VOID) ;
VOID RCE_CAM_StopCameraManager(VOID) ;
// list camera and get camera
VOID RCE_CAM_ListCameraId(VOID) ;
INT RCE_CAM_GetCamera(VOID) ;
// acquire / release camera
INT RCE_CAM_AcquireCamera(VOID) ;
VOID RCE_CAM_ReleaseCamera(VOID) ;
// set camera pixel format, resolution, fps
INT RCE_CAM_SetFormat(VOID) ;
VOID RCE_CAM_SetFps(VOID) ;
// buffer
INT RCE_CAM_AllocBuffer(VOID) ;
VOID RCE_CAM_ReleaseBuffer(VOID) ;
// request callback
INT RCE_CAM_SetRequestCb(VOID (*__RequesetCb)(Request *)) ;
VOID RCE_CAM_QueueRequset(Request *pobjRequest){pobjCamera->queueRequest(pobjRequest);}
//INT RCE_CAM_SetRequestCb(VOID) ;
// camera start / stop
INT RCE_CAM_StartCamera(VOID) ;
VOID RCE_CAM_StopCamera(VOID) ;
// data process
//static VOID RCE_CAM_ProcessData(Request *Request) ;
} ;
3.2 创建、启动与停止CameraManager
程序的第一步是创建和启动CameraManager,这是后续一切摄像头操作的起点。
创建并启动CameraManager:
INT RCE_CAM::RCE_CAM_StartCameraManager(VOID)
{
INT iRetVal = 0 ;
// 创建CameraManager对象
pobjCameraManager = std::make_unique<CameraManager>() ;
// 启动CameraManager
iRetVal = pobjCameraManager->start() ;
if(0 != iRetVal)
{
cout<<"File "<<__FILE__<<", func "<<__func__<<", line"<<__LINE__<<" : "
<<"Start camera manager failed. Return value is "<<iRetVal<<"."<<endl ;
return -1 ;
}
return 0 ;
}
在程序运行结束后,还需要关闭CameraManager:
VOID RCE_CAM::RCE_CAM_StopCameraManager(VOID)
{
pobjCameraManager->stop() ;
}
3.3 获取摄像头
如下代码获取一个摄像头:
INT RCE_CAM::RCE_CAM_GetCamera(VOID)
{
auto cameras = pobjCameraManager->cameras() ;
// 查找摄像头
if (cameras.empty())
{
cout<<"File "<<__FILE__<<", func "<<__func__<<", line"<<__LINE__<<" : "
<<"No cameras were founded on the system."<<endl ;
return -1 ;
}
// 获取第1个摄像头的ID
std::string cameraId = cameras[0]->id() ;
// 获取第1个摄像头
pobjCamera = pobjCameraManager->get(cameraId) ;
if(nullptr == pobjCamera)
{
cout<<"File "<<__FILE__<<", func "<<__func__<<", line"<<__LINE__<<" : "
<<"Get camera failed."<<endl ;
return -2 ;
}
return 0 ;
}
上面代码中,**pobjCameraManager->cameras() ;获取摄像头列表。如果系统中有多个摄像头,则需要选取一个。因为我的硬件只有一个MIPI摄像头,因此只需要打开第一个(cameras[0]->id() ;)即可。使用pobjCameraManager->get(cameraId)**获取一个摄像头对象。
3.4 独占摄像头
在摄像头使用之前,需要占用摄像头;在程序结束时,还需要释放摄像头。
占用摄像头:
INT RCE_CAM::RCE_CAM_AcquireCamera(VOID)
{
INT iRetVal = 0 ;
iRetVal = pobjCamera->acquire() ;
if(0 != iRetVal)
{
cout<<"File "<<__FILE__<<", func "<<__func__<<", line"<<__LINE__<<" : "
<<"Acquire camera failed. Return value is "<<iRetVal<<"."<<endl ;
return -1 ;
}
return 0 ;
}
释放摄像头:
VOID RCE_CAM::RCE_CAM_ReleaseCamera(VOID)
{
pobjCamera->release() ;
pobjCamera.reset() ;
}
3.5 设置摄像头输出格式
INT RCE_CAM::RCE_CAM_SetFormat(VOID)
{
INT iRetVal = 0 ;
// 生成当前配置
pobjCameraConfig = pobjCamera->generateConfiguration({StreamRole::Viewfinder}) ;
if(nullptr == pobjCameraConfig)
{
cout<<"File "<<__FILE__<<", func "<<__func__<<", line"<<__LINE__<<" : "
<<"Generate camera configuration failed."<<endl ;
return -1 ;
}
// 获取当前流配置
StreamConfiguration &StreamConfig = pobjCameraConfig->at(0) ;
cout<<"Default viewfinder configuration is : "<<StreamConfig.toString()<<endl ;
// 配置分辨率和像素格式
StreamConfig.size.width = g_stRCEConfig.usWidth ;
StreamConfig.size.height = g_stRCEConfig.usHeight ;
StreamConfig.pixelFormat = libcamera::formats::YUV420 ;
// 验证并优化设置
pobjCameraConfig->validate() ;
// 打印设置
cout<<"Validated viewfinder configuration is : "<<StreamConfig.toString()<<endl ;
// 应用设置
iRetVal = pobjCamera->configure(pobjCameraConfig.get()) ;
if(0 != iRetVal)
{
cout<<"File "<<__FILE__<<", func "<<__func__<<", line"<<__LINE__<<" : "
<<"Acquire camera failed. Return value is "<<iRetVal<<"."<<endl ;
return -2 ;
}
return 0 ;
}
大致流程:
- 先通过方法pobjCamera->generateConfiguration来生成一个默认的摄像头配置。
- 通过方法**pobjCameraConfig->at(0)**获取一条流的默认配置。
- 配置流。设定图像分辨率(宽、高),指定输出格式为YUV420。
- 使用方法**pobjCameraConfig->validate() ;**验证配置。根据文档介绍,因为我们的配置,摄像头未必正好能支持,该方法会修改这些配置为最接近的配置。但是实际上,常见的分辨率应该都没有问题,可以完全匹配。
- 使用方法**pobjCamera->configure(pobjCameraConfig.get())**将配置应用下去。
3.6 设置帧率
设置帧率,不是上面的config来设置的。它通过设置图像最大帧间隔和最小帧间隔,来实现帧率的修改:
VOID RCE_CAM::RCE_CAM_SetFps(VOID)
{
UINT uiFrameDuration ;
// 计算帧持续时间(以微秒为单位)
uiFrameDuration = 1000000 / g_stRCEConfig.uiFps ;
// 获取控制项
pobjCameraControls = std::make_unique<libcamera::ControlList>() ;
// 设置帧持续时间
pobjCameraControls->set(libcamera::controls::FrameDurationLimits, libcamera::Span<const std::int64_t, 2>({uiFrameDuration, uiFrameDuration})) ;
}
注意:此处填写的帧间隔以微秒为单位。例如对于30fps设置,需要设置最大帧间隔和最小帧间隔都为33000。
3.6 申请缓存
INT RCE_CAM::RCE_CAM_AllocBuffer(VOID)
{
INT iRetVal = 0 ;
// 为流分配缓冲区
pobjAllocator = new FrameBufferAllocator(pobjCamera) ;
for (StreamConfiguration &Cfg : *pobjCameraConfig)
{
iRetVal = pobjAllocator->allocate(Cfg.stream()) ;
if (0 > iRetVal)
{
cout<<"File "<<__FILE__<<", func "<<__func__<<", line"<<__LINE__<<" : "
<<"Allocate buffers failed. Return value is "<<iRetVal<<"."<<endl ;
return -1 ;
}
size_t BufferNum = pobjAllocator->buffers(Cfg.stream()).size() ;
cout<<"Allocated "<<BufferNum<<" buffers for stream."<< std::endl ;
}
// 获取当前被配置的流
StreamConfiguration &StreamConfig = pobjCameraConfig->at(0) ;
Stream *Stream = StreamConfig.stream() ;
const std::vector<std::unique_ptr<FrameBuffer>> &Buffers = pobjAllocator->buffers(Stream);
for (UINT i = 0; i < Buffers.size(); ++i)
{
std::unique_ptr<Request> pobjRequest = pobjCamera->createRequest() ;
if (nullptr == pobjRequest)
{
cout<<"File "<<__FILE__<<", func "<<__func__<<", line"<<__LINE__<<" : "
<<"Create request failed."<<endl ;
return -2 ;
}
const std::unique_ptr<FrameBuffer> &Buffer = Buffers[i] ;
iRetVal = pobjRequest->addBuffer(Stream, Buffer.get()) ;
if (0 > iRetVal)
{
cout<<"File "<<__FILE__<<", func "<<__func__<<", line"<<__LINE__<<" : "
<<"Add buffer to request failed."<<endl ;
return -3 ;
}
vecpobjRequests.push_back(std::move(pobjRequest));
}
return 0 ;
}
大致流程:
- 先创建一个FrameBufferAllocator对象;
- 使用上述对象为流创建缓存:pobjAllocator->allocate(Cfg.stream())。
- 使用方法pobjCamera->createRequest()创建请求。所谓请求,是可以排队到libcamera驱动中的,每个请求带着一个buffer。请求被排队后,若图像获取成功了,会使用用户提供的回调函数来处理这些请求,并从请求附带的buffer中获取图像数据。上面代码中,申请了多少缓存(我这里是申请到了4个缓存,足够了;应该是可以改的),就创建多少请求。
- 使用方法**pobjRequest->addBuffer(Stream, Buffer.get())**将buffer加入到一个request中。
- 将所有的请求(我这里是4个)打包到vecpobjRequests中。
程序运行完之后,需要释放缓存:
VOID RCE_CAM::RCE_CAM_ReleaseBuffer(VOID)
{
pobjAllocator->free(pobjCameraConfig->at(0).stream()) ;
delete pobjAllocator ;
}
3.7 设定回调函数
回调函数用于处理请求达成(即图像就绪),从而获取图像。
INT RCE_CAM::RCE_CAM_SetRequestCb(VOID (*__RequesetCb)(Request *))
{
pobjCamera->requestCompleted.connect(__RequesetCb) ;
return 0 ;
}
回调函数如下:
VOID RCE_CAM_ProcessData(Request *pobjRequest)
{
VOID *pvMmapAddr ;
UINT uiLength = g_stRCEConfig.usWidth * g_stRCEConfig.usHeight * 3 / 2 ;
UINT uiOffset = 0 ;
if (pobjRequest->status() == Request::RequestCancelled)
{
return;
}
const std::map<const Stream *, FrameBuffer *> &Buffers = pobjRequest->buffers();
for (auto bufferPair : Buffers)
{
// 更新统计:从摄像头接收的帧数和数据量
g_stCAMWorkarea.stStatistics.uiFrameCountFromCamera++ ;
g_stCAMWorkarea.stStatistics.ui64DataCountFromCamera += uiLength ;
// 获取图像缓冲
FrameBuffer *Buffer = bufferPair.second;
if(0 == g_uiFrameBufferValid)
{
// 将buffer映射到用户空间
pvMmapAddr = mmap(NULL,
uiLength,
PROT_READ | PROT_WRITE,
MAP_SHARED,
Buffer->planes()[0].fd.get(),
uiOffset);
// 复制图像
memcpy(g_pucFrameBuffer, pvMmapAddr, uiLength) ;
// 设置标记,等待编码完成
g_uiFrameBufferValid = 1 ;
// 释放映射
munmap(pvMmapAddr, uiLength) ;
// 更新统计:更新发送到编码器的帧数
g_stCAMWorkarea.stStatistics.uiFrameCountToEncoder++ ;
}
}
// 恢复请求,并重新排队请求
pobjRequest->reuse(Request::ReuseBuffers);
//objRceCam.pobjCamera->queueRequest(pobjRequest);
objRceCam.RCE_CAM_QueueRequset(pobjRequest) ;
return ;
}
前述申请的buffer不是在线程中的,需要用mmap映射到用户空间。实际上,这里的代码写的不正确。
YUV420图像返回的是3个Plane,因此需要3次mmap;或者1次mmap,但是用不同的offset来读取3个平面。实际上我试了一下,3个平面地址是连续的,因此我只一次mmap,并直接读取整个画面的图像(包括Y、U、V)。
在图像取走之后(写入文件,或者发送到编码器),需要将request再次排队,以便继续获取图像:pobjRequest->reuse(Request::ReuseBuffers); objRceCam.RCE_CAM_QueueRequset(pobjRequest) ;)。
3.7 启动和停止摄像头
上面的工作都完成后,就可以打开摄像头,获取图像数据了。
INT RCE_CAM::RCE_CAM_StartCamera(VOID)
{
INT iRetVal = 0 ;
iRetVal = pobjCamera->start(pobjCameraControls.get()) ;
// 启动相机,使相机处于running状态
if(0 != iRetVal)
{
cout<<"File "<<__FILE__<<", func "<<__func__<<", line"<<__LINE__<<" : "
<<"Start camera failed. Return value "<<iRetVal<<"."<<endl ;
return -1 ;
}
// 将request加入队列,注意仅处于running状态时才可加入
for (std::unique_ptr<Request> &request : vecpobjRequests)
{
pobjCamera->queueRequest(request.get());
}
return 0 ;
}
这里需要注意的是,摄像头一定要在running状态(也就是pobjCamera->start(pobjCameraControls.get()))之后,才能将一系列request压入到驱动中。
以下代码停止摄像头:
VOID RCE_CAM::RCE_CAM_StopCamera(VOID)
{
pobjCamera->stop() ;
}
3.7 主函数及循环
这里我写了一个线程。
VOID *RCE_CAM_Thread(VOID *pvArgs)
{
if(0 != objRceCam.RCE_CAM_StartCameraManager())
{
// 创建并启动CameraManager失败,退出线程
g_stCAMWorkarea.enThreadStatus = CAM_THREAD_STATUS_EXIT ;
goto LABEL_EXIT_WITH_NO_PROCESS ;
}
if(0 != objRceCam.RCE_CAM_GetCamera())
{
// 获取摄像头失败,退出线程
g_stCAMWorkarea.enThreadStatus = CAM_THREAD_STATUS_EXIT ;
goto LABEL_EXIT_WITH_STOP_CAMERA_MANAGER ;
}
if(0 != objRceCam.RCE_CAM_AcquireCamera())
{
// 锁定摄像头失败,退出线程
g_stCAMWorkarea.enThreadStatus = CAM_THREAD_STATUS_EXIT ;
goto LABEL_EXIT_WITH_STOP_CAMERA_MANAGER ;
}
if(0 != objRceCam.RCE_CAM_SetFormat())
{
// 配置摄像头格式失败,退出线程
g_stCAMWorkarea.enThreadStatus = CAM_THREAD_STATUS_EXIT ;
goto LABEL_EXIT_WITH_RELEASE_CAMERA ;
}
// 设置帧率
objRceCam.RCE_CAM_SetFps() ;
if(0 != objRceCam.RCE_CAM_AllocBuffer())
{
// 申请图像缓存失败,退出线程
g_stCAMWorkarea.enThreadStatus = CAM_THREAD_STATUS_EXIT ;
goto LABEL_EXIT_WITH_RELEASE_CAMERA ;
}
//if(0 != objRceCam.RCE_CAM_SetRequestCb(RCE_CAM::RCE_CAM_ProcessData))
if(0 != objRceCam.RCE_CAM_SetRequestCb(RCE_CAM_ProcessData))
{
// 连接数据处理回调函数失败,退出线程
g_stCAMWorkarea.enThreadStatus = CAM_THREAD_STATUS_EXIT ;
goto LABEL_EXIT_WITH_RELEASE_BUFFER ;
}
if(0 != objRceCam.RCE_CAM_StartCamera())
{
// 启动相机失败,退出线程
g_stCAMWorkarea.enThreadStatus = CAM_THREAD_STATUS_EXIT ;
goto LABEL_EXIT_WITH_RELEASE_BUFFER ;
}
while(1)
{
/* 更新统计信息 */
if(0 != g_stCAMWorkarea.uiUpdateStatistics)
{
g_stCAMWorkarea.uiUpdateStatistics = 0 ;
__RCE_CAM_UpdateStatistics() ;
}
/* 判断线程是否需要退出 */
if(1 == g_stCAMWorkarea.uiStopThread)
{
g_stCAMWorkarea.enThreadStatus = CAM_THREAD_STATUS_END ;
goto LABEL_EXIT_WITH_CLOSE_CAMERA ;
}
usleep(1000) ;
}
LABEL_EXIT_WITH_CLOSE_CAMERA:
objRceCam.RCE_CAM_StopCamera() ;
LABEL_EXIT_WITH_RELEASE_BUFFER :
objRceCam.RCE_CAM_ReleaseBuffer() ;
LABEL_EXIT_WITH_RELEASE_CAMERA :
objRceCam.RCE_CAM_ReleaseCamera() ;
LABEL_EXIT_WITH_STOP_CAMERA_MANAGER :
objRceCam.RCE_CAM_StopCameraManager() ;
LABEL_EXIT_WITH_NO_PROCESS :
pthread_exit(NULL);
}
4. 编译
注意,编译之前需要先安装libcamera驱动:
sudo apt install -y libcamera-dev libepoxy-dev libjpeg-dev libtiff5-dev libpng-dev
然后使用g++编译即可(下面是编译一个.o文件。取消-c,以得到一个可执行文件,但是需要把线程函数改成main函数)。
CPP_TOOL = g++
CPP_CAM_SO_FILES := /usr/lib/aarch64-linux-gnu/libboost_program_options.so.1.74.0
CPP_CAM_SO_FILES += /usr/lib/aarch64-linux-gnu/libcamera-base.so
CPP_CAM_SO_FILES += /usr/lib/aarch64-linux-gnu/libcamera.so
CPP_CAM_FLAGS := -I/usr/include/libcamera/libcamera/
CPP_CAM_FLAGS += -I/usr/include/libcamera/
CPP_CAM_FLAGS += -std=c++17
CPP_CAM_FLAGS += -lpthread
@$(CPP_TOOL) $(CPP_CAM_FLAGS) -I. -c RCE_CAM.cpp -o RCE_CAM.o