Qt/C++攝像頭採集/二維碼解析/同時採集多路/圖片傳輸/分辨率幀率可調/自動重連

一、前言

本地攝像頭的採集可以有多種方式,一般本地攝像頭會通過USB的方式連接,在嵌入式上可能大部分是CMOS之類的軟帶的接口,這些都統稱本地攝像頭,和網絡攝像頭最大區別就是一個是通過網絡來通信,一個是直接本地通信。本地攝像機的採集可以用qcamera來做,但是qcamera類在很多平臺沒有實現,比如嵌入式linux系統上幾乎是沒有用的,所以更加推薦用兼容性跨平臺最好的ffmpeg來做,在linux上當然也可以通過v4l2來實現,這個其實是整個linux系統中通用的USB攝像頭採集的框架,無論是ffmpeg還是qcamera,在linux系統上底層其他都是用v4l2來實現的,所以如果在嵌入式板子受限於內存或者存儲空間大小,用不了ffmpeg一堆庫,這個時候就可以用最底層的v4l2來採集。

很多人的應用場景要求不止採集1路本地攝像頭,可能有多路都需要同時採集,其實這個和程序沒有太大關係,能夠採集一路肯定就能多路,畢竟就是封裝的一個類,直接new出來就行,要能採集多路最大的性能瓶頸在USB帶寬,數據帶寬,還有就是USB口子的供電足不足,供電不足,也只能採集1-2路,比如有些人用的是USB集線器,上面一排USB口子,由於供電不足,就算你插4個USB攝像頭,也只能最多采集2路,帶寬又限制了分辨率和幀率,比如一個普通PC機器可以同時採集4路640x480分辨率,但是由於帶寬不足,無法同時採集4路1080P,儘管4個攝像頭都支持1080p分辨率,所以這是個綜合因素的疊加影響,遇到問題慢慢查。

這個攝像頭綜合應用,陸陸續續完善了很多年,最開始是v4l2版本,只能用在嵌入式linux上,而後從Qt5開始集成了qcamera類,所以有用qcamera來做了一個版本,現在Qt6在多媒體框架有了巨大的性能提升,又用Qt6的qcamera做了個版本,爲什麼不通用了?因爲Qt6的多媒體框架做了巨大更新調整,完全不兼容之前的類。在做視頻組件的實現用ffmpeg採集本地攝像頭也實現了,而且做的比較完善,所以後面單獨提煉出來做了ffmpeg的版本,最後測試下來發現ffmpeg是最通用的,在哪裏都能正常採集。

做二維碼採集這個功能,用的是zxing類,最初的做法是將採集的圖片發給zxing解析,實際過程發現大圖片解析很慢,比如1080P圖片大概1s只能解析3張,這個速度看起來就慢了點,如果是2K的分辨率呢。後面經過一些真實場景應用,包括菜鳥驛站裏面帶的掃描識別,發現都有個放大區域,用戶可以設置一個關心的矩形區域,要放在這個矩形區域內,能夠最大最快的識別,這是個不錯的策略,在ffmpeg中通過設置crop裁剪區域就可以直接將對應內容放大顯示,然後採集的實時畫面可以是原視頻,發送給zxing解析的就是裁剪後的畫面,這樣速度大大提升。之前大圖就算二維碼在裏面,由於在整個圖片中的佔比區域太小,也可能解析失敗,自動調整後裁剪區域,性能大幅度提升,完美解決。

二、效果圖

三、體驗地址

  1. 國內站點:https://gitee.com/feiyangqingyun
  2. 國際站點:https://github.com/feiyangqingyun
  3. 個人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 體驗地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取碼:01jf 文件名:bin_video_camera。

四、相關代碼

#include "frmzxing.h"
#include "frmmain.h"
#include "ui_frmzxing.h"
#include "qthelper.h"
#include "camerahelper.h"
#include "widgethelper.h"
#include "zxingthread.h"

frmZxing::frmZxing(QWidget *parent) : QWidget(parent), ui(new Ui::frmZxing)
{
    ui->setupUi(this);
    this->initForm();
    this->initConfig();
}

frmZxing::~frmZxing()
{
    delete ui;
}

