Qt 之相机Camera预览、拍照

概述

项目中使用到相机的预览和拍照功能,在嵌入式下由于是第三方提供的平台,不是标准的,在qml下,Camera拿不到相机信息,只能通过底层适配,但是目前适配的流程是每次控制相机都要去遍历所有相机,然后根据某些规则去找到是自己要控制的相机,所以导致相机的预览、开关、拍照等操作均反应迟缓,作为Qt开发者来说,去做了三方面的尝试:
只能说被这个第三方嵌入式平台坑惨了- -

1 纯Qml控制,通过qt的mutimedia 模块中的Camera配合VideoOutput来实现;
2 通过QCamera 和 QAbstractVideoSurface ,然后配合qml的 VideoOutput来实现;
3 通过QCamera和 QAbstractVideoSurface以及 QQuickImageProvider ,配合qml的 Image来实现;
4 通过V4l2库,QQuickImageProvider ,配合qml的 Image来实现;

项目中的想法的相机预览时候一个低分辨率,拍照的时候采用一个高分辨率,然后拍完照再设置一个低分辨率的预览,qt这方面没有实现,可能是笔者不知道方法
void QCamera::setViewfinderSettings(const QCameraViewfinderSettings &settings)
Sets the viewfinder settings.
If some parameters are not specified, or null settings are passed, the camera will choose default values.
If the camera is used to capture videos or images, the viewfinder settings might be ignored if they conflict with the capture settings. You can check the actual viewfinder settings once the camera is in the QCamera::ActiveStatus status.
Changing the viewfinder settings while the camera is in the QCamera::ActiveState state may cause the camera to be restarted

摄像头模式是捕捉视频或图片, 如何取景器的设置与拍照设置相冲突,则可能会被忽略。

第一种 纯qml实现

参考qt自己的demo 即可
Camera模式模式 Camera.CaptureStillImage
可以通过devideId来指定camera
linux下默认是后摄
captureToLocation()是capture()的重载,可以指定抓图的路径
笔者的板子上还要设置VideoOutput的orientation,并且还要设置成360°,见鬼了。
camera.viewfinder.resolution = Qt.size(2592, 1296); 设置viewfinder的分辨率,拍照的分辨率也一样被设置了。

这种实现方法是最简单的,代码最少,基本上qml的demo已经满足你所有的需求,但是笔者的板子上QtMultimedia.availableCameras竟然是空 - -,坑爹啊,导致预览和相机的开关都很慢

Camera {
        id: camera
        //deviceId:QtMultimedia.availableCameras[0].deviceId
        imageCapture {
            onImageCaptured: {
            //your process
            }
        }
  }

    VideoOutput {
        id: viewfinder
        anchors.fill: parent
        source: camera
        //autoOrientation: true
        orientation: 360
        MouseArea
        {
            anchors.fill: parent
            onClicked:
            {
                var filepath = UILogic.getCurImageName()
                camera.imageCapture.captureToLocation(filepath)
                photoPreview.imagePath = filepath+".jpg";
            }
        }
    }

第二种 通过QCamera 和 QAbstractVideoSurface ,然后配合qml的 VideoOutput来实现

VideoOutput 的介绍中说明, source 属性可以是一个自定义派生与 QObject 的子类,并提供一个类型为 QMediaObject 的属性命名为 mediaObject ,或者是一个派生与 QObject 的子类并提供一个类型为 QAbstractVideoSurface 的属性命名为 videoSurface 给 QML

class FrameProvider: public QObject {
    Q_OBJECT
    Q_PROPERTY(QAbstractVideoSurface *videoSurface READ getVideoSurface WRITE setVideoSurface NOTIFY videoSurfaceChanged)
    
public:
    FrameProvider(QObject *parent = nullptr)
        : QObject(parent) {}
    
    QAbstractVideoSurface* getVideoSurface() const { return m_surface; }
    void setVideoSurface(QAbstractVideoSurface *surface) {
        if (m_surface != surface && m_surface && m_surface->isActive()) {
            m_surface->stop();
        }
        
        m_surface = surface;
        QList<QVideoFrame::PixelFormat> list = m_surface->supportedPixelFormats();
        if (m_surface && m_format.isValid()) {
            m_format = m_surface->nearestFormat(m_format);
            m_surface->start(m_format);
        }
        emit videoSurfaceChanged();
    }
    
