Qt实现调用相机进行拍照并进行图像处理

前言

"调用摄像头拍照并做图像处理"是很多桌面项目的标配需求:人脸签到、工牌识别、质检拍照。Qt 用 Qt Multimedia 模块负责跟摄像头打交道,图像处理则交给 QImage 完成。

但真正动手会发现几个绕不开的问题:Qt5 和 Qt6 的相机 API 完全不是一套 (Qt6 重写了整个多媒体栈);拍出来的照片可能是左右镜像的;QImage 的各种 Format 枚举让人眼花;在预览回调里直接处理大图会卡住界面。

本文走完"打开相机 → 实时预览 → 拍照保存 → 图像处理"全流程,重点讲清 Qt5/Qt6 差异,并给出灰度、二值化、Sobel 边缘检测的完整实现。

一、Qt Multimedia 的架构变迁

先说清这个"大分裂",否则你搜到的教程很可能跑不起来。

Qt5 以 QCamera 为中心,配 QCameraViewfinder 预览、QCameraImageCapture 拍照。Qt6 全部废弃,改用**会话(Session)**模型:

复制代码
QCamera ──setCamera()──> QMediaCaptureSession

                         ├── setVideoOutput()  ← 预览

                         └── setImageCapture() ← 拍照

这么做是因为 Qt5 把"设备"和"功能"耦合太紧,而会话模型让"一个相机 → 多个输出"变得自然。

Qt5 与 Qt6 API 对照表

功能 Qt5 Qt6
列出摄像头 QCameraInfo::availableCameras() QMediaDevices::videoInputs()
默认摄像头 QCameraInfo::defaultCamera() QMediaDevices::defaultVideoInput()
预览控件 QCameraViewfinder QVideoWidget
绑定预览 camera->setViewfinder(vf) session->setVideoOutput(vw)
拍照控制 QCameraImageCapture QImageCapture
绑定拍照 camera->setCaptureMode(...) session->setImageCapture(...)
保存到文件 capture->capture(path) capture->captureToFile(path)
错误信号 error(int,int,QString) errorOccurred(int,int,QString)

重要提醒 :Qt6 里 QVideoWidget 在 Qt6::MultimediaWidgets,而 QCamera/QMediaCaptureSession/QImageCapture 在 Qt6::Multimedia。两个都要 link,否则报"未定义的符号"。

二、环境准备

cmake 复制代码
cmake_minimum_required(VERSION 3.16)

project(CameraDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

set(CMAKE_AUTOMOC ON)



find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia MultimediaWidgets)

add_executable(CameraDemo main.cpp camerawidget.cpp camerawidget.h

               imageprocessor.cpp imageprocessor.h)

target_link_libraries(CameraDemo PRIVATE

    Qt6::Widgets Qt6::Multimedia Qt6::MultimediaWidgets)

qmake(Qt5)版本:

bash 复制代码
QT += core gui widgets multimedia multimediawidgets

CONFIG += c++17

TARGET = CameraDemo

SOURCES += main.cpp camerawidget.cpp imageprocessor.cpp

HEADERS += camerawidget.h imageprocessor.h

平台依赖:Windows 用 Media Foundation 后端一般开箱即用;Linux 需要 GStreamer 的 good/bad/ugly 插件组;Android 需要 android.permission.CAMERA。

macOS 最容易踩坑 :必须在 Info.plist 里加下面这一条,否则一调用相机就被系统直接杀掉。

xml 复制代码
<key>NSCameraUsageDescription</key>

<string>本应用需要访问摄像头以完成拍照</string>

三、拍照全流程:Qt6 实现

头文件

cpp 复制代码
// camerawidget.h

#include <QWidget>

#include <QImage>



QT_BEGIN_NAMESPACE

class QCamera; class QMediaCaptureSession; class QImageCapture;

class QVideoWidget; class QPushButton; class QLabel;

QT_END_NAMESPACE



class CameraWidget : public QWidget

{

    Q_OBJECT

public:

    explicit CameraWidget(QWidget *parent = nullptr);

    ~CameraWidget() override;



private slots:

    void startCamera();

    void stopCamera();

    void takePhoto();

    void onImageCaptured(int id, const QImage &preview);

    void onImageSaved(int id, const QString &fileName);

    void onCaptureError(int id, int error, const QString &errorString);



private:

    QCamera              *m_camera     = nullptr;

    QMediaCaptureSession *m_session    = nullptr;

    QImageCapture        *m_capture    = nullptr;

    QVideoWidget         *m_viewfinder = nullptr;

