简介:一份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 到 QVideoWidget:QT5 摄像头显示链路先跑通
2.1 技术路线对比:为什么默认选 QCamera
动手前先做一个判断:摄像头数据从内核到界面的路有很多条,选错后面所有截图、保存逻辑都要跟着返工。常见三条路线是 QCamera、V4L2 ioctl 直读和 OpenCV VideoCapture。
| 路线 | 帧来源 | 跨平台 | 集成成本 | 适合场景 |
|---|---|---|---|---|
| QCamera + QVideoWidget | Qt 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 QList<QCameraInfo> 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 通常走 GStreamer,OV5647 这类 CSI 摄像头预览黑屏,先确认 v4l2 插件在不在:
gst-inspect-1.0 v4l2src ls -l /dev/video0 groups提示:gst-inspect 找不到 v4l2src,装 gstreamer1.0-plugins-good;ls 显示 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 是按位 flags,CaptureToFile 只落盘,CaptureToBuffer 给内存帧,两者可以同时打开;如果界面上只想显示一个"拍照成功"缩略图,开 CaptureToBuffer 读内存帧就够了,不用多写一次磁盘。
四个信号容易混,按触发顺序排一张表:
| 信号 | 触发时机 | 携带内容 | 用途 |
|---|---|---|---|
| imageCaptured | 一帧预览图解码完成 | QImage | 界面瞬时缩略水印 |
| imageAvailable | 编码后数据进内存缓冲区 | QVideoFrame | CaptureToBuffer 模式读帧 |
| imageSaved | 文件写入完成 | QString 路径 | 相册列表刷新 |
| error | 以上任一步失败 | Error 码与说明 | 排错入口 |
注意:imageCaptured 携带的 QImage 在多数驱动上是预览分辨率,可能是 320×240,不是成品,别拿它当存档原图;imageAvailable 里的 QVideoFrame 才是编码后的完整数据。
3.3 按下截图按钮:capture 返回值和真实时序
按钮处理函数里要做两次前置检查,再调 capture:
void 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/jpeg,image/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_cast<int>(error) << message; // 常见是设备已被释放、驱动不支持当前分辨率请求, // 在状态栏展示 message 比只写“拍照失败”有用。 }不要把这个槽里的 message 想象得很美好,有些后端只给一句"Camera error",所以真正排查时还是回抓 error 的类型和触发时间点。
5. QVideoProbe 抓原始帧:把截图管线升级成视频分析入口
5.1 在采集线程上挂 QVideoProbe 的三个改动点
QCameraImageCapture 的输出经过编码器,做逐帧视频分析不划算;更常见做法是 QVideoProbe 直接从采集管线复制原始帧。改动点有三个。
第一,创建 probe 并绑定相机。setSource 返回 false 表示当前后端不支持探针,只能退回 CaptureToBuffer:
m_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 降到 640x480,NV12 转换压力会小一整档。这个统计点也是后面做视频分析时判断瓶颈在采集侧还是算法侧的依据。
最后补一个编译配置:使用 QVideoWidget、QVideoProbe 时 .pro 文件里要同时写QT += multimedia multimediawidgets,只写 multimedia 会导致 QVideoWidget 和 QVideoProbe 的符号链接失败,报出一堆 undefined reference,这是 QT5 摄像头预览工程里最常被忽略的第一步。
本文还有配套的精品资源,点击获取