基于QCustomPlot 和 FFTW 应用分享

2 篇文章 0 订阅
1 篇文章 0 订阅

试验采集数据,需要查看时间域曲线、频谱计算(含复数结果输出)、查看分段详情、导出计算结果、批量处理大文件。对比Qwt QtCharts,还是选用QCustomPlot。没失望,效率还是可以,UI也还蛮好。FFTW库,一个字~香!

涉及到的细节:QCustomPlot双对数轴,x轴刻度自定义,x轴逆序,全选/全不选按钮联动,单根曲线颜色,xy轴标尺跟随,tracer更新,缩放轴切换,子线程跑FFTW,线程池管理多个子线程,同时读多个文件,曲线抽稀,FFTW策略重复执行,moveToThread跑子线程,SQLite中检索信息、QXlsx库写Excel文件。 

 Mark哈。各位看官老爷,有空可以指导哈。滤波,小波变换,去噪等还不晓得咋玩,《数字信号处理》 这门课我都不记得上过没。

  /* 点击按钮,修改曲线颜色 */
    QPushButton *poBtnColor = new QPushButton("颜色");
    poBtnColor->setMaximumWidth(50);
    connect(poBtnColor, &QPushButton::clicked, this, [=]{
        if(!poGraph->visible()){
            QMessageBox::warning(NULL, "警告", "请先勾选该曲线!",
                                 QMessageBox::Cancel,
                                 QMessageBox::Cancel);
            return ;
        }

        QColorDialog *colorDlg = new QColorDialog(this);

        QColor color = colorDlg->getColor(QColor(255,0,0));  //显示对话框并获取当前选中的颜色(显示对话框时线程阻塞,是模态显示的)

        poBtnColor->setStyleSheet(QString("color: rgb(255, 255, 255); background-color: rgb(%1, %2, %3);")
                                  .arg(color.red())
                                  .arg(color.green())
                                  .arg(color.blue()));

        poGraph->setPen(QColor(color.red(), color.green(), color.blue()));

        foreach(QCPItemTracer *poTracer,  mapGraphTracer.value(poGraph)){
            poTracer->setBrush(QColor(color.red(), color.green(), color.blue()));
        }
        ui->plotSpectrum->replot();

        //临时变量释放资源
        delete colorDlg;
        colorDlg = nullptr;
    });
