ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

QT5摄像头开发:预览、截图与保存的完整管线

QT5摄像头开发:预览、截图与保存的完整管线 简介一份QT5摄像头调用与截图保存项目资源面向需要用Qt Widgets开发多媒体或监控应用的开发者。资源以QtCamerTestOfLT完整工程为主线涵盖项目配置、界面设计与功能实现等完整源码结构聚焦QCamera、QCameraViewfinder、QCameraImageCapture等模块的联合使用可帮助读者掌握摄像头视频流显示、帧捕获、图像保存及异常处理等关键环节。压缩包共21个文件以cpp/h源码、ui界面文件、pro工程文件为主其中cpp/h实现核心逻辑ui负责可视化布局pro管理编译依赖另含exe可执行程序及debug/release编译中间文件整体仅1.31MB方便快速启动与对照学习。已有2258人学习下载。读者可获得一套可直接运行的摄像头演示工程通过cameraform源码与界面布局理解Qt图像处理与本地存储流程结合项目组织方式也可学习Qt Widgets应用从设计到编译的完整过程适合刚接触Qt多媒体编程的开发者参考。1. 把显示摄像头和截下这帧拆开QT5 才不难在 QT5 里让摄像头画面出现在窗口上是最容易的一步QCamera 对象 start()画面就进 QVideoWidget 了。真正劝退的是截图按钮按下后拿到一张黑图。原因是 QVideoWidget 渲染的是驱动直通的视频合成层任何窗口截屏都读不到它Snipaste 这类通用截图工具在这里只会截到黑块。标题里的显示视频、截图、保存照片因此是三件事对应 QT5 多媒体里三条独立取流链路QCameraViewfinder 刷新预览QCameraImageCapture 从传感器抓单帧QImageEncoderSettings 决定照片格式与落盘参数。这篇文章面向写上位机、树莓派摄像头工具或桌面采集器的开发目标是一条最小可编译的完整管线把驱动兼容、编码格式和命名策略这些事后难改的决定提前理顺。2. 从 QCamera 到 QVideoWidgetQT5 摄像头显示链路先跑通2.1 技术路线对比为什么默认选 QCamera动手前先做一个判断摄像头数据从内核到界面的路有很多条选错后面所有截图、保存逻辑都要跟着返工。常见三条路线是 QCamera、V4L2 ioctl 直读和 OpenCV VideoCapture。路线帧来源跨平台集成成本适合场景QCamera QVideoWidgetQt Multimedia 后端Windows / Linux低预览、定时抓帧、快速出界面Linux V4L2 ioctl 直读/dev/videoX仅 Linux中嵌入式、无窗口环境OpenCV VideoCapture平台原生采集 API全平台高已有 OpenCV 依赖的算法工程QCamera 并不是性能最好的方案却是和 QT5 事件循环、窗口系统整合最干净的一个。V4L2 直读能拿最大控制权但要多写 mmap、ioctl 循环和像素格式协商做出来的东西还绑死 Linux。OpenCV 把视频采集和图像算法绑在一起几百 MB 依赖换来的只是又一个 VideoCapture 封装如果最终产品是 QT5 程序摄像头这块尽量不要引第三方。后面真需要做视频分析时QT5 自带的 QVideoProbe 也能在不动界面链路的前提下拿到原始帧这一点在第 5 章展开。2.2 枚举型号怎么在 QT5 里选对摄像头多摄像头是常见需求UVC 摄像头、树莓派 OV5647 CSI 模块、USB 采集卡往往同时挂在开发机上。QT5 的枚举接口对设备模型做了统一#include QCameraInfo #include QCamera #include QVideoWidget // 第一次调用 availableCameras 才会触发多媒体后端装载 // 耗时通常在 100~300ms不要放在构造函数的同步路径里。 const QListQCameraInfo cameras QCameraInfo::availableCameras(); if (cameras.isEmpty()) { // Linux 上可能是 /dev/video* 无权限读取Windows 上 // 可能是后端插件没装全很少是硬件真的不存在。 return; } QCameraInfo selected QCameraInfo::defaultCamera(); for (const QCameraInfo info : cameras) { qDebug() device: info.deviceName() desc: info.description(); // description 是给人看的名称不同驱动差异很大 // 只能用来做弱匹配比如 contains(USB)。 if (info.description().contains(USB)) { selected info; } } QCamera *camera new QCamera(selected, this); QVideoWidget *viewfinder new QVideoWidget(this); viewfinder-setAspectRatioMode(Qt::KeepAspectRatio); camera-setViewfinder(viewfinder); viewfinder-resize(640, 480); camera-start();availableCameras 的返回值Linux 来自 v4l2 扫描 /dev/video*Windows 来自 DirectShow 枚举。deviceName 标识内核设备路径热插拔后同一个物理摄像头的标识可能变化所以配置界面里记住 description 更实用defaultCamera 在系统只有单个摄像头时返回值就是它但在双摄采集卡上把它当唯一真相会选错多数代码里还是要靠第一次枚举结果做记忆。start() 是异步动作不可能用返回值判断成功黑屏问题全都得从 error 信号里查。接一个 lambda 把错误打出来connect(camera, QCamera::error, this, [](QCamera::Error code) { qWarning() camera error: code camera-errorString(); });QCamera::Error 的常见值是 CameraError 与 ServiceMissingError前者多是设备忙或驱动异常后者是 Multimedia 后端没起来。这一段代码要在 start() 之前连上否则启动早期的报错会丢。2.3 预览黑屏的常见病根权限、后端插件与设备独占画面不出的排查有固定顺序按出现频率排三个Windows 10/11 的摄像头隐私开关。现象是枚举得到设备、start() 不报错、界面黑一片。Win10 相机无法调用摄像头但 QQ 能用的现象也常见去设置 隐私 相机里确认允许桌面应用访问相机是打开的修改后要完全退出程序再启动。Linux / 树莓派后端插件缺失。QT5 在 Linux 通常走 GStreamerOV5647 这类 CSI 摄像头预览黑屏先确认 v4l2 插件在不在gst-inspect-1.0 v4l2src ls -l /dev/video0 groups提示gst-inspect 找不到 v4l2src装 gstreamer1.0-plugins-goodls 显示 crw------- 则把当前用户加入 video 组并重新登录。 /dev/video0 被 Python 的 picamera 库独占时QT5 会一直拿不到流先释放占用进程。设备被其他应用独占。海康、大华的调试工具在后台跑时UVC 通道会被锁住QCamera 启动后状态停在 LoadedState 而不是 ActiveState。这种情况在 Qt 客户端里定时轮询 state() 能发现不必重启系统关掉占用进程即可恢复。这三类问题不解决后面的截图和保存全部无从谈起。先把第 2 章这段链路跑出稳定画面再做抓帧不迟。3. 让截图生效把 QCameraImageCapture 挂进 QT5 摄像头预览的取帧链3.1 为什么从 QVideoWidget 上截屏必然失败QCameraViewfinder 也好直接 setViewfinder 的 QVideoWidget 也好它们只是把采集到的视频帧交给窗口系统显示帧数据不会以 QImage 的形式挂在 widget 上。Windows 上视频渲染可能走硬件叠加层Linux 上又可能是 GStreamer 的 overlay 窗口普通 QWidget::grab 根本读不到换成任何屏幕级截图工具结果都一样。这个限制和 QT5 没有关系是视频渲染与桌面合成器的实现边界。正确的截图姿势是另起一条抓帧通道QCameraImageCapture 注册进同一个摄影机服务直接从采集传感器要一帧原始画面不走预览管线。两个通道相互独立因此点击截图时不需要窗口处于前台反过来少数节电驱动会在窗口完全隐藏时停掉预览流那是驱动对预览的优化不影响抓帧通道本身但会让部分后端在恢复时短暂丢帧。3.2 设置 QCameraImageCapture 并理解四个信号初始化代码只需要几行关键放在信号接线上m_capture new QCameraImageCapture(camera, this); m_capture-setCaptureDestination( QCameraImageCapture::CaptureToFile); // 文件真正写盘完成后回调 connect(m_capture, QCameraImageCapture::imageSaved, this, MainWindow::onImageSaved); // 抓帧任意环节出错都走 error connect(m_capture, QCameraImageCapture::error, this, MainWindow::onCaptureError);这个对象必须在 camera 构造之后再建顺序反了会导致 capture service 拿不到相机。captureDestination 是按位 flagsCaptureToFile 只落盘CaptureToBuffer 给内存帧两者可以同时打开如果界面上只想显示一个拍照成功缩略图开 CaptureToBuffer 读内存帧就够了不用多写一次磁盘。四个信号容易混按触发顺序排一张表信号触发时机携带内容用途imageCaptured一帧预览图解码完成QImage界面瞬时缩略水印imageAvailable编码后数据进内存缓冲区QVideoFrameCaptureToBuffer 模式读帧imageSaved文件写入完成QString 路径相册列表刷新error以上任一步失败Error 码与说明排错入口注意imageCaptured 携带的 QImage 在多数驱动上是预览分辨率可能是 320×240不是成品别拿它当存档原图imageAvailable 里的 QVideoFrame 才是编码后的完整数据。3.3 按下截图按钮capture 返回值和真实时序按钮处理函数里要做两次前置检查再调 capturevoid MainWindow::grabOneFrame() { if (camera-state() ! QCamera::ActiveState) { statusBar()-showMessage(camera not active); return; } if (!m_capture-isAvailable()) { // 摄像头服务没给抓帧通道分配资源常见于 // 设备被独占或后端插件不支持抓帧。 statusBar()-showMessage(m_capture-errorString()); return; } const QString fileName nextPhotoPath(); // 第 4 章给出实现 const int id m_capture-capture(fileName); if (id -1) { // -1 表示本次请求被直接拒绝而不是排队等待 // 同一时刻只允许一个在途 capture。 statusBar()-showMessage(m_capture-errorString()); } }capture() 返回的 id 只代表请求被接受不表示照片已经生成真正的成功信号是 imageSaved。连续点击按钮时如果上一帧还没走到 imageSaved第二次 capture 经常直接返回 -1所以连拍不要用 QTimer 设定时器硬顶更稳妥的做法是在 onImageSaved 槽里决定下一次抓帧天然串行。fileName 的后缀要和编码格式对应codec 是 image/jpeg 就写 .jpg不一致时部分后端不报错但文件头是错的最后以 QImageReader::canRead 的识别结果为准。4. 保存照片到本地编码参数、时间戳命名与路径组装4.1 编码设置分辨率与压缩质量在抓帧前定好QMQLCEfficient——不这里要写清楚QImageEncoderSettings 是独立配置对象改它不会影响正在进行的预览流。通常在初始化截图通道时一次性配好#include QImageEncoderSettings QImageEncoderSettings settings; settings.setCodec(image/jpeg); settings.setResolution(1920, 1080); settings.setQuality(QtMultimedia::VeryHighQuality); m_capture-setEncodingSettings(settings);setResolution 作用是拍照分辨率而不是预览流分辨率很多 UVC 摄像头预览 640x480、拍照能到 1920x1080两个参数互不影响。少数驱动不支持改分辨率设置会被静默忽略所以存档文件实际分辨率要以落盘后的文件为准界面不要写死已保存 1080p。codec 填的是 MIME 名称不是扩展名image/jpeg 对应 jpg/jpegimage/png 对应 png填错会在 capture 时通过 error 信号返回。质量枚举在 Qt5 的 QtMultimedia 命名空间下个别老版本编译器报找不到时把前缀换成 QMultimedia 再看头文件里的实际声明。格式选择用一张表说清格式场景注意点JPEG摄像头照片、长期归档有损二次编辑会明显降质PNG需要无损、叠文字、透明背景体积通常为 JPEG 的 3 倍以上摄像头拍照默认选 JPEG体积和画质平衡最好只有要叠加图层做标注导出时才切 PNG。4.2 用 QDateTime 时间戳组装 QString 路径保存路径最容易踩的是以为工作目录是 exe 目录。Windows 快捷方式可以指定任意启动目录用相对路径会把照片散落到未知位置所以统一用 exe 所在目录做根#include QDir #include QDateTime QString MainWindow::nextPhotoPath() { const QString dir QDir::cleanPath( QCoreApplication::applicationDirPath() QDir::separator() shots); if (!QDir(dir).exists() !QDir().mkpath(dir)) { // 只读目录、磁盘满了都会让 mkpath 失败 // 失败后不要继续拼文件名直接返回空。 statusBar()-showMessage(create dir failed: dir); return QString(); } const QString stamp QDateTime::currentDateTime() .toString(yyyyMMdd_hhmmss_zzz); return dir QDir::separator() stamp .jpg; }QDir::separator 在 Windows 上是反斜杠、Linux 上是斜杠字符串拼接时不要写死/统一走这个接口。yyyyMMdd_hhmmss_zzz 精确到毫秒单机连拍足够唯一多进程同时抓同一个目录时再追加一个随机数后缀更保险。QString 在中文路径下没有历史包袱QT5 源码文件保存成 UTF-8 后QDir 输出的地址显示和实际文件系统一致真遇到乱码先查 .cpp 文件编码不要怀疑 API。4.3 imageSaved 之后做校验不在 imageCaptured 里写盘onImageSaved 槽里不要只顾着弹提示顺手做一次文件校验把空文件和格式损坏拦在界面层#include QFileInfo #include QImageReader #include QPixmap void MainWindow::onImageSaved(int id, const QString fileName) { QFileInfo info(fileName); if (!info.exists() || info.size() 0) { // 个别后端先建空文件再异步写入槽被触发时 // 磁盘还没写完不能立刻当成品处理。 statusBar()-showMessage(file not ready: fileName); return; } statusBar()-showMessage(saved: fileName); QImageReader reader(fileName); if (reader.canRead()) { m_lastPhoto-setPixmap( QPixmap::fromImage(reader.read())); } }这里用 QImageReader 重新读一次文件头等于顺带完成了格式校验比直接载入 QPixmap 更稳。onImageSaved 的 id 参数和 capture 返回的 id 是一一对应的连拍场景下用 id 去配对文件路径避免界面只刷新最后一张造成的错位。错误处理单独走void MainWindow::onCaptureError( int id, QCameraImageCapture::Error error, const QString message) { qWarning() capture error: id static_castint(error) message; // 常见是设备已被释放、驱动不支持当前分辨率请求 // 在状态栏展示 message 比只写“拍照失败”有用。 }不要把这个槽里的 message 想象得很美好有些后端只给一句Camera error所以真正排查时还是回抓 error 的类型和触发时间点。5. QVideoProbe 抓原始帧把截图管线升级成视频分析入口5.1 在采集线程上挂 QVideoProbe 的三个改动点QCameraImageCapture 的输出经过编码器做逐帧视频分析不划算更常见做法是 QVideoProbe 直接从采集管线复制原始帧。改动点有三个。第一创建 probe 并绑定相机。setSource 返回 false 表示当前后端不支持探针只能退回 CaptureToBufferm_probe new QVideoProbe(this); if (!m_probe-setSource(camera)) { qWarning() probe not supported; } connect(m_probe, QVideoProbe::videoFrameProbed, this, MainWindow::onFrameProbed);第二槽里按帧格式取像素。NV12 是 UVC 驱动最常见的输出格式Y 平面和 UV 平面的 stride 要分开拿不能简单按宽度换算void MainWindow::onFrameProbed(const QVideoFrame frame) { QVideoFrame f frame; // 显式共享生命周期安全 if (!f.map(QAbstractVideoBuffer::ReadOnly)) return; if (f.pixelFormat() QVideoFrame::Format_NV12) { uchar *y f.bits(0); // Y 平面 uchar *uv f.bits(1); // UV 交错平面 int yStride f.bytesPerLine(0); int uvStride f.bytesPerLine(1); // 注意uvStride 不等于 yStride / 2驱动会按 // 对齐规则给 UV 额外补行转换时两个 stride 都要用。 } f.unmap(); }第三线程与频率控制。videoFrameProbed 跑在采集线程槽里不能 new QWidget 或直接更新 UI正确做法是把 Y/UV 数据拷到自己的环形缓冲区回发一个普通信号给 UI 线程处理。处理耗时超过单帧周期就会掉帧需要按目标帧率在 probe 里做降采样或隔帧分析否则视频分析结果永远落后于画面。5.2 验证 probe 是否真的在喂帧最直接的验证是在 probe 槽里统计每秒回调次数int m_count 0; qint64 m_lastStat 0; void MainWindow::onFrameProbed(const QVideoFrame frame) { m_count; const qint64 now QDateTime::currentMSecsSinceEpoch(); if (now - m_lastStat 1000) { qDebug() probe fps: m_count; m_count 0; m_lastStat now; } }fps 一直是 0说明 probe 没绑定成功或预览流没起来回查 setSource 返回值fps 只有个位数先看采集分辨率是不是设成了 4K分析场景把 QCameraViewfinderSettings 降到 640x480NV12 转换压力会小一整档。这个统计点也是后面做视频分析时判断瓶颈在采集侧还是算法侧的依据。最后补一个编译配置使用 QVideoWidget、QVideoProbe 时 .pro 文件里要同时写QT multimedia multimediawidgets只写 multimedia 会导致 QVideoWidget 和 QVideoProbe 的符号链接失败报出一堆 undefined reference这是 QT5 摄像头预览工程里最常被忽略的第一步。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进