新增手动寻峰功能

This commit is contained in:
anxinglong 2026-07-16 17:54:53 +08:00
parent 36f05cb098
commit 08eaff9122
10 changed files with 340 additions and 23 deletions

View File

@ -84,7 +84,6 @@ void CountRateAnalysisView::setData(QVector<ParticleInjectTime> data)
const int nVecSize = nAllS / nInv; const int nVecSize = nAllS / nInv;
QVector<double> vec(nVecSize); // 默认初始化为0 QVector<double> vec(nVecSize); // 默认初始化为0
double x_max = 0; double x_max = 0;
double y_max = 0; double y_max = 0;
for(auto info : data) { for(auto info : data) {

View File

@ -159,11 +159,28 @@ bool CustomQwtPlotXaxisSelector::end(bool ok)
plot()->replot(); plot()->replot();
} }
if ( (_max_x - _min_x) > 3 ) { if ( (_max_x - _min_x) > 3 ) {
if(_isShowEnergy)
{
emit selectionFinished(_min_x, _max_x); emit selectionFinished(_min_x, _max_x);
} }
else
{
emit selectionFinishedPeak(_min_x, _max_x);
}
}
return QwtPlotPicker::end(ok); return QwtPlotPicker::end(ok);
} }
bool CustomQwtPlotXaxisSelector::isShowEnergy() const
{
return _isShowEnergy;
}
void CustomQwtPlotXaxisSelector::setIsShowEnergy(bool newIsShowEnergy)
{
_isShowEnergy = newIsShowEnergy;
}
CustomQwtPlot::CustomQwtPlot(QWidget *parent) CustomQwtPlot::CustomQwtPlot(QWidget *parent)
: QwtPlot(parent) : QwtPlot(parent)
{ {

View File

@ -43,9 +43,12 @@ public:
double selectedMaxX() const; double selectedMaxX() const;
void clearSelection(); void clearSelection();
bool isShowEnergy() const;
void setIsShowEnergy(bool newIsShowEnergy);
signals: signals:
void selectionFinished(double min_x, double max_x); void selectionFinished(double min_x, double max_x);
void selectionFinishedPeak(double min_x, double max_x);
protected: protected:
virtual void move(const QPoint& pos) override; virtual void move(const QPoint& pos) override;
virtual bool end(bool ok) override; virtual bool end(bool ok) override;
@ -54,6 +57,7 @@ private:
QwtPlotShapeItem* _overlay = nullptr; QwtPlotShapeItem* _overlay = nullptr;
double _min_x; double _min_x;
double _max_x; double _max_x;
bool _isShowEnergy;
}; };
class CustomQwtPlot : public QwtPlot class CustomQwtPlot : public QwtPlot

View File