/* QVector x y 中,找到最大值&&索引和最小值&&索引 */
void SpectrumAnalysisThread::extremum(QVector<float> x, QVector<float> y)
{
    //qDebugV0()<<"input :"<<x.length()<<y;

    if(x.count() <= 2){
        /* 要这个顺序 */
        for(int i = 0; i < x.count(); i++){
            adX.append(x.at(i));
            adY.append(y.at(i));
        }
    }
    else{
        auto max = std::max_element(std::begin(y), std::end(y));
        auto min = std::min_element(std::begin(y), std::end(y));

        float biggest  = *max;
        float smallest = *min;

        auto positionmax = std::distance(std::begin(y),max);
        auto positionmin = std::distance(std::begin(y),min);

        int posmax = positionmax;
        int posmin = positionmin;

        if(posmin < posmax){
            adX.append(x.at(posmin));
            adY.append(y.at(posmin));

            adX.append(x.at(posmax));
            adY.append(y.at(posmax));
        }else{
            adX.append(x.at(posmax));
            adY.append(y.at(posmax));

            adX.append(x.at(posmin));
            adY.append(y.at(posmin));
        }
    }
}
/* 分段计算频谱,抽稀频谱曲线,循环执行FFTW plan, */
void SpectrumAnalysisThread::slotSpectrum(int iRow, QString oStrFileName, quint64 uiStart, quint64 uiEnd, quint64 uiSecs, QList<double> adTargetF)
{
    /* 1、获取完整的时间与数据 */
    HEAD oHead = PF::readHead(oStrFileName);

    /* 截取的秒数 */
    quint64 uiSecondLength = uiEnd - uiStart;

    /* 分段数,也就是接着要循环的次数 */
    quint64 uiSegCnt = uiSecondLength/uiSecs;

    long long uiMultiple = oHead.uiFS*uiSecs/2/MAX_SAMPLE;

    if(uiMultiple == 0){
        uiMultiple = 1;
    }

    if(oHead.uiFS*uiSecs < MAX_SAMPLE){
        emit sigMsg(QString("频谱图点个数:%1| ").arg(oHead.uiFS*uiSecs/2));
    }
    else{
        emit sigMsg(QString("频谱图点个数:%1| 设置的最大采样点数:%2| 倍数:%3")
                    .arg(oHead.uiFS*uiSecs/2)
                    .arg(MAX_SAMPLE)
                    .arg(uiMultiple));
    }

    QFile oFile(oStrFileName);
    if(oFile.open(QIODevice::ReadOnly)){

        QVector<float> x, y;

        QDataStream oStream(&oFile);

        oStream.setByteOrder(QDataStream::BigEndian);

        oStream.setFloatingPointPrecision(QDataStream::SinglePrecision);

        oStream.skipRawData(HEAD_LENGTH + oHead.uiFS*uiStart*DATA_LENGTH);//作者是靠占位符来占据首行的。//作者是靠占位符来占据首行的。

        int nThread = 8;

        int a = fftwf_init_threads();

        emit sigMsg(oStrFileName);

        fftwf_plan_with_nthreads(nThread);

        float *in = (float*)fftwf_malloc(sizeof(float) * oHead.uiFS*uiSecs);
        fftwf_complex *out = (fftwf_complex *)fftwf_malloc(sizeof(fftw_complex) * oHead.uiFS*uiSecs);

        fftwf_plan p = nullptr;

        p = fftwf_plan_dft_r2c_1d(oHead.uiFS*uiSecs, in, out, FFTW_ESTIMATE);

        for(int i = 0; i < uiSegCnt; i++){

            QElapsedTimer timeRead;
            timeRead.start();

            oStream.startTransaction();
            for(int j = 0; j < oHead.uiFS*uiSecs; j++){
                oStream>>in[j];
            }
            oStream.commitTransaction();

            qint64 milsecRead = timeRead.elapsed();

            QElapsedTimer time;
            time.start();

            fftwf_execute_dft_r2c(p, in, out);//执行变换

            for(quint64 j = (oHead.uiFS*uiSecs)/2; j >= 1; j--){
                float fF = ((float)1/(float)(uiSecs))*(float)j;

                float fA = 2*qSqrt(qPow(out[j][0], 2) + qPow(out[j][1], 2))/(float)(oHead.uiFS*uiSecs);

                x.append(fF);
                y.append(fA);

                if(adTargetF.contains(fF))
                {
                    emit sigStability(iRow, fF, i + 1, fA);
                }

                if(x.count() == uiMultiple*2){
                    this->extremum(x, y);
                    x.clear();
                    y.clear();
                }
            }

            qint64 milsec = time.elapsed();

            emit sigMsg(QString("片段:%1/%2| 采样率:%3sps| 分段时长:%4s| 大小:%5MB| 算耗时:%6ms| 读耗时:%7ms")
                        .arg(i + 1)
                        .arg(uiSegCnt)
                        .arg(oHead.uiFS)
                        .arg(uiSecs)
                        .arg(oHead.uiFS*uiSecs*4/8/1024/1024)
                        .arg(milsec)
                        .arg(milsecRead));

            emit sigSpectrum(iRow, QString("%1/%2").arg(i + 1).arg(uiSegCnt), adX, adY);

            adX.clear();
            adY.clear();

            adX.squeeze();
            adY.squeeze();

            emit sigProgress(iRow, 100*(float)(i+1)/(float)uiSegCnt);
        }

        emit sigErr();

        oFile.close();

        fftwf_destroy_plan(p);//销毁策略

        fftwf_cleanup_threads();

        fftwf_free(in);

        fftwf_free(out);
    }
}
/* 自定义x轴刻度 */ 
textTicker = QSharedPointer<QCPAxisTickerText>(new QCPAxisTickerText);
 ui->plotStability->xAxis->setTicker(textTicker);
 textTicker.clear();

 foreach (double dF, adTargetF) {
    textTicker->addTick(dF, QString("%1").arg(dF));
 }
