树莓派学习专题<15>:树莓派5:使用libcamera驱动获取摄像头图像

0. 项目代码

可以从下面获取到项目中的代码。
https://github.com/cdsmakc/h264_codec_base_rpi4b_rpi5_rv1106_visual_studio.git

1. 背景

之前的系列文章,都是在树莓派4上运行的。这两天又买了个树莓派5来测试。满以为重新编译一下就可以运行,结果发现存在很多问题:

  1. 树莓派5已经不支持硬件编码了,所以只能使用x264编码。
  2. V4L2在树莓派5上支持不好,之前的代码,VIDIOC_S_PARM命令和VIDIOC_G_PARM命令,在树莓派5上都报错。更关键的是,VIDIOC_STREAMON命令也无法启动流,报告参数错误。翻了树莓派的论坛,官方的回答是,强烈建议使用libcamera驱动替代V4L2驱动。
  3. 树莓派官方提供了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 ;
}

大致流程:

  1. 先通过方法pobjCamera->generateConfiguration来生成一个默认的摄像头配置。
  2. 通过方法**pobjCameraConfig->at(0)**获取一条流的默认配置。
  3. 配置流。设定图像分辨率(宽、高),指定输出格式为YUV420。
  4. 使用方法**pobjCameraConfig->validate() ;**验证配置。根据文档介绍,因为我们的配置,摄像头未必正好能支持,该方法会修改这些配置为最接近的配置。但是实际上,常见的分辨率应该都没有问题,可以完全匹配。
  5. 使用方法**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 ;
}

大致流程:

  1. 先创建一个FrameBufferAllocator对象;
  2. 使用上述对象为流创建缓存:pobjAllocator->allocate(Cfg.stream())
  3. 使用方法pobjCamera->createRequest()创建请求。所谓请求,是可以排队到libcamera驱动中的,每个请求带着一个buffer。请求被排队后,若图像获取成功了,会使用用户提供的回调函数来处理这些请求,并从请求附带的buffer中获取图像数据。上面代码中,申请了多少缓存(我这里是申请到了4个缓存,足够了;应该是可以改的),就创建多少请求。
  4. 使用方法**pobjRequest->addBuffer(Stream, Buffer.get())**将buffer加入到一个request中。
  5. 将所有的请求(我这里是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

### 树莓派5摄像头的相关信息 #### 硬件兼容性 树莓派5支持多种类型的摄像头模块,包括官方推出的Raspberry Pi Camera Module系列以及其他第三方USB摄像头设备。对于官方摄像头模块而言,其硬件接口基于CSI(Camera Serial Interface),而大多数第三方摄像头则通过USB连接至树莓派[^1]。 #### 驆动安装 针对官方摄像头模块,树莓派驱动主要依赖于V4L2(Video4Linux2)和libcamera生态。具体步骤如下: - 更新RPi固件并启用摄像头功能:运行`sudo apt-get update && sudo apt-get upgrade`以确保系统处于最新状态;随后利用`raspi-config`工具,在菜单中选择“Interfacing Options -> Camera”,将其开启,并重启设备[^2]。 - 对于USB摄像头,通常无需额外配置即可被识别为视频捕获设备,但仍需确认内核已加载相应的UVC(USB Video Class)驱动程序[^3]。 #### 使用方法 一旦完成了上述准备工作,则可以借助预装的应用或者编写脚本来操控摄像头采集静态图片或动态影像文件。例如,调用命令行实用程序`raspistill`用于拍摄照片,而`raspivid`负责录制短视频片段。如果希望进一步扩展应用范围比如实时流媒体传输,那么可以通过组合FFmpeg与Netcat(`nc`)达成目的——前者负责编码处理素材源码流转化成适合网络传播的形式后者构建起简易服务器以便客户端访问接收数据包[^4]。 ```bash # 启动摄像头服务 sudo modprobe bcm2835_v4l2 # 测试拍照功能 raspistill -o test.jpg # 开始录像并将输出重定向给netcat监听指定端口等待远程连接请求到来之后发送过去 raspivid -t 0 -h 720 -w 1080 -fps 25 -b 2000000 -o - | nc -lkv 8089 ``` 以上代码展示了如何激活BCM芯片上的虚拟摄像机节点供后续操作使用、执行基本抓拍动作保存成果到本地磁盘以及搭建简单的RTSP推送机制向局域网内的其他主机分享现场画面情景模拟过程。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值