void frmZxing::initForm()
{
    ui->frame->setFixedWidth(AppData::RightWidth);

    //關聯信號槽
    connect(ui->cameraWidget, SIGNAL(sig_receivePlayStart(int)), this, SLOT(receivePlayStart(int)));
    connect(ui->cameraWidget, SIGNAL(sig_receivePoint(int, QPoint)), this, SLOT(receivePoint(int, QPoint)));

    //實例化解析類並啓動線程
    zxing = new ZXingThread(this);
    connect(zxing, SIGNAL(receiveResult(QString, QString, int)), this, SLOT(receiveResult(QString, QString, int)));
    zxing->start();

    //啓動定時器截圖
    timer = new QTimer(this);
    connect(timer, SIGNAL(timeout()), this, SLOT(on_btnSnap_clicked()));
    if (AppConfig::Zxing_Interval > 0) {
        timer->start(AppConfig::Zxing_Interval);
    }
}

void frmZxing::initConfig()
{
    CameraHelper::loadCameraCore(ui->cboxCameraCore, AppConfig::Zxing_CameraCore);
    connect(ui->cboxCameraCore, SIGNAL(currentIndexChanged(int)), this, SLOT(saveConfig()));

    this->cameraDeviceChanged();
    connect(ui->cboxCameraName->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(saveConfig()));

    CameraHelper::loadVideoSize(ui->cboxVideoSize);
    ui->cboxVideoSize->lineEdit()->setText(AppConfig::Zxing_VideoSize);
    connect(ui->cboxVideoSize->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(saveConfig()));

    CameraHelper::loadFrameRate(ui->cboxFrameRate);
    ui->cboxFrameRate->lineEdit()->setText(QString::number(AppConfig::Zxing_FrameRate));
    connect(ui->cboxFrameRate->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(saveConfig()));

    ui->cboxVideoMode->setCurrentIndex(AppConfig::Zxing_VideoMode);
    connect(ui->cboxVideoMode, SIGNAL(currentIndexChanged(int)), this, SLOT(saveConfig()));

    zxing->setDecoder(AppConfig::Zxing_DecoderFormat);
    ui->cboxDecoderFormat->addItem("僅二維碼");
    ui->cboxDecoderFormat->addItem("一維二維");
    ui->cboxDecoderFormat->addItem("所有格式");
    ui->cboxDecoderFormat->setCurrentIndex(AppConfig::Zxing_DecoderFormat);
    connect(ui->cboxDecoderFormat, SIGNAL(currentIndexChanged(int)), this, SLOT(saveConfig()));

    ui->cboxInterval->addItem("暫停");
    ui->cboxInterval->addItem("300");
    ui->cboxInterval->addItem("500");
    ui->cboxInterval->addItem("800");
    ui->cboxInterval->addItem("1000");
    int index = ui->cboxInterval->findText(QString::number(AppConfig::Zxing_Interval));
    ui->cboxInterval->setCurrentIndex(index < 0 ? 0 : index);
    connect(ui->cboxInterval, SIGNAL(currentIndexChanged(int)), this, SLOT(saveConfig()));

    ui->txtTopLeft->setText(AppConfig::Zxing_TopLeft);
    connect(ui->txtTopLeft, SIGNAL(textChanged(QString)), this, SLOT(saveConfig()));

    ui->txtBottomRight->setText(AppConfig::Zxing_BottomRight);
    connect(ui->txtBottomRight, SIGNAL(textChanged(QString)), this, SLOT(saveConfig()));
}

void frmZxing::saveConfig()
{
    //內核變了需要重新搜索設備
    int cameraCore = ui->cboxCameraCore->itemData(ui->cboxCameraCore->currentIndex()).toInt();
    if (AppConfig::Zxing_CameraCore != cameraCore) {
        AppConfig::Zxing_CameraCore = cameraCore;
        this->cameraDeviceChanged();
    }

    //不爲空才需要改變
    QString cameraName = ui->cboxCameraName->currentText();
    if (!cameraName.isEmpty()) {
        AppConfig::Zxing_CameraName = cameraName;
    }

    AppConfig::Zxing_VideoSize = ui->cboxVideoSize->currentText();
    AppConfig::Zxing_FrameRate = ui->cboxFrameRate->currentText().toInt();
    AppConfig::Zxing_VideoMode = ui->cboxVideoMode->currentIndex();

    //變了立即重新設置
    int decoderFormat = ui->cboxDecoderFormat->currentIndex();
    if (AppConfig::Zxing_DecoderFormat != decoderFormat) {
        AppConfig::Zxing_DecoderFormat = decoderFormat;
        zxing->setDecoder(AppConfig::Zxing_DecoderFormat);
    }

    int interval = ui->cboxInterval->currentText().toInt();
    if (AppConfig::Zxing_Interval != interval) {
        AppConfig::Zxing_Interval = interval;
        if (interval > 0) {
            timer->start(interval);
        } else {
            timer->stop();
        }
    }

    AppConfig::Zxing_TopLeft = ui->txtTopLeft->text().trimmed();
    AppConfig::Zxing_BottomRight = ui->txtBottomRight->text().trimmed();
    AppConfig::writeConfig();
}