/* 围绕主频分段掐range,避免浪费内存。主频应该是由大到小排列。 */
QList<QPair<double, double> > PF::getRange(QList<double> adF)
{
    QList<QPair<double, double> > aPair;

    for(int i = 0; i < adF.count(); i++){
        QPair<double, double> pair;

        //第一个
        if(i == 0){
            pair.first = adF.at(i)*(1 + OFF_SET);
        }
        else{
            if(adF.at(i)*(1 + OFF_SET) > adF.at(i - 1)){//算出来的值大于上一个主频,则 上限设置成为上一个主频
                pair.first = adF.at(i - 1);
            }
            else{//否则,以计算值为准
                pair.first = adF.at(i)*(1 + OFF_SET);
            }
        }

        if(i == (adF.count() - 1)){//最后一个
            pair.second = adF.at(i)*(1 - OFF_SET);
        }
        else{
            if(adF.at(i)*(1 - OFF_SET) < adF.at(i + 1)){//算出来的值小于下一个主频,则 下限设置成下一个主频
                pair.second = adF.at(i + 1);
            }
            else{//否则,以计算值为准
                pair.second = adF.at(i)*(1 - OFF_SET);
            }
        }

        aPair.append(pair);

        qDebugV0()<<i<<pair.first<<adF.at(i)<<pair.first<<pair.second;
    }

    return aPair;
}
/* 时域曲线展示 */
 TimeDomainWork *poTimeDomainWork = new TimeDomainWork(iRow, oStrFileName, uiStart, uiEnd);
        poTimeDomainWork->setParent(nullptr);

        connect(poTimeDomainWidget, &TimeDomainWidget::sigMsg,      this,  &MainWindow::recvMsg);

        connect(poTimeDomainWork, &TimeDomainWork::sigMsg,      this,  &MainWindow::recvMsg);
        connect(poTimeDomainWork, &TimeDomainWork::sigProgress, this,  &MainWindow::recvProgress);

        connect(poTimeDomainWork, &TimeDomainWork::sigData,     poTimeDomainWidget, &TimeDomainWidget::recvData);

        /* 线程池 */
        QThreadPool::globalInstance()->start(poTimeDomainWork);

C++,面向对象编程,我这是纯面向百度编程。分享出来,也算的一个归纳总结,希望有类似需求的码友多多指正。

/* 2023年10月 更新记录  */

  1. 可通过秒序列和北京时间截取起止时间;
  2. 判断采样率和分段时长是否满足指定主频转换;
  3. 可勾选输出结果的形式;
  4. 可输出每个片段的复数和模(振幅值);
  5. 批量分段傅里叶变换详情、平均值汇总、相对均方误差汇总,可导出成Excel文件;
  6. 编辑时域文件信息(任务名前后缀、线号、点号、传感器编号等);
  7. 将1个时域文件根据指定位置,截成2个时域文件(无源 和 有源);
  8. 多片段分析时,进度条细化到具体某个分段;
  9. 并发线程数目是当前主机最大线程数;
  10. 多处UI控件联动设置;
  11. 批量傅里叶变换时不依赖SQLite数据库暂存数据,改用多维数组存储;
  12. 线程池。

无图无真相。最后上代码片段。

