EnergySpectrumAnalyer/src/EnergyCountPeakFitView/EnergyCountPeakFitView.cpp

1659 lines
62 KiB
C++
Raw Normal View History

#include "EnergyCountPeakFitView.h"
#include "CustomQwtPlot.h"
#include "PlotRectItem.h"
#include "csv.h"
#include <GlobalDefine.h>
#include <QCheckBox>
#include <QDebug>
#include <QDialog>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QMenu>
#include <QPushButton>
#include <QwtLegend>
#include <QwtPlotCanvas>
#include <QwtPlotCurve>
#include <QwtPlotMarker>
#include <QwtScaleDiv>
#include <QwtScaleMap>
#include <QwtText>
#include <QDialog>
#include <QPushButton>
#include <QCheckBox>
#include <QwtScaleDiv>
#include <QFileInfo>
2026-04-15 18:17:16 +08:00
#include "PlotRectItem.h"
#include <QInputDialog>
#include <QDebug>
#include <QPainter>
#include <QMessageBox>
#include <QListWidget>
#include <QDateTime>
#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDir>
#include <QTableView>
#include <QStandardItemModel>
#include <QHeaderView>
#include "MeasureAnalysisProjectModel.h"
#include "BusyIndicator.h"
QJsonObject PeakFitHistoryItem::toJson() const
{
QJsonObject obj;
obj["timestamp"] = timestamp.toString(Qt::ISODate);
QJsonArray curvesJson;
for (const auto& curve : curveList) {
QJsonObject curveObj;
curveObj["amplitude"] = curve.amplitude;
curveObj["sigma"] = curve.sigma;
curveObj["sigmoidH"] = curve.sigmoidH;
curveObj["sigmoidW"] = curve.sigmoidW;
curveObj["baseline"] = curve.baseline;
curveObj["center"] = curve.center;
curveObj["xMin"] = curve.xMin;
curveObj["xMax"] = curve.xMax;
curveObj["fwhm"] = curve.fwhm;
curveObj["area"] = curve.area;
//保存框选区域
if (!curve.selectionRect.isNull()) {
QJsonObject rectObj;
rectObj["left"] = curve.selectionRect.left();
rectObj["top"] = curve.selectionRect.top();
rectObj["right"] = curve.selectionRect.right();
rectObj["bottom"] = curve.selectionRect.bottom();
curveObj["selectionRect"] = rectObj;
}
curvesJson.append(curveObj);
}
obj["curves"] = curvesJson;
return obj;
}
PeakFitHistoryItem PeakFitHistoryItem::fromJson(const QJsonObject& obj)
{
PeakFitHistoryItem item;
item.timestamp = QDateTime::fromString(obj["timestamp"].toString(), Qt::ISODate);
QJsonArray curvesJson = obj["curves"].toArray();
for (const auto& value : curvesJson) {
FitCurveData curve;
QJsonObject curveObj = value.toObject();
curve.amplitude = curveObj["amplitude"].toDouble();
curve.sigma = curveObj["sigma"].toDouble();
curve.sigmoidH = curveObj["sigmoidH"].toDouble();
curve.sigmoidW = curveObj["sigmoidW"].toDouble();
curve.baseline = curveObj["baseline"].toDouble();
curve.center = curveObj["center"].toDouble();
curve.xMin = curveObj["xMin"].toDouble();
curve.xMax = curveObj["xMax"].toDouble();
curve.fwhm = curveObj["fwhm"].toDouble(0.0);
curve.area = curveObj["area"].toDouble(0.0);
//加载框选区域
if (curveObj.contains("selectionRect")) {
QJsonObject rectObj = curveObj["selectionRect"].toObject();
curve.selectionRect = QRectF(
rectObj["left"].toDouble(),
rectObj["top"].toDouble(),
rectObj["right"].toDouble() - rectObj["left"].toDouble(),
rectObj["bottom"].toDouble() - rectObj["top"].toDouble()
);
}
item.curveList.append(curve);
}
return item;
}
EnergyCountPeakFitView::EnergyCountPeakFitView(QWidget* parent)
: MeasureAnalysisView { parent }
{
this->setViewType(PlotFrame);
QHBoxLayout* layout = new QHBoxLayout(this);
this->_plot = new CustomQwtPlot(this);
layout->addWidget(this->_plot);
setupPlot();
this->_menu = new QMenu(this);
setupMenu();
}
EnergyCountPeakFitView::~EnergyCountPeakFitView()
{
LOG_DEBUG(QStringLiteral(u"%1析构.").arg(this->GetViewName()));
2026-04-15 18:17:16 +08:00
delete _rubberBand;
clearAllSelectionRects();
clearFitCurves();
}
void EnergyCountPeakFitView::InitViewWorkspace(const QString& project_name)
{
Q_UNUSED(project_name);
if (project_name.isEmpty())
return;
auto project_model = ProjectList::Instance()->GetProjectModel(project_name);
if (!project_model)
return;
QDir project_dir(project_model->GetProjectDir());
_workspace = project_dir.filePath(this->GetViewName());
if (!QDir(_workspace).exists())
project_dir.mkpath(_workspace);
_historyFilePath = QDir(_workspace).filePath("peak_fit_history.json");
loadHistoryFromFile();
}
void EnergyCountPeakFitView::SetAnalyzeDataFilename(const QMap<QString, QVariant>& data_files_set)
{
if (!data_files_set.isEmpty()) {
const QString& data_name = data_files_set.firstKey();
const QString& data_filename = data_files_set.first().toString();
if (QFileInfo(data_filename).exists()) {
loadDataFromFile(data_name, data_filename);
}
}
}
void EnergyCountPeakFitView::setupPlot()
{
_plot->setCanvasBackground(Qt::white);
QwtPlotCanvas* canvas = qobject_cast<QwtPlotCanvas*>(_plot->canvas());
canvas->setFrameStyle(QFrame::NoFrame);
QFont font = this->font();
font.setBold(false);
QwtText energy_label = QStringLiteral(u"能量(KeV)");
energy_label.setFont(font);
QwtText count_label = QStringLiteral(u"计数");
count_label.setFont(font);
_plot->setAxisTitle(QwtPlot::xBottom, energy_label);
_plot->setAxisTitle(QwtPlot::yLeft, count_label);
// set axis auto scale
_plot->setAxisAutoScale(QwtPlot::xBottom, true);
_plot->setAxisAutoScale(QwtPlot::yLeft, true);
_plot->enableAxis(QwtPlot::xBottom);
_plot->enableAxis(QwtPlot::yLeft);
// 设置QWT图例
QwtLegend* legend = new QwtLegend();
legend->setDefaultItemMode(QwtLegendData::ReadOnly);
_plot->insertLegend(legend, QwtPlot::RightLegend);
_plot->SetAxisDragScale(QwtPlot::xBottom, true);
QwtPlotCurve *dummyFit = new QwtPlotCurve(QStringLiteral(u"拟合数据"));
dummyFit->setPen(QPen(Qt::blue, 2));
dummyFit->setSamples(QVector<double>(), QVector<double>()); // 无数据
dummyFit->setVisible(false); // 不在画布上绘制
dummyFit->setItemAttribute(QwtPlotItem::Legend, true); // 保证图例显示(默认 true
dummyFit->attach(_plot);
// 本底数据(虚拟,图例用)
QwtPlotCurve *dummyBg = new QwtPlotCurve(QStringLiteral(u"本底数据"));
dummyBg->setPen(QPen(Qt::yellow, 2));
dummyBg->setSamples(QVector<double>(), QVector<double>());
dummyBg->setVisible(false);
dummyBg->attach(_plot);
2026-04-15 18:17:16 +08:00
_data_selector = new CustomQwtPlotXaxisSelector(_plot->canvas());
_data_selector->setEnabled(false);
2026-04-15 18:17:16 +08:00
// 启用鼠标追踪,并安装事件过滤器
_plot->canvas()->setMouseTracking(true);
_plot->canvas()->installEventFilter(this);
}
void EnergyCountPeakFitView::setupMenu()
{
this->setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &EnergyCountPeakFitView::customContextMenuRequested, [this](const QPoint& pos) {
this->_menu->exec(this->mapToGlobal(pos));
});
QAction* action_plot_reset = this->_menu->addAction(QStringLiteral(u"还原"));
action_plot_reset->setObjectName("plot_reset");
connect(action_plot_reset, &QAction::triggered, [this]() {
this->_plot->ResetPlot();
});
this->_menu->addSeparator();
2026-05-12 20:52:43 +08:00
//QAction* action_set_curve_show = this->_menu->addAction(QStringLiteral(u"选择曲线"));
//action_set_curve_show->setObjectName("curve_show_setting");
//connect(action_set_curve_show, &QAction::triggered, this, &EnergyCountPeakFitView::onActionCurveShowSetting);
QAction* action_plot_config = this->_menu->addAction(QStringLiteral(u"图表配置"));
action_plot_config->setObjectName("plot_config");
connect(action_plot_config, &QAction::triggered, this, &EnergyCountPeakFitView::onActionPlotConfigure);
2026-04-15 18:17:16 +08:00
this->_menu->addSeparator();
QAction* action_refit_rect = this->_menu->addAction(QStringLiteral(u"重新拟合当前区域"));
connect(action_refit_rect, &QAction::triggered, this, &EnergyCountPeakFitView::onActionRefitCurrentRect);
2026-05-12 20:52:43 +08:00
//QAction* action_save_fit = this->_menu->addAction(QStringLiteral(u"保存当前拟合结果"));
//connect(action_save_fit, &QAction::triggered, this, &EnergyCountPeakFitView::onActionSaveCurrentFit);
QAction* action_show_history = this->_menu->addAction(QStringLiteral(u"峰拟合结果"));
connect(action_show_history, &QAction::triggered, this, &EnergyCountPeakFitView::onActionShowFitHistory);
//删除当前悬停的框选区域
QAction* action_delete_hovered_rect = this->_menu->addAction(QStringLiteral(u"删除当前框选区域"));
connect(action_delete_hovered_rect, &QAction::triggered, this, &EnergyCountPeakFitView::onActionDeleteHoveredRect);
2026-05-12 20:52:43 +08:00
//QAction* action_hint = this->_menu->addAction(QStringLiteral(u"框选提示:按住 Ctrl 键并拖动左键"));
//action_hint->setEnabled(false);
}
void EnergyCountPeakFitView::loadDataFromFile(const QString& data_name, const QString& filename)
{
//加载新数据前清空旧数据
_currentFitRecords.clear();
clearFitCurves();
clearAllSelectionRects();
_plot->replot();
std::string address_str = QString(QStringLiteral(u"能量(KeV)")).toStdString();
std::string count_str = QString(QStringLiteral(u"计数")).toStdString();
io::CSVReader<
2,
io::trim_chars<' ', '\t'>,
io::double_quote_escape<',', '"'>,
io::throw_on_overflow,
io::empty_line_comment>
reader(QStrToSysPath(filename));
reader.read_header(io::ignore_extra_column, address_str, count_str);
double energy;
unsigned long long energy_count;
QVector<double> x, y;
while (reader.read_row(energy, energy_count)) {
x.push_back(energy);
y.push_back(energy_count);
}
// 绘制曲线
QwtPlotCurve* curve = new QwtPlotCurve(QStringLiteral(u"原始数据"));
curve->setPen(QPen(Qt::gray, 2)); // 原始数据统一灰色
curve->setSamples(x, y);
_plot->AddCurve(curve,false);
}
void EnergyCountPeakFitView::onActionCurveShowSetting()
{
if (!_curve_show_setting_dlg) {
_curve_show_setting_dlg = new QDialog(this, Qt::Dialog | Qt::WindowCloseButtonHint);
_curve_show_setting_dlg->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
_curve_show_setting_dlg->setSizeGripEnabled(false);
_curve_show_setting_dlg->setWindowTitle(QString(QStringLiteral(u"选择%1曲线显示").arg(this->_plot->title().text())));
_curve_show_setting_dlg->setWindowModality(Qt::WindowModal);
_curve_show_setting_dlg->setModal(false);
QVBoxLayout* layout = new QVBoxLayout(_curve_show_setting_dlg);
QStringList curve_name_list;
for (QwtPlotCurve* curve : this->_plot->GetCurveList()) {
curve_name_list.append(curve->title().text());
}
// 自动计算多列排布
int num_columns = std::sqrt(this->_plot->GetCurveList().size());
if (num_columns == 0)
num_columns = 1;
QVBoxLayout* checkbox_layout = new QVBoxLayout();
QHBoxLayout* checkbox_column_layout = new QHBoxLayout();
QMap<QwtPlotCurve*, QCheckBox*> curve_checkbox_map;
auto addCurveToLayout = [&](const QString& curve_name) {
QwtPlotCurve* curve = this->_plot->GetCurve(curve_name);
QCheckBox* check_box = new QCheckBox(curve->title().text());
check_box->setChecked(curve->isVisible());
checkbox_column_layout->addWidget(check_box);
connect(check_box, &QCheckBox::stateChanged, [curve](int state) {
curve->setVisible(state == Qt::Checked);
curve->plot()->replot();
});
curve_checkbox_map[curve] = check_box;
if (checkbox_column_layout->count() >= num_columns) {
checkbox_layout->addLayout(checkbox_column_layout);
checkbox_column_layout = new QHBoxLayout();
}
};
for (const QString& ch_name : curve_name_list) {
addCurveToLayout(ch_name);
}
if (checkbox_column_layout->count() < num_columns) {
checkbox_column_layout->addStretch();
}
checkbox_layout->addLayout(checkbox_column_layout);
// 全选和反选
auto curveCheckboxUpdate = [this, curve_checkbox_map]() {
for (QwtPlotCurve* curve : this->_plot->GetCurveList()) {
curve_checkbox_map[curve]->setChecked(curve->isVisible());
}
};
QHBoxLayout* button_layout = new QHBoxLayout();
QPushButton* btn_all_select = new QPushButton(QString(QStringLiteral(u"全选")));
connect(btn_all_select, &QPushButton::clicked, [this, curveCheckboxUpdate]() {
for (QwtPlotCurve* curve : this->_plot->GetCurveList()) {
curve->setVisible(true);
}
curveCheckboxUpdate();
this->_plot->replot();
});
button_layout->addWidget(btn_all_select);
QPushButton* btn_reserve_select = new QPushButton(QString(QStringLiteral(u"反选")));
connect(btn_reserve_select, &QPushButton::clicked, [this, curveCheckboxUpdate]() {
for (QwtPlotCurve* curve : this->_plot->GetCurveList()) {
curve->setVisible(!curve->isVisible());
}
curveCheckboxUpdate();
this->_plot->replot();
});
button_layout->addWidget(btn_reserve_select);
layout->addLayout(button_layout);
layout->addLayout(checkbox_layout);
}
_curve_show_setting_dlg->show();
}
void EnergyCountPeakFitView::onActionPlotConfigure()
{
2026-04-15 18:17:16 +08:00
}
void EnergyCountPeakFitView::clearAllSelectionRects()
{
for (PlotRectItem* item : _selectionRectItems) {
if (item) {
item->detach();
delete item;
}
}
_selectionRectItems.clear();
_plot->replot();
2026-04-15 18:17:16 +08:00
}
bool EnergyCountPeakFitView::eventFilter(QObject* watched, QEvent* event)
{
if (watched == _plot->canvas()) {
QMouseEvent* me = static_cast<QMouseEvent*>(event);
// 按下 Ctrl+左键 开始框选
if (event->type() == QEvent::MouseButtonPress &&
2026-05-12 20:52:43 +08:00
me->button() == Qt::LeftButton /*&&*/
/*(me->modifiers() & Qt::ControlModifier)*/) {
2026-04-15 18:17:16 +08:00
startSelection(me->pos());
2026-04-15 18:17:16 +08:00
return true;
}
// 移动时更新橡皮筋
else if (event->type() == QEvent::MouseMove && _isSelecting) {
updateSelection(me->pos());
return true;
}
// 释放左键完成框选
else if (event->type() == QEvent::MouseButtonRelease &&
me->button() == Qt::LeftButton && _isSelecting) {
finishSelection();
fadeSelectionRectBorders();
2026-04-15 18:17:16 +08:00
return true;
}
//鼠标移动时检测悬停
else if (event->type() == QEvent::MouseMove) {
updateHoverState(me->pos());
return true;
}
2026-04-15 18:17:16 +08:00
}
return MeasureAnalysisView::eventFilter(watched, event);
}
void EnergyCountPeakFitView::startSelection(const QPoint& pos)
{
_isSelecting = true;
_selectionStart = pos;
2026-04-15 18:17:16 +08:00
if (!_rubberBand) {
_rubberBand = new QRubberBand(QRubberBand::Rectangle, _plot->canvas());
}
int canvasHeight = _plot->canvas()->height();
// 初始矩形宽度为0高度为画布全高
_rubberBand->setGeometry(QRect(_selectionStart.x(), 0, 0, canvasHeight));
_rubberBand->show();
2026-04-15 18:17:16 +08:00
}
void EnergyCountPeakFitView::updateSelection(const QPoint& pos)
{
if (!_rubberBand)
return;
int canvasHeight = _plot->canvas()->height();
int x1 = _selectionStart.x();
int x2 = pos.x();
int left = qMin(x1, x2);
int width = qAbs(x1 - x2);
_rubberBand->setGeometry(QRect(left, 0, width, canvasHeight));
2026-04-15 18:17:16 +08:00
}
void EnergyCountPeakFitView::finishSelection()
{
if (_rubberBand)
{
QRect finalRect = _rubberBand->geometry();
_rubberBand->hide();
if (finalRect.width() > 2) {
const QwtScaleMap xMap = _plot->canvasMap(QwtPlot::xBottom);
double xMin = xMap.invTransform(finalRect.left());
double xMax = xMap.invTransform(finalRect.right());
if (xMin > xMax) std::swap(xMin, xMax);
double yMin = _plot->axisScaleDiv(QwtPlot::yLeft).lowerBound();
double yMax = _plot->axisScaleDiv(QwtPlot::yLeft).upperBound();
QRectF plotRect(xMin, yMin, xMax - xMin, yMax - yMin);
// addSelectionRect(plotRect);
// 获取曲线数据
QList<QwtPlotCurve*> curves = _plot->GetCurveList();
if (!curves.isEmpty()) {
QwtPlotCurve* originalCurve = curves.first();
QVector<double> origX, origY;
for (size_t i = 0; i < originalCurve->dataSize(); ++i) {
origX.push_back(originalCurve->sample(i).x());
origY.push_back(originalCurve->sample(i).y());
}
QVector<double> roiX, roiY;
for (int i = 0; i < origX.size(); ++i) {
if (origX[i] >= xMin && origX[i] <= xMax) {
roiX.push_back(origX[i]);
roiY.push_back(origY[i]);
}
}
if (roiX.size() >= 3) {
QStringList algorithms;
2026-05-12 20:52:43 +08:00
algorithms << QStringLiteral(u"y=A*exp(-pow(x-P,2)/(2*pow(delt,2))) + H/(1+exp((x-P)/W)) + C");
bool ok = false;
QString selected = QInputDialog::getItem(this,
QStringLiteral(u"选择拟合算法"),
QStringLiteral(u"请选择用于当前框选区域的峰拟合算法:"),
algorithms,
0,
false,
&ok);
2026-05-12 20:52:43 +08:00
if (ok && selected == QStringLiteral(u"y=A*exp(-pow(x-P,2)/(2*pow(delt,2))) + H/(1+exp((x-P)/W)) + C"))
{
arma::vec armaRoiX(roiX.size()), armaRoiY(roiY.size());
for (int i = 0; i < roiX.size(); ++i) {
armaRoiX(i) = roiX[i];
armaRoiY(i) = roiY[i];
}
// 3. 执行拟合(曲线自动添加到主图)
QList<PeakFitResult> results = performPeakFitting(roiX, roiY/*, &userP0*/);
if (results.isEmpty()) {
qDebug()<< QStringLiteral(u"拟合结果:") << QStringLiteral(u"未检测到有效峰或拟合失败。");
} else {
addSelectionRect(plotRect);
fadeSelectionRectBorders(); // 框选边框变为虚线(与原逻辑一致)
if (!_selectionRectItems.isEmpty() && !results.isEmpty())
{
PlotRectItem* lastRect = _selectionRectItems.last();
const PeakFitResult& r = results.first();
// 传入峰位、FWHM、面积、本底
lastRect->setPeakData(r.center, r.fwhm, r.area, r.baseline);
_plot->replot(); // 刷新显示
}
saveCurrentFitToHistory();
}
}
} else {
qDebug() << QStringLiteral(u"框选区域内数据点不足3个无法拟合。");
}
}
}
}
_isSelecting = false;
}
2026-04-15 18:17:16 +08:00
void EnergyCountPeakFitView::addSelectionRect(const QRectF& plotRect)
{
PlotRectItem* rectItem = new PlotRectItem("Selection");
rectItem->setAxes(QwtPlot::xBottom, QwtPlot::yLeft);
rectItem->setRect(plotRect);
// 设置填充为透明(无填充)
rectItem->setBrush(QBrush(Qt::transparent)); // 或 Qt::NoBrush
// 设置边框颜色为红色线宽2
rectItem->setPen(QPen(Qt::red, 2));
rectItem->setSelectionType("current");
rectItem->setSelectionIndex(_selectionRectItems.size());
rectItem->attach(_plot);
_selectionRectItems.append(rectItem);
_plot->replot();
}
void EnergyCountPeakFitView::fadeSelectionRectBorders()
{
for (PlotRectItem* item : _selectionRectItems) {
if (item) {
QPen fadedPen(Qt::red, 1, Qt::DashLine); // 可根据原样式调整
item->setPen(fadedPen);
}
}
_plot->replot();
}
QList<PeakFitResult> EnergyCountPeakFitView::performPeakFitting(const QVector<double> &x, const QVector<double> &y)
{
QList<PeakFitResult> results;
if (x.size() != y.size() || x.size() < 3)
{
qDebug() << QStringLiteral(u"数据长度不足");
return results;
}
// 转换为 Armadillo 向量
arma::vec armaX(x.size()), armaY(y.size());
for (int i = 0; i < x.size(); ++i) {
armaX(i) = x[i];
armaY(i) = y[i];
}
arma::vec p0 = EstimatePhotonPeakModelInitialParams(armaX, armaY);
arma::vec p_fit = NolinearLeastSquaresCurveFit::Lsqcurvefit(PhotonPeakModel, armaX, armaY, p0);
double sigma = p_fit(1);
double fwhm = sigma * 2.355;
FitParams defaultParams;
defaultParams.A = p_fit(0);
defaultParams.delt = p_fit(1);
defaultParams.H = p_fit(2);
defaultParams.W = p_fit(3);
defaultParams.C = p_fit(4);
defaultParams.P = p_fit(5);
PeakFitParamsDialog paramDlg(defaultParams, this);
if (paramDlg.exec() != QDialog::Accepted)
return results; // 用户取消
FitParams userParams = paramDlg.getParams();
AdaptiveSimpsonIntegrate::FitFunction fitFunc;
fitFunc.A = userParams.A ;//p_fit(0);
fitFunc.delt= userParams.delt;//p_fit(1);
fitFunc.H = userParams.H ;//p_fit(2);
fitFunc.W = userParams.W ;//p_fit(3);
fitFunc.C = userParams.C ;//p_fit(4);
fitFunc.P = userParams.P ;//p_fit(5);
double lower = fitFunc.P - 3.0 * fitFunc.delt;
double upper = fitFunc.P + 3.0 * fitFunc.delt;
double area = AdaptiveSimpsonIntegrate::integrate(fitFunc, lower, upper);
PeakFitResult result;
result.center = p_fit(5);
result.amplitude = p_fit(0);
result.sigma = p_fit(1);
result.fwhm = fwhm;
result.area = area;
result.baseline = p_fit(4);
result.sigmoidH = p_fit(2);
result.sigmoidW = p_fit(3);
QList<QwtPlotCurve*> curves = createFitCurve(result, armaX);
for (QwtPlotCurve* c : curves) {
_fitCurves.append(c);
}
results.append(result);
_plot->replot();
2026-04-15 18:17:16 +08:00
_lastFitResult = result;
_lastFitParams = p_fit;
_hasLastFit = true;
CurrentFitRecord record;
record.result = result;
record.params = p_fit;
record.xMin = armaX.min(); // 记录X范围
record.xMax = armaX.max();
_currentFitRecords.append(record); // 加入当前多曲线列表
return results;
2026-04-15 18:17:16 +08:00
}
QwtPlotCurve* EnergyCountPeakFitView::createFitCurve(const PeakFitResult& result, double xMin, double xMax, const QString& name)
2026-04-15 18:17:16 +08:00
{
const int numPoints = 200;
QVector<double> xs, ys,ysTw;
double step = (xMax - xMin) / (numPoints - 1);
arma::vec p(6);
p(0) = result.amplitude;
p(1) = result.sigma;
p(2) = result.sigmoidH;
p(3) = result.sigmoidW;
p(4) = result.baseline;
p(5) = result.center;
for (int i = 0; i < numPoints; ++i) {
double x = xMin + i * step;
xs.append(x);
double y = PhotonPeakModel(x, p);
ys.append(y);
}
QwtPlotCurve* curve = new QwtPlotCurve(QStringLiteral(u"拟合数据"));
curve->setPen(QPen(Qt::blue, 2));
curve->setSamples(xs, ys);
curve->attach(_plot);
return curve;
2026-04-15 18:17:16 +08:00
}
QList<QwtPlotCurve *> EnergyCountPeakFitView::createFitCurve(const PeakFitResult &result, arma::vec xVec)
{
QList<QwtPlotCurve*> curveList; // 用于返回
QVector<double> xs, ys ,ysTw;
arma::vec p(6);
p(0) = result.amplitude;
p(1) = result.sigma;
p(2) = result.sigmoidH;
p(3) = result.sigmoidW;
p(4) = result.baseline;
p(5) = result.center;
for (int i = 0; i < xVec.size(); ++i) {
double x = xVec.at(i);
xs.append(x);
double y = PhotonPeakModel(x, p);
ys.append(y);
}
arma::vec yTw = PhotonPeakModelTuowei(xVec,p);
for (int i = 0;i<yTw.size();++i)
{
double y =yTw.at(i);
ysTw.append(y);
}
QwtPlotCurve* curve = new QwtPlotCurve(QStringLiteral(u"拟合数据"));
curve->setPen(QPen(Qt::blue, 2));
curve->setSamples(xs, ys);
curve->setItemAttribute(QwtPlotItem::Legend, false);
curve->attach(_plot);
curveList.append(curve); // 加入列表
QwtPlotCurve* curveTw = new QwtPlotCurve(QStringLiteral(u"本底数据"));
curveTw->setPen(QPen(Qt::yellow, 2));
curveTw->setSamples(xs, ysTw);
curveTw->setItemAttribute(QwtPlotItem::Legend, false);
curveTw->attach(_plot);
curveList.append(curveTw); // 加入列表
return curveList;
}
void EnergyCountPeakFitView::clearFitCurves()
2026-04-15 18:17:16 +08:00
{
for (QwtPlotCurve* curve : _fitCurves) {
curve->detach();
delete curve;
}
_fitCurves.clear();
_currentFitRecords.clear();
_plot->replot();
2026-04-15 18:17:16 +08:00
}
void EnergyCountPeakFitView::loadHistoryFromFile()
{
if (!QFileInfo::exists(_historyFilePath))
return;
QFile file(_historyFilePath);
if (!file.open(QIODevice::ReadOnly))
return;
QByteArray data = file.readAll();
QJsonDocument doc = QJsonDocument::fromJson(data);
if (!doc.isArray())
return;
_fitHistoryList.clear();
QJsonArray arr = doc.array();
for (const auto& val : arr) {
if (val.isObject())
_fitHistoryList.append(PeakFitHistoryItem::fromJson(val.toObject()));
}
}
void EnergyCountPeakFitView::saveHistoryToFile()
{
QJsonArray arr;
for (const auto& item : _fitHistoryList) {
arr.append(item.toJson());
}
QJsonDocument doc(arr);
QFile file(_historyFilePath);
if (file.open(QIODevice::WriteOnly)) {
file.write(doc.toJson());
}
}
void EnergyCountPeakFitView::saveCurrentFitToHistory()
{
if (_currentFitRecords.isEmpty()) return;
//只取最新的一次拟合结果(列表中的最后一条)
const auto& latestRecord = _currentFitRecords.last();
int latestRectIndex = _currentFitRecords.size() - 1; // 对应的框选区域索引
PeakFitHistoryItem item;
item.timestamp = QDateTime::currentDateTime();
// 仅构建当前这一次的曲线数据
FitCurveData curve;
curve.amplitude = latestRecord.params(0);
curve.sigma = latestRecord.params(1);
curve.sigmoidH = latestRecord.params(2);
curve.sigmoidW = latestRecord.params(3);
curve.baseline = latestRecord.params(4);
curve.center = latestRecord.params(5);
curve.xMin = latestRecord.xMin;
curve.xMax = latestRecord.xMax;
curve.fwhm = latestRecord.result.fwhm;
curve.area = latestRecord.result.area;
// 关联对应的框选区域(最新的那个)
if (latestRectIndex < _selectionRectItems.size()) {
curve.selectionRect = _selectionRectItems[latestRectIndex]->rect();
}
item.curveList.append(curve); // 仅添加这一条曲线
_fitHistoryList.append(item);
saveHistoryToFile();
}
void EnergyCountPeakFitView::displayFitFromHistory(const PeakFitHistoryItem& item)
{
// 遍历历史中的所有曲线,逐一重建
for (const auto& curve : item.curveList) {
// 1. 生成X轴数据
const int numPoints = 200;
QVector<double> xs, ys, ysTw;
double step = (curve.xMax - curve.xMin) / (numPoints - 1);
// 2. 构建参数向量
arma::vec p(6);
p(0) = curve.amplitude;
p(1) = curve.sigma;
p(2) = curve.sigmoidH;
p(3) = curve.sigmoidW;
p(4) = curve.baseline;
p(5) = curve.center;
// 3. 生成拟合曲线数据
for (int i = 0; i < numPoints; ++i) {
double x = curve.xMin + i * step;
xs.append(x);
ys.append(PhotonPeakModel(x, p));
}
// 4. 创建并添加拟合曲线
QwtPlotCurve* fitCurve = new QwtPlotCurve(QStringLiteral(u"历史拟合数据"));
fitCurve->setPen(QPen(Qt::blue, 2));
fitCurve->setSamples(xs, ys);
fitCurve->setItemAttribute(QwtPlotItem::Legend, false);
fitCurve->attach(_plot);
_fitCurves.append(fitCurve);
// 5. 生成本底曲线数据
arma::vec xVec(numPoints);
for (int i = 0; i < numPoints; ++i) xVec(i) = xs[i];
arma::vec yTw = PhotonPeakModelTuowei(xVec, p);
for (int i = 0; i < yTw.size(); ++i) ysTw.append(yTw(i));
// 6. 创建并添加本底曲线
QwtPlotCurve* bgCurve = new QwtPlotCurve(QStringLiteral(u"历史本底数据"));
bgCurve->setPen(QPen(Qt::yellow, 2, Qt::DashLine));
bgCurve->setSamples(xs, ysTw);
bgCurve->setItemAttribute(QwtPlotItem::Legend, false);
bgCurve->attach(_plot);
_fitCurves.append(bgCurve);
// 7. 恢复并绘制框选区域
if (!curve.selectionRect.isNull()) {
PlotRectItem* histRect = new PlotRectItem("HistorySelection");
//显式绑定坐标轴,确保坐标映射正确
histRect->setAxes(QwtPlot::xBottom, QwtPlot::yLeft);
histRect->setRect(curve.selectionRect);
histRect->setBrush(QBrush(Qt::transparent));
// 历史框选区域用红色虚线区分
histRect->setPen(QPen(Qt::red, 2, Qt::DashLine));
histRect->setPeakData(curve.center, curve.fwhm, curve.area, curve.baseline);
histRect->attach(_plot);
_selectionRectItems.append(histRect);
}
}
_plot->replot();
}
void EnergyCountPeakFitView::onActionSaveCurrentFit()
{
saveCurrentFitToHistory();
}
void EnergyCountPeakFitView::onActionShowFitHistory()
{
bool hasCurveData = false;
for (const auto& item : _fitHistoryList) {
if (!item.curveList.isEmpty()) {
hasCurveData = true;
break;
}
}
if (!hasCurveData) {
QMessageBox::information(this,
QStringLiteral(u"峰拟合结果"),
QStringLiteral(u"暂无历史拟合数据,请先进行拟合并保存。"));
return;
}
QDialog dlg(this);
dlg.setWindowTitle(QStringLiteral(u"峰拟合结果历史"));
dlg.setMinimumSize(1000, 500);
QVBoxLayout* layout = new QVBoxLayout(&dlg);
QStandardItemModel* model = new QStandardItemModel(&dlg);
QStringList headers;
headers << "" << "振幅(A)" << "高斯宽度(delt)" << "Sigmoid幅度(H)" << "Sigmoid宽度(W)" << "基线(C)" << "峰中心(P)";
model->setHorizontalHeaderLabels(headers);
QVector<QPair<int, int>> rowToDataIndices;
model->blockSignals(true);
for (int historyIdx = 0; historyIdx < _fitHistoryList.size(); ++historyIdx) {
const auto& historyItem = _fitHistoryList[historyIdx];
for (int curveIdx = 0; curveIdx < historyItem.curveList.size(); ++curveIdx) {
const auto& curve = historyItem.curveList[curveIdx];
QList<QStandardItem*> rowItems;
QStandardItem* checkItem = new QStandardItem();
checkItem->setCheckable(true);
checkItem->setEditable(false);
bool isCurrentlyDisplayed = false;
for (const auto& ref : _displayedHistoryCurves) {
if (ref.historyIndex == historyIdx && ref.curveIndex == curveIdx) {
isCurrentlyDisplayed = true;
break;
}
}
checkItem->setCheckState(isCurrentlyDisplayed ? Qt::Checked : Qt::Unchecked);
rowItems.append(checkItem);
// 2. 振幅 (A)
QStandardItem* itemA = new QStandardItem(QString::number(curve.amplitude, 'f', 4));
itemA->setEditable(false);
rowItems.append(itemA);
// 3. Sigma (delt)
QStandardItem* itemDelt = new QStandardItem(QString::number(curve.sigma, 'f', 4));
itemDelt->setEditable(false);
rowItems.append(itemDelt);
// 4. SigmoidH (H)
QStandardItem* itemH = new QStandardItem(QString::number(curve.sigmoidH, 'f', 4));
itemH->setEditable(false);
rowItems.append(itemH);
// 5. SigmoidW (W)
QStandardItem* itemW = new QStandardItem(QString::number(curve.sigmoidW, 'f', 4));
itemW->setEditable(false);
rowItems.append(itemW);
// 6. 本底 (C)
QStandardItem* itemC = new QStandardItem(QString::number(curve.baseline, 'f', 4));
itemC->setEditable(false);
rowItems.append(itemC);
// 7. 峰位 (P)
QStandardItem* itemP = new QStandardItem(QString::number(curve.center, 'f', 4));
itemP->setEditable(false);
rowItems.append(itemP);
model->appendRow(rowItems);
rowToDataIndices.append(qMakePair(historyIdx, curveIdx));
}
}
model->blockSignals(false);
QHBoxLayout* topBtnLayout = new QHBoxLayout();
QPushButton* btnSelectAll = new QPushButton(QStringLiteral(u"全选"), &dlg);
QPushButton* btnInvertSelection = new QPushButton(QStringLiteral(u"反选"), &dlg);
topBtnLayout->addWidget(btnSelectAll);
topBtnLayout->addWidget(btnInvertSelection);
topBtnLayout->addStretch();
layout->addLayout(topBtnLayout);
QTableView* tableView = new QTableView(&dlg);
tableView->setModel(model);
tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
tableView->setSelectionMode(QAbstractItemView::SingleSelection);
tableView->setEditTriggers(QAbstractItemView::NoEditTriggers);
tableView->horizontalHeader()->setStretchLastSection(true);
tableView->verticalHeader()->setVisible(false);
tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
layout->addWidget(tableView);
QPushButton* btnDelete = new QPushButton(QStringLiteral(u"删除选中记录"), &dlg);
QPushButton* btnClose = new QPushButton(QStringLiteral(u"关闭"), &dlg);
QHBoxLayout* bottomBtnLayout = new QHBoxLayout();
bottomBtnLayout->addStretch();
bottomBtnLayout->addWidget(btnDelete);
bottomBtnLayout->addWidget(btnClose);
layout->addLayout(bottomBtnLayout);
// --- 全选按钮 ---
connect(btnSelectAll, &QPushButton::clicked, [&]() {
model->blockSignals(true);
for (int row = 0; row < model->rowCount(); ++row) {
model->item(row, 0)->setCheckState(Qt::Checked);
}
model->blockSignals(false);
tableView->viewport()->update();
QList<FitCurveData> selectedCurves;
QList<DisplayedCurveRef> selectedRefs;
for (int row = 0; row < model->rowCount(); ++row) {
if (model->item(row, 0)->checkState() == Qt::Checked) {
QPair<int, int> indices = rowToDataIndices[row];
selectedCurves.append(_fitHistoryList[indices.first].curveList[indices.second]);
DisplayedCurveRef ref;
ref.historyIndex = indices.first;
ref.curveIndex = indices.second;
selectedRefs.append(ref);
}
}
displaySelectedCurves(selectedCurves, selectedRefs);
});
// --- 反选按钮 ---
connect(btnInvertSelection, &QPushButton::clicked, [&]() {
model->blockSignals(true);
for (int row = 0; row < model->rowCount(); ++row) {
QStandardItem* item = model->item(row, 0);
item->setCheckState(item->checkState() == Qt::Checked ? Qt::Unchecked : Qt::Checked);
}
model->blockSignals(false);
tableView->viewport()->update();
QList<FitCurveData> selectedCurves;
QList<DisplayedCurveRef> selectedRefs;
for (int row = 0; row < model->rowCount(); ++row) {
if (model->item(row, 0)->checkState() == Qt::Checked) {
QPair<int, int> indices = rowToDataIndices[row];
selectedCurves.append(_fitHistoryList[indices.first].curveList[indices.second]);
DisplayedCurveRef ref;
ref.historyIndex = indices.first;
ref.curveIndex = indices.second;
selectedRefs.append(ref);
}
}
displaySelectedCurves(selectedCurves, selectedRefs);
});
// --- 复选框状态改变 ---
connect(model, &QStandardItemModel::itemChanged, [&](QStandardItem* /*item*/) {
QList<FitCurveData> selectedCurves;
QList<DisplayedCurveRef> selectedRefs;
for (int row = 0; row < model->rowCount(); ++row) {
if (model->item(row, 0)->checkState() == Qt::Checked) {
QPair<int, int> indices = rowToDataIndices[row];
selectedCurves.append(_fitHistoryList[indices.first].curveList[indices.second]);
DisplayedCurveRef ref;
ref.historyIndex = indices.first;
ref.curveIndex = indices.second;
selectedRefs.append(ref);
}
}
displaySelectedCurves(selectedCurves, selectedRefs);
});
// --- 删除选中记录按钮(核心精准删除逻辑) ---
connect(btnDelete, &QPushButton::clicked, [&]() {
// 1. 收集要删除的历史索引
QSet<int> historyIndicesToDelete;
for (int row = 0; row < model->rowCount(); ++row) {
if (model->item(row, 0)->checkState() == Qt::Checked) {
QPair<int, int> indices = rowToDataIndices[row];
historyIndicesToDelete.insert(indices.first);
}
}
if (historyIndicesToDelete.isEmpty()) {
QMessageBox::warning(&dlg, QStringLiteral(u"提示"), QStringLiteral(u"请先勾选要删除的记录。"));
return;
}
// 2. 确认删除
QString msg = QStringLiteral(u"确定要删除选中的 %1 条历史记录吗?").arg(historyIndicesToDelete.size());
QMessageBox::StandardButton reply = QMessageBox::question(&dlg, QStringLiteral(u"确认删除"), msg, QMessageBox::Yes | QMessageBox::No);
if (reply != QMessageBox::Yes) return;
// 3. 先从界面上精准删除显示(从后往前删)
QList<int> sortedDisplayIndices;
for (int i = 0; i < _displayedHistoryCurves.size(); ++i) {
if (historyIndicesToDelete.contains(_displayedHistoryCurves[i].historyIndex)) {
sortedDisplayIndices.append(i);
}
}
std::sort(sortedDisplayIndices.begin(), sortedDisplayIndices.end(), std::greater<int>());
for (int dispIdx : sortedDisplayIndices) {
const auto& ref = _displayedHistoryCurves[dispIdx];
// 删除曲线(先删本底,再删拟合)
if (ref.curveStartIndex + 1 < _fitCurves.size()) {
_fitCurves[ref.curveStartIndex + 1]->detach();
delete _fitCurves[ref.curveStartIndex + 1];
_fitCurves.removeAt(ref.curveStartIndex + 1);
_fitCurves[ref.curveStartIndex]->detach();
delete _fitCurves[ref.curveStartIndex];
_fitCurves.removeAt(ref.curveStartIndex);
}
// 删除框选
if (ref.rectIndex < _selectionRectItems.size()) {
_selectionRectItems[ref.rectIndex]->detach();
delete _selectionRectItems[ref.rectIndex];
_selectionRectItems.removeAt(ref.rectIndex);
}
// 移除引用
_displayedHistoryCurves.removeAt(dispIdx);
}
// 4. 收集剩余显示的数据(用于重建)
QList<FitCurveData> remainingCurves;
QList<DisplayedCurveRef> remainingRefs;
for (const auto& ref : _displayedHistoryCurves) {
remainingCurves.append(_fitHistoryList[ref.historyIndex].curveList[ref.curveIndex]);
remainingRefs.append(ref);
}
// 5. 更新剩余引用的 historyIndex处理索引偏移
QList<int> sortedHistoryIndices = historyIndicesToDelete.values();
std::sort(sortedHistoryIndices.begin(), sortedHistoryIndices.end(), std::greater<int>());
for (auto& ref : remainingRefs) {
int offset = 0;
for (int delIdx : sortedHistoryIndices) {
if (delIdx < ref.historyIndex) offset++;
}
ref.historyIndex -= offset;
}
// 6. 删除历史数据
for (int historyIdx : sortedHistoryIndices) {
_fitHistoryList.removeAt(historyIdx);
}
// 7. 重建界面显示(确保索引正确)
clearFitCurves();
clearAllSelectionRects();
_displayedHistoryCurves.clear();
if (!remainingCurves.isEmpty()) {
displaySelectedCurves(remainingCurves, remainingRefs);
} else {
_plot->replot();
}
// 8. 保存并刷新
saveHistoryToFile();
dlg.accept();
onActionShowFitHistory();
});
connect(btnClose, &QPushButton::clicked, &dlg, &QDialog::accept);
dlg.exec();
}
void EnergyCountPeakFitView::onActionDeleteHoveredRect()
{
if (!_hoveredRectItem) {
QMessageBox::information(this, QStringLiteral(u"提示"),
QStringLiteral(u"请先将鼠标移动到要删除的框选区域上(区域边框会变为实线),然后再点击此菜单。"));
return;
}
//使用新接口获取类型和索引
QString rectType = _hoveredRectItem->selectionType();
int rectIndex = _hoveredRectItem->selectionIndex();
if (rectType == "current") {
if (rectIndex < 0 || rectIndex >= _currentFitRecords.size()) {
QMessageBox::warning(this, QStringLiteral(u"错误"), QStringLiteral(u"框选区域索引无效。"));
return;
}
int curveStartIdx = rectIndex * 2;
int historyIndex = _currentFitRecords[rectIndex].historyIndex;
// 1. 删除拟合曲线和本底曲线
if (curveStartIdx + 1 < _fitCurves.size()) {
_fitCurves[curveStartIdx + 1]->detach();
delete _fitCurves[curveStartIdx + 1];
_fitCurves.removeAt(curveStartIdx + 1);
_fitCurves[curveStartIdx]->detach();
delete _fitCurves[curveStartIdx];
_fitCurves.removeAt(curveStartIdx);
}
// 2. 删除当前拟合记录
_currentFitRecords.removeAt(rectIndex);
// 3. 删除框选区域
_hoveredRectItem->detach();
delete _hoveredRectItem;
_selectionRectItems.removeAt(rectIndex);
_hoveredRectItem = nullptr;
// 4. 删除对应的历史记录
if (historyIndex >= 0 && historyIndex < _fitHistoryList.size()) {
_fitHistoryList.removeAt(historyIndex);
saveHistoryToFile();
// 5. 更新剩余当前记录的历史索引
for (auto& record : _currentFitRecords) {
if (record.historyIndex > historyIndex) {
record.historyIndex--;
}
}
// 6. 更新历史显示记录的索引
for (auto& ref : _displayedHistoryCurves) {
if (ref.historyIndex > historyIndex) {
ref.historyIndex--;
}
if (ref.curveStartIndex > curveStartIdx) {
ref.curveStartIndex -= 2;
}
if (ref.rectIndex > rectIndex) {
ref.rectIndex--;
}
}
}
// 7. 更新剩余当前框选的索引
for (int i = rectIndex; i < _selectionRectItems.size(); ++i) {
PlotRectItem* item = _selectionRectItems[i];
if (item->selectionType() == "current") {
item->setSelectionIndex(i);
} else if (item->selectionType() == "history") {
// 更新历史框选的索引
for (int j = 0; j < _displayedHistoryCurves.size(); ++j) {
if (_displayedHistoryCurves[j].rectIndex == i) {
item->setSelectionIndex(j);
break;
}
}
}
}
} else if (rectType == "history") {
if (rectIndex < 0 || rectIndex >= _displayedHistoryCurves.size()) {
QMessageBox::warning(this, QStringLiteral(u"错误"), QStringLiteral(u"历史框选区域索引无效。"));
return;
}
const DisplayedCurveRef& ref = _displayedHistoryCurves[rectIndex];
int historyIndex = ref.historyIndex;
int curveStartIdx = ref.curveStartIndex;
int rectIdx = ref.rectIndex;
// 1. 删除拟合曲线和本底曲线
if (curveStartIdx + 1 < _fitCurves.size()) {
_fitCurves[curveStartIdx + 1]->detach();
delete _fitCurves[curveStartIdx + 1];
_fitCurves.removeAt(curveStartIdx + 1);
_fitCurves[curveStartIdx]->detach();
delete _fitCurves[curveStartIdx];
_fitCurves.removeAt(curveStartIdx);
}
// 2. 删除框选区域
_hoveredRectItem->detach();
delete _hoveredRectItem;
_selectionRectItems.removeAt(rectIdx);
_hoveredRectItem = nullptr;
// 3. 从显示引用列表中移除
_displayedHistoryCurves.removeAt(rectIndex);
// 4. 删除对应的历史记录
if (historyIndex >= 0 && historyIndex < _fitHistoryList.size()) {
_fitHistoryList.removeAt(historyIndex);
saveHistoryToFile();
// 5. 更新剩余历史显示记录的索引
for (auto& r : _displayedHistoryCurves) {
if (r.historyIndex > historyIndex) {
r.historyIndex--;
}
if (r.curveStartIndex > curveStartIdx) {
r.curveStartIndex -= 2;
}
if (r.rectIndex > rectIdx) {
r.rectIndex--;
}
}
// 6. 更新当前记录的历史索引
for (auto& record : _currentFitRecords) {
if (record.historyIndex > historyIndex) {
record.historyIndex--;
}
}
}
// 7. 更新剩余历史框选的索引
for (int i = rectIndex; i < _displayedHistoryCurves.size(); ++i) {
const DisplayedCurveRef& r = _displayedHistoryCurves[i];
PlotRectItem* item = _selectionRectItems[r.rectIndex];
item->setSelectionIndex(i);
}
// 8. 更新当前框选的索引
for (int i = 0; i < _currentFitRecords.size(); ++i) {
PlotRectItem* item = _selectionRectItems[i];
if (item->selectionType() == "current" && item->selectionIndex() > rectIdx) {
item->setSelectionIndex(item->selectionIndex() - 1);
}
}
}
_plot->replot();
}
void EnergyCountPeakFitView::onActionRefitCurrentRect()
{
// 检查是否有悬停的框选区域
if (!_hoveredRectItem) {
QMessageBox::information(this, QStringLiteral(u"提示"),
QStringLiteral(u"请先将鼠标移动到要重新拟合的框选区域上(区域边框会变为实线),然后再点击此菜单。"));
return;
}
// 获取框选类型
QString rectType = _hoveredRectItem->selectionType();
int rectIndex = _hoveredRectItem->selectionIndex();
if (rectType == "current") {
int rectIdxInList = _selectionRectItems.indexOf(_hoveredRectItem);
if (rectIdxInList == -1 || rectIdxInList >= _currentFitRecords.size()) {
QMessageBox::warning(this, QStringLiteral(u"错误"),
QStringLiteral(u"未找到对应区域的拟合记录,无法重新拟合。"));
return;
}
const CurrentFitRecord& originalRecord = _currentFitRecords[rectIdxInList];
arma::vec originalParams = originalRecord.params;
FitParams defaultParams;
defaultParams.A = originalParams(0);
defaultParams.delt = originalParams(1);
defaultParams.H = originalParams(2);
defaultParams.W = originalParams(3);
defaultParams.C = originalParams(4);
defaultParams.P = originalParams(5);
PeakFitParamsDialog paramDlg(defaultParams, this);
if (paramDlg.exec() != QDialog::Accepted) {
return;
}
FitParams userParams = paramDlg.getParams();
arma::vec newParams(6);
newParams(0) = userParams.A;
newParams(1) = userParams.delt;
newParams(2) = userParams.H;
newParams(3) = userParams.W;
newParams(4) = userParams.C;
newParams(5) = userParams.P;
double newFwhm = newParams(1) * 2.355;
AdaptiveSimpsonIntegrate::FitFunction fitFunc;
fitFunc.A = newParams(0);
fitFunc.delt = newParams(1);
fitFunc.H = newParams(2);
fitFunc.W = newParams(3);
fitFunc.C = newParams(4);
fitFunc.P = newParams(5);
double lower = fitFunc.P - 3.0 * fitFunc.delt;
double upper = fitFunc.P + 3.0 * fitFunc.delt;
double newArea = AdaptiveSimpsonIntegrate::integrate(fitFunc, lower, upper);
PeakFitResult newResult;
newResult.center = newParams(5);
newResult.amplitude = newParams(0);
newResult.sigma = newParams(1);
newResult.fwhm = newFwhm;
newResult.area = newArea;
newResult.baseline = newParams(4);
newResult.sigmoidH = newParams(2);
newResult.sigmoidW = newParams(3);
int curveStartIdx = rectIdxInList * 2;
if (curveStartIdx + 1 < _fitCurves.size()) {
_fitCurves[curveStartIdx]->detach();
_fitCurves[curveStartIdx + 1]->detach();
delete _fitCurves[curveStartIdx];
delete _fitCurves[curveStartIdx + 1];
_fitCurves.removeAt(curveStartIdx + 1);
_fitCurves.removeAt(curveStartIdx);
}
arma::vec xVec;
QList<QwtPlotCurve*> curves = _plot->GetCurveList();
if (!curves.isEmpty()) {
QwtPlotCurve* originalCurve = curves.first();
QVector<double> origX;
for (size_t i = 0; i < originalCurve->dataSize(); ++i) {
origX.push_back(originalCurve->sample(i).x());
}
QVector<double> roiX;
for (int i = 0; i < origX.size(); ++i) {
if (origX[i] >= originalRecord.xMin && origX[i] <= originalRecord.xMax) {
roiX.push_back(origX[i]);
}
}
xVec.set_size(roiX.size());
for (int i = 0; i < roiX.size(); ++i) {
xVec(i) = roiX[i];
}
}
QList<QwtPlotCurve*> newCurves = createFitCurve(newResult, xVec);
for (int i = 0; i < newCurves.size(); ++i) {
_fitCurves.insert(curveStartIdx + i, newCurves[i]);
}
_hoveredRectItem->setPeakData(newResult.center, newResult.fwhm, newResult.area, newResult.baseline);
CurrentFitRecord newRecord;
newRecord.result = newResult;
newRecord.params = newParams;
newRecord.xMin = originalRecord.xMin;
newRecord.xMax = originalRecord.xMax;
newRecord.historyIndex = originalRecord.historyIndex; // 保留历史索引
_currentFitRecords.replace(rectIdxInList, newRecord);
_plot->replot();
} else if (rectType == "history") {
if (rectIndex < 0 || rectIndex >= _displayedHistoryCurves.size()) {
QMessageBox::warning(this, QStringLiteral(u"错误"),
QStringLiteral(u"未找到对应历史区域的拟合记录,无法重新拟合。"));
return;
}
// 1. 获取历史数据引用
const DisplayedCurveRef& ref = _displayedHistoryCurves[rectIndex];
int historyIdx = ref.historyIndex;
int curveIdxInHistory = ref.curveIndex;
int curveStartIdx = ref.curveStartIndex;
int rectIdxInList = ref.rectIndex;
if (historyIdx < 0 || historyIdx >= _fitHistoryList.size()) return;
if (curveIdxInHistory < 0 || curveIdxInHistory >= _fitHistoryList[historyIdx].curveList.size()) return;
// 2. 从历史记录中提取原始参数
const FitCurveData& originalCurveData = _fitHistoryList[historyIdx].curveList[curveIdxInHistory];
FitParams defaultParams;
defaultParams.A = originalCurveData.amplitude;
defaultParams.delt = originalCurveData.sigma;
defaultParams.H = originalCurveData.sigmoidH;
defaultParams.W = originalCurveData.sigmoidW;
defaultParams.C = originalCurveData.baseline;
defaultParams.P = originalCurveData.center;
// 3. 弹出参数编辑对话框
PeakFitParamsDialog paramDlg(defaultParams, this);
if (paramDlg.exec() != QDialog::Accepted) {
return;
}
// 4. 获取用户修改后的参数
FitParams userParams = paramDlg.getParams();
arma::vec newParams(6);
newParams(0) = userParams.A;
newParams(1) = userParams.delt;
newParams(2) = userParams.H;
newParams(3) = userParams.W;
newParams(4) = userParams.C;
newParams(5) = userParams.P;
// 5. 计算新的 FWHM 和峰面积
double newFwhm = newParams(1) * 2.355;
AdaptiveSimpsonIntegrate::FitFunction fitFunc;
fitFunc.A = newParams(0);
fitFunc.delt = newParams(1);
fitFunc.H = newParams(2);
fitFunc.W = newParams(3);
fitFunc.C = newParams(4);
fitFunc.P = newParams(5);
double lower = fitFunc.P - 3.0 * fitFunc.delt;
double upper = fitFunc.P + 3.0 * fitFunc.delt;
double newArea = AdaptiveSimpsonIntegrate::integrate(fitFunc, lower, upper);
// 6. 构建新的结果结构体
PeakFitResult newResult;
newResult.center = newParams(5);
newResult.amplitude = newParams(0);
newResult.sigma = newParams(1);
newResult.fwhm = newFwhm;
newResult.area = newArea;
newResult.baseline = newParams(4);
newResult.sigmoidH = newParams(2);
newResult.sigmoidW = newParams(3);
// 7. 删除旧的拟合曲线和本底曲线
if (curveStartIdx + 1 < _fitCurves.size()) {
_fitCurves[curveStartIdx]->detach();
_fitCurves[curveStartIdx + 1]->detach();
delete _fitCurves[curveStartIdx];
delete _fitCurves[curveStartIdx + 1];
_fitCurves.removeAt(curveStartIdx + 1);
_fitCurves.removeAt(curveStartIdx);
}
// 8. 从原始数据中提取该区域的 X 值(用于重新生成曲线)
arma::vec xVec;
QList<QwtPlotCurve*> curves = _plot->GetCurveList();
if (!curves.isEmpty()) {
QwtPlotCurve* originalCurve = curves.first();
QVector<double> origX;
for (size_t i = 0; i < originalCurve->dataSize(); ++i) {
origX.push_back(originalCurve->sample(i).x());
}
QVector<double> roiX;
for (int i = 0; i < origX.size(); ++i) {
if (origX[i] >= originalCurveData.xMin && origX[i] <= originalCurveData.xMax) {
roiX.push_back(origX[i]);
}
}
xVec.set_size(roiX.size());
for (int i = 0; i < roiX.size(); ++i) {
xVec(i) = roiX[i];
}
}
// 9. 生成新的拟合曲线并插入到原位置
QList<QwtPlotCurve*> newCurves = createFitCurve(newResult, xVec);
for (int i = 0; i < newCurves.size(); ++i) {
_fitCurves.insert(curveStartIdx + i, newCurves[i]);
}
// 10. 更新框选区域的峰数据标签
_hoveredRectItem->setPeakData(newResult.center, newResult.fwhm, newResult.area, newResult.baseline);
// 11. 更新内存中的历史数据
FitCurveData newCurveData = originalCurveData; // 复制旧数据
newCurveData.amplitude = newParams(0);
newCurveData.sigma = newParams(1);
newCurveData.sigmoidH = newParams(2);
newCurveData.sigmoidW = newParams(3);
newCurveData.baseline = newParams(4);
newCurveData.center = newParams(5);
newCurveData.fwhm = newFwhm;
newCurveData.area = newArea;
// 替换历史列表中的数据
_fitHistoryList[historyIdx].curveList.replace(curveIdxInHistory, newCurveData);
// 12. 自动保存到文件
saveHistoryToFile();
// 13. 重绘图表
_plot->replot();
QMessageBox::information(this, QStringLiteral(u"提示"), QStringLiteral(u"重新拟合成功,历史数据已更新。"));
}
}
//检测鼠标是否悬停在框选区域上
void EnergyCountPeakFitView::updateHoverState(const QPoint& mousePos) {
// 将鼠标像素坐标转换为 plot 坐标
const QwtScaleMap xMap = _plot->canvasMap(QwtPlot::xBottom);
const QwtScaleMap yMap = _plot->canvasMap(QwtPlot::yLeft);
const double x = xMap.invTransform(mousePos.x());
const double y = yMap.invTransform(mousePos.y());
const QPointF plotPos(x, y);
PlotRectItem* newHoveredItem = nullptr;
// 倒序遍历(优先检测最上层的区域)
for (auto it = _selectionRectItems.rbegin(); it != _selectionRectItems.rend(); ++it) {
PlotRectItem* item = *it;
if (item->rect().contains(plotPos)) {
newHoveredItem = item;
break;
}
}
// 更新悬停状态
if (newHoveredItem != _hoveredRectItem) {
// 取消之前的悬停
if (_hoveredRectItem) {
_hoveredRectItem->setHovered(false);
}
// 设置新的悬停
_hoveredRectItem = newHoveredItem;
if (_hoveredRectItem) {
_hoveredRectItem->setHovered(true);
}
// 重绘
_plot->replot();
}
}
void EnergyCountPeakFitView::displaySelectedCurves(const QList<FitCurveData> &curves, const QList<DisplayedCurveRef>& refs)
{
// 先清除之前的拟合曲线和框选区域
clearFitCurves();
clearAllSelectionRects();
//重建显示引用列表,记录索引
_displayedHistoryCurves.clear();
// 遍历显示曲线
for (int i = 0; i < curves.size(); ++i) {
const auto& curve = curves[i];
const auto& ref = refs[i];
// 1. 生成X轴数据
const int numPoints = 200;
QVector<double> xs, ys, ysTw;
double step = (curve.xMax - curve.xMin) / (numPoints - 1);
// 2. 构建参数向量
arma::vec p(6);
p(0) = curve.amplitude;
p(1) = curve.sigma;
p(2) = curve.sigmoidH;
p(3) = curve.sigmoidW;
p(4) = curve.baseline;
p(5) = curve.center;
// 3. 生成拟合曲线数据
for (int j = 0; j < numPoints; ++j) {
double x = curve.xMin + j * step;
xs.append(x);
ys.append(PhotonPeakModel(x, p));
}
//记录当前曲线和框选的起始索引
DisplayedCurveRef newRef;
newRef.historyIndex = ref.historyIndex;
newRef.curveIndex = ref.curveIndex;
newRef.curveStartIndex = _fitCurves.size();
newRef.rectIndex = _selectionRectItems.size();
// 4. 创建并添加拟合曲线
QwtPlotCurve* fitCurve = new QwtPlotCurve(QStringLiteral(u"历史拟合数据"));
fitCurve->setPen(QPen(Qt::blue, 2));
fitCurve->setSamples(xs, ys);
fitCurve->setItemAttribute(QwtPlotItem::Legend, false);
fitCurve->attach(_plot);
_fitCurves.append(fitCurve);
// 5. 生成本底曲线数据
arma::vec xVec(numPoints);
for (int j = 0; j < numPoints; ++j) xVec(j) = xs[j];
arma::vec yTw = PhotonPeakModelTuowei(xVec, p);
for (int j = 0; j < yTw.size(); ++j) ysTw.append(yTw(j));
// 6. 创建并添加本底曲线
QwtPlotCurve* bgCurve = new QwtPlotCurve(QStringLiteral(u"历史本底数据"));
bgCurve->setPen(QPen(Qt::yellow, 2, Qt::DashLine));
bgCurve->setSamples(xs, ysTw);
bgCurve->setItemAttribute(QwtPlotItem::Legend, false);
bgCurve->attach(_plot);
_fitCurves.append(bgCurve);
// 7. 恢复并绘制框选区域
if (!curve.selectionRect.isNull()) {
PlotRectItem* histRect = new PlotRectItem("HistorySelection");
histRect->setAxes(QwtPlot::xBottom, QwtPlot::yLeft);
histRect->setRect(curve.selectionRect);
histRect->setBrush(QBrush(Qt::transparent));
histRect->setPen(QPen(Qt::red, 2, Qt::DashLine));
histRect->setPeakData(curve.center, curve.fwhm, curve.area, curve.baseline);
histRect->setSelectionType("history");
histRect->setSelectionIndex(_displayedHistoryCurves.size());
histRect->attach(_plot);
_selectionRectItems.append(histRect);
}
// 8. 将记录了索引的 newRef 加入列表
_displayedHistoryCurves.append(newRef);
}
_plot->replot();
}