ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Qt5摄像头抓拍实战:QCamera与QVideoWidget的完整实现

Qt5摄像头抓拍实战:QCamera与QVideoWidget的完整实现 简介一份基于QT5与C的摄像头调用与截图抓拍示例工程适合刚接触Qt多媒体开发的初学者也适合需快速集成相机预览功能的桌面应用开发者。工程完整演示了从项目配置到界面布局的实现路径pro文件中引入widgets、multimedia、multimediawidgets模块自定义QWidget子类中集成QCamera、QCameraViewfinder与QCameraImageCapture完成摄像头初始化、实时取景、信号槽连接和图像捕获并附带摄像头错误处理与图像显示回调用法UI文件展示了如何布置触发按钮和截图预览标签读者可借此理解QT5多媒体模块的完整调用流程。压缩包共6个文件包括2个cpp源文件、1个头文件、1个UI布局文件、1个pro工程文件与1个user配置文件整体仅5KB结构精简而完整适合直接导入工程查看运行也便于在此基础上扩展拍照保存、分辨率设置等功能。已有764人学习下载对希望快速上手QT5摄像头开发的读者来说是一份可运行的入门级参考范例。1. 为什么用Qt5做摄像头抓拍一个比OpenCV更贴近桌面的选择很多人一提到摄像头采集第一反应是OpenCV。可如果只是做一个点击按钮、500毫秒内拿到一张截图的桌面小工具为它带上一整个CV库显得有点重。Qt5自带的QCamera、QCameraImageCapture两个类配合QVideoWidget做预览就能把“打开某个摄像头、看到取景画面、按一下存一张PNG”整个流程闭环。这套方案最大的特点是不引第三方库摄像头状态、窗口嵌入和文件保存全部走Qt自己的事件循环和你的界面代码天然同线程。下面按这条链路展开覆盖类职责、最小可运行工程、参数边界以及Linux下最常见的V4L2和CSI摄像头踩坑点。适合已经把Qt工程跑起来、需要往工具里加“拍照登记”“设备调试”“画面抓取”这类功能的工程师。2. 先理清Qt5的摄像头采集链路设备枚举、取景器与抓拍对象的划分2.1 Multimedia模块是入口pro文件先把依赖挂上Qt5的摄像头API不在widgets模块里属于multimedia多媒体框架。写qmake工程时最低配置是这样QT core gui multimedia multimediawidgets greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET camera_snapshot TEMPLATE app SOURCES main.cpp MainWindow.cpp HEADERS MainWindow.hmultimedia提供QCamera、QCameraImageCapture这些核心类multimediawidgets提供QVideoWidget这类可以直接嵌入布局的渲染控件。两个模块缺一个都会编译失败缺后者最常见报错是找不到QVideoWidget头文件。CMake用户对应写find_package(Qt5 COMPONENTS Multimedia MultimediaWidgets REQUIRED)链接时把两个target都带上。如果你的项目以后要迁到Qt6这里有一个提前要踩的坑Qt6把QCameraInfo并入了QMediaDevices枚举方式从availableCameras()变成了QMediaDevices::videoInputs()。老代码到Qt6会直接编译不过建议在封装层留一个设备枚举接口别让业务代码到处直接调QCameraInfo。2.2 三个核心类各管一段QCameraInfo、QCamera与QCameraImageCapture类职责关键API/信号QCameraInfo枚举系统里的摄像头提供描述信息和默认设备availableCameras()、isNull()、description()QCamera控制摄像头的开关状态持有取景器和错误状态机start()、stop()、stateChanged()、error信号QCameraImageCapture对当前视频流做静态抓拍负责编码和写文件capture()、imageCaptured()、imageSaved()初学者最常见的误解是QCamera自己就能拍照。实际上QCamera只管设备生命周期从Unloaded到Loaded再到ActiveState每一步都要走异步状态切换。“按一下拍一张”这个动作是QCameraImageCapture干的它挂在QCamera对象上共享同一个设备句柄。这种拆分的好处是预览和拍照互不阻塞。你可以在预览线程一直跑的情况下随时发起一次capture()摄像头硬件会用当前帧或者重新曝光出一帧抓完后继续回到预览流。如果这两个职责不分开截图逻辑就得自己管理“先停预览、再抓一帧、再恢复预览”的状态界面会明显卡顿。2.3 取景器不是可有可无QVideoWidget与QAbstractVideoSurface的分工QCamera不负责画画面。视频帧从驱动出来之后需要一个“投递目标”。最简单的目标就是QVideoWidgetm_camera-setViewfinder(m_viewfinder);这一行必须在start()之前调用。m_viewfinder是MainWindow里new出来的QVideoWidget放入布局之后就会显示实时画面。QVideoWidget内部实现的是QAbstractVideoSurface接口。摄像头把每一帧送给这个surfacesurface再交给平台相关的渲染路径画到窗口上。如果你想要的不只是“看”而是“拿到每一帧原始数据做处理”就不能用QVideoWidget而是要实现自己的QAbstractVideoSurface子类把相机绑定到自定义surface上。这个思路是第五章进阶内容的入口这里先记住setViewfinder不仅仅是个显示开关它决定了视频帧的最终去向。3. 最小可运行的摄像头抓拍工程从启动到保存单张PNG3.1 工程骨架与信号连接先建一个干净的主窗口类把相机相关的三个对象和UI控件都声明出来// MainWindow.h #pragma once #include QCamera #include QCameraImageCapture #include QCameraInfo #include QMainWindow #include QVideoWidget QT_BEGIN_NAMESPACE class QComboBox; class QPushButton; QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); private slots: void startCamera(); void stopCamera(); void captureSnapshot(); void onImageSaved(int id, const QString fileName); private: QCamera *m_camera nullptr; QCameraImageCapture *m_imageCapture nullptr; QVideoWidget *m_viewfinder nullptr; QComboBox *m_cameraBox nullptr; QPushButton *m_startBtn nullptr; QPushButton *m_stopBtn nullptr; QPushButton *m_captureBtn nullptr; };构造函数里创建控件和布局并把按钮的clicked信号连到三个槽函数。这里不贴完整布局代码布局顺序后面单独说。需要留意的一点QCameraInfo要放进QVariant里存到下拉框必须在包含QCameraInfo头文件的源文件里写Q_DECLARE_METATYPE(QCameraInfo)否则QVariant::fromValue(info)编译不过。3.2 设备枚举把系统摄像头填进下拉框void MainWindow::refreshCameraList() { m_cameraBox-clear(); const QListQCameraInfo cameras QCameraInfo::availableCameras(); for (const QCameraInfo cam : cameras) { QString name cam.description(); if (name.isEmpty()) name cam.deviceName(); // Linux 下形如 /dev/video0 m_cameraBox-addItem(name, QVariant::fromValue(cam)); } }description()返回的是厂商提供的设备名Windows上通常带型号信息Linux上经常是空字符串这时退回用deviceName()它在Linux下就是/dev/videoN路径调试时一眼能看出选的是哪个节点。QCameraInfo只是当前时刻的设备快照不要长期保存并在摄像头拔插后继续使用它枚举结果要在每次打开前重新获取。3.3 打开摄像头state、error信号与start时机void MainWindow::startCamera() { if (m_camera m_camera-state() QCamera::ActiveState) return; if (!m_camera) { QCameraInfo info m_cameraBox-currentData().valueQCameraInfo(); if (info.isNull()) return; m_camera new QCamera(info, this); m_camera-setViewfinder(m_viewfinder); m_imageCapture new QCameraImageCapture(m_camera, this); m_imageCapture-setCaptureDestination( QCameraImageCapture::CaptureToFile); connect(m_camera, QOverloadQCamera::Error::of(QCamera::error), this, [this](QCamera::Error) { statusBar()-showMessage(m_camera-errorString()); }); connect(m_imageCapture, QCameraImageCapture::imageSaved, this, MainWindow::onImageSaved); } m_camera-start(); }QCamera::error是重载信号Qt5编译器要求用QOverload显式指定QCamera::Error参数才能正确连接。errorString()在设备被占用、驱动异常时能给出比“打开失败”更具体的描述建议至少打印到状态栏。setViewfinder必须发生在start之前否则摄像头设备起来了帧没有接收方界面上只有黑屏看起来像没打开成功。start()本身是异步的调用后摄像头要经过加载驱动、协商格式、启动数据流三个阶段才进入ActiveState。不要在start()后面同步调用capture()这个时候设备还没准备好抓拍请求会被静默丢弃或报InvalidRequestError。正确做法是在UI层等待stateChanged信号或者干脆在start之后用QTimer::singleShot等几百毫秒。3.4 截图抓拍的实现capture()、编码与文件名生成这是整个功能的核心代码很短但参数细节不少void MainWindow::captureSnapshot() { if (!m_imageCapture || !m_camera || m_camera-state() ! QCamera::ActiveState) { return; } QString dir QDir::home().filePath(Snapshots); if (!QDir().mkpath(dir)) { qWarning() create dir failed dir; return; } QString fileName QDir(dir).filePath(QString(shot_%1.png) .arg(QDateTime::currentDateTime().toString(yyyyMMdd_HHmmss_zzz))); m_imageCapture-capture(fileName); }capture()接受一个完整文件路径作为第一个参数。路径里的目录必须已经存在QDir::home().filePath(Snapshots)只是拼接路径字符串不会自动建目录所以先用mkpath确保目录存在。不少人在网上搜“qt5 qstring file not find”大部分情况不是路径拼错而是目录没创建。文件名用时间戳加毫秒toString(yyyyMMdd_HHmmss_zzz)里的zzz是毫秒避免连续点击按钮时文件名覆盖。扩展名用.pngQt会根据扩展名选择编码器PNG无损且速度快抓拍登记照片够用要更小的文件可以改成.jpg但第一次编码会慢一些。保存完成的信号处理void MainWindow::onImageSaved(int id, const QString fileName) { Q_UNUSED(id); statusBar()-showMessage(QString(已保存: %1).arg(fileName)); }imageSaved信号保证了文件已经完整写入磁盘在这个槽里做“已保存”提示不会出现文件还没落盘的情况。还有一个容易忽略的imageCaptured(int, QImage)信号它在按下快门后立刻返回一张预览图适合做界面上的瞬间回显但这张QImage的生命周期只在信号体内想留着用必须深拷贝。3.5 UI布局与状态提示布局上遵循一个原则QVideoWidget必须在布局里占一个可变空间按钮放在它下方。常见做法是QVBoxLayout *layout new QVBoxLayout; layout-addWidget(m_viewfinder, 1); // 预览区占满剩余空间 layout-addWidget(cameraRowLayout); // 下拉框按钮一行m_viewfinder创建后先resize(640, 480)再放进布局窗口初始尺寸不会太离谱。摄像头启动后取景画面会自动跟随设备输出的分辨率调整到控件的显示尺寸。状态提示统一走statusBar()-showMessage()不要用QMessageBox否则抓拍频繁时会弹窗打断连续操作。4. 摄像头参数、分辨率与常见坑从V4L2节点到热插拔4.1 先查设备支持的参数集而不是猜分辨率摄像头不是设置什么分辨率就给你什么分辨率。USB摄像头支持的格式和尺寸范围是固定的盲目请求一个不支持的组合设备会回退到默认值画面比例可能不对。正确流程是等摄像头进入LoadedState之后再枚举能力connect(m_camera, QCamera::stateChanged, this, [this](QCamera::State state) { if (state QCamera::LoadedState) { const auto sizes m_camera-supportedViewfinderResolutions(); qDebug() 取景分辨率: sizes; const auto codecs m_imageCapture-supportedImageCodecs(); qDebug() 抓拍编码: codecs; } });一定要在LoadedState之后查询。刚new出QCamera时设备还没加载这时候查supportedViewfinderResolutions返回的可能是空列表。支持的编码列表来自QCameraImageCapture而不是QCamera因为抓拍编码是抓拍器负责的事。设置分辨率用QCameraViewfinderSettingsQCameraViewfinderSettings fs; fs.setResolution(1280, 720); fs.setMinimumFrameRate(15.0); fs.setMaximumFrameRate(30.0); m_camera-setViewfinderSettings(fs);帧率是“最小值和最大值”的区间不是单一数值这是Qt5的API设计别按直觉传一个setFrameRate(30)没有这个方法。设备会在这个范围内根据曝光自动浮动光线暗时掉到15帧以下是正常现象。4.2 取景分辨率与抓拍分辨率是两回事取景分辨率控制的是预览流而抓拍出来的图片尺寸由设备的静态采集能力决定。很多摄像头的传感器可以输出比预览大得多的静态帧比如取景只跑640x480抓拍却能出1920x1080甚至更大。想让抓拍尺寸可控需要单独设置编码参数QImageEncoderSettings enc; enc.setCodec(image/png); enc.setResolution(1280, 720); m_imageCapture-setEncodingSettings(enc);setResolution请求的仍然是“期望值”。设备支持多大就输出多大不支持时会按就近原则回退。如果对抓拍尺寸有严格校验需求在imageSaved后读文件尺寸做检查而不是默认信任设备返回的数据。抓拍瞬间还有一个硬件级现象某些USB摄像头会暂停预览一两秒画面像冻结了一样这是摄像头在重新曝光大尺寸静态帧不是Qt线程卡住。工厂里做大分辨率质量检测时这个耗时直接决定节拍量产项目要提前评估。4.3 Linux下V4L2、驱动节点与权限问题Linux平台上Qt5的摄像头后端走V4L2枚举的就是/dev/videoN设备节点。排查问题时先用系统工具看设备状态ls -l /dev/video* v4l2-ctl --list-devices v4l2-ctl --device/dev/video0 --list-formats-ext很多摄像头会生成两个节点比如video0和video1其中一个是capture节点另一个是metadata或者ISP控制节点。QCameraInfo通常会正确过滤但碰到奇怪的设备时--list-devices能看出每个节点的capabilities。权限问题的典型表现是QCameraInfo能枚举到摄像头start之后立刻报CameraError。检查ls -l /dev/video0如果归属root:video当前用户不在video组里就没有读写权限。解决方式sudo usermod -aG video $USER重新登录后生效。树莓派OV5647这类CSI摄像头前提是驱动已经映射成V4L2节点。可以用libcamera-hello先验证传感器链路正常再确认系统里有/dev/video0这类节点如果跑的是纯libcamera不带V4L2兼容层的最小镜像QCameraInfo里什么都枚举不到。Jetson平台上同理nvarguscamerasrc那条CSI通路如果不落到标准V4L2节点Qt5这边就是看不见设备。现象原因处置availableCameras()为空驱动没加载、无V4L2节点查/dev/video*确认内核模块start后立刻CameraError设备被占用或权限不足关闭OBS/浏览器摄像头加video组保存路径没有文件目录不存在、编码器不支持mkpathchecked via supportedImageCodecs()画面黑屏但有流分辨率协商失败打印实际帧尺寸改小分辨率4.4 热插拔处理轮询设备列表是Qt5最稳的做法QCameraInfo没有设备插拔通知事件这是Qt5的接口设计缺口。Qt6新增的QMediaDevices有videoInputsChanged信号但Qt5只能轮询。工程上的常见做法是QTimer *poll new QTimer(this); connect(poll, QTimer::timeout, this, [this]() { const int oldCount m_cameraBox-count(); const int newCount QCameraInfo::availableCameras().count(); if (oldCount ! newCount) refreshCameraList(); }); poll-start(2000);两秒轮询一次足够不会产生可感知的开销。轮询后要同步做两件事如果摄像头被拔掉正在使用的QCamera会触发错误信号这时必须stop并deleteLater下次重新new一个QCamera如果插入新摄像头下拉框的currentIndex可能指向旧设备要复位到新列表的第一个有效项。别尝试复用同一个QCamera对象做热恢复它的内部状态在设备消失后是不可靠的。5. 从单张抓拍到连续帧处理QAbstractVideoSurface的进阶用法5.1 自定义视频表面接管每一帧抓拍模式解决的是“按一下出一张图”但智能车循迹、产线视觉检测这类场景需要的是“每一帧都不丢”。这时就不能依赖QCameraImageCapture了它走的是编码存盘路径连续调用每秒最多几帧。正确做法是把相机绑定到自定义的QAbstractVideoSurface子类class RawSurface : public QAbstractVideoSurface { Q_OBJECT public: QListQVideoFrame::PixelFormat supportedPixelFormats(QAbstractVideoBuffer::HandleType) const override { return {QVideoFrame::Format_RGB32, QVideoFrame::Format_ARGB32, QVideoFrame::Format_YUV420P}; } bool present(const QVideoFrame frame) override { QVideoFrame f frame; if (f.map(QAbstractVideoBuffer::ReadOnly)) { emit frameReady(f); // 把帧交给业务层 f.unmap(); } return true; } signals: void frameReady(const QVideoFrame frame); };使用时光是把m_camera-setViewfinder(surface)换成自定义对象其他逻辑不变。注意两点present()运行在Qt媒体框架的采集线程里不要在信号处理里做耗时算法否则采集线程被阻塞画面会掉帧frame的内存由surface持有信号出去之后下一帧可能复用同一块缓冲接收方必须拷走数据。5.2 帧到QImage的转换与验证方法QVideoFrame转QImage的核心是map()拿到内存指针再用QImage的浅拷贝构造包一层最后copy一份脱离帧缓冲QImage frameToImage(const QVideoFrame frame) { QVideoFrame f frame; if (!f.map(QAbstractVideoBuffer::ReadOnly)) return QImage(); QImage image; if (f.pixelFormat() QVideoFrame::Format_RGB32 || f.pixelFormat() QVideoFrame::Format_ARGB32) { image QImage(f.bits(), f.width(), f.height(), f.bytesPerLine(), f.pixelFormat() QVideoFrame::Format_RGB32 ? QImage::Format_RGB32 : QImage::Format_ARGB32); } f.unmap(); return image.copy(); }这段代码有两个细节值得说明。第一bytesPerLine()必须作为QImage的stride参数传入否则图像会斜切第二函数返回值最后做了一次copy()因为QImage的浅拷贝构造出来的对象还引用着frame内部缓冲frame释放后那块内存就作废了深拷贝出去才安全。验证摄像头是否真的按预期分辨率跑在present()里打印每帧的frame.width()、frame.height()和pixelFormat()连续统计一秒钟的调用次数就是实际帧率。如果发现协商的分辨率和实际帧尺寸不一致回到第四章查supportedViewfinderResolutions。像素格式是YUV420P且无法map到系统内存、只能拿GPU纹理端帧时建议用libyuv走I420转RGB再交给QImage::Format_RGB888比Qt原生隐式转换稳得多这句留给后面踩坑的人。本文还有配套的精品资源点击获取
返回列表