EnergySpectrumAnalyer/src/EnergyCountPeakFitView/EnergyCountPeakFitView.cpp
2026-05-12 20:52:43 +08:00

1659 lines
62 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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>
#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()));
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);
_data_selector = new CustomQwtPlotXaxisSelector(_plot->canvas());
_data_selector->setEnabled(false);
// 启用鼠标追踪,并安装事件过滤器
_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();
//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);
this->_menu->addSeparator();
QAction* action_refit_rect = this->_menu->addAction(QStringLiteral(u"重新拟合当前区域"));
connect(action_refit_rect, &QAction::triggered, this, &EnergyCountPeakFitView::onActionRefitCurrentRect);
//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);
//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()
{
}
void EnergyCountPeakFitView::clearAllSelectionRects()
{
for (PlotRectItem* item : _selectionRectItems) {
if (item) {
item->detach();
delete item;
}
}
_selectionRectItems.clear();
_plot->replot();
}
bool EnergyCountPeakFitView::eventFilter(QObject* watched, QEvent* event)
{
if (watched == _plot->canvas()) {
QMouseEvent* me = static_cast<QMouseEvent*>(event);
// 按下 Ctrl+左键 开始框选
if (event->type() == QEvent::MouseButtonPress &&
me->button() == Qt::LeftButton /*&&*/
/*(me->modifiers() & Qt::ControlModifier)*/) {
startSelection(me->pos());
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();
return true;
}
//鼠标移动时检测悬停
else if (event->type() == QEvent::MouseMove) {
updateHoverState(me->pos());
return true;
}
}
return MeasureAnalysisView::eventFilter(watched, event);
}
void EnergyCountPeakFitView::startSelection(const QPoint& pos)
{
_isSelecting = true;
_selectionStart = pos;
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();
}
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));
}
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;
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);
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;
}
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();
_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;
}
QwtPlotCurve* EnergyCountPeakFitView::createFitCurve(const PeakFitResult& result, double xMin, double xMax, const QString& name)
{
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;
}
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()
{
for (QwtPlotCurve* curve : _fitCurves) {
curve->detach();
delete curve;
}
_fitCurves.clear();
_currentFitRecords.clear();
_plot->replot();
}
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();
}