@ -20,6 +20,7 @@
#include "DataCalcProcess/FindPeaksBySvd.h" #include "DataCalcProcess/FindPeaksBySvd.h"
#include "DataCalcProcess/GaussPolyCoe.h" #include "DataCalcProcess/GaussPolyCoe.h"
#include "DataCalcProcess/NolinearLeastSquaresCurveFit.h" #include "DataCalcProcess/NolinearLeastSquaresCurveFit.h"
#include "DataCalcProcess/AdaptiveSimpsonIntegrate.h"
#include "EnergyScaleDataModel.h" #include "EnergyScaleDataModel.h"
#include "DataCalcProcess/CoincidenceSpectrumProcess.h" #include "DataCalcProcess/CoincidenceSpectrumProcess.h"
#include "BackgroundTaskListModel.h" #include "BackgroundTaskListModel.h"
@ -588,7 +589,39 @@ bool AutoFindPeaksTask::IsValidSetWorkParameters() const
bool AutoFindPeaksTask::processTask() bool AutoFindPeaksTask::processTask()
{ {
QString result_filename = QDir(this->_result_dir).filePath(QStringLiteral(u"自动寻峰结果.csv")); QString result_filename = QDir(this->_result_dir).filePath(QStringLiteral(u"自动寻峰结果.csv"));
std::ofstream out_file(QStrToSysPath(result_filename)); const std::string sys_result_path = QStrToSysPath(result_filename);
bool need_write_header = false;
{
// 以只读模式打开文件,判断是否为空
std::ifstream check_file(sys_result_path);
if (!check_file.is_open())
{
// 文件不存在,需要写表头
need_write_header = true;
}
else
{
// 文件存在,判断是否为空文件
check_file.seekg(0, std::ios::end);
std::streampos file_size = check_file.tellg();
if (file_size == 0)
{
need_write_header = true;
}
check_file.close();
}
}
std::ofstream out_file(QStrToSysPath(result_filename),std::ios::app);
if (!out_file.is_open())
{
LOG_WARN(QStringLiteral(u"[%1]无法打开寻峰结果文件:%2").arg(this->GetProjectName()).arg(result_filename));
return false;
}
// 仅空文件才写入表头
if (need_write_header)
{
std::string channel_str = QString(QStringLiteral(u"通道")).toStdString(); std::string channel_str = QString(QStringLiteral(u"通道")).toStdString();
std::string addr_str = QString(QStringLiteral(u"峰位")).toStdString(); std::string addr_str = QString(QStringLiteral(u"峰位")).toStdString();
std::string left_addr_str = QString(QStringLiteral(u"左边界")).toStdString(); std::string left_addr_str = QString(QStringLiteral(u"左边界")).toStdString();
@ -599,6 +632,7 @@ bool AutoFindPeaksTask::processTask()
std::string area_str = QString(QStringLiteral(u"峰面积")).toStdString(); std::string area_str = QString(QStringLiteral(u"峰面积")).toStdString();
out_file << channel_str << "," << addr_str << "," << left_addr_str << "," << lright_addr_str out_file << channel_str << "," << addr_str << "," << left_addr_str << "," << lright_addr_str
<< "," << width_str << "," << height_str << "," << fwhm_str << "," << area_str << "\n"; << "," << width_str << "," << height_str << "," << fwhm_str << "," << area_str << "\n";
}
QStringList ch_count_data_name = this->_data_files_set.keys(); QStringList ch_count_data_name = this->_data_files_set.keys();
std::sort(ch_count_data_name.begin(), ch_count_data_name.end(), [](const QString& a, const QString& b) { std::sort(ch_count_data_name.begin(), ch_count_data_name.end(), [](const QString& a, const QString& b) {
@ -1480,3 +1514,166 @@ void RealtimeGvfDataWorker::changeEnergyCountData(QList<ParticleData> &dataList,
LOG_WARN(QStringLiteral(u"部分通道的能量计数数据写入失败")); LOG_WARN(QStringLiteral(u"部分通道的能量计数数据写入失败"));
} }
} }
QString ManualFindPeaksTask::GetTaskName()
{
return QStringLiteral(u"[%1]粒子通道计数谱手动寻峰处理").arg(this->GetProjectName());
}
void ManualFindPeaksTask::SetAnalysisType(AnalysisType analysis_type)
{
this->_analysis_type = analysis_type;
}
void ManualFindPeaksTask::SetDataMap(const QMap<QString, ChannelSelectionData> &data_set)
{
_dataManual = data_set;
}
void ManualFindPeaksTask::SetResultDir(const QString &result_dir)
{
this->_result_dir = result_dir;
}
void ManualFindPeaksTask::SetFindPeakSetpWinWidth(int step_win_width)
{
this->_step_win_width = step_win_width;
}
bool ManualFindPeaksTask::IsValidSetWorkParameters() const
{
return (!this->_dataManual.isEmpty())
&& (!this->_result_dir.isEmpty())
&& (this->_step_win_width > 0)
&& DataProcessTask::IsValidSetWorkParameters();
}
bool ManualFindPeaksTask::processTask()
{
QString result_filename = QDir(this->_result_dir).filePath(QStringLiteral(u"自动寻峰结果.csv"));
const std::string sys_result_path = QStrToSysPath(result_filename);
bool need_write_header = false;
{
// 以只读模式打开文件,判断是否为空
std::ifstream check_file(sys_result_path);
if (!check_file.is_open())
{
need_write_header = true;
}
else
{
check_file.seekg(0, std::ios::end);
std::streampos file_size = check_file.tellg();
if (file_size == 0)
{
need_write_header = true;
}
check_file.close();
}
}
std::ofstream out_file(sys_result_path,std::ios::app);
// 仅空文件才写入表头
if (need_write_header)
{
std::string channel_str = QString(QStringLiteral(u"通道")).toStdString();
std::string addr_str = QString(QStringLiteral(u"峰位")).toStdString();
std::string left_addr_str = QString(QStringLiteral(u"左边界")).toStdString();
std::string lright_addr_str = QString(QStringLiteral(u"右边界")).toStdString();
std::string width_str = QString(QStringLiteral(u"峰宽")).toStdString();
std::string height_str = QString(QStringLiteral(u"峰高")).toStdString();
std::string fwhm_str = QString(QStringLiteral(u"FWHM")).toStdString();
std::string area_str = QString(QStringLiteral(u"峰面积")).toStdString();
out_file << channel_str << "," << addr_str << "," << left_addr_str << "," << lright_addr_str
<< "," << width_str << "," << height_str << "," << fwhm_str << "," << area_str << "\n";
}
// 按通道号排序
QStringList ch_name_list = this->_dataManual.keys();
std::sort(ch_name_list.begin(), ch_name_list.end(), [](const QString& a, const QString& b) {
int num_a = ExtractNumberFromString(a);
int num_b = ExtractNumberFromString(b);
return num_a < num_b;
});
for (const QString& channel_name : ch_name_list) {
const ChannelSelectionData& chData = _dataManual.value(channel_name);
if (chData.x.size() < 6) {
LOG_WARN(QStringLiteral(u"[%1]%2 框选数据点过少(<6),跳过拟合")
.arg(this->GetProjectName()).arg(channel_name));
continue;
}
// 转 arma 格式
arma::vec armaX(chData.x.size()), armaY(chData.x.size());
for (size_t i = 0; i < chData.x.size(); ++i) {
armaX(i) = chData.x[i];
armaY(i) = chData.y[i];
}
try {
arma::vec p0 = EstimatePhotonPeakModelInitialParams(armaX, armaY);
arma::vec p_fit = NolinearLeastSquaresCurveFit::Lsqcurvefit(
PhotonPeakModel, armaX, armaY, p0
);
double amplitude = p_fit(0); // 峰振幅
double sigma = p_fit(1); // 高斯sigma
double center = p_fit(5); // 峰中心(峰位)
double fwhm = sigma * 2.355;
double baseline = p_fit(4); // 本底
double peakHeight = amplitude + baseline; // 峰高 = 振幅 + 本底
int left_bound = static_cast<int>(std::round(center - 3.0 * sigma));
int right_bound = static_cast<int>(std::round(center + 3.0 * sigma));
int peak_width = right_bound - left_bound;
int peak_pos = static_cast<int>(std::round(center));
AdaptiveSimpsonIntegrate::FitFunction fitFunc;
fitFunc.A = p_fit(0);
fitFunc.delt = p_fit(1);
fitFunc.H = p_fit(2);
fitFunc.W = p_fit(3);
fitFunc.C = p_fit(4);
fitFunc.P = p_fit(5);
double lower = fitFunc.P - 3.0 * fitFunc.delt;
double upper = fitFunc.P + 3.0 * fitFunc.delt;
double peak_area = AdaptiveSimpsonIntegrate::integrate(fitFunc, lower, upper);
// 5. 写入CSV
std::string channel_std = channel_name.toStdString();
out_file << channel_std << ","
<< peak_pos << ","
<< left_bound << ","
<< right_bound << ","
<< peak_width << ","
<< static_cast<int>(std::round(peakHeight)) << ","
<< static_cast<int>(std::round(fwhm)) << ","
<< peak_area << "\n";
// LOG_INFO(QStringLiteral(u"[%1]%2 手动寻峰完成:峰位=%3, FWHM=%4, 面积=%5")
// .arg(this->GetProjectName()).arg(channel_name)
// .arg(peak_pos).arg(fwhm, 0, 'f', 2).arg(peak_area, 0, 'f', 2));
} catch (const std::exception& e) {
LOG_WARN(QStringLiteral(u"[%1]%2 手动寻峰拟合异常:%3")
.arg(this->GetProjectName()).arg(channel_name)
.arg(QString::fromStdString(e.what())));
continue;
} catch (...) {
LOG_WARN(QStringLiteral(u"[%1]%2 手动寻峰拟合未知异常")
.arg(this->GetProjectName()).arg(channel_name));
continue;
}
}
out_file.close();
const QString& project_name = GetProjectName();
MeasureAnalysisProjectModel* project_model = ProjectList::Instance()->GetProjectModel(project_name);
if (project_model != nullptr) {
project_model->SetAnalysisCustomData(
this->_analysis_type,
QString("AutoFindPeaksResult"),
result_filename
);
}
LOG_INFO(QStringLiteral(u"[%1]手动框选区域寻峰全部完成.").arg(this->GetProjectName()));
return true;
}

View File

@ -14,6 +14,13 @@
class EnergyScaleDataModel; class EnergyScaleDataModel;
class MeasureAnalysisProjectModel; class MeasureAnalysisProjectModel;
struct ChannelSelectionData {
QString channelName; // 通道名,如"通道1"
QVector<int> x; // 框选范围内的道址
QVector<int> y; // 对应计数
};
namespace DataProcessWorkPool namespace DataProcessWorkPool
{ {
class DataProcessTask : public QRunnable class DataProcessTask : public QRunnable
@ -103,7 +110,7 @@ namespace DataProcessWorkPool
private: private:
virtual bool processTask() override; virtual bool processTask() override;
}; };
//自动寻峰
class AutoFindPeaksTask : public DataProcessTask class AutoFindPeaksTask : public DataProcessTask
{ {
public: public:
@ -121,6 +128,27 @@ namespace DataProcessWorkPool
QString _result_dir; QString _result_dir;
int _step_win_width = 7; int _step_win_width = 7;
}; };
//手动寻峰
class ManualFindPeaksTask : public DataProcessTask
{
public:
QString GetTaskName();
void SetAnalysisType(AnalysisType analysis_type);
// void SetDataFileList(const QMap<QString, QVariant>& data_files_set);
void SetDataMap(const QMap<QString,ChannelSelectionData>& data_set);
void SetResultDir(const QString& result_dir);
void SetFindPeakSetpWinWidth(int step_win_width);
virtual bool IsValidSetWorkParameters() const override;
private:
virtual bool processTask() override;
private:
AnalysisType _analysis_type;
// QMap<QString, QVariant> _data_files_set;
QString _result_dir;
QMap<QString,ChannelSelectionData> _dataManual;
int _step_win_width = 7;
};
class ChannelEnergyScaleFittingTask : public DataProcessTask class ChannelEnergyScaleFittingTask : public DataProcessTask
{ {

View File

@ -290,3 +290,8 @@ QVariantMap FindPeaksResultDialog::peakInfo(QTableWidgetItem *item, bool show_pe
peak_infos["show_peak_area"] = show_peak_area && show_peak; peak_infos["show_peak_area"] = show_peak_area && show_peak;
return peak_infos; return peak_infos;
} }
QTableWidget *FindPeaksResultDialog::peaks_result_table() const
{
return _peaks_result_table;
}

View File

@ -20,11 +20,13 @@ public:
void SetChannelNameList(const QStringList& ch_name_list); void SetChannelNameList(const QStringList& ch_name_list);
void UpdatePeakResult(); void UpdatePeakResult();
QAbstractTableModel *GetPeakResultDataModel() const; QAbstractTableModel *GetPeakResultDataModel() const;
private:
void saveCurrentPeakResult(); void saveCurrentPeakResult();
QVariantMap peakInfo(QTableWidgetItem* item, bool show_peak, bool show_peak_area); QVariantMap peakInfo(QTableWidgetItem* item, bool show_peak, bool show_peak_area);
QTableWidget *peaks_result_table() const;
private:
signals: signals:
void peakInfoChanged(QVariantMap peak_infos); void peakInfoChanged(QVariantMap peak_infos);

View File

@ -1,6 +1,5 @@
#include "ParticleCountPlotView.h" #include "ParticleCountPlotView.h"
#include "CustomQwtPlot.h" #include "CustomQwtPlot.h"
#include "DataProcessWorkPool.h"
#include "MeasureAnalysisProjectModel.h" #include "MeasureAnalysisProjectModel.h"
#include "csv.h" #include "csv.h"
#include <GlobalDefine.h> #include <GlobalDefine.h>
@ -161,7 +160,7 @@ void ParticleCountPlotView::setupMenu()
connect(action_auto_find_peaks, &QAction::triggered, this, &ParticleCountPlotView::onActionAutoFindPeaks); connect(action_auto_find_peaks, &QAction::triggered, this, &ParticleCountPlotView::onActionAutoFindPeaks);
QAction* action_manual_find_peaks = menu_find_peaks->addAction(QStringLiteral(u"手动寻峰")); QAction* action_manual_find_peaks = menu_find_peaks->addAction(QStringLiteral(u"手动寻峰"));
action_manual_find_peaks->setObjectName("manual_find_peaks"); action_manual_find_peaks->setObjectName("manual_find_peaks");
connect(action_auto_find_peaks, &QAction::triggered, this, &ParticleCountPlotView::onActionManualFindPeaks); connect(action_manual_find_peaks, &QAction::triggered, this, &ParticleCountPlotView::onActionManualFindPeaks);
QAction* action_set_energy_scale = this->_menu->addAction(QStringLiteral(u"能量刻度")); QAction* action_set_energy_scale = this->_menu->addAction(QStringLiteral(u"能量刻度"));
action_set_energy_scale->setObjectName("energy_scale"); action_set_energy_scale->setObjectName("energy_scale");
connect(action_set_energy_scale, &QAction::triggered, this, &ParticleCountPlotView::onActionEnergyScale); connect(action_set_energy_scale, &QAction::triggered, this, &ParticleCountPlotView::onActionEnergyScale);
@ -205,6 +204,7 @@ void ParticleCountPlotView::setupPlot()
_data_selector = new CustomQwtPlotXaxisSelector(_plot->canvas()); _data_selector = new CustomQwtPlotXaxisSelector(_plot->canvas());
_data_selector->setEnabled(false); _data_selector->setEnabled(false);
connect(_data_selector,&CustomQwtPlotXaxisSelector::selectionFinishedPeak,this,&ParticleCountPlotView::onManualPeakRangeSelected);
} }
void ParticleCountPlotView::setupPeakResultDlg() void ParticleCountPlotView::setupPeakResultDlg()
@ -316,6 +316,8 @@ void ParticleCountPlotView::updatePlotPeakInfo(const QVariantMap &peak_infos)
this->_plot->replot(); this->_plot->replot();
} }
void ParticleCountPlotView::onAutoFindPeaksFinished(bool ok, const QString &project_name, const QVariant& data) void ParticleCountPlotView::onAutoFindPeaksFinished(bool ok, const QString &project_name, const QVariant& data)
{ {
Q_UNUSED(project_name); Q_UNUSED(project_name);
@ -460,7 +462,65 @@ void ParticleCountPlotView::onActionAutoFindPeaks()
void ParticleCountPlotView::onActionManualFindPeaks() void ParticleCountPlotView::onActionManualFindPeaks()
{ {
// _data_selector->setEnabled(true); if (!_data_selector){
return;
}
_data_selector->setEnabled(true);
_data_selector->setIsShowEnergy(false);
// 自动打开寻峰结果表格方便查看新增峰
if (_find_peaks_result_dlg)
_find_peaks_result_dlg->show();
}
void ParticleCountPlotView::onManualPeakRangeSelected(double minX, double maxX)
{
// 关闭框选工具
_data_selector->setEnabled(false);
// 获取32个通道的框选数据
QMap<QString, ChannelSelectionData> allData = getSelectedRangeData(minX, maxX);
auto manual_find_peaks_task = new DataProcessWorkPool::ManualFindPeaksTask;
const QString& project_name = GetProjectName();
manual_find_peaks_task->SetAnalysisType(this->GetAnalyzeType());
// manual_find_peaks_task->SetDataFileList(this->_data_files_set_ptr);
manual_find_peaks_task->SetFindPeakSetpWinWidth(35);
manual_find_peaks_task->SetResultDir(this->_workspace);
manual_find_peaks_task->SetDataMap(allData);
manual_find_peaks_task->SetFinishedNotifier(this, "onAutoFindPeaksFinished", project_name);
manual_find_peaks_task->StartTask();
}
QMap<QString, ChannelSelectionData> ParticleCountPlotView::getSelectedRangeData(double minX, double maxX)
{
QMap<QString, ChannelSelectionData> result;
// 获取所有曲线
QList<QwtPlotCurve*> curveList = _plot->GetCurveList();
for (QwtPlotCurve* curve : curveList) {
const QString& chName = curve->title().text();
ChannelSelectionData chData;
chData.channelName = chName;
// 取曲线原始样本
const QwtSeriesData<QPointF>& series = *curve->data();
size_t total = series.size();
for (size_t i = 0; i < total; ++i) {
double xVal = series.sample(i).x();
double yVal = series.sample(i).y();
// 筛选在框选范围内的点(含边界)
if (xVal >= minX && xVal <= maxX) {
chData.x.push_back(xVal);
chData.y.push_back(yVal);
}
}
result.insert(chName, chData);
}
return result;
} }
void ParticleCountPlotView::onActionEnergyScale() void ParticleCountPlotView::onActionEnergyScale()
@ -468,6 +528,8 @@ void ParticleCountPlotView::onActionEnergyScale()
if (_batch_energy_scale_dlg && _data_selector) { if (_batch_energy_scale_dlg && _data_selector) {
_batch_energy_scale_dlg->show(); _batch_energy_scale_dlg->show();
_data_selector->setEnabled(true); _data_selector->setEnabled(true);
_data_selector->setIsShowEnergy(true);
} }
} }