    QLabel               *m_status     = nullptr;

    QPushButton          *m_btnStart   = nullptr;

    QPushButton          *m_btnShot    = nullptr;

};

实现

cpp 复制代码
// camerawidget.cpp

#include "camerawidget.h"

#include "imageprocessor.h"



#include <QCamera>

#include <QMediaCaptureSession>

#include <QImageCapture>

#include <QVideoWidget>

#include <QMediaDevices>

#include <QCameraDevice>

#include <QPushButton>

#include <QLabel>

#include <QHBoxLayout>

#include <QVBoxLayout>

#include <QStandardPaths>

#include <QDateTime>

#include <QDir>

#include <QMessageBox>

#include <QDebug>



CameraWidget::CameraWidget(QWidget *parent) : QWidget(parent)

{

    setWindowTitle(QStringLiteral("拍照 + 图像处理"));



    m_viewfinder = new QVideoWidget(this);

    m_viewfinder->setMinimumSize(640, 480);



    m_status   = new QLabel(QStringLiteral("相机未启动"), this);

    m_btnStart = new QPushButton(QStringLiteral("启动相机"), this);

    m_btnShot  = new QPushButton(QStringLiteral("拍照"), this);

    m_btnShot->setEnabled(false);



    auto *btnLayout = new QHBoxLayout;

    btnLayout->addWidget(m_btnStart);

    btnLayout->addWidget(m_btnShot);

    btnLayout->addWidget(m_status, 1);



    auto *root = new QVBoxLayout(this);

    root->addWidget(m_viewfinder, 1);

    root->addLayout(btnLayout);



    connect(m_btnStart, &QPushButton::clicked, this, [this]{

        (m_camera && m_camera->isActive()) ? stopCamera() : startCamera();

    });

    connect(m_btnShot, &QPushButton::clicked, this, &CameraWidget::takePhoto);

}



CameraWidget::~CameraWidget()

{

    if (m_camera && m_camera->isActive())

        m_camera->stop();          // 先停,让会话退出活动状态

}



void CameraWidget::startCamera()

{

    // 1. 找设备:优先默认,没有就放弃

    const QCameraDevice dev = QMediaDevices::defaultVideoInput();

    if (dev.isNull()) {

        QMessageBox::warning(this, QStringLiteral("提示"),

                             QStringLiteral("未检测到可用摄像头"));

        return;

    }



    // 2. 建对象,父子关系交给 Qt 对象树管理,避免销毁顺序出错

    if (!m_camera) {

        m_camera  = new QCamera(dev, this);

        m_session = new QMediaCaptureSession(this);

        m_capture = new QImageCapture(this);



        // 3. 会话把三者串起来------Qt6 的核心范式

        m_session->setCamera(m_camera);

        m_session->setVideoOutput(m_viewfinder);

        m_session->setImageCapture(m_capture);



        // 4. 连接信号

        connect(m_capture, &QImageCapture::imageCaptured,

                this, &CameraWidget::onImageCaptured);

        connect(m_capture, &QImageCapture::imageSaved,

                this, &CameraWidget::onImageSaved);

        connect(m_capture, &QImageCapture::errorOccurred,

                this, &CameraWidget::onCaptureError);



        // 5. 相机错误必须连,否则设备被占用时用户毫无察觉

        connect(m_camera, &QCamera::errorOccurred, this,

                [this](QCamera::Error err, const QString &msg){

            if (err != QCamera::NoError) {

                m_status->setText(QStringLiteral("相机错误: ") + msg);

                m_btnShot->setEnabled(false);

            }

        });

    }



    m_camera->start();                 // 启动预览

    m_btnStart->setText(QStringLiteral("停止相机"));

    m_btnShot->setEnabled(true);

    m_status->setText(QStringLiteral("相机已启动,可以拍照了"));

}



void CameraWidget::stopCamera()

{

    if (m_camera) m_camera->stop();

    m_btnStart->setText(QStringLiteral("启动相机"));

    m_btnShot->setEnabled(false);

    m_status->setText(QStringLiteral("相机已停止"));

}



void CameraWidget::takePhoto()

{

    if (!m_capture || !m_camera || !m_camera->isActive())

        return;



    const QString dir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);

    QDir().mkpath(dir);                // 目录不存在会导致保存静默失败

    const QString path = QStringLiteral("%1/photo_%2.jpg")

        .arg(dir, QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss_zzz"));



    m_capture->captureToFile(path);    // 落盘,同时发 imageCaptured 和 imageSaved

    m_status->setText(QStringLiteral("正在保存到 ") + path);

}