void frmZxing::initPara()
{
    //設置顯示窗體參數
    WidgetPara widgetPara = ui->cameraWidget->getWidgetPara();
    widgetPara.borderWidth = 1;
    widgetPara.videoMode = (VideoMode)AppConfig::Zxing_VideoMode;
    ui->cameraWidget->setWidgetPara(widgetPara);

    //設置採集線程參數
    CameraPara cameraPara = ui->cameraWidget->getCameraPara();
    cameraPara.cameraCore = (CameraCore)AppConfig::Zxing_CameraCore;
    cameraPara.cameraName = AppConfig::Zxing_CameraName;
    cameraPara.videoSize = AppConfig::Zxing_VideoSize;
    cameraPara.frameRate = AppConfig::Zxing_FrameRate;
    ui->cameraWidget->setCameraPara(cameraPara);
}

void frmZxing::append(int type, const QString &data, bool clear)
{
    static int maxCount = 50;
    static int currentCount = 0;
    QtHelper::appendMsg(ui->txtMain, type, data, maxCount, currentCount, clear);
    ui->txtMain->moveCursor(QTextCursor::End);
}

void frmZxing::createImage(const QString &text)
{
    if (!text.isEmpty()) {
        QImage image = zxing->encodeData(text, QSize(250, 250));
        ui->labResult->setImage(image, true);
    }
}

void frmZxing::cameraDeviceChanged()
{
    frmMain::getCameraInfo((CameraCore)AppConfig::Zxing_CameraCore);
    CameraHelper::loadCameraName(ui->cboxCameraName, AppConfig::Zxing_CameraName);
}

void frmZxing::snapImage(const QImage &image, const QString &snapName)
{
    QImage img = image;
    if (snapName != "file") {
        //有左上右下座標說明需要裁減
        int w = image.width();
        int h = image.height();
        if (!AppConfig::Zxing_TopLeft.isEmpty() && !AppConfig::Zxing_BottomRight.isEmpty()) {
            QRect rect = WidgetHelper::getRect(AppConfig::Zxing_TopLeft, AppConfig::Zxing_BottomRight);
            img = img.copy(rect);
            QString msg = QString("執行裁剪(原分辨率: %1 x %2  新分辨率: %3 x %4)").arg(w).arg(h).arg(img.width()).arg(img.height());
            append(4, msg);
            img.save("f:/1.jpg", "jpg");
        }

        //防止圖片過大導致解析很慢
        static int maxWidth = 1280;
        static int maxHeight = 720;
        w = img.width();
        h = img.height();
        if (w > maxWidth || h > maxHeight) {
            img = img.scaled(QSize(maxWidth, maxHeight), Qt::KeepAspectRatio);
            QString msg = QString("執行縮放(原分辨率: %1 x %2  新分辨率: %3 x %4)").arg(w).arg(h).arg(maxWidth).arg(maxHeight);
            append(4, msg);
        }
    }

    //添加到線程中解析
    zxing->append("", img);
    //顯示圖片
    ui->labImage->setImage(img, true);
}

void frmZxing::receiveResult(const QString &flag, const QString &text, int time)
{
    append(2, QString("(用時: %1 毫秒) 結果: %2").arg(time).arg(text));
    ui->txtResult->setText(text);
    //重新生成新的二維碼圖片
    createImage(text);
}

void frmZxing::receivePlayStart(int time)
{
    //如果存在裁剪區域則設置圖形
    if (!AppConfig::Zxing_TopLeft.isEmpty() && !AppConfig::Zxing_BottomRight.isEmpty()) {
        GraphInfo graph;
        graph.rect = WidgetHelper::getRect(AppConfig::Zxing_TopLeft, AppConfig::Zxing_BottomRight);
        graph.borderWidth = WidgetHelper::getBorderWidth(ui->cameraWidget);
        ui->cameraWidget->setGraph(graph);
    }
}

void frmZxing::receivePoint(int type, const QPoint &point)
{
    QString text = QString("%1, %2").arg(point.x()).arg(point.y());
    if (ui->rbtnTopLeft->isChecked()) {
        ui->txtTopLeft->setText(text);
    } else {
        ui->txtBottomRight->setText(text);
    }
}