    void setFormat(int width, int height, QVideoFrame::PixelFormat frameFormat) {
        QSize size(width, height);
        QVideoSurfaceFormat format(size, frameFormat);
        if (m_surface) {
            if (m_surface->isActive())
                m_surface->stop();
            m_format = m_surface->nearestFormat(m_format);
            m_surface->start(m_format);
        }
    }
    
public slots:
    void setFrame(const QVideoFrame &frame) {
        if (m_surface) 
        //do Your process 格式转换
            m_surface->present(frame);
    }
    
signals:
    void videoSurfaceChanged();
    
private:
    QAbstractVideoSurface *m_surface = NULL;
    QVideoSurfaceFormat m_format;
};

将对象注入到全局 Context 提供 QML 使用

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

    QGuiApplication app(argc, argv);

    auto camera = new QCamera();

    FrameProvider fp;
    fp.setFormat(1280,720, QVideoFrame::PixelFormat::Format_YUV420P);

    auto probe = new QVideoProbe();
    if(probe->setSource(camera))
    {
        QObject::connect(probe, SIGNAL(videoFrameProbed(const QVideoFrame &)), &fp, SLOT(setFrame(const QVideoFrame &)));
    }

    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("frameProvider", &fp);

    camera->start();
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

main.qml文件

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")

    VideoOutput {
        id: display
        visible: true
        objectName: "display"
        anchors.fill :parent
        source: frameProvider
    }
}

第二种方法有时候会出现不出图,问题定位思路:
1 m_surface->start(m_format); 判断是否start成功
2 setFrame(const QVideoFrame &frame) 判断frame的图像格式,是否与设置的一致

主要是QAbstractVideoSurface 的图像格式和相机图像格式冲突,需要转换,在 setFrame 函数中提前转换,
思路如下(仅提供参考):
QVideoFrame 的变形与QImage之间的转换

QVideoFrame f(size, QSize(width, height), width, QVideoFrame::Format_YUV420P); 
if (f.map(QAbstractVideoBuffer::WriteOnly)) {
	 memcpy(f.bits(), data, size); 
	 f.setStartTime(0); 
	 f.unmap(); 
	 emit newFrameAvailable(f); 
}


bool present(const QVideoFrame &frame)
{
    if (frame.isValid()) {
        QVideoFrame cloneFrame(frame);
        cloneFrame.map(QAbstractVideoBuffer::ReadOnly);
        int format= QVideoFrame::imageFormatFromPixelFormat(cloneFrame.pixelFormat());
        if (format != QImage::Format_Invalid)
        {
            QImage image(cloneFrame.bits(),
                         cloneFrame.width(),
                         cloneFrame.height(),
                         cloneFrame.bytesPerLine(),
                         format);
            //do something
        }
        else {
            if (cloneFrame.pixelFormat() == QVideoFrame::Format_YUYV)
            {
                int nbytes = cloneFrame.mappedBytes();
                char *rgb24 = new char[nbytes*2]();
                convert_yuv_to_rgb_buffer((unsigned char*)cloneFrame.bits(), (unsigned char*)rgb24, cloneFrame.width(), cloneFrame.height());
                //不用QT的话,这里可以直接把图片rgb24buffer写入文件image.bmp中,
                //注意保存格式,bmp是位图,而类似jpg,png之类的是经压缩过的。
                QImage img((unsigned char*)rgb24, cloneFrame.width(), cloneFrame.height(), cloneFrame.width()*3, QImage::Format_RGB888);
                emit frameAvailable(img);
                delete[] rgb24;
            }
            else
            {
                int nbytes = cloneFrame.mappedBytes();
                QImage image = QImage::fromData(cloneFrame.bits(), nbytes);
                emit frameAvailable(image);
            }
        }
        cloneFrame.unmap();
        return true;
    }
    return false;
}

第三种 通过QCamera和 QAbstractVideoSurface以及 QQuickImageProvider ,配合qml的 Image来实现

核心代码如下:
踏坑经验:图片刷新不出来
qml 的Image处理
或者 Image 设置
注意,这里刷新图片的时候必须先设置为空img.source = “”,否则无法刷新。 Image 属性加上一句:cache:false 就搞定了,不显示的原因是 Image 自动做了缓存,把缓存去掉就行了。
这样一来,C++端设置新图片后会发送信号,在QML端连接信号然后去图片目录下去取出来刷新当前控件