void CameraWidget::onImageCaptured(int id, const QImage &preview)

{

    Q_UNUSED(id)

    qDebug() << "捕获到图像,尺寸:" << preview.size() << "格式:" << preview.format();



    // 注意:这里在 UI 线程;处理 4K 大图要挪到工作线程(见坑点 4)

    const QImage gray = ImageProcessor::toGray(preview);

    const QImage bin  = ImageProcessor::binarize(gray, 128);

    const QImage edge = ImageProcessor::sobelEdge(gray);

    Q_UNUSED(bin) Q_UNUSED(edge)



    m_status->setText(QStringLiteral("已捕获并完成图像处理"));

}



void CameraWidget::onImageSaved(int id, const QString &fileName)

{

    Q_UNUSED(id)

    m_status->setText(QStringLiteral("已保存: ") + fileName);

}



void CameraWidget::onCaptureError(int id, int error, const QString &errorString)

{

    Q_UNUSED(id) Q_UNUSED(error)

    m_status->setText(QStringLiteral("拍照失败: ") + errorString);

}

Qt5 版本的差异

Qt5 的流程是:QCameraInfo::defaultCamera() 找设备 → new QCamera(info, this) → setViewfinder(new QCameraViewfinder) 预览 → setCaptureMode(QCamera::CaptureStillImage) → 拍照前先 searchAndLock() 对焦锁定,capture(path) 后再 unlock()。要点是没有 session 对象 ,预览和拍照都直接挂在 QCamera 上;且 QCameraViewfinder 这个类在 Qt6 里已被删除,统一用 QVideoWidget。

四、用 QImage 做图像处理

QImage::Format 常用值

Format 每像素字节 说明 处理友好度
Format_RGB32 4 0xffRRGGBB,不透明 ⭐⭐⭐ 最推荐
Format_ARGB32 4 带 alpha ⭐⭐⭐
Format_RGB888 3 无 alpha ⭐⭐
Format_Grayscale8 1 8 位灰度(Qt 5.5+) ⭐⭐⭐ 灰度首选
Format_Indexed8 1 调色板索引 ⭐ 要配 colorTable

黄金法则 :处理前先 convertToFormat() 成 Format_RGB32(彩色)或 Format_Grayscale8(灰度),再去访问像素。直接对未知格式按固定步长读,是崩溃和花屏的根源。

灰度化

灰度用 ITU-R BT.601 亮度权重,比简单的 (R+G+B)/3 更符合人眼感知:

复制代码
Gray = 0.299*R + 0.587*G + 0.114*B
cpp 复制代码
// imageprocessor.h

#include <QImage>



class ImageProcessor

{

public:

    ImageProcessor() = delete;

    static QImage toGray(const QImage &src);

    static QImage binarize(const QImage &gray, int threshold);

    static QImage sobelEdge(const QImage &gray);

};
cpp 复制代码
// imageprocessor.cpp

#include "imageprocessor.h"

#include <QtMath>



QImage ImageProcessor::toGray(const QImage &src)

{

    if (src.isNull()) return {};



    const QImage img = src.convertToFormat(QImage::Format_RGB32);  // 统一格式

    QImage gray(img.size(), QImage::Format_Grayscale8);



    for (int y = 0; y < img.height(); ++y) {

        const QRgb *srcLine = reinterpret_cast<const QRgb *>(img.constScanLine(y));

        uchar      *dstLine = gray.scanLine(y);



        for (int x = 0; x < img.width(); ++x) {

            const QRgb px = srcLine[x];

            // +0.5 是四舍五入,qBound 防溢出

            const int v = int(0.299 * qRed(px) + 0.587 * qGreen(px)

                              + 0.114 * qBlue(px) + 0.5);

            dstLine[x] = uchar(qBound(0, v, 255));

        }

    }

    return gray;

}

为什么用 scanLine() 而不是 pixel()/setPixel()? 后者是逐像素函数调用还要处理格式转换,比直接指针访问慢几十倍。1920×1080 的图,pixel() 版要几百毫秒,scanLine() 版只要几毫秒。

二值化

cpp 复制代码
QImage ImageProcessor::binarize(const QImage &gray, int threshold)

{

    if (gray.isNull()) return {};



    const QImage img = (gray.format() == QImage::Format_Grayscale8)

                           ? gray : toGray(gray);

    QImage out(img.size(), QImage::Format_Grayscale8);



    for (int y = 0; y < img.height(); ++y) {

        const uchar *srcLine = img.constScanLine(y);

        uchar       *dstLine = out.scanLine(y);

        for (int x = 0; x < img.width(); ++x)

            dstLine[x] = (srcLine[x] >= threshold) ? 255 : 0;

    }

    return out;

}