void frmZxing::on_btnPlay_clicked()
{
    this->initPara();
    if (!ui->cameraWidget->init()) {
        return;
    }

    //關聯採集線程信號槽
    CameraThread *cameraThread = ui->cameraWidget->getCameraThread();
    connect(cameraThread, SIGNAL(cameraDeviceChanged()), this, SLOT(cameraDeviceChanged()));
    connect(cameraThread, SIGNAL(snapImage(QImage, QString)), this, SLOT(snapImage(QImage, QString)));

    ui->cameraWidget->play();
    ui->widget->setEnabled(false);
}

void frmZxing::on_btnStop_clicked()
{
    ui->cameraWidget->stop();
    ui->widget->setEnabled(true);
}

void frmZxing::on_btnPause_clicked()
{
    ui->cameraWidget->pause();
}

void frmZxing::on_btnNext_clicked()
{
    ui->cameraWidget->next();
}

void frmZxing::on_btnSnap_clicked()
{
    CameraThread *thread = ui->cameraWidget->getCameraThread();
    if (thread && thread->getIsOk()) {
        append(4, "執行截圖並解析二維碼");
        ui->cameraWidget->snap();
    }
}

void frmZxing::on_btnFile_clicked()
{
    QString fileName = QFileDialog::getOpenFileName(this, "打開", "", "*.jpg *.png");
    if (!fileName.isEmpty()) {
        QImage image(fileName);
        snapImage(image, "file");
    }
}

void frmZxing::on_btnCreate_clicked()
{
    createImage(ui->txtResult->text());
}s

五、功能特點

  1. 同時支持 qcamera、ffmpeg、v4l2 三種內核解析本地攝像頭。
  2. 提供函數 findCamera 自動搜索環境中的所有本地攝像頭設備,搜索結果信號發出。
  3. 支持自動搜索和指定設備兩種模式,自動搜索模式下會將搜索到的第一個設備作爲當前設備打開。
  4. 支持同時打開多路設備,親測4路,受限於具體的環境比如帶寬。
  5. 支持自動重連,默認開啓,失敗後會自動重新搜索和嘗試打開。
  6. ffmpeg方案、v4l2方案都支持回調模式(採集後轉成QImage繪製)和句柄模式(採集後YUV數據GPU繪製,性能高)。
  7. 視頻顯示位置自動調整算法,當視頻分辨率超過顯示控件大小則等比例縮放居中顯示,不超過則原尺寸居中顯示,還可設置拉伸填充顯示。(自動調整、等比例縮放、拉伸填充)。
  8. 可選不同的分辨率來打開攝像頭,支持 160x120、320x240、640x480、800x600、1280x720、1280x960、1920x1080 等。
  9. 可選不同的幀率來打開攝像頭,支持 0(採用默認值)、5、、10、15、20、25、30 等。
  10. 支持抓拍截圖,傳入文件名則自動保存截圖文件,不傳入則將圖片數據QImage信號發出。
  11. 提供函數接口 開始播放play、停止播放stop、暫停播放pause、繼續播放next。
  12. 支持動態熱插拔加載,包括自動讀取所有設備名稱到下拉框。
  13. 支持錄像文件存儲,提供開始錄像recordStart、暫停錄像recordPause、停止錄像recordStop 等函數。
  14. 提供二維碼示例,自動採集畫面識別二維碼,支持自動將識別到的二維碼重新生成大圖。
  15. 二維碼識別支持設置熱點區域,對該區域內的圖片進行裁切並識別,在大分辨率圖像採集的時候非常有用,提升速度和效率。
  16. 支持選擇圖片文件解析二維碼,手動輸入文本內容生成二維碼。
  17. 提供圖片傳輸示例,自動將打開的攝像頭視頻實時傳輸出去,服務器端接收後解析顯示。此方案可以作爲將本地的攝像頭實時畫面遠程傳輸,比如嵌入式板子上的攝像頭畫面傳輸到PC端顯示。
  18. 支持等比例拉伸填充顯示,畫面寬高小於顯示控件的寬高則以原視頻大小爲準,大於則按照顯示控件的尺寸等比例縮放居中。
  19. 視頻控件懸浮條自帶開始和停止錄像切換、聲音靜音切換、抓拍截圖、關閉視頻等功能。
  20. 音頻組件支持聲音波形值數據解析,可以根據該值繪製波形曲線和柱狀聲音條,默認提供了聲音振幅信號。
  21. 代碼框架和結構優化到極致,性能彪悍,持續迭代更新升級。
  22. 源碼支持Qt4、Qt5、Qt6,兼容所有版本。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章