View File

@ -5,6 +5,7 @@
#include <QWidget> #include <QWidget>
#include <QDialog> #include <QDialog>
#include "MeasureAnalysisView.h" #include "MeasureAnalysisView.h"
#include "DataProcessWorkPool.h"
class QDialog; class QDialog;
class QMenu; class QMenu;
@ -15,6 +16,7 @@ class CustomQwtPlotXaxisSelector;
class BatchEnergyScaleDialog; class BatchEnergyScaleDialog;
class FindPeaksResultDialog; class FindPeaksResultDialog;
class ParticleCountPlotView : public MeasureAnalysisView class ParticleCountPlotView : public MeasureAnalysisView
{ {
Q_OBJECT Q_OBJECT
@ -35,6 +37,8 @@ private:
void loadDataFromFile(const QString &data_name, const QString& filename); void loadDataFromFile(const QString &data_name, const QString& filename);
void loadPeaksResultToTable(QTableWidget* peaks_result_table); void loadPeaksResultToTable(QTableWidget* peaks_result_table);
void updatePlotPeakInfo(const QVariantMap& peak_infos); void updatePlotPeakInfo(const QVariantMap& peak_infos);
void onManualPeakRangeSelected(double minX, double maxX);
QMap<QString, ChannelSelectionData> getSelectedRangeData(double minX, double maxX);
private slots: private slots:
void onAutoFindPeaksFinished(bool ok, const QString &project_name, const QVariant& data); void onAutoFindPeaksFinished(bool ok, const QString &project_name, const QVariant& data);
@ -45,7 +49,6 @@ private slots:
void onActionManualFindPeaks(); void onActionManualFindPeaks();
void onActionEnergyScale(); void onActionEnergyScale();
void onActionPlotConfigure(); void onActionPlotConfigure();
signals: signals:
void applyEnergyScale(); void applyEnergyScale();