阈值写死 128 在光照不均时效果很差。实际项目建议用**大津法(Otsu's method)**按直方图自动求阈值:统计 0~255 的直方图后,遍历每个候选阈值把像素分成前景/背景两类,计算类间方差,取方差最大者。它只需一遍直方图加一次 256 次遍历,代价极低,但比固定阈值鲁棒得多。

Sobel 边缘检测

Sobel 用两个 3×3 卷积核分别算水平和垂直梯度,再取模长:

复制代码
Gx:  -1  0  +1        Gy:  -1  -2  -1

     -2  0  +2              0   0   0

     -1  0  +1             +1  +2  +1
cpp 复制代码
QImage ImageProcessor::sobelEdge(const QImage &gray)

{

    if (gray.isNull()) return {};



    const QImage img = (gray.format() == QImage::Format_Grayscale8)

                           ? gray : toGray(gray);

    const int w = img.width(), h = img.height();

    QImage out(img.size(), QImage::Format_Grayscale8);



    for (int y = 0; y < h; ++y) {

        uchar *dstLine = out.scanLine(y);

        for (int x = 0; x < w; ++x) {

            // 边界像素置黑:3x3 核在边缘会越界读

            if (x == 0 || y == 0 || x == w - 1 || y == h - 1) {

                dstLine[x] = 0;

                continue;

            }

            auto at = [&](int dx, int dy) -> int {

                return img.constScanLine(y + dy)[x + dx];

            };



            const int gx = -at(-1,-1) + at(1,-1) - 2*at(-1,0)

                           + 2*at(1,0) - at(-1,1) + at(1,1);

            const int gy = -at(-1,-1) - 2*at(0,-1) - at(1,-1)

                           + at(-1,1) + 2*at(0,1) + at(1,1);



            const int mag = int(qSqrt(double(gx * gx + gy * gy)));

            dstLine[x] = uchar(qBound(0, mag, 255));

        }

    }

    return out;

}

性能提醒 :上面 at() 在 3×3 循环里反复调 constScanLine(),每次都有边界检查和指针计算。生产代码应预取三行指针:

cpp 复制代码
const uchar *rowPrev = img.constScanLine(y - 1);

const uchar *rowCurr = img.constScanLine(y);

const uchar *rowNext = img.constScanLine(y + 1);



// 若不需要精确模长,用城市距离近似更快:

const int mag = qMin(255, qAbs(gx) + qAbs(gy));

展示与亮度调节

处理完的图像用 QLabel::setPixmap(QPixmap::fromImage(img.scaled(label->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation))) 就能显示。做"阈值实时调节"时记得把灰度图缓存在成员变量里,滑块变化只重跑二值化------缓存中间结果是图像处理界面流畅的关键。

亮度调节就是在 Format_RGB32 下逐像素给 R/G/B 加同一个偏移量,关键是每个通道都要 qBound(0, v, 255) 裁剪,否则整数溢出会回绕成诡异颜色(比如 250+20 变成 14)。

常见坑点

坑点 1:图像是左右镜像的

预览常做镜像(像照镜子自然),但捕获的原始数据方向取决于设备和平台,可能刚好相反。判断方法:拍一张带文字的纸,看文字正不正,正了就别翻。

cpp 复制代码
QImage fixed = captured.mirrored(true, false);   // Qt5

// Qt6 里 mirrored() 已被删除!改用 flipped():

QImage fixed = captured.flipped(Qt::Horizontal); // Qt6

mirrored() → flipped() 是 Qt5→Qt6 的典型 API 变更。

坑点 2:capture() 与 captureToFile() 用混

方法 是否落盘 发 imageCaptured 发 imageSaved
capture() ❌ ✅ ❌
captureToFile(path) ✅ ✅ ✅

用 capture() 拿到图想自己存,得手动 img.save(path, "JPG", 95)。用 captureToFile() 则要记住文件路径是从 imageSaved(id, path) 拿的 。另外 path 的目录不存在会静默失败,务必先 QDir().mkpath()。

坑点 3:QImage 格式不是 RGB32 却按 RGB32 读

❌ 错误写法:

cpp 复制代码
const QRgb *line = reinterpret_cast<const QRgb *>(captured.constScanLine(y));

// 若实际是 3 字节/像素,每行错位,图像呈现斜条纹

✅ 正确写法:

cpp 复制代码
const QImage img = captured.convertToFormat(QImage::Format_RGB32);

// 现在可以放心按 4 字节/像素读

任何像素级操作之前,先 convertToFormat()。 这一步拷贝开销换来的是绝对安全。

坑点 4:跨线程处理大图卡界面

imageCaptured 是在主线程发出的,在里面跑 4K 图的 Sobel,界面会明显冻结。

❌ 错误写法:

cpp 复制代码
void CameraWidget::onImageCaptured(int, const QImage &img)

{

    const QImage edge = ImageProcessor::sobelEdge(img);   // 200ms+,界面卡死

    m_label->setPixmap(QPixmap::fromImage(edge));

}

✅ 正确写法,把耗时处理丢到工作线程:

cpp 复制代码
class ImageWorker : public QObject {

    Q_OBJECT

public slots:

    void process(const QImage &img) {

        emit processed(ImageProcessor::sobelEdge(ImageProcessor::toGray(img)));

    }

signals:

    void processed(const QImage &result);

};



auto *thread = new QThread(this);

auto *worker = new ImageWorker;

worker->moveToThread(thread);        // 必须在 connect 之前

thread->start();



connect(this, &CameraWidget::imageReady, worker, &ImageWorker::process);

connect(worker, &ImageWorker::processed, this, [this](const QImage &r){

    m_label->setPixmap(QPixmap::fromImage(r));   // 回到主线程,安全

});

补充:QCamera 必须在它的创建线程 里使用,跨线程调 start()/stop() 会崩。

坑点 5:相机对象销毁顺序错误

QMediaCaptureSession、QCamera、QImageCapture 互相引用,销毁顺序乱了程序退出时会崩。✅ 正确做法是给它们同一个父对象 (都以 this 为父),靠 Qt 对象树按构造逆序销毁;析构里只需先 stop()。真正会崩的是把它们 new 成无父对象的裸指针再手动删。

坑点 6:设备被占用 / 无设备时毫无提示

m_camera->start() 不会抛异常,失败只通过 errorOccurred 报告。用户可能没摄像头,或相机被会议软件独占。❌ 别 start() 之后就写"相机已启动"假装成功;✅ 必须连错误信号(QCamera::CameraError / ResourceError),把提示显示出来并禁用拍照按钮。监听热插拔用 QMediaDevices::videoInputsChanged 信号。

总结

整个流程是一条流水线:

阶段 Qt6 关键类 要点
找设备 QMediaDevices::defaultVideoInput() 判 isNull()
建会话 QMediaCaptureSession 相机/预览/拍照三者串一起
预览 QVideoWidget Qt5 是 QCameraViewfinder
拍照 QImageCapture::captureToFile() 目录要先 mkpath
处理 QImage + scanLine() 先 convertToFormat
展示 QLabel::setPixmap 大图丢工作线程

图像处理三板斧的要点:

算法 核心 易错点
灰度化 0.299R + 0.587G + 0.114B 别用 (R+G+B)/3
二值化 阈值比较,或大津法自适应 阈值写死 128 效果差
Sobel 两个 3×3 核取梯度模长 边界要跳过,别越界读

最后划两个跨版本差异的重点:Qt6 用 QMediaCaptureSession 取代了 Qt5 的 setViewfinder/setCaptureMode 那套;QImage::mirrored() 在 Qt6 里改成了 flipped()。 另外 QVideoWidget 在 Qt6::MultimediaWidgets 模块,与 Qt6::Multimedia 要分别 link------这是编译报"未定义符号"时第一个该检查的地方。

相关推荐
wangqiaowq1 小时前
AST 安全求值
java·开发语言
我们从未走散1 小时前
AI 生图平台如何避免资损:用“扣款与凭据同事务”构建三态账本
开发语言·数据库·php
weixin_440730502 小时前
字符编码笔记-utf-8 、unicode、gbk,ascii编码格式
开发语言·笔记
励志不掉头发的内向程序员3 小时前
【从零写一个CAD 03】三个 double 值得单独一个类吗:把视图变换抽成 View
开发语言·c++·qt·学习·系统架构
PHP实战开发录3 小时前
PHP接口大整数为什么变了
开发语言·php
临床数据科学和人工智能兴趣组6 小时前
在R语言中, 使用 as.factor() 函数转换数值型变量
开发语言·r语言
andyweike9 小时前
php笔记
开发语言·笔记·php
蒸蒸yyyyzwd10 小时前
秋招学习笔记 day46
c++·八股
Circ.10 小时前
Python 调试 Litellm 本地大模型接口:模型列表与对话接口实操踩坑记录
开发语言·python