试验采集数据,需要查看时间域曲线、频谱计算(含复数结果输出)、查看分段详情、导出计算结果、批量处理大文件。对比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月 更新记录 */
- 可通过秒序列和北京时间截取起止时间;
- 判断采样率和分段时长是否满足指定主频转换;
- 可勾选输出结果的形式;
- 可输出每个片段的复数和模(振幅值);
- 批量分段傅里叶变换详情、平均值汇总、相对均方误差汇总,可导出成Excel文件;
- 编辑时域文件信息(任务名前后缀、线号、点号、传感器编号等);
- 将1个时域文件根据指定位置,截成2个时域文件(无源 和 有源);
- 多片段分析时,进度条细化到具体某个分段;
- 并发线程数目是当前主机最大线程数;
- 多处UI控件联动设置;
- 批量傅里叶变换时不依赖SQLite数据库暂存数据,改用多维数组存储;
- 线程池。
无图无真相。最后上代码片段。
//批量 处理 出平均值结果
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 更灵活。