Image{
        id:img
        anchors.fill: parent
    }
    Connections{
        target: CodeImage
        onCallQmlRefeshImg:{
            img.source = "image://CodeImg/"+ Math.random()
        }
    }
class QtCamera : public QObject
{
    Q_OBJECT
public:
    explicit QtCamera(QCameraInfo cameraInfo = QCameraInfo::defaultCamera(), QObject *parent = 0);
    ~QtCamera();
    
    Q_INVOKABLE bool start();
    Q_INVOKABLE bool stop();
    Q_INVOKABLE bool isStarted();
    Q_INVOKABLE bool capture(QString filePath);
    QtImageProvider *m_pImageProvider;
signals:
    void imageOutput(QImage image);
private slots:
    void grabImage(QImage image);
private:
    QCamera *m_camera;
    QtCameraCapture *m_cameraCapture;
    QCameraInfo   m_cameraDeviceInfo;
    bool      m_started;
};
QtCamera::QtCamera(QCameraInfo cameraInfo, QObject *parent) : QObject(parent)
{
    m_started = false;
    m_camera = NULL;
    m_cameraCapture = NULL;

    if (cameraInfo.isNull()) {
        qDebug() << __LINE__ << __FUNCTION__;
        return;
    }

    m_cameraDeviceInfo = cameraInfo;
    m_cameraCapture = new QtCameraCapture;
    connect(m_cameraCapture, SIGNAL(frameAvailable(QImage)), this, SLOT(grabImage(QImage)));

    m_pImageProvider = new QtImageProvider;
}

QtCamera::~QtCamera()
{
    if (m_camera != NULL) {
        delete m_camera;
        m_camera = NULL;
    }

    if (m_cameraCapture != NULL) {
        delete m_cameraCapture;
        m_cameraCapture = NULL;
    }
}

bool QtCamera::start()
{
    if (m_started) {
        return false;
    }
    else {
        if (m_cameraDeviceInfo.isNull()) {
            return false;
        }
        else {
            m_camera = new QCamera(m_cameraDeviceInfo);
            m_camera->setViewfinder(m_cameraCapture);
            m_camera->start();
            m_started = true;
        }

    }

    return true;
}

bool QtCamera::stop()
{
    m_camera->stop();
    m_started = false;
    return true;
}

bool QtCamera::isStarted()
{
    return m_started;
}

bool QtCamera::capture(QString filePath)
{
    if (! m_started) {
        return false;
    }
    //return m_image.save(filePath, "jpg");
}

void QtCamera::grabImage(QImage image)
{
    //m_image = image.mirrored(false, true);

    m_pImageProvider->img = image;
    emit imageOutput(image);
}

自定义的QAbstractVideoSurface,处理抓到frame数据

class QtCameraCapture : public QAbstractVideoSurface
{
    Q_OBJECT
public:
    enum PixelFormat {
        Format_Invalid,
        Format_ARGB32,
        Format_ARGB32_Premultiplied,
        ...
        Format_User = 1000
    };

    Q_ENUM(PixelFormat)
    explicit QtCameraCapture(QObject *parent = 0);
    QList<QVideoFrame::PixelFormat> supportedPixelFormats(
            QAbstractVideoBuffer::HandleType handleType = QAbstractVideoBuffer::NoHandle) const;
    bool present(const QVideoFrame &frame) override;
signals:
    void frameAvailable(QImage frame);
};
QtCameraCapture::QtCameraCapture(QObject *parent) : QAbstractVideoSurface(parent)
{

}

QList<QVideoFrame::PixelFormat> QtCameraCapture::supportedPixelFormats(QAbstractVideoBuffer::HandleType handleType) const
{
    Q_UNUSED(handleType);
    return QList<QVideoFrame::PixelFormat>()
            << QVideoFrame::Format_ARGB32
            << QVideoFrame::Format_ARGB32_Premultiplied
            ...
            << QVideoFrame::Format_AdobeDng;
}