//批量 处理 出平均值结果
void MainWindow::on_pbBatch_clicked()
{
    QList<int> aiRow = this->getSelectedRows();
    if(aiRow.isEmpty()){
        return;
    }

    aiRowSelected.clear();

    aiRowSelected = aiRow;

    int iStart = ui->spinBoxStart->value();
    int iEnd   = ui->spinBoxEnd->value();
    int iStep  = ui->spinBoxStep->value();

    QList<double> adMF;
    adMF.clear();
    adMF = PF::getListF(ui->comboBoxMF->currentIndex(), poSet);

    if(!PF::flOK(iStep, adMF)){
        return;
    }

    //详细值
    if(poTabWidgetDetails != nullptr){
        delete poTabWidgetDetails;
        poTabWidgetDetails = nullptr;
    }

    poTabWidgetDetails = new QTabWidget;

    poTabWidgetDetails->setWindowTitle("分段详情(单位:μV)");
    poTabWidgetDetails->setWindowIcon(QIcon(":/new/prefix1/image/segment.png"));
    poTabWidgetDetails->setMinimumSize(QSize(800, 600));
    poTabWidgetDetails->setIconSize(QSize(32, 32));

    //平均值汇总
    if(poTableWidgetAvg != nullptr){
        delete poTableWidgetAvg;
        poTableWidgetAvg = nullptr;
    }

    poTableWidgetAvg = new QTableWidget;
    poTableWidgetAvg->setRowCount(ui->tableWidget->rowCount());
    poTableWidgetAvg->setColumnCount(1 + adMF.count());

    QStringList headers;

    headers.append("频率(Hz)→");
    foreach(double dF, adMF){
        headers.append(QString::number(dF));
    }

    poTableWidgetAvg->setHorizontalHeaderLabels(headers);
    poTableWidgetAvg->setContextMenuPolicy(Qt::CustomContextMenu);
    poTableWidgetAvg->setSelectionBehavior(QAbstractItemView::SelectRows);     //选中单元格,行还是列
    poTableWidgetAvg->setSelectionMode(QAbstractItemView::ExtendedSelection);    //多选模式
    poTableWidgetAvg->setShowGrid(false);
    poTableWidgetAvg->setStyleSheet("QTableView::item {border: 1px solid black;}");
    //相对均方误差汇总
    if(poTableWidgetErr != nullptr){
        delete poTableWidgetErr;
        poTableWidgetErr = nullptr;
    }

    poTableWidgetErr = new QTableWidget;
    poTableWidgetErr->setRowCount(ui->tableWidget->rowCount());
    poTableWidgetErr->setColumnCount(1 + adMF.count());
    poTableWidgetErr->setHorizontalHeaderLabels(headers);
    poTableWidgetErr->setContextMenuPolicy(Qt::CustomContextMenu);
    poTableWidgetErr->setSelectionBehavior(QAbstractItemView::SelectRows);     //选中单元格,行还是列
    poTableWidgetErr->setSelectionMode(QAbstractItemView::ExtendedSelection);    //多选模式
    poTableWidgetErr->setShowGrid(false);
    poTableWidgetErr->setStyleSheet("QTableView::item {border: 1px solid black;}");

    //平均值和相对均方误差 AVG  &&  ERR
    QTabWidget *poTabWidgetSummary = new QTabWidget;
    poTabWidgetSummary->setWindowTitle("结果汇总");
    poTabWidgetSummary->setWindowIcon(QIcon(":/new/prefix1/image/summary.png"));

    poTabWidgetSummary->setMinimumSize(QSize(1000, 618));
    poTabWidgetSummary->setIconSize(QSize(32, 32));
    poTabWidgetSummary->insertTab(0, poTableWidgetAvg, QIcon(":/new/prefix1/image/avg.png"), "平均值(μV)");
    poTabWidgetSummary->insertTab(1, poTableWidgetErr, QIcon(":/new/prefix1/image/err.png"), "相对均方误差(%)");

    poTabWidgetSummary->show();

    for(int i = 0; i < aiRow.count(); ++i){

            QString oStrFileName = ui->tableWidget->item(aiRow.at(i), HEADER_FILE_NAME)->data(Qt::DisplayRole).toString();

            QComboBox *poComboBoxTag = (QComboBox *)ui->tableWidget->cellWidget(aiRow.at(i), HEADER_COMPONENT);
            QString oStrTag = poComboBoxTag->currentText();

            //重复计算时,应该把上一把的进度清零
            QProgressBar *poProgressBar = (QProgressBar *)ui->tableWidget->cellWidget(aiRow.at(i), HEADER_BAR);

            poProgressBar->setValue(0);

            ui->tableWidget->repaint();

            //每个文件的分段转换结果详情,展示在此表格中
            BatchDFTWidget *poWidget = new BatchDFTWidget(aiRow.at(i), oStrFileName, adMF);

            HEAD oHead = PF::readHead(oStrFileName);

            if(! (PF::fsOK(oHead.uiFS, adMF))){

            this->recvMsg(QString("警告! 文件名: %1, 采样率(%2sps)过低,不满足目标主频需求!").arg(oStrFileName).arg(oHead.uiFS));
            continue;
            }

            QString oStrLabel = QString("第%1行 T(%2)_L(%3)_S(%4)_D(%5)_CH%6_%7")
                                    .arg(aiRow.at(i) + 1)
                                    .arg(oHead.oStrTaskName)
                                    .arg(oHead.oStrLine)
                                    .arg(oHead.oStrSite)
                                    .arg(oHead.uiDev)
                                    .arg(oHead.uiCh)
                                    .arg(oStrTag);

            //poTabWidgetDetails->insertTab(i, poWidget, QIcon(":/new/prefix1/image/fragment.png"), oStrLabel);

            QString oStrLabelPool = QString("T(%1)_L(%2)_S(%3)_D(%4)_CH%5_%6")
                                    .arg(oHead.oStrTaskName)
                                    .arg(oHead.oStrLine)
                                    .arg(oHead.oStrSite)
                                    .arg(oHead.uiDev)
                                    .arg(oHead.uiCh)
                                    .arg(oStrTag);

            poTableWidgetAvg->setItem(aiRow.at(i), 0, new QTableWidgetItem(oStrLabelPool));
            poTableWidgetAvg->item(aiRow.at(i), 0)->setFlags(poTableWidgetAvg->item(aiRow.at(i), 0)->flags() & ~Qt::ItemIsEditable);

            poTableWidgetErr->setItem(aiRow.at(i), 0, new QTableWidgetItem(oStrLabelPool));
            poTableWidgetErr->item(aiRow.at(i), 0)->setFlags(poTableWidgetErr->item(aiRow.at(i), 0)->flags() & ~Qt::ItemIsEditable);

            BatchDFTWorker *poWorker = new BatchDFTWorker(aiRow.at(i), oStrFileName, iStart, iEnd, iStep, adMF);

            //connect(poWorker, &BatchDFTWorker::sigProgress,  this,     &MainWindow::recvProgress);
            //connect(poWorker, &BatchDFTWorker::sigCompleted, poWidget, &BatchDFTWidget::recvCompleted);

            connect(poWorker, &BatchDFTWorker::sigAvg, this, &MainWindow::recvAvg);
            connect(poWorker, &BatchDFTWorker::sigErr, this, &MainWindow::recvErr);

            QThreadPool::globalInstance()->start(poWorker);
    }

    poTableWidgetAvg->resizeColumnsToContents();
    poTableWidgetErr->resizeColumnsToContents();

    poSet->setValue("BATCH_START", iStart);
    poSet->setValue("BATCH_END", iEnd);
    poSet->setValue("BATCH_Step", iStep);
    poSet->setValue("BATCH_MAIN_FREQ", ui->comboBoxMF->currentIndex());

    xlsxResult.insertSheet(0, "振幅平均值");
    xlsxResult.insertSheet(1, "相对均方误差");

    PF::scrollToRigth(ui->tableWidget);
}

之前的UI都是在designer拖出来的。手打 代码写UI 更灵活。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值