bool QtCameraCapture::present(const QVideoFrame &frame)
{
    if (frame.isValid()) {
        QVideoFrame cloneFrame(frame);
        cloneFrame.map(QAbstractVideoBuffer::ReadOnly);
        int format= QVideoFrame::imageFormatFromPixelFormat(cloneFrame.pixelFormat());
        qDebug() << cloneFrame.size() <<  cloneFrame.pixelFormat();
        if (format != QImage::Format_Invalid)
        {
            const QImage image(cloneFrame.bits(),
                                       cloneFrame.width(),
                                       cloneFrame.height(),
                                       QVideoFrame::imageFormatFromPixelFormat(cloneFrame.pixelFormat()));
                    emit frameAvailable(image);
                    cloneFrame.unmap();
                    return true;
        }
        else {
            if (cloneFrame.pixelFormat() == QVideoFrame::Format_YUYV)
            {
                qDebug() << "QVideoFrame::Format_YUYV";
                ...
            }
            else
            {
                int nbytes = frame.mappedBytes();
                emit frameAvailable(QImage::fromData(frame.bits(), nbytes));
            }
        }
        cloneFrame.unmap();
        return true;
    }
    else {
        qDebug() << "frame is not valid";
    }
    return false;
}

实际上,关键的地方在于能够提供正确的 QVideoFrame,并设置正确的 Format,

而 QVideoFrame 支持的格式相当多,例如 NV12、NV21、YUYV 等等,然后使用 QAbstractVideoSurface::present(const QVideoFrame &frame) 传递给 VideoOutput 即可呈现。

#include <QQuickImageProvider>

class QtImageProvider : public QQuickImageProvider
{
public:
    explicit QtImageProvider(QObject *parent = nullptr);

    QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize);
    QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize);

    QImage img;
signals:

};
#include "qtimageprovider.h"
QtImageProvider::QtImageProvider(QObject *parent) : QQuickImageProvider(QQuickImageProvider::Image)
{
}

QImage QtImageProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize)
{
    return this->img;
}

QPixmap QtImageProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize)
{
    return QPixmap::fromImage(this->img);
}

注册ImageProvider

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    
    QGuiApplication app(argc, argv);
    QQmlApplicationEngine engine;
    
    QtCamera * pcamera = new QtCamera;
    engine.rootContext()->setContextProperty("zcamera",pcamera);
    engine.addImageProvider(QLatin1String("CodeImg"), pcamera->m_pImageProvider);
    
    
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                     &app, [url](QObject *obj, const QUrl &objUrl) {
        if (!obj && url == objUrl)
            QCoreApplication::exit(-1);
    }, Qt::QueuedConnection);
    engine.load(url);
    
    return app.exec();
}

qml文件

Image{
    id:img
    anchors.fill: parent
}
Connections{
    target: zcamera
    onImageOutput:{
        img.source = ""
        img.source = "image://CodeImg"
    }
}

第四种 通过V4l2库,QQuickImageProvider ,配合qml的 Image来实现

思路是定时去设备拿图像,然后刷新界面


	this->timer = new QTimer(this);
//	connect(this->timer, SIGNAL(timeout()), this, SLOT(update()));
    connect(timer, SIGNAL(timeout()), this, SLOT(update()));
    this->timer->start(100);
    
void ShowVideo::paintEvent(QPaintEvent *)
{
    qDebug()<<"Start updat"<<endl;
	/*	获取视频采集的初始数据	*/
	this->myVideoDevice->get_frame((void **)(&(this->pVideoData)), &uDataLength);
	
	/*	数据转换	*/
    convert_yuv_to_rgb_buffer(this->pVideoData, this->pChangedVideoData, 640, 480);
	
	/*	加载数据	*/
    this->pImage->loadFromData(this->pChangedVideoData, 640 * 480 * 3 * sizeof(char));
	
	/*	显示数据	*/
    this->pLabel->setPixmap(QPixmap::fromImage(*pImage));
	
	/*	处理后的数据帧重新放到队列末尾		*/
    this->myVideoDevice->unget_frame();
}

总结

相机在不同平台上,表现大有不同,在Android上相机有多个通道,预览和拍照走不同的通道来实现,可分别控制。但是qt在嵌入式下的平台上只有一个通道,所以基本实现不了预览和拍照分辨率的相互切换。笔者的代码也基本上传到了csdn供下载
第二种方法 QCamera+VideoOutput

第三种方法QCamera和+QAbstractVideoSurface+ QQuickImageProvider + Image

第四种方法 V4l2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值