diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f5be19..3c3c3c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,12 +17,16 @@ endif() if(NOT DEFINED VTK_DIR AND EXISTS "C:/Toolkits/VTK/lib/cmake/vtk-8.1") set(VTK_DIR "C:/Toolkits/VTK/lib/cmake/vtk-8.1" CACHE PATH "") endif() +if(NOT DEFINED JSONCPP_DIR AND EXISTS "C:/Toolkits/jsoncpp/lib/cmake/jsoncpp") + set(JSONCPP_DIR "C:/Toolkits/jsoncpp/lib/cmake/jsoncpp" CACHE PATH "") +endif() find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets) find_package(ITK REQUIRED) include(${ITK_USE_FILE}) find_package(VTK REQUIRED) include(${VTK_USE_FILE}) +find_package(jsoncpp REQUIRED) if("${VTK_QT_VERSION}" STREQUAL "") message(FATAL_ERROR "VTK was not built with Qt support") @@ -39,8 +43,12 @@ set(SOURCES src/ProfilePlotWidget.cpp src/ImageListPanel.cpp src/dvh/DvhComparePage.cpp - src/dvh/DvhPlotWidget.cpp + src/dvh/DvhCenterPart_ui.cpp + src/dvh/DvhCenterPart_slot.cpp src/dvh/dcmIO.cpp + src/gamma/GammaComparePage.cpp + src/gamma/RoiLoader.cpp + src/gamma/HotLegendWidget.cpp resources/resources.qrc ) @@ -55,9 +63,12 @@ set(HEADERS src/ProfilePlotWidget.h src/ImageListPanel.h src/dvh/DvhComparePage.h - src/dvh/DvhPlotWidget.h + src/dvh/DvhCenterPart.h src/dvh/dvhTypes.h src/dvh/dcmIO.h + src/gamma/GammaComparePage.h + src/gamma/RoiLoader.h + src/gamma/HotLegendWidget.h ) if(WIN32) @@ -69,11 +80,17 @@ add_executable(DoseCompare ${SOURCES} ${HEADERS} ${APP_ICON_RC}) target_include_directories(DoseCompare PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/dvh + ${CMAKE_CURRENT_SOURCE_DIR}/src/gamma ) +get_target_property(JSON_INC_PATH jsoncpp_lib INTERFACE_INCLUDE_DIRECTORIES) +if(JSON_INC_PATH) + target_include_directories(DoseCompare PRIVATE ${JSON_INC_PATH}) +endif() target_link_libraries(DoseCompare PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets ${ITK_LIBRARIES} ${VTK_LIBRARIES} + jsoncpp_lib ) add_executable(smoke_slice_test src/smoke_slice_test.cpp src/DoseImage.cpp) @@ -96,4 +113,10 @@ add_custom_command(TARGET DoseCompare POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_SOURCE_DIR}/algo/calcDVHLibraryD.dll $/algo/calcDVHLibraryD.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/algo/GammaMapLib.dll + $/algo/GammaMapLib.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/algo/param.json + $/algo/param.json ) diff --git a/algo/GammaMapLib.dll b/algo/GammaMapLib.dll new file mode 100644 index 0000000..6a4476b Binary files /dev/null and b/algo/GammaMapLib.dll differ diff --git a/algo/gammaExample.txt b/algo/gammaExample.txt new file mode 100644 index 0000000..b652a7a --- /dev/null +++ b/algo/gammaExample.txt @@ -0,0 +1,101 @@ +void CenterPart::calculateGma() +{ + // first get gamma information from GUI + int dta = this->gmDLEd->text().toInt(); + int dd = this->gmVLEd->text().toInt(); + int thred = this->gmCLEd->text().toInt(); + + if (dta <= 0 || dd <= 0 || thred <= 0 || + dta > 7 || dd > 7 || thred > 100) + { + QMessageBox::warning(this, tr("Warning"), \ + tr("Load Wrong gamma parameters!"), QMessageBox::Yes); + return; + } + + // load DLL + std::string autoDgDllFile = this->execPath + "/algo/GammaMapLib"; + std::string postFix = ".dll"; +#ifdef _DEBUG + postFix = "D.dll"; +#endif + autoDgDllFile += postFix; + HINSTANCE hInst = LoadLibrary(autoDgDllFile.c_str()); + + // check dll file + if (hInst == nullptr) + { + QMessageBox::warning(this, tr("Warning"), \ + tr("No reference GammaMapLib DLL!"), QMessageBox::Yes); + return; + } + + // function + typedef float(*pGamma3D)(float * originR, + float * spacingR, + int* sizeR, + float * doseR, + float * originE, + float * spacingE, + int* sizeE, + float * doseE, + float** gammaMap, + int dta, + int dd, + int thred); + pGamma3D gamma3D = (pGamma3D)GetProcAddress(hInst, "gamma3D"); + + // check function + if (gamma3D == nullptr) + { + QMessageBox::warning(this, tr("Warning"), \ + tr("No reference gamma3D function in DLL!"), QMessageBox::Yes); + return; + } + + std::cout << "POOOKOK" << std::endl; + // load image + int ret = 0; + std::string refDoseF = this->rtdPath + "/doseRef.mhd"; + std::string evaDoseF = this->rtdPath + "/doseCuMcMc.mhd"; + + if (this->mtdTyp == 0) + { + evaDoseF = this->rtdPath + "/doseGoMcMc.mhd"; + } + else if (this->mtdTyp == 1) + { + evaDoseF = this->rtdPath + "/doseCuMcMC.mhd"; + } + else if (this->mtdTyp == 2) + { + evaDoseF = this->rtdPath + "/doseVsMcMc.mhd"; + } + else if (this->mtdTyp == 3) + { + evaDoseF = this->rtdPath + "/doseDcPB.mhd"; + } + + float originR[3]; + float spacingR[3]; + int sizeR[3]; + float* doseR; + float originE[3]; + float spacingE[3]; + int sizeE[3]; + float* doseE; + ret |= readMhd(refDoseF, &doseR, originR, spacingR, sizeR); + ret |= readMhd(evaDoseF, &doseE, originE, spacingE, sizeE); + + if (ret < 0) + { + QMessageBox::warning(this, tr("Warning"), \ + tr("No reference or result dose!"), QMessageBox::Yes); + return; + } + + // call + float* gammaMap; + float gamma = gamma3D(originR, spacingR, sizeR, doseR, originE, spacingE, sizeE, doseE, &gammaMap, dta, dd, thred); + this->gmaLEd->setText(QString::number(gamma * 100, 'f', 5)); +} \ No newline at end of file diff --git a/algo/param.json b/algo/param.json new file mode 100644 index 0000000..82f7c71 --- /dev/null +++ b/algo/param.json @@ -0,0 +1,11 @@ +{ + "description": "Parameters for display DVH", + "imgFile": "D:/MANTEIA/Data/0000473770", + "rtsFile": "D:/MANTEIA/Data/0000473770/rt/RS.1.2.246.352.71.4.404685894334.585553.20220217103912.dcm", + "doseSet": [ + { + "tagName": "TPS", + "path": "D:/MANTEIA/Data/0000473770/rt/RD.1.2.246.352.71.7.404685894334.6007006.20220217102441.dcm" + } + ] +} \ No newline at end of file diff --git a/resources/app-gamma.png b/resources/app-gamma.png new file mode 100644 index 0000000..a73d4f2 Binary files /dev/null and b/resources/app-gamma.png differ diff --git a/resources/resources.qrc b/resources/resources.qrc index 1afee1b..3278e8b 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -5,5 +5,6 @@ dosecompare.ico app-profile.png app-dvh.png + app-gamma.png diff --git a/src/AppShell.cpp b/src/AppShell.cpp index eda2d18..1d42022 100644 --- a/src/AppShell.cpp +++ b/src/AppShell.cpp @@ -2,11 +2,13 @@ #include "LauncherHomeWidget.h" #include "ProfileComparePage.h" #include "dvh/DvhComparePage.h" +#include "gamma/GammaComparePage.h" #include #include #include #include +#include AppShell::AppShell(QWidget* parent) : QMainWindow(parent) @@ -34,13 +36,16 @@ AppShell::AppShell(QWidget* parent) m_home = new LauncherHomeWidget(m_stack); m_profile = new ProfileComparePage(m_stack); m_dvh = new DvhComparePage(m_stack); + m_gamma = new GammaComparePage(m_stack); m_stack->addWidget(m_home); m_stack->addWidget(m_profile); m_stack->addWidget(m_dvh); + m_stack->addWidget(m_gamma); connect(m_home, &LauncherHomeWidget::openProfileRequested, this, &AppShell::openProfile); connect(m_home, &LauncherHomeWidget::openDvhRequested, this, &AppShell::openDvh); + connect(m_home, &LauncherHomeWidget::openGammaRequested, this, &AppShell::openGamma); showHome(); } @@ -73,6 +78,17 @@ void AppShell::openDvhApp() openDvh(); } +void AppShell::openGamma() +{ + m_stack->setCurrentWidget(m_gamma); + updateChrome(); +} + +void AppShell::openGammaApp() +{ + openGamma(); +} + void AppShell::updateChrome() { const bool onHome = (m_stack->currentWidget() == m_home); @@ -81,6 +97,8 @@ void AppShell::updateChrome() setWindowTitle(tr("DoseCompare")); else if (m_stack->currentWidget() == m_profile) setWindowTitle(tr("DoseCompare — Profile Comparison")); - else + else if (m_stack->currentWidget() == m_dvh) setWindowTitle(tr("DoseCompare — DVH Comparison")); + else + setWindowTitle(tr("DoseCompare — Gamma Analysis")); } diff --git a/src/AppShell.h b/src/AppShell.h index b432fa9..2747aef 100644 --- a/src/AppShell.h +++ b/src/AppShell.h @@ -6,6 +6,7 @@ class QStackedWidget; class LauncherHomeWidget; class ProfileComparePage; class DvhComparePage; +class GammaComparePage; class AppShell : public QMainWindow { Q_OBJECT @@ -15,11 +16,13 @@ public: ProfileComparePage* profilePage() const { return m_profile; } void openProfileApp(); void openDvhApp(); + void openGammaApp(); private slots: void showHome(); void openProfile(); void openDvh(); + void openGamma(); private: void updateChrome(); @@ -28,5 +31,6 @@ private: LauncherHomeWidget* m_home = nullptr; ProfileComparePage* m_profile = nullptr; DvhComparePage* m_dvh = nullptr; + GammaComparePage* m_gamma = nullptr; QAction* m_backAct = nullptr; }; diff --git a/src/DoseImage.cpp b/src/DoseImage.cpp index 167d642..2ee2e5b 100644 --- a/src/DoseImage.cpp +++ b/src/DoseImage.cpp @@ -100,6 +100,18 @@ DoseImage::Pointer DoseImage::loadFromFile(const QString& path, QString* error) return dose; } +DoseImage::Pointer DoseImage::fromItkImage(DoseImageType::Pointer image, const QString& name) +{ + if (!image) + return nullptr; + auto dose = Pointer(new DoseImage()); + dose->m_name = name; + dose->m_image = image; + dose->computeStats(); + dose->ensureInterpolator(); + return dose; +} + void DoseImage::computeStats() { using Calc = itk::MinimumMaximumImageCalculator; diff --git a/src/DoseImage.h b/src/DoseImage.h index 6f6b274..2058597 100644 --- a/src/DoseImage.h +++ b/src/DoseImage.h @@ -15,11 +15,13 @@ public: using Pointer = std::shared_ptr; static Pointer loadFromFile(const QString& path, QString* error = nullptr); + static Pointer fromItkImage(DoseImageType::Pointer image, const QString& name); const QString& name() const { return m_name; } const QString& path() const { return m_path; } QColor color() const { return m_color; } void setColor(const QColor& c) { m_color = c; } + void setName(const QString& name) { m_name = name; } DoseImageType::Pointer image() const { return m_image; } diff --git a/src/ImageListPanel.cpp b/src/ImageListPanel.cpp index 10715e5..ef62a09 100644 --- a/src/ImageListPanel.cpp +++ b/src/ImageListPanel.cpp @@ -25,14 +25,34 @@ ImageListPanel::ImageListPanel(ImageManager* manager, QWidget* parent) connect(m_manager, &ImageManager::profileSelectionChanged, this, &ImageListPanel::refresh); } +void ImageListPanel::setCheckMode(CheckMode mode) +{ + m_checkMode = mode; + refresh(); +} + +void ImageListPanel::setRoleIds(const QString& backgroundId, const QString& frontId) +{ + m_bgId = backgroundId; + m_fgId = frontId; + if (m_checkMode == CheckMode::RoleIndicator) + refresh(); +} + void ImageListPanel::refresh() { m_block = true; m_list->clear(); for (const auto& img : m_manager->images()) { auto* item = new QListWidgetItem(img.dose->name(), m_list); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(img.profileVisible ? Qt::Checked : Qt::Unchecked); + item->setFlags((item->flags() | Qt::ItemIsUserCheckable) & ~Qt::ItemIsEditable); + if (m_checkMode == CheckMode::RoleIndicator) { + const bool on = (img.id == m_bgId || (!m_fgId.isEmpty() && img.id == m_fgId)); + item->setCheckState(on ? Qt::Checked : Qt::Unchecked); + // Keep checkable for display, but RoleIndicator ignores user edits. + } else { + item->setCheckState(img.profileVisible ? Qt::Checked : Qt::Unchecked); + } item->setData(Qt::UserRole, img.id); item->setForeground(img.dose->color()); item->setToolTip(img.dose->path()); @@ -44,6 +64,17 @@ void ImageListPanel::onItemChanged(QListWidgetItem* item) { if (m_block || !item) return; + + if (m_checkMode == CheckMode::RoleIndicator) { + // Display-only: revert any user toggle. + const QString id = item->data(Qt::UserRole).toString(); + const bool on = (id == m_bgId || (!m_fgId.isEmpty() && id == m_fgId)); + m_block = true; + item->setCheckState(on ? Qt::Checked : Qt::Unchecked); + m_block = false; + return; + } + const QString id = item->data(Qt::UserRole).toString(); m_manager->setProfileVisible(id, item->checkState() == Qt::Checked); } diff --git a/src/ImageListPanel.h b/src/ImageListPanel.h index 6a19b71..1527dda 100644 --- a/src/ImageListPanel.h +++ b/src/ImageListPanel.h @@ -2,6 +2,7 @@ #include #include +#include class ImageManager; class QListWidgetItem; @@ -9,8 +10,16 @@ class QListWidgetItem; class ImageListPanel : public QWidget { Q_OBJECT public: + enum class CheckMode { + ProfileToggle, // Profile app: user toggles profile visibility + RoleIndicator // Gamma app: checkbox mirrors Background/Front selection + }; + explicit ImageListPanel(ImageManager* manager, QWidget* parent = nullptr); + void setCheckMode(CheckMode mode); + void setRoleIds(const QString& backgroundId, const QString& frontId); + public slots: void refresh(); @@ -24,4 +33,7 @@ private: ImageManager* m_manager = nullptr; QListWidget* m_list = nullptr; bool m_block = false; + CheckMode m_checkMode = CheckMode::ProfileToggle; + QString m_bgId; + QString m_fgId; }; diff --git a/src/LauncherHomeWidget.cpp b/src/LauncherHomeWidget.cpp index b377850..3e9a96a 100644 --- a/src/LauncherHomeWidget.cpp +++ b/src/LauncherHomeWidget.cpp @@ -51,8 +51,8 @@ QPushButton* makeAppCard(const QString& title, auto* card = new QPushButton(parent); card->setCursor(Qt::PointingHandCursor); card->setFlat(true); - card->setMinimumSize(280, 340); - card->setMaximumWidth(360); + card->setMinimumSize(240, 320); + card->setMaximumWidth(300); card->setAttribute(Qt::WA_StyledBackground, true); card->setStyleSheet( QStringLiteral( @@ -154,6 +154,14 @@ LauncherHomeWidget::LauncherHomeWidget(QWidget* parent) connect(dvhCard, &QPushButton::clicked, this, &LauncherHomeWidget::openDvhRequested); row->addWidget(dvhCard); + auto* gammaCard = makeAppCard( + tr("Gamma Analysis"), + tr("Background / Front 三维 Gamma 通过率与 Gamma Map"), + QStringLiteral(":/app-gamma.png"), + this); + connect(gammaCard, &QPushButton::clicked, this, &LauncherHomeWidget::openGammaRequested); + row->addWidget(gammaCard); + row->addStretch(1); root->addLayout(row, 1); } diff --git a/src/LauncherHomeWidget.h b/src/LauncherHomeWidget.h index 24909a4..f5464a2 100644 --- a/src/LauncherHomeWidget.h +++ b/src/LauncherHomeWidget.h @@ -10,4 +10,5 @@ public: signals: void openProfileRequested(); void openDvhRequested(); + void openGammaRequested(); }; diff --git a/src/SliceViewWidget.cpp b/src/SliceViewWidget.cpp index 7579c83..b6b187e 100644 --- a/src/SliceViewWidget.cpp +++ b/src/SliceViewWidget.cpp @@ -1,4 +1,5 @@ #include "SliceViewWidget.h" +#include "HotLegendWidget.h" #include #include @@ -153,6 +154,9 @@ SliceViewWidget::SliceViewWidget(ViewAxis axis, QWidget* parent) m_pickLabel->hide(); m_pickLabel->raise(); + m_colorBar = new HotLegendWidget(this); + m_colorBar->hide(); + auto style = vtkSmartPointer::New(); m_vtkWidget->GetInteractor()->SetInteractorStyle(style); @@ -351,9 +355,32 @@ void SliceViewWidget::updateOrientationLabels() m_labelBottom->raise(); m_labelLeft->raise(); m_labelRight->raise(); + if (m_colorBar && m_colorBarVisible) { + const int barW = m_colorBar->width(); + const int barH = std::max(80, height() - 36); + m_colorBar->setGeometry(width() - barW - 8, (height() - barH) / 2, barW, barH); + m_colorBar->show(); + m_colorBar->raise(); + } m_pickLabel->raise(); } +void SliceViewWidget::setPickLabelMode(PickLabelMode mode) +{ + m_pickLabelMode = mode; +} + +void SliceViewWidget::setColorBarVisible(bool visible, double vmin, double vmax) +{ + m_colorBarVisible = visible; + if (m_colorBar) { + m_colorBar->setRange(vmin, vmax); + if (!visible) + m_colorBar->hide(); + } + updateOrientationLabels(); +} + void SliceViewWidget::setBackground(DoseImage::Pointer bg) { m_bg = std::move(bg); @@ -808,7 +835,9 @@ void SliceViewWidget::updatePickOverlay(const QPoint& pos) } const float bVal = m_bg->sampleWorld(world.x, world.y, world.z); QString text; - if (m_fg) { + if (m_pickLabelMode == PickLabelMode::ValueOnly) { + text = QStringLiteral("%1").arg(bVal, 0, 'f', 3); + } else if (m_fg) { const float fVal = m_fg->sampleWorld(world.x, world.y, world.z); text = QStringLiteral("F: %1\nB: %2").arg(fVal, 0, 'f', 3).arg(bVal, 0, 'f', 3); } else { diff --git a/src/SliceViewWidget.h b/src/SliceViewWidget.h index 54a5620..e809a2f 100644 --- a/src/SliceViewWidget.h +++ b/src/SliceViewWidget.h @@ -36,6 +36,12 @@ public: void setColorMap(DoseColorMap map); DoseColorMap colorMap() const { return m_colorMap; } + enum class PickLabelMode { FrontBackground = 0, ValueOnly = 1 }; + void setPickLabelMode(PickLabelMode mode); + + // Optional in-view Hot colorbar (e.g. gamma 0–2). + void setColorBarVisible(bool visible, double vmin = 0.0, double vmax = 2.0); + bool visibleRangeForWorldAxis(int worldAxis, double& rmin, double& rmax) const; double parallelScale() const; @@ -86,6 +92,7 @@ private: double m_window = 1.0; double m_level = 0.5; DoseColorMap m_colorMap = DoseColorMap::Gray; + PickLabelMode m_pickLabelMode = PickLabelMode::FrontBackground; bool m_hasCursor = false; bool m_cameraReady = false; double m_halfU = 50.0; @@ -99,6 +106,8 @@ private: QLabel* m_labelLeft = nullptr; QLabel* m_labelRight = nullptr; QLabel* m_pickLabel = nullptr; + class HotLegendWidget* m_colorBar = nullptr; + bool m_colorBarVisible = false; vtkSmartPointer m_renderer; vtkSmartPointer m_bgSlice; diff --git a/src/dvh/DvhCenterPart.h b/src/dvh/DvhCenterPart.h new file mode 100644 index 0000000..dab2aeb --- /dev/null +++ b/src/dvh/DvhCenterPart.h @@ -0,0 +1,122 @@ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "dvhTypes.h" +#include "itkStlCompat.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "dcmIO.h" + +class DvhCenterPart : public QWidget { + Q_OBJECT +public: + QRadioButton* staRBtn = nullptr; + QPushButton* calcBtn = nullptr; + QTextEdit* jsEdTEd = nullptr; + QTextEdit* optsTEd = nullptr; + QProgressBar* progBar = nullptr; + + QPushButton* allSBtn = nullptr; + QPushButton* cacSBtn = nullptr; + QPushButton* refrBtn = nullptr; + QListWidget* roiListW = nullptr; + + QLabel* imgLab = nullptr; + QLabel* rtsLab = nullptr; + QCheckBox* domCkb = nullptr; + QLineEdit* imgLEd = nullptr; + QLineEdit* rtsLEd = nullptr; + QPushButton* imgBtn = nullptr; + QPushButton* rtsBtn = nullptr; + QTableWidget* dsPathTW = nullptr; + + QPushButton* adDBtn = nullptr; + QPushButton* rmDBtn = nullptr; + + QHBoxLayout* mainLayout = nullptr; + QVBoxLayout* rsboxLayout = nullptr; + QGroupBox* selcDVHsBox = nullptr; + QGroupBox* showDVHsBox = nullptr; + QGroupBox* showDsPtBox = nullptr; + QGroupBox* showJSONBox = nullptr; + QGroupBox* showOutPBox = nullptr; + QGroupBox* ctrolGrpBox = nullptr; + + QVTKOpenGLWidget* vtkWidget = nullptr; + vtkSmartPointer vtkView; + vtkSmartPointer vtkChart; + int doseNum = 0; + StrucSet* strucSet = nullptr; + std::string imgPath; + std::string rtsPath; + std::vector dseNameVec; + std::vector rtdPathVec; + std::vector chckBoxVec; + std::vector chckDseVec; + std::vector nameDseVec; + std::vector lnEdDseVec; + std::vector ptBtDseVec; + +public: + explicit DvhCenterPart(QWidget* parent = nullptr); + ~DvhCenterPart() override; + + void setupUi(); + void creatSelcDVHsBox(); + void creatShowDVHsBox(); + void creatShowDsPtBox(); + void creatShowJSONBox(); + void creatShowOutPBox(); + void creatCtrolGrpBox(); + +public slots: + void calculateDVH(); + void msg_about(); + void msg_set(); + void open(); + void save(); + + void freeStrucSetDat(); + void updateROIList(); + + void checkAllRoi(); + void canclAllRoi(); + void reflshRoiVw(); + + int loadJSON(); + int writeJSON(int keyV); + void dseDialogOpn(); + void imgDialogOpn(); + void rtsDialogOpn(); + + void addDoseTable(); + void rmvDoseTable(); + +private: + QString algoDir() const; + QString toUiString(const std::string& s) const; + std::string fromUiString(const QString& s) const; +}; diff --git a/src/dvh/DvhCenterPart_slot.cpp b/src/dvh/DvhCenterPart_slot.cpp new file mode 100644 index 0000000..9818a6b --- /dev/null +++ b/src/dvh/DvhCenterPart_slot.cpp @@ -0,0 +1,1081 @@ +#include "DvhCenterPart.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// struc +struct uc3 +{ + unsigned char r; + unsigned char g; + unsigned char b; + uc3(unsigned char rIn, + unsigned char gIn, + unsigned char bIn): + r(rIn), g(gIn), b(bIn) + {} +}; + +// static function +template +static std::vector removeStdVec(std::vector vec, int id); + +// create color table +static void createColorTable(std::vector& colorTab); + + +void DvhCenterPart::save() +{ + //QString jsonInfo = this->jsEdTEd->toPlainText(); + //QByteArray jsonInfoUint8 = jsonInfo.toLocal8Bit(); + //QString jsonPath = algoDir() + QStringLiteral("/param.json"); + //QFile file(jsonPath); + //file.open(QIODevice::WriteOnly | QIODevice::Text); + //file.write((const char*)jsonInfoUint8.data(), jsonInfoUint8.length()); + //file.close(); + writeJSON(1); + this->progBar->setValue(10); +} + +//void DvhCenterPart::cbk(int prog, std::string status) +//{ +// //printf("prog: %d %s\n", prog, status); +// progBar->setValue(prog / 10); +// optsTEd->append(toUiString(status)); +//} +// +//QProgressBar* DvhCenterPart::progBar; +//QTextEdit* DvhCenterPart::optsTEd; + +void DvhCenterPart::open() +{ + QString jsonPath = algoDir() + QStringLiteral("/param.json"); + + if (!jsonPath.isEmpty()) + { + QFile* file = new QFile; + file->setFileName(jsonPath); + bool ok = file->open(QIODevice::ReadOnly); + + if (ok) + { + QTextStream in(file); + this->jsEdTEd->setText(in.readAll()); + file->close(); + delete file; + } + else + { + QMessageBox::information(this, "Error Message", "Open File:" + file->errorString()); + return; + } + } + + this->progBar->setValue(10); +} + + +void DvhCenterPart::msg_about() +{ + //QMessageBox::information(this, tr("About"), \ + // tr("For calculating interpolate-free Dose-Volume Histogram \n" + // "by Shengyu Wu, Zhigang Yuan & Huashan SHENG!"), QMessageBox::Yes); + QMessageBox message(QMessageBox::NoIcon, "About", + QString::fromUtf8(u8"关注公众号: 山哥的岛屿 了解更多 \n" + u8"author: 山哥SHENG\n" + u8"e-mail: shs3701001@sjtu.edu.cn\n" + u8"version:1.0.0.0\n" + u8"data: 2023-4-3")); + message.setIconPixmap(QPixmap("./icons/qcode.jpg")); + message.exec(); +} + +void DvhCenterPart::msg_set() +{ + QMessageBox::information(this, tr("Setting"), \ + tr("No settings now!"), QMessageBox::Yes); +} + + +void DvhCenterPart::calculateDVH() +{ + //..................................................................... + // clear plot + //..................................................................... + this->vtkChart->ClearPlots(); + this->optsTEd->clear(); + //..................................................................... + // initial StrucSet + //..................................................................... + freeStrucSetDat(); + //..................................................................... + // get DLL + //..................................................................... + QString postFix = QStringLiteral(".dll"); +#ifdef _DEBUG + postFix = QStringLiteral("D.dll"); +#endif + const QString calcDvhDllPath = algoDir() + QStringLiteral("/calcDVHLibrary") + postFix; + QLibrary dvhLib(calcDvhDllPath); + if (!dvhLib.load()) + { + QMessageBox::warning(this, tr("Warning"), + tr("No DVH algorithm DLL!\n%1\n%2").arg(calcDvhDllPath, dvhLib.errorString()), + QMessageBox::Yes); + this->progBar->setValue(10); + this->staRBtn->setCheckable(false); + return; + } + + typedef int(*pDvhAlgoFunc)(const float * ofs, const float * spc, const int* scz, const float * dose, StrucSet & sSet); + pDvhAlgoFunc dvhAlgoFunc = (pDvhAlgoFunc)dvhLib.resolve("calcStrucDVH"); + + if (dvhAlgoFunc == nullptr) + { + QMessageBox::warning(this, tr("Warning"), + tr("No DVh algorithm function in DLL!"), QMessageBox::Yes); + this->progBar->setValue(10); + this->staRBtn->setCheckable(false); + dvhLib.unload(); + return; + } + + this->progBar->setValue(1); + this->optsTEd->append(toUiString("Load DLL successfully!")); + ////..................................................................... + //// save JSON + ////..................................................................... + //QString jsonInfo = this->jsEdTEd->toPlainText(); + //QByteArray jsonInfoUint8 = jsonInfo.toLocal8Bit(); + //QString jsonPath = algoDir() + QStringLiteral("/param.json"); + //QFile file(jsonPath); + //file.open(QIODevice::WriteOnly | QIODevice::Text); + //file.write((const char*)jsonInfoUint8.data(), jsonInfoUint8.length()); + //file.close(); + //this->progBar->setValue(3); + //..................................................................... + // save Path information to JSON + //..................................................................... + int ret = writeJSON(0); + + if (ret < 0) return; + + //..................................................................... + // JSON parameter + //..................................................................... + QString jsonPath = algoDir() + QStringLiteral("/param.json"); + std::ifstream inFile(jsonPath.toLocal8Bit().constData(), std::ios::in); + + if (!inFile.is_open()) + { + std::cerr << "Open the JOSON file failed" << std::endl; + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Open the JOSON file failed")); + this->staRBtn->setChecked(false); + dvhLib.unload(); + return; + } + + Json::Value root; + Json::Reader reader; + + if (!reader.parse(inFile, root, false)) + { + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Parse the JOSON file failed")); + this->staRBtn->setChecked(false); + dvhLib.unload(); + return; + } + + this->progBar->setValue(2); + this->optsTEd->append(toUiString("Load Json file successfully!")); + //..................................................................... + // parsing DICOM input path + //..................................................................... + std::string imgFile = root["imgFile"].asString(); + std::string rtsFile = root["rtsFile"].asString(); + int doseGroupSz = root["doseSet"].size(); + int dNum = doseGroupSz; + + // check number of dose + if (doseGroupSz < 0 || doseGroupSz > 5) + { + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Wrong dose number, check JSON file")); + this->staRBtn->setChecked(false); + dvhLib.unload(); + return; + } + + std::vector dgTagName; + std::vector dgPath; + + for (int gid = 0; gid < doseGroupSz; gid++) + { + dgTagName.push_back(root["doseSet"][gid]["tagName"].asString()); + dgPath.push_back(root["doseSet"][gid]["path"].asString()); + } + + this->progBar->setValue(3); + this->optsTEd->append(toUiString("Parse Json file successfully!")); + this->optsTEd->append(toUiString("Loading image data...")); + //..................................................................... + // read image + //..................................................................... + itk::Image::Pointer img; + std::string postfix = imgFile.substr(imgFile.length() - 3); + + if (strcmp(postfix.c_str(), "mhd") == 0 || + strcmp(postfix.c_str(), "nii") == 0) + ret = itkMetaDataReader(imgFile, img); + else + ret = itkDicomSeriesReader(imgFile, img); + + if (ret < 0) + { + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Error in DICOM image: " + std::to_string(ret))); + this->staRBtn->setChecked(false); + dvhLib.unload(); + return; + } + + this->progBar->setValue(4); + this->optsTEd->append(toUiString("Load image successfully!")); + this->optsTEd->append(toUiString("Loading dose data...")); + //..................................................................... + // read dose set + //..................................................................... + std::vector::Pointer> doseGroup; + + for (int gid = 0; gid < doseGroupSz; ++gid) + { + itk::Image::Pointer dose; + postfix = dgPath[gid].substr(dgPath[gid].length() - 3); + + if (strcmp(postfix.c_str(), "mhd") == 0 || + strcmp(postfix.c_str(), "nii") == 0) + ret = itkMetaDataReader(dgPath[gid], dose); + else if (strcmp(postfix.c_str(), "dcm") == 0) + ret = itkDcmDoseReader(dgPath[gid], dose); + else + ret = -1; + + if (ret < 0) + { + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Error in loading dose : " + std::to_string(ret))); + this->staRBtn->setChecked(false); + dvhLib.unload(); + return; + } + + doseGroup.push_back(dose); + } + + this->progBar->setValue(5); + this->optsTEd->append(toUiString("Load dose successfully!")); + this->optsTEd->append(toUiString("Loading structure data...")); + //..................................................................... + // structure + //..................................................................... + std::vector nameVec; + std::vector::Pointer> maskVec; + // load structure + postfix = rtsFile.substr(rtsFile.length() - 3); + + if (strcmp(postfix.c_str(), "son") == 0) + ret = itkRtJsonStructReader(rtsFile, img, nameVec, maskVec); + else if (strcmp(postfix.c_str(), "dcm") == 0) + ret = itkRtStructureReader(rtsFile, img, nameVec, maskVec); + else + ret = -1; + + if (ret < 0) + { + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Error in loading Rt structure: " + std::to_string(ret))); + this->staRBtn->setChecked(false); + dvhLib.unload(); + return; + } + + this->progBar->setValue(7); + this->optsTEd->append(toUiString("Load Rt structure successfully!")); + this->optsTEd->append(toUiString("DVH calculating...")); + //..................................................................... + // to Structure set + //..................................................................... + this->doseNum = doseGroupSz; + this->strucSet = new StrucSet[doseGroupSz]; + + for (int gid = 0; gid < doseGroupSz; ++gid) + { + strucSet[gid].tagName = dgTagName[gid]; + strucSet[gid].strucNum = maskVec.size(); + strucSet[gid].struc = new Struc[strucSet[gid].strucNum]; + + for (int sid = 0; sid < maskVec.size(); ++sid) + { + Struc* struc = strucSet[gid].struc + sid; + struc->strucName = nameVec[sid]; + struc->h_dat = maskVec[sid]->GetBufferPointer(); + + for (int i = 0; i < 3; ++i) + { + struc->origin[i] = maskVec[sid]->GetOrigin()[i]; + struc->spacing[i] = maskVec[sid]->GetSpacing()[i]; + struc->size[i] = maskVec[sid]->GetLargestPossibleRegion().GetSize()[i]; + } + + struc->datSz = struc->size[0] * struc->size[1] * struc->size[2]; + } + + //................................................................. + // dose + //................................................................. + float dsOrigin[3]; + float dsSpacing[3]; + int dsSize[3]; + + for (int i = 0; i < 3; ++i) + { + dsOrigin[i] = doseGroup[gid]->GetOrigin()[i]; + dsSpacing[i] = doseGroup[gid]->GetSpacing()[i]; + dsSize[i] = doseGroup[gid]->GetLargestPossibleRegion().GetSize()[i]; + } + + // float* dosePtr = doseGroup[gid]->GetBufferPointer(); + float* dosePtr = doseGroup[gid]->GetBufferPointer(); + //................................................................. + // update for dvh + //................................................................. + dvhAlgoFunc(dsOrigin, dsSpacing, dsSize, dosePtr, strucSet[gid]); + //................................................................. + // printf the min max mean value + //................................................................. + this->optsTEd->append("....................Statistic Result.................."); + + for (int i = 0; i < strucSet[gid].strucNum; ++i) + { + int datSz = strucSet[gid].struc[i].innSz; + double sum = 0; + + for (int k = 0; k < datSz; ++k) + sum += strucSet[gid].struc[i].h_innDat[k]; + + sum /= datSz; + std::string minInfo = strucSet[gid].tagName + "'s " + + strucSet[gid].struc[i].strucName + " min: " + + std::to_string(strucSet[gid].struc[i].h_innDat[0]); + std::string maxInfo = strucSet[gid].tagName + "'s " + + strucSet[gid].struc[i].strucName + " max: " + + std::to_string(strucSet[gid].struc[i].h_innDat[datSz - 1]); + std::string aveInfo = strucSet[gid].tagName + "'s " + + strucSet[gid].struc[i].strucName + " ave: " + + std::to_string(sum); + this->optsTEd->append(toUiString(minInfo)); + this->optsTEd->append(toUiString(maxInfo)); + this->optsTEd->append(toUiString(aveInfo)); + this->optsTEd->append(QString("")); + } + } + + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Calculate DVHs successfully!")); + // update ROI list + updateROIList(); + //..................................................................... + // Free Library + //..................................................................... + dvhLib.unload(); + this->staRBtn->setChecked(true); + //..................................................................... + // display + //..................................................................... + //..................................................................... + // get max dose + //..................................................................... + float maxDose = -1; + + for (int gid = 0; gid < doseGroupSz; ++gid) + { + //..................................................................... + // loop for each structure + //..................................................................... + for (int i = 0; i < strucSet->strucNum; ++i) + { + Struc* struc = strucSet[gid].struc + i; + int innSz = struc->innSz; + + if (struc->h_innDat[innSz - 1] > maxDose) + maxDose = struc->h_innDat[innSz - 1]; + } + } + + //..................................................................... + // information + //..................................................................... + int strucNum = strucSet->strucNum; + // color + std::vector colorTab; + createColorTable(colorTab); + float* dvhSpl = (float*)malloc(sizeof(float) * DVH_MAX_SPN); + this->vtkChart->GetAxis(vtkAxis::BOTTOM)->SetRange(0, maxDose * 1.2); + + //..................................................................... + // loop for each dose in dose group + //..................................................................... + for (int gid = 0; gid < doseGroupSz; ++gid) + { + //..................................................................... + // loop for each structure + //..................................................................... + for (int i = 0; i < strucNum; ++i) + { + Struc* struc = strucSet[gid].struc + i; + //................................................................. + // resize for a reasonable data length for DVH + //................................................................. + int dvhSz = struc->innSz; + float* dvhDat = struc->h_innDat; + + // LINE plots need >= 2 points (placeholder at 0 + at least one DVH sample). + if (dvhSz < 1 || !dvhDat) + continue; + + if (dvhSz > DVH_MAX_SPN) + { + // head and tail the same + dvhSpl[0] = dvhDat[0]; + dvhSpl[DVH_MAX_SPN - 1] = dvhDat[dvhSz - 1]; + dvhSz = DVH_MAX_SPN; + float span = (struc->innSz - 1) * 1.f / (DVH_MAX_SPN - 1); + // downsample + + for (int k = 1; k < DVH_MAX_SPN - 1; ++k) + { + float p = (k * span); + int id0 = floor(p); + int id1 = id0 + 1; + float w0 = id1 - p; + float w1 = p - id0; + dvhSpl[k] = w0 * dvhDat[id0] + w1 * dvhDat[id1]; + //struc->coorD.push_back(dvhSpl[k]); + } + + dvhDat = dvhSpl; + } + + //................................................................. + // create a float array + //................................................................. + // Create a table with some points in it + vtkSmartPointer table = vtkSmartPointer::New(); + vtkSmartPointer arrX = vtkSmartPointer::New(); + vtkSmartPointer arrY = vtkSmartPointer::New(); + std::string strucNameStr = struc->strucName; + std::string tagNameStr = strucSet[gid].tagName; + arrX->SetName("dose"); + arrY->SetName((strucNameStr + "(" + tagNameStr + ")").c_str()); + table->AddColumn(arrX); + table->AddColumn(arrY); + table->SetNumberOfRows(dvhSz + 1); + table->SetValue(0, 0, 0.0); + table->SetValue(0, 1, 1.0); + + for (int j = 0; j < dvhSz; ++j) + { + table->SetValue(j + 1, 0, dvhDat[j]); + table->SetValue(j + 1, 1, 1.0 - (j + 1) / (1.f * dvhSz)); + } + + vtkPlot* plot1 = this->vtkChart->AddPlot(vtkChart::LINE); + plot1->SetInputData(table, 0, 1); + plot1->SetColor(colorTab[i % colorTab.size()].r, + colorTab[i % colorTab.size()].g, + colorTab[i % colorTab.size()].b, 255); + plot1->SetWidth(2); + plot1->GetPen()->SetLineType(gid % 5 + 1); + } + } + + // free + free(dvhSpl); + vtkView->GetScene()->AddItem(vtkChart); + vtkView->Render(); + this->progBar->setValue(10); +} + + + +void DvhCenterPart::freeStrucSetDat() +{ + //..................................................................... + // free data + //..................................................................... + for (int gid = 0; gid < this->doseNum; ++gid) + { + if (this->strucSet == nullptr) break; + + // if structures + if (this->strucSet[gid].struc == nullptr) + break; + + for (int sid = 0; sid < this->strucSet[gid].strucNum; ++sid) + { + Struc* struc = this->strucSet[gid].struc + sid; + + // free h_innDat & notice h_dat from ITK Smart Pointer + if (struc->h_innDat != nullptr) + free(struc->h_innDat); + } + + delete[] this->strucSet[gid].struc; + } + + if (this->strucSet != nullptr) + { + delete[] this->strucSet; + this->strucSet = nullptr; + } + + this->doseNum = 0; +} + + +void DvhCenterPart::updateROIList() +{ + if (this->strucSet == nullptr || this->doseNum == 0) + return; + + // clear first + for (int i = 0; i < this->roiListW->count(); i++) + { + delete this->roiListW->takeItem(0); + } + + for (int i = 0; i < this->chckBoxVec.size(); i++) + { + this->chckBoxVec[i]->disconnect(); + delete this->chckBoxVec[i]; + } + + this->chckBoxVec.clear(); + this->roiListW->clear(); + + // list + for (int i = 0; i < this->strucSet->strucNum; ++i) + { + QListWidgetItem* item = new QListWidgetItem(); + QCheckBox* checkBox = new QCheckBox(); + // type + checkBox->setChecked(true); + checkBox->setText(toUiString(this->strucSet->struc[i].strucName) + + " "); /* space for click trick*/ + this->roiListW->addItem(item); + this->roiListW->setItemWidget(item, checkBox); + this->chckBoxVec.push_back(checkBox); + // slot + connect(checkBox, SIGNAL(clicked(bool)), this, SLOT(reflshRoiVw())); + } + + this->allSBtn->setEnabled(true); + this->cacSBtn->setEnabled(true); + this->refrBtn->setEnabled(true); +} + +void DvhCenterPart::checkAllRoi() +{ + for (int i = 0; i < this->chckBoxVec.size(); i++) + { + this->chckBoxVec[i]->setChecked(true); + } +} + +void DvhCenterPart::canclAllRoi() +{ + for (int i = 0; i < this->chckBoxVec.size(); i++) + { + this->chckBoxVec[i]->setChecked(false); + } +} + +void DvhCenterPart::reflshRoiVw() +{ + // clear first + this->vtkChart->ClearPlots(); + vtkView->GetScene()->RemoveItem(this->vtkChart); + // info + int strucNum = this->strucSet->strucNum; + // color + std::vector colorTab; + createColorTable(colorTab); + float* dvhSpl = (float*)malloc(sizeof(float) * DVH_MAX_SPN); + + //..................................................................... + // loop for each item + //..................................................................... + for (int i = 0; i < strucNum; ++i) + { + QListWidgetItem* item = roiListW->item(i); + + if (static_cast(roiListW->itemWidget(item))->isChecked()) + { + this->chckBoxVec[i]->setChecked(true); + } + else + { + this->chckBoxVec[i]->setChecked(false); + } + } + + //..................................................................... + // loop for each dose in dose group + //..................................................................... + for (int gid = 0; gid < this->doseNum; ++gid) + { + //..................................................................... + // loop for each structure + //..................................................................... + for (int i = 0; i < strucNum; ++i) + { + if (this->chckBoxVec[i]->isChecked()) + { + Struc* struc = strucSet[gid].struc + i; + //................................................................. + // resize for a reasonable data length for DVH + //................................................................. + int dvhSz = struc->innSz; + float* dvhDat = struc->h_innDat; + + // LINE plots need >= 2 points (placeholder at 0 + at least one DVH sample). + if (dvhSz < 1 || !dvhDat) + continue; + + if (dvhSz > DVH_MAX_SPN) + { + // head and tail the same + dvhSpl[0] = dvhDat[0]; + dvhSpl[DVH_MAX_SPN - 1] = dvhDat[dvhSz - 1]; + dvhSz = DVH_MAX_SPN; + float span = (struc->innSz - 1) * 1.f / (DVH_MAX_SPN - 1); + // downsample + + for (int k = 1; k < DVH_MAX_SPN - 1; ++k) + { + float p = (k * span); + int id0 = floor(p); + int id1 = id0 + 1; + float w0 = id1 - p; + float w1 = p - id0; + dvhSpl[k] = w0 * dvhDat[id0] + w1 * dvhDat[id1]; + } + + dvhDat = dvhSpl; + } + + //................................................................. + // create a float array + //................................................................. + // Create a table with some points in it + vtkSmartPointer table = vtkSmartPointer::New(); + vtkSmartPointer arrX = vtkSmartPointer::New(); + vtkSmartPointer arrY = vtkSmartPointer::New(); + std::string strucNameStr = struc->strucName; + std::string tagNameStr = strucSet[gid].tagName; + arrX->SetName("dose"); + arrY->SetName((strucNameStr + "(" + tagNameStr + ")").c_str()); + table->AddColumn(arrX); + table->AddColumn(arrY); + table->SetNumberOfRows(dvhSz + 1); + table->SetValue(0, 0, 0.0); + table->SetValue(0, 1, 1.0); + + for (int j = 0; j < dvhSz; ++j) + { + table->SetValue(j + 1, 0, dvhDat[j]); + table->SetValue(j + 1, 1, 1.0 - (j + 1) / (1.f * dvhSz)); + } + + vtkPlot* plot1 = this->vtkChart->AddPlot(vtkChart::LINE); + plot1->SetInputData(table, 0, 1); + plot1->SetColor(colorTab[i % colorTab.size()].r, + colorTab[i % colorTab.size()].g, + colorTab[i % colorTab.size()].b, 255); + plot1->SetWidth(2); + plot1->GetPen()->SetLineType(gid % 5 + 1); + } + } + } + + // free + free(dvhSpl); + vtkView->GetScene()->AddItem(vtkChart); + vtkView->Render(); + this->progBar->setValue(10); +} + + +int DvhCenterPart::loadJSON() +{ + // init + this->imgPath = ""; + this->rtsPath = ""; + this->rtdPathVec.clear(); + this->dseNameVec.clear(); + this->doseNum = 0; + // parse + QString jsonPath = algoDir() + QStringLiteral("/param.json"); + std::ifstream inFile(jsonPath.toLocal8Bit().constData(), std::ios::in); + + if (!inFile.is_open()) + { + std::cerr << "Open the JOSON file failed" << std::endl; + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Open the JOSON file failed")); + this->staRBtn->setChecked(false); + return -1; + } + + Json::Value root; + Json::Reader reader; + + if (!reader.parse(inFile, root, false)) + { + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Parse the JOSON file failed")); + this->staRBtn->setChecked(false); + return -1; + } + + this->progBar->setValue(2); + this->optsTEd->append(toUiString("Load Json file successfully!")); + //..................................................................... + // parsing DICOM input path + //..................................................................... + this->imgPath = root["imgFile"].asString(); + this->rtsPath = root["rtsFile"].asString(); + this->doseNum = root["doseSet"].size(); + int dNum = this->doseNum; + + // check number of dose + if (dNum < 0 || dNum > 5) + { + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Wrong dose number, check JSON file")); + this->staRBtn->setChecked(false); + return -1; + } + + for (int gid = 0; gid < dNum; gid++) + { + this->dseNameVec.push_back(root["doseSet"][gid]["tagName"].asString()); + this->rtdPathVec.push_back(root["doseSet"][gid]["path"].asString()); + } + + this->progBar->setValue(10); + this->optsTEd->append(toUiString("Parse Json file successfully!")); + this->optsTEd->append(toUiString("Loading image data...")); + return 0; +} + +int DvhCenterPart::writeJSON(int keyV) +{ + //..................................................................... + // save Path information to JSON + //..................................................................... + std::string jsonPath = fromUiString(algoDir() + QStringLiteral("/param.json")); + this->imgPath = fromUiString(this->imgLEd->text()); + this->rtsPath = fromUiString(this->rtsLEd->text()); + this->doseNum = this->dsPathTW->rowCount(); + + for (int i = 0; i < this->doseNum; ++i) + { + this->dseNameVec[i] = fromUiString(this->nameDseVec[i]->text()); + this->rtdPathVec[i] = fromUiString(this->lnEdDseVec[i]->text()); + } + + if (this->imgPath.length() > 0) + std::replace(this->imgPath.begin(), this->imgPath.end(), '\\', '/'); + + if (this->rtsPath.length() > 0) + std::replace(this->rtsPath.begin(), this->rtsPath.end(), '\\', '/'); + + for (int i = 0; i < this->doseNum; ++i) + { + if (this->rtdPathVec[i].length() > 0) + std::replace(this->rtdPathVec[i].begin(), this->rtdPathVec[i].end(), '\\', '/'); + } + + //..................................................................... + // save Path information to JSON + //..................................................................... + Json::Value root; + root["description"] = Json::Value("Parameters for display DVH"); + root["imgFile"] = Json::Value(this->imgPath); + root["rtsFile"] = Json::Value(this->rtsPath); + + for (int i = 0; i < doseNum; ++i) + { + if (this->chckDseVec[i]->isChecked() || keyV == 1) + { + Json::Value doseSet; + doseSet["tagName"] = Json::Value(this->dseNameVec[i]); + doseSet["path"] = Json::Value(this->rtdPathVec[i]); + root["doseSet"].append(doseSet); + } + } + + // write to file + Json::StyledWriter sw; + std::ofstream os; + os.open(jsonPath, std::ios::ate | std::ios::out); + + if (!os.is_open()) + { + QMessageBox::information(this, tr("WRITE JSON WARNING"), + tr("ERROR: can not find or create the dose JSON FILE"), + tr("Yes")); + return -1; + } + + os << sw.write(root); + os.close(); + return 0; +} + + +void DvhCenterPart::dseDialogOpn() +{ + // get button id + int idx = -1; + QPushButton* Btn = qobject_cast(sender()); + + for (int i = 0; i < this->doseNum; i++) + { + if (Btn == this->ptBtDseVec[i]) + { + idx = i; + break; + } + } + + if (idx < 0) + { + return; + } + + // open DICOM file + QString QfileName; + + // check the current vale + if (fromUiString(this->lnEdDseVec[idx]->text()).length() > 0) + QfileName = QFileDialog::getOpenFileName(this, tr("Open DICOM RTS file"), + fromUiString(this->lnEdDseVec[idx]->text()).c_str(), tr("Dose File (*.dcm *.nii *.mhd)")); + else + QfileName = QFileDialog::getOpenFileName(this, tr("Open DICOM RTS file"), "./", + tr("Dose File (*.dcm *.nii *.mhd)")); + + std::string filename = fromUiString(QfileName); + + if (filename.length() > 0) + std::replace(filename.begin(), filename.end(), '\\', '/'); + else + filename = fromUiString(this->lnEdDseVec[idx]->text()); + + // to Line edit + this->lnEdDseVec[idx]->setText(toUiString(filename)); +} + +void DvhCenterPart::imgDialogOpn() +{ + // open folder or file + std::string fName; + + if (this->domCkb->isChecked()) + { + QString QFolderName; + + // check the current vale + if (fromUiString(this->imgLEd->text()).length() > 0) + QFolderName = QFileDialog::getExistingDirectory(this, tr("Open image Directory"), + fromUiString(this->imgLEd->text()).c_str(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + else + QFolderName = QFileDialog::getExistingDirectory(this, tr("Open image Directory"), + "./", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + fName = fromUiString(QFolderName); + } + else + { + QString QfileName; + + // check the current vale + if (fromUiString(this->rtsLEd->text()).length() > 0) + QfileName = QFileDialog::getOpenFileName(this, tr("Open struct file"), + fromUiString(this->rtsLEd->text()).c_str(), tr("Meta Image(*.nii *.mhd)")); + else + QfileName = QFileDialog::getOpenFileName(this, tr("Open struct file"), "./", + tr("Struct File (*.dcm *.json)")); + + fName = fromUiString(QfileName); + } + + if (fName.length() > 0) + std::replace(fName.begin(), fName.end(), '\\', '/'); + else + fName = fromUiString(this->imgLEd->text()); + + // to Line edit + this->imgLEd->setText(toUiString(fName)); +} + +// open folder or file diag +void DvhCenterPart::rtsDialogOpn() +{ + // open DICOM file + QString QfileName; + + // check the current vale + if (fromUiString(this->rtsLEd->text()).length() > 0) + QfileName = QFileDialog::getOpenFileName(this, tr("Open struct file"), + fromUiString(this->rtsLEd->text()).c_str(), tr("Struct File (*.dcm *.json)")); + else + QfileName = QFileDialog::getOpenFileName(this, tr("Open struct file"), "./", + tr("Struct File (*.dcm *.json)")); + + std::string filename = fromUiString(QfileName); + + if (filename.length() > 0) + std::replace(filename.begin(), filename.end(), '\\', '/'); + else + filename = fromUiString(this->rtsLEd->text()); + + // to Line edit + this->rtsLEd->setText(toUiString(filename)); +} + + +// Add dose table +void DvhCenterPart::addDoseTable() +{ + if (this->doseNum > 4) return; + + // num ++ + this->doseNum = this->doseNum + 1; + int curId = this->doseNum - 1; + // push back std vector + this->dseNameVec.push_back("New_" + std::to_string(int(curId))); + this->rtdPathVec.push_back("C:/"); + // add widget + this->dsPathTW->setRowCount(this->doseNum); + this->chckDseVec.push_back(new QCheckBox()); + this->chckDseVec[curId]->setChecked(true); + this->nameDseVec.push_back(new QLineEdit(toUiString(this->dseNameVec[curId]))); + this->nameDseVec[curId]->setStyleSheet("background:transparent;border-width:0;border-style:outset"); + this->lnEdDseVec.push_back(new QLineEdit()); + this->lnEdDseVec[curId]->setStyleSheet("background:transparent;border-width:0;border-style:outset"); + this->lnEdDseVec[curId]->setText(toUiString(this->rtdPathVec[curId])); + this->lnEdDseVec[curId]->setCursorPosition(0); + this->ptBtDseVec.push_back(new QPushButton(QWidget::tr(".."))); + connect(this->ptBtDseVec[curId], SIGNAL(clicked(bool)), this, SLOT(dseDialogOpn())); + this->dsPathTW->setCellWidget(curId, 0, this->chckDseVec[curId]); + this->dsPathTW->setCellWidget(curId, 1, this->nameDseVec[curId]); + this->dsPathTW->setCellWidget(curId, 2, this->lnEdDseVec[curId]); + this->dsPathTW->setCellWidget(curId, 3, this->ptBtDseVec[curId]); +} + + +// remove table +void DvhCenterPart::rmvDoseTable() +{ + if (this->doseNum == 0) return; + + int currId = this->dsPathTW->currentRow(); + std::cout << "current row: " << currId << std::endl; + std::string warnStr = "Need Delete Row" + std::to_string(currId) + " ?"; + + // warning + switch (QMessageBox::information(this, tr("DELETE WARNING"), + toUiString(warnStr), + tr("Yes"), tr("No"), 0, 1)) + { + case 0: + break; + + case 1: + return; + } + + // delete + this->dseNameVec = removeStdVec(this->dseNameVec, currId); + this->rtdPathVec = removeStdVec(this->rtdPathVec, currId); + this->chckDseVec = removeStdVec(this->chckDseVec, currId); + // free new + this->ptBtDseVec[currId]->disconnect(); + delete this->nameDseVec[currId]; + delete this->lnEdDseVec[currId]; + delete this->ptBtDseVec[currId]; + this->nameDseVec = removeStdVec(this->nameDseVec, currId); + this->lnEdDseVec = removeStdVec(this->lnEdDseVec, currId); + this->ptBtDseVec = removeStdVec(this->ptBtDseVec, currId); + // table + this->doseNum = this->doseNum - 1; + this->dsPathTW->removeRow(currId); + return; +} + + + + +// static function +template +static std::vector removeStdVec(std::vector vec, int id) +{ + if (id >= vec.size()) return vec; + + std::vector ret; + + for (int i = 0; i < vec.size(); ++i) + { + if (i != id) + { + ret.push_back(vec[i]); + } + } + + // check + if (ret.size() == vec.size() - 1) + { + vec.clear(); + return ret; + } + else + return vec; +} + + +// create color table +static void createColorTable(std::vector& colorTab) +{ + colorTab.push_back(uc3(255, 0, 255)); + colorTab.push_back(uc3(227, 23, 13)); + colorTab.push_back(uc3(128, 42, 42)); + colorTab.push_back(uc3(255, 128, 0)); + colorTab.push_back(uc3(255, 153, 18)); + colorTab.push_back(uc3(127, 255, 0)); + colorTab.push_back(uc3(34, 139, 34)); + colorTab.push_back(uc3(64, 224, 205)); + colorTab.push_back(uc3(3, 168, 158)); + colorTab.push_back(uc3(0, 0, 255)); + colorTab.push_back(uc3(218, 112, 214)); + colorTab.push_back(uc3(138, 43, 226)); +} \ No newline at end of file diff --git a/src/dvh/DvhCenterPart_ui.cpp b/src/dvh/DvhCenterPart_ui.cpp new file mode 100644 index 0000000..0e66cef --- /dev/null +++ b/src/dvh/DvhCenterPart_ui.cpp @@ -0,0 +1,273 @@ +#include "DvhCenterPart.h" + +#include +#include +#include +#include +#include + +DvhCenterPart::DvhCenterPart(QWidget* parent) + : QWidget(parent) +{ + this->setupUi(); +} + +DvhCenterPart::~DvhCenterPart() +{ + freeStrucSetDat(); +} + + +void DvhCenterPart::setupUi() +{ + this->doseNum = 0; + this->strucSet = nullptr; + // select unit type + this->staRBtn = new QRadioButton(); + this->calcBtn = new QPushButton(QWidget::tr("CALCULATE")); + this->jsEdTEd = new QTextEdit(); + this->optsTEd = new QTextEdit(); + this->progBar = new QProgressBar(); + this->progBar->setMinimum(0); + this->progBar->setMaximum(10); + this->progBar->setValue(10); + this->allSBtn = new QPushButton(QWidget::tr("ALL")); + this->cacSBtn = new QPushButton(QWidget::tr("CLC")); + this->refrBtn = new QPushButton(QWidget::tr("DSP")); + this->roiListW = new QListWidget(); + // Table for dose path + this->imgLab = new QLabel(QWidget::tr("Image :")); + this->rtsLab = new QLabel(QWidget::tr("Struc :")); + this->imgLab->setFixedSize(QSize(70, 20)); + this->rtsLab->setFixedSize(QSize(70, 20)); + this->domCkb = new QCheckBox(QWidget::tr("DCM or Meta Img")); + this->domCkb->setStyleSheet("QCheckBox::indicator {\n" + "width:24px;height:24px;\n}" + "QCheckBox::indicator:checked {image: url(./icons/dicom.png);}" + "QCheckBox::indicator:unchecked {image: url(./icons/meta.png);}"); + this->domCkb->setChecked(true); + this->imgLEd = new QLineEdit(); + this->rtsLEd = new QLineEdit(); + this->imgBtn = new QPushButton(QWidget::tr("...")); + this->rtsBtn = new QPushButton(QWidget::tr("...")); + this->imgBtn->setFixedSize(QSize(20, 20)); + this->rtsBtn->setFixedSize(QSize(20, 20)); + connect(this->imgBtn, SIGNAL(clicked(bool)), this, SLOT(imgDialogOpn())); + connect(this->rtsBtn, SIGNAL(clicked(bool)), this, SLOT(rtsDialogOpn())); + this->adDBtn = new QPushButton(QWidget::tr("+")); + this->rmDBtn = new QPushButton(QWidget::tr("-")); + connect(this->adDBtn, SIGNAL(clicked(bool)), this, SLOT(addDoseTable())); + connect(this->rmDBtn, SIGNAL(clicked(bool)), this, SLOT(rmvDoseTable())); + this->dsPathTW = new QTableWidget(this); + this->dsPathTW->setColumnCount(4); + this->dsPathTW->setColumnWidth(0, 30); + this->dsPathTW->setColumnWidth(1, 50); + this->dsPathTW->setColumnWidth(2, 380); + this->dsPathTW->setColumnWidth(3, 30); + this->dsPathTW->verticalHeader()->hide(); + this->dsPathTW->setSelectionMode(QTableWidget::SingleSelection); + QStringList labelsW; + labelsW.append(""); + labelsW.append("Name"); + labelsW.append("Dose Path"); + labelsW.append(""); + this->dsPathTW->setHorizontalHeaderLabels(labelsW); + this->dsPathTW->setEditTriggers(QAbstractItemView::NoEditTriggers); + this->dsPathTW->setSelectionBehavior(QAbstractItemView::SelectRows); + this->dsPathTW->horizontalHeader()->setStyleSheet("QHeaderView::section {background-color:rgba(100,100,100,0.3);color: green;}"); + this->dsPathTW->setShowGrid(false); + this->dsPathTW->setSelectionBehavior(QAbstractItemView::SelectRows); + this->dsPathTW->setContextMenuPolicy(Qt::CustomContextMenu); + //this->roiListW->setStyleSheet("QListWidget::item{margin:2px}"); + connect(calcBtn, SIGNAL(clicked(bool)), this, SLOT(calculateDVH())); + connect(allSBtn, SIGNAL(clicked(bool)), this, SLOT(checkAllRoi())); + connect(cacSBtn, SIGNAL(clicked(bool)), this, SLOT(canclAllRoi())); + connect(refrBtn, SIGNAL(clicked(bool)), this, SLOT(reflshRoiVw())); + this->jsEdTEd->setWordWrapMode(QTextOption::NoWrap); + this->optsTEd->setWordWrapMode(QTextOption::NoWrap); + //this->jsEdTEd->setStyleSheet("QTextEdit{background-color:rgba(0,0,0,0);}"); + QFont font = QFont("Consolas", 9, 2); + this->jsEdTEd->setFont(font); + this->optsTEd->setStyleSheet("QTextEdit{background-color:rgba(0,0,0,0);}"); + // type + this->staRBtn->setStyleSheet("QRadioButton::indicator {\n" + "width:26px;height:26px;\n}" + "QRadioButton::indicator:checked {image: url(./icons/ok.png);}" + "QRadioButton::indicator:unchecked {image: url(./icons/wr.png);}" + ); + this->staRBtn->setChecked(true); + this->staRBtn->setDisabled(true); + this->allSBtn->setDisabled(true); + this->cacSBtn->setDisabled(true); + this->refrBtn->setDisabled(true); + // lay out + this->mainLayout = new QHBoxLayout(); + this->rsboxLayout = new QVBoxLayout(); + // box + this->selcDVHsBox = new QGroupBox("ROI LISTS"); + this->showDVHsBox = new QGroupBox("DVH RESULTs"); + this->showDsPtBox = new QGroupBox("PATHS"); + this->showJSONBox = new QGroupBox("JSON OF DOSE & STRUCTURE"); + this->showOutPBox = new QGroupBox("OUT PUT INFO."); + this->ctrolGrpBox = new QGroupBox("CONTROL"); + //...................................................................... + this->creatSelcDVHsBox(); + this->creatShowDVHsBox(); + this->creatShowDsPtBox(); + this->creatShowJSONBox(); + this->creatShowOutPBox(); + this->creatCtrolGrpBox(); + //...................................................................... + this->rsboxLayout->addWidget(this->showDsPtBox, 5); + //this->rsboxLayout->addWidget(this->showJSONBox, 5); + this->rsboxLayout->addWidget(this->ctrolGrpBox, 1); + this->rsboxLayout->addWidget(this->showOutPBox, 4); + this->mainLayout->addWidget(this->selcDVHsBox, 1); + this->mainLayout->addWidget(this->showDVHsBox, 6); + this->mainLayout->addLayout(rsboxLayout, 4); + //end set layout + this->setLayout(mainLayout); +} + +void DvhCenterPart::creatSelcDVHsBox() +{ + QGridLayout* layout = new QGridLayout(); + layout->addWidget(this->allSBtn, 0, 0); + layout->addWidget(this->cacSBtn, 0, 1); + layout->addWidget(this->refrBtn, 0, 2); + layout->addWidget(this->roiListW, 1, 0, 5, 3); + this->selcDVHsBox->setLayout(layout); +} + +void DvhCenterPart::creatShowDVHsBox() +{ + QVBoxLayout* layout = new QVBoxLayout(); + this->vtkWidget = new QVTKOpenGLWidget(this); + layout->addWidget(vtkWidget); + + // Same wiring as original dvhViewer (ContextView -> widget), adapted for QVTKOpenGLWidget. + auto renWin = vtkSmartPointer::New(); + renWin->SetMultiSamples(0); + this->vtkWidget->SetRenderWindow(renWin); + + this->vtkView = vtkSmartPointer::New(); + this->vtkChart = vtkSmartPointer::New(); + vtkChart->GetLegend()->GetBrush()->SetOpacity(64); + vtkChart->GetLegend()->SetInteractive(true); + vtkChart->SetShowLegend(true); + vtkChart->GetAxis(vtkAxis::BOTTOM)->SetTitle("Dose"); + vtkChart->GetAxis(vtkAxis::BOTTOM)->SetRange(0, 70); + vtkChart->GetAxis(vtkAxis::BOTTOM)->SetBehavior(vtkAxis::FIXED); + vtkChart->GetAxis(vtkAxis::LEFT)->SetTitle("Volume"); + vtkChart->GetAxis(vtkAxis::LEFT)->SetRange(0, 1.02); + vtkChart->GetAxis(vtkAxis::LEFT)->SetBehavior(vtkAxis::FIXED); + vtkView->GetScene()->AddItem(vtkChart); + this->vtkView->SetRenderWindow(renWin); + this->vtkView->SetInteractor(this->vtkWidget->GetInteractor()); + this->vtkWidget->GetInteractor()->Initialize(); + + // Empty chart on startup. Do not add a 1-point LINE plot — VTK Context2D requires >= 2 points. + vtkChart->ClearPlots(); + this->showDVHsBox->setLayout(layout); +} + +void DvhCenterPart::creatShowJSONBox() +{ + QGridLayout* layout = new QGridLayout(); + layout->addWidget(this->jsEdTEd); + this->open(); + this->showJSONBox->setLayout(layout); +} + +void DvhCenterPart::creatShowOutPBox() +{ + QGridLayout* layout = new QGridLayout(); + layout->addWidget(this->optsTEd); + this->optsTEd->setReadOnly(1); + this->showOutPBox->setLayout(layout); +} + + +void DvhCenterPart::creatCtrolGrpBox() +{ + QHBoxLayout* layout = new QHBoxLayout(); + layout->addWidget(this->progBar, 8); + layout->addWidget(this->staRBtn, 1); + layout->addWidget(this->calcBtn, 1); + this->ctrolGrpBox->setLayout(layout); +} + +void DvhCenterPart::creatShowDsPtBox() +{ + //..................................................................... + // Layout + //..................................................................... + QGridLayout* layout = new QGridLayout(); + layout->addWidget(this->imgLab, 0, 0, 1, 1); + layout->addWidget(this->imgLEd, 0, 1, 1, 11); + layout->addWidget(this->imgBtn, 0, 12, 1, 1); + layout->addWidget(this->rtsLab, 1, 0, 1, 1); + layout->addWidget(this->rtsLEd, 1, 1, 1, 11); + layout->addWidget(this->rtsBtn, 1, 12, 1, 1); + layout->addWidget(this->domCkb, 2, 0, 1, 8); + layout->addWidget(this->adDBtn, 2, 9, 1, 2); + layout->addWidget(this->rmDBtn, 2, 11, 1, 2); + layout->addWidget(this->dsPathTW, 3, 0, 8, 13); + this->showDsPtBox->setLayout(layout); + // load JSON + int ret = loadJSON(); + + if (ret < 0) return; + + //..................................................................... + // create list + //..................................................................... + this->imgLEd->setText(toUiString(this->imgPath)); + this->rtsLEd->setText(toUiString(this->rtsPath)); + this->imgLEd->setCursorPosition(0); + this->rtsLEd->setCursorPosition(0); + this->dsPathTW->setRowCount(this->doseNum); + + for (int i = 0; i < this->doseNum; ++i) + { + this->chckDseVec.push_back(new QCheckBox()); + this->chckDseVec[i]->setChecked(true); + this->nameDseVec.push_back(new QLineEdit(toUiString(this->dseNameVec[i]))); + this->nameDseVec[i]->setStyleSheet("background:transparent;border-width:0;border-style:outset"); + this->lnEdDseVec.push_back(new QLineEdit()); + this->lnEdDseVec[i]->setStyleSheet("background:transparent;border-width:0;border-style:outset"); + this->lnEdDseVec[i]->setText(toUiString(this->rtdPathVec[i])); + this->lnEdDseVec[i]->setCursorPosition(0); + this->ptBtDseVec.push_back(new QPushButton(QWidget::tr(".."))); + connect(this->ptBtDseVec[i], SIGNAL(clicked(bool)), this, SLOT(dseDialogOpn())); + this->dsPathTW->setCellWidget(i, 0, this->chckDseVec[i]); + this->dsPathTW->setCellWidget(i, 1, this->nameDseVec[i]); + this->dsPathTW->setCellWidget(i, 2, this->lnEdDseVec[i]); + this->dsPathTW->setCellWidget(i, 3, this->ptBtDseVec[i]); + } +} + +QString DvhCenterPart::algoDir() const +{ + return QCoreApplication::applicationDirPath() + QStringLiteral("/algo"); +} + +QString DvhCenterPart::toUiString(const std::string& s) const +{ + // Prefer UTF-8 when valid; otherwise local 8-bit (GBK/GB18030 on CN Windows). + const QByteArray raw(s.data(), static_cast(s.size())); + QTextCodec* utf8 = QTextCodec::codecForName("UTF-8"); + QTextCodec::ConverterState state; + QString q = utf8->toUnicode(raw.constData(), raw.size(), &state); + if (state.invalidChars > 0) + q = QString::fromLocal8Bit(raw); + q.replace(QChar('\0'), QLatin1Char(' ')); + return q.trimmed(); +} + +std::string DvhCenterPart::fromUiString(const QString& s) const +{ + const QByteArray bytes = s.toLocal8Bit(); + return std::string(bytes.constData(), static_cast(bytes.size())); +} + diff --git a/src/dvh/DvhComparePage.cpp b/src/dvh/DvhComparePage.cpp index e905d2d..2bf3239 100644 --- a/src/dvh/DvhComparePage.cpp +++ b/src/dvh/DvhComparePage.cpp @@ -1,560 +1,15 @@ -#ifndef NOMINMAX -#define NOMINMAX -#endif #include "DvhComparePage.h" -#include "DvhPlotWidget.h" -#include "dcmIO.h" +#include "DvhCenterPart.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace { - -struct Rgb { - int r, g, b; -}; - -static QVector dvhPalette() -{ - return { - {255, 0, 255}, {227, 23, 13}, {128, 42, 42}, {255, 128, 0}, - {255, 153, 18}, {127, 255, 0}, {34, 139, 34}, {64, 224, 205}, - {3, 168, 158}, {0, 0, 255}, {218, 112, 214}, {138, 43, 226} - }; -} - -static Qt::PenStyle dosePenStyle(int gid) -{ - static const Qt::PenStyle styles[] = { - Qt::SolidLine, Qt::DashLine, Qt::DotLine, Qt::DashDotLine, Qt::DashDotDotLine - }; - return styles[gid % 5]; -} - -static bool isMetaPath(const QString& path) -{ - const QString l = path.toLower(); - return l.endsWith(QStringLiteral(".mhd")) || l.endsWith(QStringLiteral(".mha")) - || l.endsWith(QStringLiteral(".nii")) || l.endsWith(QStringLiteral(".nii.gz")); -} - -static bool isDcmPath(const QString& path) -{ - const QString l = path.toLower(); - return l.endsWith(QStringLiteral(".dcm")) || l.endsWith(QStringLiteral(".dicom")); -} - -} // namespace DvhComparePage::DvhComparePage(QWidget* parent) : QWidget(parent) { - buildUi(); + auto* lay = new QVBoxLayout(this); + lay->setContentsMargins(0, 0, 0, 0); + m_center = new DvhCenterPart(this); + lay->addWidget(m_center); } -DvhComparePage::~DvhComparePage() -{ - freeResults(); -} - -void DvhComparePage::buildUi() -{ - // Original dvhViewer layout: - // [ ROI LISTS | DVH RESULTS | PATHS / CONTROL / OUTPUT ] - auto* root = new QHBoxLayout(this); - root->setContentsMargins(8, 8, 8, 8); - root->setSpacing(8); - - // ---- Left: ROI LISTS ---- - auto* roiBox = new QGroupBox(tr("ROI LISTS"), this); - auto* roiLay = new QGridLayout(roiBox); - auto* allBtn = new QPushButton(tr("ALL"), this); - auto* noneBtn = new QPushButton(tr("CLC"), this); - auto* refreshBtn = new QPushButton(tr("DSP"), this); - connect(allBtn, &QPushButton::clicked, this, &DvhComparePage::onSelectAllRoi); - connect(noneBtn, &QPushButton::clicked, this, &DvhComparePage::onClearAllRoi); - connect(refreshBtn, &QPushButton::clicked, this, &DvhComparePage::onRoiVisibilityChanged); - roiLay->addWidget(allBtn, 0, 0); - roiLay->addWidget(noneBtn, 0, 1); - roiLay->addWidget(refreshBtn, 0, 2); - m_roiList = new QListWidget(this); - roiLay->addWidget(m_roiList, 1, 0, 5, 3); - root->addWidget(roiBox, 1); - - // ---- Center: DVH RESULTS ---- - auto* plotBox = new QGroupBox(tr("DVH RESULTs"), this); - auto* plotLay = new QVBoxLayout(plotBox); - plotLay->setContentsMargins(4, 8, 4, 4); - m_plot = new DvhPlotWidget(this); - plotLay->addWidget(m_plot, 1); - root->addWidget(plotBox, 7); - - // ---- Right: PATHS + CONTROL + OUTPUT (~20% narrower than before) ---- - auto* right = new QVBoxLayout(); - root->addLayout(right, 3); - - auto* pathBox = new QGroupBox(tr("PATHS"), this); - auto* pathLay = new QGridLayout(pathBox); - - auto* imgLab = new QLabel(tr("Image :"), this); - imgLab->setFixedWidth(70); - m_imgEdit = new QLineEdit(this); - m_imgEdit->setPlaceholderText(tr("CT/参考图像:文件夹、.mhd、.nii")); - auto* imgBtn = new QPushButton(tr("..."), this); - imgBtn->setFixedSize(24, 24); - connect(imgBtn, &QPushButton::clicked, this, &DvhComparePage::onBrowseImage); - pathLay->addWidget(imgLab, 0, 0); - pathLay->addWidget(m_imgEdit, 0, 1, 1, 10); - pathLay->addWidget(imgBtn, 0, 11); - - auto* rtsLab = new QLabel(tr("Struc :"), this); - rtsLab->setFixedWidth(70); - m_rtsEdit = new QLineEdit(this); - m_rtsEdit->setPlaceholderText(tr("RTSTRUCT .dcm")); - auto* rtsBtn = new QPushButton(tr("..."), this); - rtsBtn->setFixedSize(24, 24); - connect(rtsBtn, &QPushButton::clicked, this, &DvhComparePage::onBrowseRts); - pathLay->addWidget(rtsLab, 1, 0); - pathLay->addWidget(m_rtsEdit, 1, 1, 1, 10); - pathLay->addWidget(rtsBtn, 1, 11); - - auto* addDose = new QPushButton(tr("+"), this); - auto* rmDose = new QPushButton(tr("-"), this); - addDose->setFixedWidth(36); - rmDose->setFixedWidth(36); - connect(addDose, &QPushButton::clicked, this, &DvhComparePage::onAddDose); - connect(rmDose, &QPushButton::clicked, this, &DvhComparePage::onRemoveDose); - pathLay->addWidget(addDose, 2, 9); - pathLay->addWidget(rmDose, 2, 10); - - m_doseTable = new QTableWidget(0, 2, this); - m_doseTable->setHorizontalHeaderLabels({tr("Name"), tr("Dose Path")}); - m_doseTable->horizontalHeader()->setStretchLastSection(true); - m_doseTable->verticalHeader()->setVisible(false); - m_doseTable->setSelectionBehavior(QAbstractItemView::SelectRows); - m_doseTable->setSelectionMode(QAbstractItemView::SingleSelection); - m_doseTable->setShowGrid(false); - pathLay->addWidget(m_doseTable, 3, 0, 8, 12); - right->addWidget(pathBox, 5); - - auto* ctrlBox = new QGroupBox(tr("CONTROL"), this); - auto* ctrlLay = new QHBoxLayout(ctrlBox); - m_progress = new QProgressBar(this); - m_progress->setRange(0, 10); - m_progress->setValue(0); - m_calcBtn = new QPushButton(tr("CALCULATE"), this); - m_calcBtn->setMinimumWidth(100); - connect(m_calcBtn, &QPushButton::clicked, this, &DvhComparePage::onCalculate); - ctrlLay->addWidget(m_progress, 8); - ctrlLay->addWidget(m_calcBtn, 1); - right->addWidget(ctrlBox, 1); - - auto* logBox = new QGroupBox(tr("OUT PUT INFO."), this); - auto* logLay = new QVBoxLayout(logBox); - m_log = new QTextEdit(this); - m_log->setReadOnly(true); - m_log->setFont(QFont(QStringLiteral("Consolas"), 9)); - logLay->addWidget(m_log); - right->addWidget(logBox, 4); -} - -QString DvhComparePage::defaultOpenDir() const -{ - QSettings settings(QStringLiteral("Manteia"), QStringLiteral("DoseCompare")); - const QString last = settings.value(QStringLiteral("dvhLastOpenDir")).toString(); - if (!last.isEmpty() && QDir(last).exists()) - return last; - return QCoreApplication::applicationDirPath(); -} - -void DvhComparePage::rememberOpenPath(const QString& path) -{ - const QString dir = QFileInfo(path).absolutePath(); - if (dir.isEmpty()) - return; - QSettings settings(QStringLiteral("Manteia"), QStringLiteral("DoseCompare")); - settings.setValue(QStringLiteral("dvhLastOpenDir"), dir); -} - -QString DvhComparePage::resolveAlgoDllPath() const -{ - const QString base = QCoreApplication::applicationDirPath() + QStringLiteral("/algo/"); -#ifdef _DEBUG - const QString name = QStringLiteral("calcDVHLibraryD.dll"); -#else - const QString name = QStringLiteral("calcDVHLibrary.dll"); -#endif - return base + name; -} - -void DvhComparePage::appendLog(const QString& msg) -{ - m_log->append(msg); -} - -void DvhComparePage::onBrowseImage() -{ - const QString path = QFileDialog::getExistingDirectory( - this, tr("选择图像目录(DICOM 序列)"), defaultOpenDir()); - if (!path.isEmpty()) { - m_imgEdit->setText(QDir::toNativeSeparators(path)); - rememberOpenPath(path); - return; - } - const QString file = QFileDialog::getOpenFileName( - this, tr("选择图像文件"), defaultOpenDir(), - tr("Image (*.mhd *.mha *.nii *.nii.gz);;All (*.*)")); - if (file.isEmpty()) - return; - m_imgEdit->setText(QDir::toNativeSeparators(file)); - rememberOpenPath(file); -} - -void DvhComparePage::onBrowseRts() -{ - const QString file = QFileDialog::getOpenFileName( - this, tr("选择 RT Structure"), defaultOpenDir(), - tr("DICOM (*.dcm);;All (*.*)")); - if (file.isEmpty()) - return; - m_rtsEdit->setText(QDir::toNativeSeparators(file)); - rememberOpenPath(file); -} - -void DvhComparePage::onAddDose() -{ - if (m_doseTable->rowCount() >= 5) { - QMessageBox::information(this, tr("提示"), tr("最多支持 5 个剂量计划。")); - return; - } - const QString file = QFileDialog::getOpenFileName( - this, tr("选择剂量文件"), defaultOpenDir(), - tr("Dose (*.dcm *.mhd *.mha *.nii *.nii.gz);;All (*.*)")); - if (file.isEmpty()) - return; - rememberOpenPath(file); - const int row = m_doseTable->rowCount(); - m_doseTable->insertRow(row); - m_doseTable->setItem(row, 0, new QTableWidgetItem(QStringLiteral("Plan%1").arg(row + 1))); - m_doseTable->setItem(row, 1, new QTableWidgetItem(QDir::toNativeSeparators(file))); -} - -void DvhComparePage::onRemoveDose() -{ - const auto rows = m_doseTable->selectionModel()->selectedRows(); - for (int i = rows.size() - 1; i >= 0; --i) - m_doseTable->removeRow(rows[i].row()); -} - -void DvhComparePage::freeResults() -{ - if (!m_strucSet) { - m_doseNum = 0; - return; - } - for (int gid = 0; gid < m_doseNum; ++gid) { - if (!m_strucSet[gid].struc) - continue; - for (int sid = 0; sid < m_strucSet[gid].strucNum; ++sid) { - Struc* s = m_strucSet[gid].struc + sid; - if (s->h_innDat) { - free(s->h_innDat); - s->h_innDat = nullptr; - } - } - delete[] m_strucSet[gid].struc; - m_strucSet[gid].struc = nullptr; - } - delete[] m_strucSet; - m_strucSet = nullptr; - m_doseNum = 0; - m_keptDoses.clear(); - m_keptMasks.clear(); -} - -void DvhComparePage::onCalculate() -{ - m_log->clear(); - m_plot->setCurves({}); - freeResults(); - m_progress->setValue(0); - - const QString imgPath = m_imgEdit->text().trimmed(); - const QString rtsPath = m_rtsEdit->text().trimmed(); - if (imgPath.isEmpty() || rtsPath.isEmpty()) { - QMessageBox::warning(this, tr("警告"), tr("请先指定图像与 RT Structure 路径。")); - return; - } - - QVector> doses; - for (int r = 0; r < m_doseTable->rowCount(); ++r) { - const QString tag = m_doseTable->item(r, 0) ? m_doseTable->item(r, 0)->text().trimmed() : QString(); - const QString path = m_doseTable->item(r, 1) ? m_doseTable->item(r, 1)->text().trimmed() : QString(); - if (path.isEmpty()) - continue; - doses.push_back({tag.isEmpty() ? QStringLiteral("Plan%1").arg(r + 1) : tag, path}); - } - if (doses.isEmpty()) { - QMessageBox::warning(this, tr("警告"), tr("请至少添加一个剂量文件路径。")); - return; - } - if (doses.size() > 5) { - QMessageBox::warning(this, tr("警告"), tr("剂量计划数量不能超过 5。")); - return; - } - - // Let user pick dose file if path cell empty was skipped; also allow browse via double-click later. - // If path looks incomplete, try open dialog for empty ones already skipped. - - const QString dllPath = resolveAlgoDllPath(); - QLibrary lib(dllPath); - if (!lib.load()) { - QMessageBox::warning(this, tr("警告"), - tr("无法加载 DVH 算法库:\n%1\n%2").arg(dllPath, lib.errorString())); - m_progress->setValue(10); - return; - } - - typedef int (*DvhAlgoFunc)(const float* ofs, const float* spc, const int* scz, - const float* dose, StrucSet& sSet); - auto* dvhAlgoFunc = reinterpret_cast(lib.resolve("calcStrucDVH")); - if (!dvhAlgoFunc) { - QMessageBox::warning(this, tr("警告"), tr("DLL 中找不到 calcStrucDVH。")); - lib.unload(); - return; - } - - appendLog(tr("已加载 DLL。")); - m_progress->setValue(1); - - int ret = 0; - itk::Image::Pointer img; - const std::string imgStd = imgPath.toLocal8Bit().constData(); - if (isMetaPath(imgPath)) - ret = itkMetaDataReader(imgStd, img); - else - ret = itkDicomSeriesReader(imgStd, img); - - if (ret < 0 || !img) { - appendLog(tr("加载图像失败: %1").arg(ret)); - QMessageBox::warning(this, tr("错误"), tr("加载图像失败。")); - lib.unload(); - return; - } - appendLog(tr("图像加载成功。")); - m_progress->setValue(3); - - std::vector::Pointer> doseGroup; - for (const auto& d : doses) { - itk::Image::Pointer dose; - const std::string p = d.second.toLocal8Bit().constData(); - if (isMetaPath(d.second)) - ret = itkMetaDataReader(p, dose); - else if (isDcmPath(d.second)) - ret = itkDcmDoseReader(p, dose); - else - ret = -1; - if (ret < 0 || !dose) { - appendLog(tr("加载剂量失败: %1").arg(d.second)); - QMessageBox::warning(this, tr("错误"), tr("加载剂量失败:%1").arg(d.second)); - lib.unload(); - return; - } - doseGroup.push_back(dose); - } - m_keptDoses = doseGroup; - appendLog(tr("剂量加载成功(%1)。").arg(doseGroup.size())); - m_progress->setValue(5); - - std::vector nameVec; - std::vector::Pointer> maskVec; - ret = itkRtStructureReader(rtsPath.toLocal8Bit().constData(), img, nameVec, maskVec); - if (ret < 0 || maskVec.empty()) { - appendLog(tr("加载 RT Structure 失败: %1").arg(ret)); - QMessageBox::warning(this, tr("错误"), tr("加载 RT Structure 失败。")); - lib.unload(); - return; - } - m_keptMasks = maskVec; - appendLog(tr("结构加载成功(%1 个 ROI)。").arg(static_cast(maskVec.size()))); - m_progress->setValue(7); - appendLog(tr("正在计算 DVH…")); - - const int doseGroupSz = static_cast(doseGroup.size()); - m_doseNum = doseGroupSz; - m_strucSet = new StrucSet[doseGroupSz]; - std::memset(m_strucSet, 0, sizeof(StrucSet) * doseGroupSz); - - for (int gid = 0; gid < doseGroupSz; ++gid) { - m_strucSet[gid].tagName = doses[gid].first.toStdString(); - m_strucSet[gid].strucNum = static_cast(maskVec.size()); - m_strucSet[gid].struc = new Struc[m_strucSet[gid].strucNum]; - std::memset(m_strucSet[gid].struc, 0, sizeof(Struc) * m_strucSet[gid].strucNum); - - for (int sid = 0; sid < static_cast(maskVec.size()); ++sid) { - Struc* struc = m_strucSet[gid].struc + sid; - struc->strucName = nameVec[sid]; - struc->h_dat = maskVec[sid]->GetBufferPointer(); - for (int i = 0; i < 3; ++i) { - struc->origin[i] = static_cast(maskVec[sid]->GetOrigin()[i]); - struc->spacing[i] = static_cast(maskVec[sid]->GetSpacing()[i]); - struc->size[i] = static_cast(maskVec[sid]->GetLargestPossibleRegion().GetSize()[i]); - } - struc->datSz = struc->size[0] * struc->size[1] * struc->size[2]; - } - - float dsOrigin[3], dsSpacing[3]; - int dsSize[3]; - for (int i = 0; i < 3; ++i) { - dsOrigin[i] = static_cast(doseGroup[gid]->GetOrigin()[i]); - dsSpacing[i] = static_cast(doseGroup[gid]->GetSpacing()[i]); - dsSize[i] = static_cast(doseGroup[gid]->GetLargestPossibleRegion().GetSize()[i]); - } - float* dosePtr = doseGroup[gid]->GetBufferPointer(); - dvhAlgoFunc(dsOrigin, dsSpacing, dsSize, dosePtr, m_strucSet[gid]); - - appendLog(QStringLiteral(".................... %1 ....................").arg(doses[gid].first)); - for (int i = 0; i < m_strucSet[gid].strucNum; ++i) { - const int datSz = m_strucSet[gid].struc[i].innSz; - if (datSz <= 0 || !m_strucSet[gid].struc[i].h_innDat) - continue; - double sum = 0; - for (int k = 0; k < datSz; ++k) - sum += m_strucSet[gid].struc[i].h_innDat[k]; - sum /= datSz; - const QString name = QString::fromStdString(m_strucSet[gid].struc[i].strucName); - appendLog(tr("%1 / %2 min=%3 max=%4 ave=%5") - .arg(doses[gid].first, name) - .arg(m_strucSet[gid].struc[i].h_innDat[0], 0, 'f', 3) - .arg(m_strucSet[gid].struc[i].h_innDat[datSz - 1], 0, 'f', 3) - .arg(sum, 0, 'f', 3)); - } - } - - m_progress->setValue(10); - appendLog(tr("DVH 计算完成。")); - lib.unload(); - - rebuildRoiList(); - refreshPlot(); -} - -void DvhComparePage::rebuildRoiList() -{ - m_roiList->clear(); - m_roiChecks.clear(); - if (!m_strucSet || m_doseNum <= 0) - return; - - for (int i = 0; i < m_strucSet[0].strucNum; ++i) { - auto* item = new QListWidgetItem(m_roiList); - auto* check = new QCheckBox(QString::fromStdString(m_strucSet[0].struc[i].strucName), m_roiList); - check->setChecked(true); - m_roiList->addItem(item); - m_roiList->setItemWidget(item, check); - m_roiChecks.push_back(check); - connect(check, &QCheckBox::toggled, this, &DvhComparePage::onRoiVisibilityChanged); - } -} - -void DvhComparePage::onSelectAllRoi() -{ - for (QCheckBox* c : m_roiChecks) - c->setChecked(true); -} - -void DvhComparePage::onClearAllRoi() -{ - for (QCheckBox* c : m_roiChecks) - c->setChecked(false); -} - -void DvhComparePage::onRoiVisibilityChanged() -{ - refreshPlot(); -} - -void DvhComparePage::refreshPlot() -{ - QVector curves; - if (!m_strucSet || m_doseNum <= 0) { - m_plot->setCurves(curves); - return; - } - - const auto palette = dvhPalette(); - float maxDose = 0.f; - QVector sampleBuf(DVH_MAX_SPN); - - for (int gid = 0; gid < m_doseNum; ++gid) { - for (int i = 0; i < m_strucSet[gid].strucNum; ++i) { - if (i < m_roiChecks.size() && !m_roiChecks[i]->isChecked()) - continue; - Struc* struc = m_strucSet[gid].struc + i; - if (!struc->h_innDat || struc->innSz <= 0) - continue; - - int dvhSz = struc->innSz; - float* dvhDat = struc->h_innDat; - if (dvhSz > DVH_MAX_SPN) { - sampleBuf[0] = dvhDat[0]; - sampleBuf[DVH_MAX_SPN - 1] = dvhDat[dvhSz - 1]; - const float span = (struc->innSz - 1) * 1.f / (DVH_MAX_SPN - 1); - for (int k = 1; k < DVH_MAX_SPN - 1; ++k) { - const float p = k * span; - const int id0 = static_cast(std::floor(p)); - const int id1 = id0 + 1; - const float w0 = id1 - p; - const float w1 = p - id0; - sampleBuf[k] = w0 * dvhDat[id0] + w1 * dvhDat[id1]; - } - dvhDat = sampleBuf.data(); - dvhSz = DVH_MAX_SPN; - } - - DvhCurve curve; - curve.name = QStringLiteral("%1 (%2)") - .arg(QString::fromStdString(struc->strucName)) - .arg(QString::fromStdString(m_strucSet[gid].tagName)); - const Rgb rgb = palette[i % palette.size()]; - curve.color = QColor(rgb.r, rgb.g, rgb.b); - curve.style = dosePenStyle(gid); - curve.points.reserve(dvhSz + 1); - curve.points.push_back(QPointF(0.0, 1.0)); - for (int j = 0; j < dvhSz; ++j) { - curve.points.push_back(QPointF(dvhDat[j], 1.0 - (j + 1) / (1.0 * dvhSz))); - if (dvhDat[j] > maxDose) - maxDose = dvhDat[j]; - } - curves.push_back(curve); - } - } - - m_plot->setXMax(maxDose > 0 ? maxDose * 1.2 : 1.0); - m_plot->setCurves(curves); -} +DvhComparePage::~DvhComparePage() = default; diff --git a/src/dvh/DvhComparePage.h b/src/dvh/DvhComparePage.h index 7acb2e3..32f3c89 100644 --- a/src/dvh/DvhComparePage.h +++ b/src/dvh/DvhComparePage.h @@ -1,63 +1,16 @@ #pragma once -#include "dvhTypes.h" -#include "itkStlCompat.h" - #include -#include -#include -#include -#include - -class QLineEdit; -class QListWidget; -class QTableWidget; -class QTextEdit; -class QProgressBar; -class QCheckBox; -class QPushButton; -class DvhPlotWidget; +class DvhCenterPart; +// Thin shell page that hosts the original dvhViewer CenterPart UI. class DvhComparePage : public QWidget { Q_OBJECT public: explicit DvhComparePage(QWidget* parent = nullptr); ~DvhComparePage() override; -private slots: - void onBrowseImage(); - void onBrowseRts(); - void onAddDose(); - void onRemoveDose(); - void onCalculate(); - void onSelectAllRoi(); - void onClearAllRoi(); - void onRoiVisibilityChanged(); - private: - void buildUi(); - void freeResults(); - void rebuildRoiList(); - void refreshPlot(); - QString defaultOpenDir() const; - void rememberOpenPath(const QString& path); - QString resolveAlgoDllPath() const; - void appendLog(const QString& msg); - - QLineEdit* m_imgEdit = nullptr; - QLineEdit* m_rtsEdit = nullptr; - QTableWidget* m_doseTable = nullptr; - QListWidget* m_roiList = nullptr; - QTextEdit* m_log = nullptr; - QProgressBar* m_progress = nullptr; - DvhPlotWidget* m_plot = nullptr; - QPushButton* m_calcBtn = nullptr; - - QVector m_roiChecks; - - int m_doseNum = 0; - StrucSet* m_strucSet = nullptr; - std::vector::Pointer> m_keptDoses; - std::vector::Pointer> m_keptMasks; + DvhCenterPart* m_center = nullptr; }; diff --git a/src/dvh/dcmIO.cpp b/src/dvh/dcmIO.cpp index fc146d6..bca0d3e 100644 --- a/src/dvh/dcmIO.cpp +++ b/src/dvh/dcmIO.cpp @@ -1,6 +1,6 @@ #include "dcmIO.h" +#include #include -#include //......................................................................... // Get a itk 3-dim float IMAGE from a given META data file @@ -526,4 +526,86 @@ int itkRtStructureReader( return 0; } +//......................................................................... +// Get a set of itk 3-dim unsigned short masks by json format +//......................................................................... +int itkRtJsonStructReader( + std::string rtsJsonFilePath, + itk::Image::Pointer image, + std::vector& nameVec, + std::vector::Pointer>& maskVec) +{ + // read the JSON file + std::ifstream inFile(rtsJsonFilePath, std::ios::in); + if (!inFile.is_open()) + { + std::cout << "Open the JSON file failed" << std::endl; + return -1; + } + + Json::Value root; + Json::Reader reader; + + if (!reader.parse(inFile, root, false)) + { + std::cout << "Parse the JSON file failed" << std::endl; + std::cout << reader.getFormatedErrorMessages() << std::endl; + return -1; + } + + int roiNum = root["roiList"].size(); + nameVec.reserve(roiNum); + maskVec.reserve(roiNum); + + //..................................................................... + // parsing JSON file for getting the body NII file path + //..................................................................... + for (int i = 0; i < roiNum; ++i) + { + std::string roiIdStr = root["roiList"][i]["roiId"].asString(); + int roiId = atoi(roiIdStr.c_str()); + int preFixId1 = rtsJsonFilePath.find_last_of('\\'); + int preFixId2 = rtsJsonFilePath.find_last_of('/'); + int lastPoai = rtsJsonFilePath.length() - 1; + preFixId1 = preFixId1 == lastPoai ? preFixId2 : preFixId1; + std::string doseFolder = rtsJsonFilePath.substr(0, std::max(preFixId1, preFixId2)); + std::string niiFile = root["roiList"][i]["niiPath"].asString(); + // check niiFile path relative path or absolute path + std::string::size_type idx = niiFile.find(":"); + + if (idx == std::string::npos) + niiFile = doseFolder + "/" + root["roiList"][i]["niiPath"].asString(); + + std::string roiName = root["roiList"][i]["roiName"].asString(); + // push back the vector + nameVec.push_back(roiName); + //................................................................. + // load NII image + //................................................................. + itk::Image::Pointer strucMask; + int ret = itkMetaMaskReader(niiFile, strucMask); + + // check + if (ret < 0) + { + std::cout << "Error in reading " << roiName << std::endl; + return -1; + } + + // check the mask orientation + if (fabs(strucMask->GetDirection()[0][0] + + strucMask->GetDirection()[1][1] + + strucMask->GetDirection()[2][2] - 3) > 1e-5) + { + std::cout << "Sorry ROI: " << roiName + << "orientation not supported!" << std::endl; + return -1; + } + + // push back the vector + maskVec.push_back(strucMask); + } + + return 0; +} diff --git a/src/dvh/dcmIO.h b/src/dvh/dcmIO.h index 35d5d0a..05d34dd 100644 --- a/src/dvh/dcmIO.h +++ b/src/dvh/dcmIO.h @@ -7,8 +7,6 @@ #include "itkStlCompat.h" #include "dvhTypes.h" -#define DBG_DISP false - #include #include #include @@ -57,6 +55,8 @@ #include #include +#define DBG_DISP false + int itkMetaDataReader( const std::string metaDataFile, itk::Image::Pointer& image); @@ -78,3 +78,9 @@ int itkRtStructureReader( itk::Image::Pointer image, std::vector& nameVec, std::vector::Pointer>& maskVec); + +int itkRtJsonStructReader( + std::string rtsJsonFilePath, + itk::Image::Pointer image, + std::vector& nameVec, + std::vector::Pointer>& maskVec); diff --git a/src/dvh/dcmIO_orig.h b/src/dvh/dcmIO_orig.h new file mode 100644 index 0000000..fd9d7b0 --- /dev/null +++ b/src/dvh/dcmIO_orig.h @@ -0,0 +1,103 @@ +#ifndef __DCMIO_H__ +#define __DCMIO_H__ + +#include "viewer.h" + +// system +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// for itk image reader +#include +#include +#include +#include +#include + +// for radiotherapy structure file reader +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// GDCM +#include +#include +#include +#include +#include +#include + +#define DBG_DISP true + +int itkMetaDataReader( + const std::string metaDataFile, + itk::Image::Pointer& image); + +int itkDcmDoseReader( + const std::string dcmDoseFile, + itk::Image::Pointer& dose); + +int itkDicomSeriesReader( + const std::string dicomFilesPath, + itk::Image::Pointer& image); + +int itkRtStructureReader( + std::string rtsFilePath, + itk::Image::Pointer image, + std::vector& nameVec, + std::vector::Pointer>& maskVec); + +int itkRtJsonStructReader( + std::string rtsJsonFilePath, + itk::Image::Pointer image, + std::vector& nameVec, + std::vector::Pointer>& maskVec); + + + + + + + + + + + + + +#endif //__DCMIO_H__ \ No newline at end of file diff --git a/src/gamma/GammaComparePage.cpp b/src/gamma/GammaComparePage.cpp new file mode 100644 index 0000000..b7b9974 --- /dev/null +++ b/src/gamma/GammaComparePage.cpp @@ -0,0 +1,995 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "GammaComparePage.h" +#include "ImageManager.h" +#include "ImageListPanel.h" +#include "SliceViewWidget.h" +#include "RoiLoader.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +GammaComparePage::GammaComparePage(QWidget* parent) + : QWidget(parent) +{ + m_manager = new ImageManager(this); + buildUi(); + setAcceptDrops(true); + connect(m_manager, &ImageManager::imageListChanged, this, &GammaComparePage::refreshImageSelectors); +} + +void GammaComparePage::buildUi() +{ + auto* pageLay = new QVBoxLayout(this); + pageLay->setContentsMargins(0, 0, 0, 0); + pageLay->setSpacing(0); + + auto* topBar = new QWidget(this); + auto* topLay = new QVBoxLayout(topBar); + topLay->setContentsMargins(8, 6, 8, 6); + topLay->setSpacing(4); + + auto* row1 = new QHBoxLayout(); + row1->setSpacing(6); + auto* importBtn = new QPushButton(tr("导入剂量..."), this); + connect(importBtn, &QPushButton::clicked, this, &GammaComparePage::onImportDose); + row1->addWidget(importBtn); + + row1->addWidget(new QLabel(tr(" Background "), this)); + m_bgCombo = new QComboBox(this); + m_bgCombo->setMinimumWidth(130); + row1->addWidget(m_bgCombo); + row1->addWidget(new QLabel(tr(" Front "), this)); + m_frontCombo = new QComboBox(this); + m_frontCombo->setMinimumWidth(130); + row1->addWidget(m_frontCombo); + + row1->addWidget(new QLabel(tr(" 剂量切换 "), this)); + m_opacitySlider = new QSlider(Qt::Horizontal, this); + m_opacitySlider->setRange(0, 100); + m_opacitySlider->setValue(50); + m_opacitySlider->setFixedWidth(100); + row1->addWidget(m_opacitySlider); + m_opacityLabel = new QLabel(tr("50%"), this); + m_opacityLabel->setFixedWidth(40); + m_opacityLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + row1->addWidget(m_opacityLabel); + + row1->addWidget(new QLabel(tr(" Colormap "), this)); + m_colorMapCombo = new QComboBox(this); + m_colorMapCombo->addItem(tr("Gray"), static_cast(DoseColorMap::Gray)); + m_colorMapCombo->addItem(tr("Rainbow"), static_cast(DoseColorMap::Rainbow)); + m_colorMapCombo->addItem(tr("Jet"), static_cast(DoseColorMap::Jet)); + m_colorMapCombo->addItem(tr("Hot"), static_cast(DoseColorMap::Hot)); + m_colorMapCombo->setMinimumWidth(90); + row1->addWidget(m_colorMapCombo); + row1->addStretch(1); + topLay->addLayout(row1); + + auto* row2 = new QHBoxLayout(); + row2->setSpacing(8); + row2->addWidget(new QLabel(tr("Structure"), this)); + m_structEdit = new QLineEdit(this); + m_structEdit->setPlaceholderText(tr("RTSTRUCT .dcm 或 ROI .json")); + m_structEdit->setFixedWidth(280); + row2->addWidget(m_structEdit); + auto* structBtn = new QPushButton(tr("..."), this); + structBtn->setFixedWidth(28); + connect(structBtn, &QPushButton::clicked, this, &GammaComparePage::onBrowseStructure); + row2->addWidget(structBtn); + + auto* paramSep = new QFrame(this); + paramSep->setFrameShape(QFrame::VLine); + paramSep->setFrameShadow(QFrame::Sunken); + row2->addWidget(paramSep); + + row2->addWidget(new QLabel(tr("DTA"), this)); + m_dtaSpin = new QSpinBox(this); + m_dtaSpin->setRange(1, 7); + m_dtaSpin->setValue(3); + m_dtaSpin->setFixedWidth(48); + row2->addWidget(m_dtaSpin); + row2->addWidget(new QLabel(tr("DD"), this)); + m_ddSpin = new QSpinBox(this); + m_ddSpin->setRange(1, 7); + m_ddSpin->setValue(3); + m_ddSpin->setFixedWidth(48); + row2->addWidget(m_ddSpin); + row2->addWidget(new QLabel(tr("Thr%"), this)); + m_thSpin = new QSpinBox(this); + m_thSpin->setRange(1, 100); + m_thSpin->setValue(10); + m_thSpin->setFixedWidth(52); + row2->addWidget(m_thSpin); + + m_calcBtn = new QPushButton(tr("计算 Gamma"), this); + m_calcBtn->setMinimumWidth(100); + connect(m_calcBtn, &QPushButton::clicked, this, &GammaComparePage::onCalculateGamma); + row2->addWidget(m_calcBtn); + + auto* resultBox = new QFrame(this); + resultBox->setObjectName(QStringLiteral("gammaResultBox")); + resultBox->setStyleSheet(QStringLiteral( + "#gammaResultBox {" + " background: #f3f5f8;" + " border: 1px solid #d5dae3;" + " border-radius: 6px;" + "}" + "#gammaResultBox QLabel { color: #2c3340; }" + "#gammaResultBox QProgressBar {" + " border: 1px solid #c5ccd8;" + " border-radius: 4px;" + " background: #ffffff;" + " text-align: center;" + " min-height: 18px;" + "}" + "#gammaResultBox QProgressBar::chunk {" + " background: #3d7eff;" + " border-radius: 3px;" + "}")); + auto* resultLay = new QHBoxLayout(resultBox); + resultLay->setContentsMargins(10, 4, 10, 4); + resultLay->setSpacing(10); + auto* resultTag = new QLabel(tr("结果"), resultBox); + resultTag->setStyleSheet(QStringLiteral("color:#6a7385; font-weight:600;")); + resultLay->addWidget(resultTag); + + m_passLabel = new QLabel(tr("Pass: --"), resultBox); + m_passLabel->setMinimumWidth(120); + QFont pf = m_passLabel->font(); + pf.setBold(true); + pf.setPointSize(pf.pointSize() + 1); + m_passLabel->setFont(pf); + resultLay->addWidget(m_passLabel); + + m_progress = new QProgressBar(resultBox); + m_progress->setRange(0, 100); + m_progress->setValue(0); + m_progress->setMinimumWidth(160); + m_progress->setFormat(tr("进度 %p%")); + resultLay->addWidget(m_progress, 1); + + row2->addWidget(resultBox, 1); + topLay->addLayout(row2); + + pageLay->addWidget(topBar); + + connect(m_bgCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &GammaComparePage::onBackgroundChanged); + connect(m_frontCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &GammaComparePage::onFrontChanged); + connect(m_opacitySlider, &QSlider::valueChanged, this, &GammaComparePage::onOpacityChanged); + connect(m_colorMapCombo, QOverload::of(&QComboBox::currentIndexChanged), + this, &GammaComparePage::onColorMapChanged); + + auto* central = new QWidget(this); + auto* root = new QHBoxLayout(central); + pageLay->addWidget(central, 1); + + auto* leftCol = new QVBoxLayout(); + leftCol->setSpacing(6); + m_listPanel = new ImageListPanel(m_manager, this); + m_listPanel->setFixedWidth(220); + m_listPanel->setCheckMode(ImageListPanel::CheckMode::RoleIndicator); + connect(m_listPanel, &ImageListPanel::requestDelete, this, &GammaComparePage::onDeleteImage); + leftCol->addWidget(m_listPanel, 1); + + auto* roiHeader = new QLabel(tr("ROI"), this); + leftCol->addWidget(roiHeader); + m_roiList = new QListWidget(this); + m_roiList->setSelectionMode(QAbstractItemView::NoSelection); + m_roiList->setFixedWidth(220); + connect(m_roiList, &QListWidget::itemChanged, this, &GammaComparePage::onRoiItemChanged); + leftCol->addWidget(m_roiList, 1); + rebuildRoiList(); + root->addLayout(leftCol); + + auto* right = new QVBoxLayout(); + root->addLayout(right, 1); + + auto addDoseSlice = [&](ViewAxis axis, SliceViewWidget*& view, QSlider*& slider, + const QString& title, QHBoxLayout* row) { + auto* col = new QVBoxLayout(); + col->addWidget(new QLabel(title, this)); + view = new SliceViewWidget(axis, this); + col->addWidget(view, 1); + slider = new QSlider(Qt::Horizontal, this); + slider->setRange(0, 0); + col->addWidget(slider); + row->addLayout(col, 1); + connect(view, &SliceViewWidget::cursorChanged, this, &GammaComparePage::onCursorChanged); + connect(view, &SliceViewWidget::zoomScaleChanged, this, &GammaComparePage::onZoomScaleChanged); + connect(view, &SliceViewWidget::panWorldDelta, this, &GammaComparePage::onPanWorldDelta); + }; + + auto* doseRow = new QHBoxLayout(); + addDoseSlice(ViewAxis::Axial, m_axial, m_sliderAxial, tr("1 剂量 轴位"), doseRow); + addDoseSlice(ViewAxis::Sagittal, m_sagittal, m_sliderSagittal, tr("2 剂量 矢状"), doseRow); + addDoseSlice(ViewAxis::Coronal, m_coronal, m_sliderCoronal, tr("3 剂量 冠状"), doseRow); + right->addLayout(doseRow, 3); + + connect(m_sliderAxial, &QSlider::valueChanged, this, &GammaComparePage::onSliceSliderChanged); + connect(m_sliderSagittal, &QSlider::valueChanged, this, &GammaComparePage::onSliceSliderChanged); + connect(m_sliderCoronal, &QSlider::valueChanged, this, &GammaComparePage::onSliceSliderChanged); + + auto addGammaSlice = [&](ViewAxis axis, SliceViewWidget*& view, QSlider*& slider, + const QString& title, QHBoxLayout* row) { + auto* col = new QVBoxLayout(); + col->addWidget(new QLabel(title, this)); + view = new SliceViewWidget(axis, this); + col->addWidget(view, 1); + slider = new QSlider(Qt::Horizontal, this); + slider->setRange(0, 0); + col->addWidget(slider); + row->addLayout(col, 1); + connect(view, &SliceViewWidget::cursorChanged, this, &GammaComparePage::onCursorChanged); + connect(view, &SliceViewWidget::zoomScaleChanged, this, [this](double s) { + if (m_blockGammaZoom) + return; + m_blockGammaZoom = true; + for (auto* v : {m_gAxial, m_gSagittal, m_gCoronal}) { + if (v) + v->setParallelScaleShared(s); + } + m_blockGammaZoom = false; + }); + connect(view, &SliceViewWidget::panWorldDelta, this, [this](double dx, double dy, double dz) { + if (m_blockViewSync) + return; + m_blockViewSync = true; + for (auto* v : {m_axial, m_sagittal, m_coronal, m_gAxial, m_gSagittal, m_gCoronal}) { + if (v) + v->applyWorldPanShared(dx, dy, dz); + } + m_blockViewSync = false; + }); + }; + + auto* gammaRow = new QHBoxLayout(); + addGammaSlice(ViewAxis::Axial, m_gAxial, m_gSliderAxial, tr("4 Gamma map 轴位"), gammaRow); + addGammaSlice(ViewAxis::Sagittal, m_gSagittal, m_gSliderSagittal, tr("5 Gamma map 矢状"), gammaRow); + addGammaSlice(ViewAxis::Coronal, m_gCoronal, m_gSliderCoronal, tr("6 Gamma map 冠状"), gammaRow); + right->addLayout(gammaRow, 3); + + for (auto* v : {m_gAxial, m_gSagittal, m_gCoronal}) { + v->setPickLabelMode(SliceViewWidget::PickLabelMode::ValueOnly); + v->setColorMap(DoseColorMap::Hot); + v->setWindowLevel(2.0, 1.0); + v->setColorBarVisible(true, 0.0, 2.0); + } + + connect(m_gSliderAxial, &QSlider::valueChanged, this, &GammaComparePage::onSliceSliderChanged); + connect(m_gSliderSagittal, &QSlider::valueChanged, this, &GammaComparePage::onSliceSliderChanged); + connect(m_gSliderCoronal, &QSlider::valueChanged, this, &GammaComparePage::onSliceSliderChanged); + + m_statusLabel = new QLabel( + tr("Gamma 仅比较 Background 与 Front。可选 ROI 做遮罩后对比。"), this); + m_statusLabel->setContentsMargins(8, 4, 8, 4); + pageLay->addWidget(m_statusLabel); +} + +QString GammaComparePage::defaultOpenDir() const +{ + QSettings settings(QStringLiteral("Manteia"), QStringLiteral("DoseCompare")); + const QString last = settings.value(QStringLiteral("gammaLastOpenDir")).toString(); + if (!last.isEmpty() && QDir(last).exists()) + return last; + return QCoreApplication::applicationDirPath(); +} + +void GammaComparePage::rememberOpenPath(const QString& path) +{ + const QString dir = QFileInfo(path).absolutePath(); + if (dir.isEmpty()) + return; + QSettings settings(QStringLiteral("Manteia"), QStringLiteral("DoseCompare")); + settings.setValue(QStringLiteral("gammaLastOpenDir"), dir); +} + +bool GammaComparePage::isSupportedDosePath(const QString& path) const +{ + const QString lower = path.toLower(); + return lower.endsWith(QStringLiteral(".mhd")) || lower.endsWith(QStringLiteral(".mha")) + || lower.endsWith(QStringLiteral(".nii")) || lower.endsWith(QStringLiteral(".nii.gz")) + || lower.endsWith(QStringLiteral(".dcm")) || lower.endsWith(QStringLiteral(".dicom")); +} + +bool GammaComparePage::isSupportedDropPath(const QString& path) const +{ + const QString lower = path.toLower(); + if (lower.endsWith(QStringLiteral(".json"))) + return true; + return isSupportedDosePath(path); +} + +QString GammaComparePage::resolveGammaDllPath() const +{ + const QString base = QCoreApplication::applicationDirPath() + QStringLiteral("/algo/"); +#ifdef _DEBUG + const QString dbg = base + QStringLiteral("GammaMapLibD.dll"); + if (QFileInfo::exists(dbg)) + return dbg; +#endif + return base + QStringLiteral("GammaMapLib.dll"); +} + +void GammaComparePage::onImportDose() +{ + const QString path = QFileDialog::getOpenFileName( + this, tr("导入剂量图像"), defaultOpenDir(), + tr("Dose (*.dcm *.mhd *.mha *.nii *.nii.gz);;All (*.*)")); + if (path.isEmpty()) + return; + QString err; + if (!loadDoseFile(path, &err)) + QMessageBox::warning(this, tr("导入失败"), err.isEmpty() ? tr("未知错误") : err); +} + +void GammaComparePage::onBrowseStructure() +{ + const QString path = QFileDialog::getOpenFileName( + this, tr("加载勾画"), defaultOpenDir(), + tr("Structure (*.dcm *.json);;DICOM RTSTRUCT (*.dcm);;ROI JSON (*.json);;All (*.*)")); + if (path.isEmpty()) + return; + QString err; + if (!loadStructuresFromPath(path, &err)) + QMessageBox::warning(this, tr("勾画加载失败"), err); +} + +bool GammaComparePage::loadStructuresFromPath(const QString& path, QString* error) +{ + m_structEdit->setText(QDir::toNativeSeparators(path)); + rememberOpenPath(path); + + m_roiNames.clear(); + m_roiMasks.clear(); + + auto ref = bgDose(); + if (!ref || !ref->image()) { + rebuildRoiList(); + if (m_statusLabel) + m_statusLabel->setText(tr("已写入 Structure 路径。加载 Background 后将自动解析 ROI。")); + return true; + } + + QString err; + const int ret = loadRoiStructures(path, ref->image(), m_roiNames, m_roiMasks, &err); + if (ret < 0) { + rebuildRoiList(); + if (error) + *error = err; + return false; + } + rebuildRoiList(); + if (m_statusLabel) + m_statusLabel->setText(tr("已加载 %1 个 ROI。").arg(static_cast(m_roiNames.size()))); + return true; +} + +void GammaComparePage::rebuildRoiList() +{ + m_blockRoiCheck = true; + m_roiList->clear(); + auto* noneItem = new QListWidgetItem(tr("(无 — 全剂量 Gamma)"), m_roiList); + noneItem->setFlags((noneItem->flags() | Qt::ItemIsUserCheckable) & ~Qt::ItemIsEditable); + noneItem->setCheckState(Qt::Checked); + noneItem->setData(Qt::UserRole, -1); + for (int i = 0; i < static_cast(m_roiNames.size()); ++i) { + auto* item = new QListWidgetItem(QString::fromStdString(m_roiNames[i]), m_roiList); + item->setFlags((item->flags() | Qt::ItemIsUserCheckable) & ~Qt::ItemIsEditable); + item->setCheckState(Qt::Unchecked); + item->setData(Qt::UserRole, i); + } + m_blockRoiCheck = false; +} + +int GammaComparePage::selectedRoiIndex() const +{ + for (int r = 0; r < m_roiList->count(); ++r) { + auto* item = m_roiList->item(r); + if (item && item->checkState() == Qt::Checked) + return item->data(Qt::UserRole).toInt(); + } + return -1; +} + +void GammaComparePage::setRoiCheckedExclusive(int listRow) +{ + m_blockRoiCheck = true; + for (int r = 0; r < m_roiList->count(); ++r) { + auto* item = m_roiList->item(r); + if (!item) + continue; + item->setCheckState(r == listRow ? Qt::Checked : Qt::Unchecked); + } + m_blockRoiCheck = false; +} + +void GammaComparePage::onRoiItemChanged(QListWidgetItem* item) +{ + if (m_blockRoiCheck || !item) + return; + if (item->checkState() != Qt::Checked) { + // Keep at least one checked (None). + bool any = false; + for (int r = 0; r < m_roiList->count(); ++r) { + if (m_roiList->item(r)->checkState() == Qt::Checked) { + any = true; + break; + } + } + if (!any) + setRoiCheckedExclusive(0); + return; + } + setRoiCheckedExclusive(m_roiList->row(item)); +} + +void GammaComparePage::syncDoseRoleChecks() +{ + if (m_listPanel) + m_listPanel->setRoleIds(currentBgId(), currentFgId()); +} + +void GammaComparePage::dragEnterEvent(QDragEnterEvent* event) +{ + if (!event->mimeData() || !event->mimeData()->hasUrls()) { + event->ignore(); + return; + } + for (const QUrl& url : event->mimeData()->urls()) { + if (url.isLocalFile() && isSupportedDropPath(url.toLocalFile())) { + event->acceptProposedAction(); + return; + } + } + event->ignore(); +} + +void GammaComparePage::dropEvent(QDropEvent* event) +{ + if (!event->mimeData() || !event->mimeData()->hasUrls()) { + event->ignore(); + return; + } + int ok = 0; + QStringList errors; + for (const QUrl& url : event->mimeData()->urls()) { + if (!url.isLocalFile()) + continue; + const QString path = url.toLocalFile(); + if (!isSupportedDropPath(path)) + continue; + + const DropContentKind kind = classifyMedicalPath(path); + if (kind == DropContentKind::Structure) { + QString err; + if (loadStructuresFromPath(path, &err)) + ++ok; + else if (!err.isEmpty()) + errors << err; + continue; + } + + // Dose (or unknown DICOM treated as dose) + if (kind == DropContentKind::Dose || kind == DropContentKind::Unknown) { + if (!isSupportedDosePath(path)) { + errors << tr("%1: 无法识别为剂量或勾画").arg(QFileInfo(path).fileName()); + continue; + } + QString err; + if (loadDoseFile(path, &err)) + ++ok; + else + errors << QStringLiteral("%1: %2").arg(QFileInfo(path).fileName(), + err.isEmpty() ? tr("失败") : err); + } + } + if (!errors.isEmpty()) + QMessageBox::warning(this, tr("部分导入提示"), errors.join(QStringLiteral("\n"))); + if (ok > 0 || !errors.isEmpty()) + event->acceptProposedAction(); + else + event->ignore(); +} + +void GammaComparePage::applyBgFgAfterLoad(const QString& newId, int countBefore) +{ + m_pendingBgId.clear(); + m_pendingFgId.clear(); + m_hasPendingRoles = false; + if (countBefore == 0) { + m_pendingBgId = newId; + m_hasPendingRoles = true; + } else if (countBefore == 1) { + m_pendingFgId = newId; + m_hasPendingRoles = true; + } +} + +void GammaComparePage::promoteBgFgAfterDelete(const QString& deletedId, const QString& oldBg, + const QString& oldFg) +{ + m_pendingBgId = oldBg; + m_pendingFgId = oldFg; + m_hasPendingRoles = true; + if (deletedId == oldBg) + m_pendingBgId.clear(); + if (deletedId == oldFg) + m_pendingFgId.clear(); + const auto& imgs = m_manager->images(); + if (m_pendingBgId.isEmpty() && !imgs.empty()) + m_pendingBgId = imgs.front().id; + if (m_pendingFgId.isEmpty()) { + for (const auto& img : imgs) { + if (img.id != m_pendingBgId) { + m_pendingFgId = img.id; + break; + } + } + } +} + +bool GammaComparePage::loadDoseFile(const QString& path, QString* error) +{ + rememberOpenPath(path); + const int before = static_cast(m_manager->images().size()); + const QString id = m_manager->loadFile(path, error); + if (id.isEmpty()) + return false; + applyBgFgAfterLoad(id, before); + refreshImageSelectors(); + refreshDoseViews(); + if (m_statusLabel) + m_statusLabel->setText(tr("已加载: %1").arg(path)); + return true; +} + +void GammaComparePage::onDeleteImage(const QString& id) +{ + const QString oldBg = currentBgId(); + const QString oldFg = currentFgId(); + promoteBgFgAfterDelete(id, oldBg, oldFg); + m_manager->remove(id); + refreshImageSelectors(); + refreshDoseViews(); +} + +QString GammaComparePage::currentBgId() const +{ + return m_bgCombo->currentData().toString(); +} + +QString GammaComparePage::currentFgId() const +{ + return m_frontCombo->currentData().toString(); +} + +DoseImage::Pointer GammaComparePage::bgDose() const +{ + return m_manager->imageById(currentBgId()); +} + +DoseImage::Pointer GammaComparePage::fgDose() const +{ + const QString id = currentFgId(); + if (id.isEmpty() || id == currentBgId()) + return nullptr; + return m_manager->imageById(id); +} + +void GammaComparePage::refreshImageSelectors() +{ + QString oldBg = currentBgId(); + QString oldFg = currentFgId(); + if (m_hasPendingRoles) { + if (!m_pendingBgId.isEmpty()) + oldBg = m_pendingBgId; + oldFg = m_pendingFgId; + m_hasPendingRoles = false; + } + + m_bgCombo->blockSignals(true); + m_frontCombo->blockSignals(true); + m_bgCombo->clear(); + m_frontCombo->clear(); + m_frontCombo->addItem(tr("(无)"), QString()); + for (const auto& img : m_manager->images()) { + m_bgCombo->addItem(img.dose->name(), img.id); + m_frontCombo->addItem(img.dose->name(), img.id); + } + + int bgIdx = m_bgCombo->findData(oldBg); + if (bgIdx < 0 && m_bgCombo->count() > 0) + bgIdx = 0; + m_bgCombo->setCurrentIndex(bgIdx); + + int fgIdx = m_frontCombo->findData(oldFg); + if (fgIdx < 0) { + fgIdx = 0; + if (m_manager->images().size() >= 2 && bgIdx >= 0) { + const QString bgId = m_bgCombo->itemData(bgIdx).toString(); + for (int i = 1; i < m_frontCombo->count(); ++i) { + if (m_frontCombo->itemData(i).toString() != bgId) { + fgIdx = i; + break; + } + } + } + } + m_frontCombo->setCurrentIndex(fgIdx); + const bool multi = m_manager->images().size() >= 2; + m_frontCombo->setEnabled(multi); + m_opacitySlider->setEnabled(multi); + m_bgCombo->blockSignals(false); + m_frontCombo->blockSignals(false); + syncDoseRoleChecks(); +} + +void GammaComparePage::onFrontChanged(int) +{ + syncDoseRoleChecks(); + refreshDoseViews(); +} + +void GammaComparePage::onBackgroundChanged(int) +{ + syncDoseRoleChecks(); + refreshDoseViews(); + syncSliceSliders(); + const QString path = m_structEdit->text().trimmed(); + if (!path.isEmpty()) { + QString err; + loadStructuresFromPath(path, &err); + } +} + +void GammaComparePage::onOpacityChanged(int value) +{ + m_opacityLabel->setText(QStringLiteral("%1%").arg(value)); + const double op = value / 100.0; + for (auto* v : {m_axial, m_sagittal, m_coronal}) + v->setOpacity(op); +} + +void GammaComparePage::onColorMapChanged(int) +{ + const auto map = static_cast(m_colorMapCombo->currentData().toInt()); + for (auto* v : {m_axial, m_sagittal, m_coronal}) { + if (v) + v->setColorMap(map); + } +} + +void GammaComparePage::applyGammaMapDisplay() +{ + for (auto* v : {m_gAxial, m_gSagittal, m_gCoronal}) { + if (!v) + continue; + v->setColorMap(DoseColorMap::Hot); + v->setWindowLevel(2.0, 1.0); + v->setPickLabelMode(SliceViewWidget::PickLabelMode::ValueOnly); + v->setColorBarVisible(true, 0.0, 2.0); + } +} + +void GammaComparePage::refreshDoseViews() +{ + auto bg = bgDose(); + auto fg = fgDose(); + const double op = m_opacitySlider->value() / 100.0; + for (auto* v : {m_axial, m_sagittal, m_coronal}) { + v->setBackground(bg); + v->setFront(fg); + v->setOpacity(op); + if (bg) + v->resetViewToVolumeCenter(); + } + if (bg) { + double bmin[3], bmax[3]; + bg->worldBounds(bmin, bmax); + m_cursor = {(bmin[0] + bmax[0]) * 0.5, (bmin[1] + bmax[1]) * 0.5, + (bmin[2] + bmax[2]) * 0.5}; + syncSharedZoomFromDoseViews(); + onColorMapChanged(0); + } + syncSliceSliders(); + setCursor(m_cursor, false); +} + +void GammaComparePage::refreshGammaViews() +{ + for (auto* v : {m_gAxial, m_gSagittal, m_gCoronal}) { + v->setBackground(m_gammaMap); + v->setFront(nullptr); + v->setOpacity(0.0); + if (m_gammaMap) + v->resetViewToVolumeCenter(); + } + applyGammaMapDisplay(); + if (m_gammaMap) + syncSharedZoomFromGammaViews(); + syncSliceSliders(); + setCursor(m_cursor, false); +} + +void GammaComparePage::syncSharedZoomFromDoseViews() +{ + if (!m_axial || !m_sagittal || !m_coronal) + return; + const double s = std::max({m_axial->parallelScale(), m_sagittal->parallelScale(), + m_coronal->parallelScale()}); + m_blockViewSync = true; + m_axial->setParallelScaleShared(s); + m_sagittal->setParallelScaleShared(s); + m_coronal->setParallelScaleShared(s); + m_blockViewSync = false; +} + +void GammaComparePage::syncSharedZoomFromGammaViews() +{ + if (!m_gAxial || !m_gSagittal || !m_gCoronal) + return; + const double s = std::max({m_gAxial->parallelScale(), m_gSagittal->parallelScale(), + m_gCoronal->parallelScale()}); + m_blockGammaZoom = true; + m_gAxial->setParallelScaleShared(s); + m_gSagittal->setParallelScaleShared(s); + m_gCoronal->setParallelScaleShared(s); + m_blockGammaZoom = false; +} + +void GammaComparePage::onZoomScaleChanged(double parallelScale) +{ + if (m_blockViewSync) + return; + m_blockViewSync = true; + for (auto* v : {m_axial, m_sagittal, m_coronal}) { + if (v) + v->setParallelScaleShared(parallelScale); + } + m_blockViewSync = false; +} + +void GammaComparePage::onPanWorldDelta(double dx, double dy, double dz) +{ + if (m_blockViewSync) + return; + m_blockViewSync = true; + for (auto* v : {m_axial, m_sagittal, m_coronal, m_gAxial, m_gSagittal, m_gCoronal}) { + if (v) + v->applyWorldPanShared(dx, dy, dz); + } + m_blockViewSync = false; +} + +void GammaComparePage::syncSliceSliders() +{ + auto fillSlider = [&](QSlider* slider, DoseImage::Pointer vol, int worldAxis, double cursorVal) { + if (!slider) + return; + slider->blockSignals(true); + if (!vol) { + slider->setRange(0, 0); + slider->blockSignals(false); + return; + } + double bmin[3], bmax[3]; + vol->worldBounds(bmin, bmax); + const double sp = vol->spacing()[worldAxis]; + const int n = std::max(0, static_cast(std::round((bmax[worldAxis] - bmin[worldAxis]) / std::max(sp, 1e-6)))); + slider->setRange(0, n); + const int idx = static_cast(std::round((cursorVal - bmin[worldAxis]) / std::max(sp, 1e-6))); + slider->setValue(std::max(0, std::min(n, idx))); + slider->blockSignals(false); + }; + + auto bg = bgDose(); + fillSlider(m_sliderAxial, bg, 2, m_cursor.z); + fillSlider(m_sliderSagittal, bg, 0, m_cursor.x); + fillSlider(m_sliderCoronal, bg, 1, m_cursor.y); + fillSlider(m_gSliderAxial, m_gammaMap, 2, m_cursor.z); + fillSlider(m_gSliderSagittal, m_gammaMap, 0, m_cursor.x); + fillSlider(m_gSliderCoronal, m_gammaMap, 1, m_cursor.y); +} + +void GammaComparePage::onSliceSliderChanged(int) +{ + if (m_blockSliders) + return; + auto bg = bgDose(); + auto vol = m_gammaMap ? m_gammaMap : bg; + if (!bg && !m_gammaMap) + return; + + auto worldFromSlider = [&](QSlider* slider, DoseImage::Pointer img, int axis) -> double { + if (!slider || !img) + return 0.0; + double bmin[3], bmax[3]; + img->worldBounds(bmin, bmax); + const double sp = img->spacing()[axis]; + return bmin[axis] + slider->value() * sp; + }; + + Vec3d c = m_cursor; + if (sender() == m_sliderAxial || sender() == m_gSliderAxial) + c.z = worldFromSlider(qobject_cast(sender()), + sender() == m_gSliderAxial ? m_gammaMap : bg, 2); + else if (sender() == m_sliderSagittal || sender() == m_gSliderSagittal) + c.x = worldFromSlider(qobject_cast(sender()), + sender() == m_gSliderSagittal ? m_gammaMap : bg, 0); + else if (sender() == m_sliderCoronal || sender() == m_gSliderCoronal) + c.y = worldFromSlider(qobject_cast(sender()), + sender() == m_gSliderCoronal ? m_gammaMap : bg, 1); + setCursor(c, true); +} + +void GammaComparePage::onCursorChanged(Vec3d world) +{ + setCursor(world, false); +} + +void GammaComparePage::setCursor(const Vec3d& world, bool fromSlider) +{ + m_cursor = world; + for (auto* v : {m_axial, m_sagittal, m_coronal, m_gAxial, m_gSagittal, m_gCoronal}) { + if (v) + v->setCursorWorld(world); + } + if (!fromSlider) { + m_blockSliders = true; + syncSliceSliders(); + m_blockSliders = false; + } +} + +void GammaComparePage::onCalculateGamma() +{ + auto bg = bgDose(); + auto fg = fgDose(); + if (!bg || !fg) { + QMessageBox::warning(this, tr("警告"), tr("请先设置不同的 Background 与 Front 剂量。")); + return; + } + + const int dta = m_dtaSpin->value(); + const int dd = m_ddSpin->value(); + const int thred = m_thSpin->value(); + + m_progress->setValue(5); + QApplication::processEvents(); + + const QString dllPath = resolveGammaDllPath(); + QLibrary lib(dllPath); + if (!lib.load()) { + QMessageBox::warning(this, tr("警告"), + tr("无法加载 GammaMapLib:\n%1\n%2").arg(dllPath, lib.errorString())); + m_progress->setValue(0); + return; + } + + typedef float (*Gamma3DFn)(float* originR, float* spacingR, int* sizeR, float* doseR, + float* originE, float* spacingE, int* sizeE, float* doseE, + float** gammaMap, int dta, int dd, int thred); + auto* gamma3D = reinterpret_cast(lib.resolve("gamma3D")); + if (!gamma3D) { + QMessageBox::warning(this, tr("警告"), tr("DLL 中找不到 gamma3D。")); + lib.unload(); + m_progress->setValue(0); + return; + } + m_progress->setValue(15); + QApplication::processEvents(); + + RoiFloatImage::Pointer doseR = bg->image(); + RoiFloatImage::Pointer doseE = fg->image(); + + const int roiIdx = selectedRoiIndex(); + if (roiIdx >= 0) { + if (roiIdx >= static_cast(m_roiMasks.size())) { + QMessageBox::warning(this, tr("警告"), tr("ROI 选择无效,请重新加载勾画。")); + lib.unload(); + m_progress->setValue(0); + return; + } + m_progress->setValue(30); + QApplication::processEvents(); + auto maskR = resampleMaskToImage(m_roiMasks[roiIdx], doseR); + auto maskE = resampleMaskToImage(m_roiMasks[roiIdx], doseE); + doseR = applyMaskToDose(doseR, maskR); + doseE = applyMaskToDose(doseE, maskE); + if (m_statusLabel) + m_statusLabel->setText(tr("ROI 内 Gamma:%1").arg(QString::fromStdString(m_roiNames[roiIdx]))); + } else if (m_statusLabel) { + m_statusLabel->setText(tr("全剂量 Gamma 计算中…")); + } + + m_progress->setValue(50); + QApplication::processEvents(); + + float originR[3], spacingR[3], originE[3], spacingE[3]; + int sizeR[3], sizeE[3]; + for (int i = 0; i < 3; ++i) { + originR[i] = static_cast(doseR->GetOrigin()[i]); + spacingR[i] = static_cast(doseR->GetSpacing()[i]); + sizeR[i] = static_cast(doseR->GetLargestPossibleRegion().GetSize()[i]); + originE[i] = static_cast(doseE->GetOrigin()[i]); + spacingE[i] = static_cast(doseE->GetSpacing()[i]); + sizeE[i] = static_cast(doseE->GetLargestPossibleRegion().GetSize()[i]); + } + + float* gammaPtr = nullptr; + const float pass = gamma3D(originR, spacingR, sizeR, doseR->GetBufferPointer(), + originE, spacingE, sizeE, doseE->GetBufferPointer(), + &gammaPtr, dta, dd, thred); + m_progress->setValue(85); + QApplication::processEvents(); + + if (!gammaPtr) { + QMessageBox::warning(this, tr("警告"), tr("gamma3D 未返回 gamma map。")); + lib.unload(); + m_progress->setValue(0); + return; + } + + auto gammaImg = RoiFloatImage::New(); + RoiFloatImage::RegionType region; + RoiFloatImage::IndexType start; + start.Fill(0); + RoiFloatImage::SizeType sz; + sz[0] = sizeR[0]; + sz[1] = sizeR[1]; + sz[2] = sizeR[2]; + region.SetIndex(start); + region.SetSize(sz); + gammaImg->SetRegions(region); + gammaImg->SetOrigin(doseR->GetOrigin()); + gammaImg->SetSpacing(doseR->GetSpacing()); + gammaImg->SetDirection(doseR->GetDirection()); + gammaImg->Allocate(); + + const size_t nVox = static_cast(sizeR[0]) * sizeR[1] * sizeR[2]; + std::memcpy(gammaImg->GetBufferPointer(), gammaPtr, nVox * sizeof(float)); + free(gammaPtr); + + m_gammaMap = DoseImage::fromItkImage(gammaImg, QStringLiteral("GammaMap")); + m_passLabel->setText(tr("Pass: %1%").arg(pass * 100.0, 0, 'f', 3)); + refreshGammaViews(); + + m_progress->setValue(100); + if (m_statusLabel) + m_statusLabel->setText(tr("Gamma 计算完成。Pass = %1%").arg(pass * 100.0, 0, 'f', 3)); + lib.unload(); +} diff --git a/src/gamma/GammaComparePage.h b/src/gamma/GammaComparePage.h new file mode 100644 index 0000000..f872651 --- /dev/null +++ b/src/gamma/GammaComparePage.h @@ -0,0 +1,125 @@ +#pragma once + +#include "types.h" +#include "DoseImage.h" +#include "RoiLoader.h" + +#include +#include +#include + +class ImageManager; +class SliceViewWidget; +class ImageListPanel; +class QComboBox; +class QSlider; +class QLabel; +class QLineEdit; +class QListWidget; +class QListWidgetItem; +class QPushButton; +class QProgressBar; +class QSpinBox; +class QDragEnterEvent; +class QDropEvent; + +class GammaComparePage : public QWidget { + Q_OBJECT +public: + explicit GammaComparePage(QWidget* parent = nullptr); + ~GammaComparePage() override = default; + + bool loadDoseFile(const QString& path, QString* error = nullptr); + +protected: + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent* event) override; + +private slots: + void onImportDose(); + void onBrowseStructure(); + void onDeleteImage(const QString& id); + void onFrontChanged(int index); + void onBackgroundChanged(int index); + void onOpacityChanged(int value); + void onColorMapChanged(int index); + void onSliceSliderChanged(int value); + void onCursorChanged(Vec3d world); + void refreshImageSelectors(); + void refreshDoseViews(); + void refreshGammaViews(); + void syncSliceSliders(); + void onZoomScaleChanged(double parallelScale); + void onPanWorldDelta(double dx, double dy, double dz); + void syncSharedZoomFromDoseViews(); + void syncSharedZoomFromGammaViews(); + void onCalculateGamma(); + void onRoiItemChanged(QListWidgetItem* item); + +private: + void buildUi(); + QString currentBgId() const; + QString currentFgId() const; + DoseImage::Pointer bgDose() const; + DoseImage::Pointer fgDose() const; + void setCursor(const Vec3d& world, bool fromSlider); + QString defaultOpenDir() const; + void rememberOpenPath(const QString& path); + bool isSupportedDosePath(const QString& path) const; + bool isSupportedDropPath(const QString& path) const; + void applyBgFgAfterLoad(const QString& newId, int countBefore); + void promoteBgFgAfterDelete(const QString& deletedId, const QString& oldBg, const QString& oldFg); + bool loadStructuresFromPath(const QString& path, QString* error); + void rebuildRoiList(); + int selectedRoiIndex() const; + void setRoiCheckedExclusive(int listRow); + void syncDoseRoleChecks(); + void applyGammaMapDisplay(); + QString resolveGammaDllPath() const; + + ImageManager* m_manager = nullptr; + ImageListPanel* m_listPanel = nullptr; + QListWidget* m_roiList = nullptr; + QLineEdit* m_structEdit = nullptr; + bool m_blockRoiCheck = false; + + SliceViewWidget* m_axial = nullptr; + SliceViewWidget* m_sagittal = nullptr; + SliceViewWidget* m_coronal = nullptr; + SliceViewWidget* m_gAxial = nullptr; + SliceViewWidget* m_gSagittal = nullptr; + SliceViewWidget* m_gCoronal = nullptr; + bool m_blockViewSync = false; + bool m_blockGammaZoom = false; + + QComboBox* m_frontCombo = nullptr; + QComboBox* m_bgCombo = nullptr; + QComboBox* m_colorMapCombo = nullptr; + QSlider* m_opacitySlider = nullptr; + QLabel* m_opacityLabel = nullptr; + + QSlider* m_sliderAxial = nullptr; + QSlider* m_sliderSagittal = nullptr; + QSlider* m_sliderCoronal = nullptr; + QSlider* m_gSliderAxial = nullptr; + QSlider* m_gSliderSagittal = nullptr; + QSlider* m_gSliderCoronal = nullptr; + + QSpinBox* m_dtaSpin = nullptr; + QSpinBox* m_ddSpin = nullptr; + QSpinBox* m_thSpin = nullptr; + QPushButton* m_calcBtn = nullptr; + QLabel* m_passLabel = nullptr; + QProgressBar* m_progress = nullptr; + QLabel* m_statusLabel = nullptr; + + Vec3d m_cursor; + bool m_blockSliders = false; + QString m_pendingFgId; + QString m_pendingBgId; + bool m_hasPendingRoles = false; + + std::vector m_roiNames; + std::vector m_roiMasks; + DoseImage::Pointer m_gammaMap; +}; diff --git a/src/gamma/HotLegendWidget.cpp b/src/gamma/HotLegendWidget.cpp new file mode 100644 index 0000000..845d13c --- /dev/null +++ b/src/gamma/HotLegendWidget.cpp @@ -0,0 +1,52 @@ +#include "HotLegendWidget.h" + +#include +#include +#include + +HotLegendWidget::HotLegendWidget(QWidget* parent) + : QWidget(parent) +{ + setFixedWidth(42); + setAttribute(Qt::WA_TransparentForMouseEvents); + setAttribute(Qt::WA_TranslucentBackground); +} + +void HotLegendWidget::setRange(double vmin, double vmax) +{ + m_min = vmin; + m_max = vmax; + update(); +} + +void HotLegendWidget::paintEvent(QPaintEvent*) +{ + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + + // Soft panel so it stays readable on the dark VTK view. + p.setPen(Qt::NoPen); + p.setBrush(QColor(0, 0, 0, 110)); + p.drawRoundedRect(rect().adjusted(1, 1, -1, -1), 4, 4); + + const QRect bar(6, 16, 12, height() - 32); + QLinearGradient g(bar.topLeft(), bar.bottomLeft()); + g.setColorAt(0.00, QColor(255, 255, 255)); + g.setColorAt(0.35, QColor(255, 255, 0)); + g.setColorAt(0.70, QColor(255, 0, 0)); + g.setColorAt(1.00, QColor(0, 0, 0)); + p.fillRect(bar, g); + p.setPen(QColor(220, 220, 220)); + p.setBrush(Qt::NoBrush); + p.drawRect(bar); + + QFont f = font(); + f.setPointSize(8); + f.setBold(true); + p.setFont(f); + p.setPen(QColor(245, 245, 245)); + p.drawText(QRect(20, 10, width() - 22, 16), Qt::AlignLeft | Qt::AlignVCenter, + QString::number(m_max, 'f', 0)); + p.drawText(QRect(20, height() - 26, width() - 22, 16), Qt::AlignLeft | Qt::AlignVCenter, + QString::number(m_min, 'f', 0)); +} diff --git a/src/gamma/HotLegendWidget.h b/src/gamma/HotLegendWidget.h new file mode 100644 index 0000000..78a7bf0 --- /dev/null +++ b/src/gamma/HotLegendWidget.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +// Vertical Hot colorbar with numeric range labels (e.g. 0–2 for gamma). +class HotLegendWidget : public QWidget { + Q_OBJECT +public: + explicit HotLegendWidget(QWidget* parent = nullptr); + + void setRange(double vmin, double vmax); + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + double m_min = 0.0; + double m_max = 2.0; +}; diff --git a/src/gamma/RoiLoader.cpp b/src/gamma/RoiLoader.cpp new file mode 100644 index 0000000..9b7b27d --- /dev/null +++ b/src/gamma/RoiLoader.cpp @@ -0,0 +1,195 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "RoiLoader.h" +#include "dcmIO.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +DropContentKind classifyMedicalPath(const QString& path) +{ + if (path.isEmpty()) + return DropContentKind::Unknown; + const QString lower = path.toLower(); + if (lower.endsWith(QStringLiteral(".json"))) { + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) + return DropContentKind::Unknown; + const QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); + if (doc.isObject() && doc.object().contains(QStringLiteral("roiList"))) + return DropContentKind::Structure; + return DropContentKind::Unknown; + } + if (lower.endsWith(QStringLiteral(".mhd")) || lower.endsWith(QStringLiteral(".mha")) + || lower.endsWith(QStringLiteral(".nii")) || lower.endsWith(QStringLiteral(".nii.gz"))) + return DropContentKind::Dose; + + if (lower.endsWith(QStringLiteral(".dcm")) || lower.endsWith(QStringLiteral(".dicom"))) { + gdcm::Reader reader; + reader.SetFileName(path.toLocal8Bit().constData()); + if (!reader.Read()) + return DropContentKind::Unknown; + gdcm::MediaStorage ms; + ms.SetFromFile(reader.GetFile()); + if (ms == gdcm::MediaStorage::RTStructureSetStorage) + return DropContentKind::Structure; + if (ms == gdcm::MediaStorage::RTDoseStorage) + return DropContentKind::Dose; + // Other DICOM: treat as dose candidate (CT series folder is handled separately). + const gdcm::DataSet& ds = reader.GetFile().GetDataSet(); + if (ds.FindDataElement(gdcm::Tag(0x3006, 0x0020))) + return DropContentKind::Structure; + if (ds.FindDataElement(gdcm::Tag(0x3004, 0x000e)) || ds.FindDataElement(gdcm::Tag(0x3004, 0x000E))) + return DropContentKind::Dose; + return DropContentKind::Dose; + } + return DropContentKind::Unknown; +} + +int loadRoiStructures( + const QString& path, + RoiFloatImage::Pointer referenceImage, + std::vector& nameVec, + std::vector& maskVec, + QString* error) +{ + nameVec.clear(); + maskVec.clear(); + if (!referenceImage) { + if (error) + *error = QStringLiteral("缺少参考图像网格"); + return -1; + } + if (path.isEmpty() || !QFileInfo::exists(path)) { + if (error) + *error = QStringLiteral("勾画文件不存在"); + return -1; + } + + const QString lower = path.toLower(); + if (lower.endsWith(QStringLiteral(".dcm")) || lower.endsWith(QStringLiteral(".dicom"))) { + const int ret = itkRtStructureReader(path.toLocal8Bit().constData(), referenceImage, nameVec, maskVec); + if (ret < 0) { + if (error) + *error = QStringLiteral("读取 RTSTRUCT 失败 (%1)").arg(ret); + return ret; + } + return 0; + } + + if (lower.endsWith(QStringLiteral(".json"))) { + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) { + if (error) + *error = QStringLiteral("无法打开 JSON"); + return -1; + } + const QJsonDocument doc = QJsonDocument::fromJson(f.readAll()); + if (!doc.isObject()) { + if (error) + *error = QStringLiteral("JSON 格式错误"); + return -1; + } + const QJsonArray roiList = doc.object().value(QStringLiteral("roiList")).toArray(); + if (roiList.isEmpty()) { + if (error) + *error = QStringLiteral("JSON 中没有 roiList"); + return -1; + } + + const QString folder = QFileInfo(path).absolutePath(); + for (const QJsonValue& v : roiList) { + const QJsonObject o = v.toObject(); + QString niiPath = o.value(QStringLiteral("niiPath")).toString(); + const QString roiName = o.value(QStringLiteral("roiName")).toString(); + if (niiPath.isEmpty()) + continue; + if (!QFileInfo(niiPath).isAbsolute()) + niiPath = QDir(folder).filePath(niiPath); + + RoiMaskType::Pointer mask; + const int ret = itkMetaMaskReader(niiPath.toLocal8Bit().constData(), mask); + if (ret < 0 || !mask) { + if (error) + *error = QStringLiteral("读取 ROI NIfTI 失败: %1").arg(niiPath); + return -1; + } + nameVec.push_back(roiName.isEmpty() ? QFileInfo(niiPath).baseName().toStdString() + : roiName.toStdString()); + maskVec.push_back(mask); + } + if (maskVec.empty()) { + if (error) + *error = QStringLiteral("未加载到任何 ROI"); + return -1; + } + return 0; + } + + if (error) + *error = QStringLiteral("仅支持 .dcm RTSTRUCT 或 .json ROI 列表"); + return -1; +} + +RoiMaskType::Pointer resampleMaskToImage( + RoiMaskType::Pointer mask, + RoiFloatImage::Pointer target) +{ + if (!mask || !target) + return nullptr; + + using Resample = itk::ResampleImageFilter; + using Interp = itk::NearestNeighborInterpolateImageFunction; + auto filter = Resample::New(); + auto interp = Interp::New(); + filter->SetInterpolator(interp); + filter->SetInput(mask); + filter->SetSize(target->GetLargestPossibleRegion().GetSize()); + filter->SetOutputOrigin(target->GetOrigin()); + filter->SetOutputSpacing(target->GetSpacing()); + filter->SetOutputDirection(target->GetDirection()); + filter->SetDefaultPixelValue(0); + filter->Update(); + return filter->GetOutput(); +} + +RoiFloatImage::Pointer applyMaskToDose( + RoiFloatImage::Pointer dose, + RoiMaskType::Pointer maskOnDoseGrid) +{ + if (!dose) + return nullptr; + + using Dup = itk::ImageDuplicator; + auto dup = Dup::New(); + dup->SetInputImage(dose); + dup->Update(); + auto out = dup->GetOutput(); + + if (!maskOnDoseGrid) + return out; + + itk::ImageRegionIterator dit(out, out->GetLargestPossibleRegion()); + itk::ImageRegionConstIterator mit(maskOnDoseGrid, maskOnDoseGrid->GetLargestPossibleRegion()); + for (dit.GoToBegin(), mit.GoToBegin(); !dit.IsAtEnd() && !mit.IsAtEnd(); ++dit, ++mit) { + if (mit.Get() == 0) + dit.Set(0.f); + } + return out; +} diff --git a/src/gamma/RoiLoader.h b/src/gamma/RoiLoader.h new file mode 100644 index 0000000..506fa98 --- /dev/null +++ b/src/gamma/RoiLoader.h @@ -0,0 +1,35 @@ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "itkStlCompat.h" + +#include +#include +#include + +#include + +using RoiMaskType = itk::Image; +using RoiFloatImage = itk::Image; + +enum class DropContentKind { Dose, Structure, Unknown }; + +DropContentKind classifyMedicalPath(const QString& path); + +int loadRoiStructures( + const QString& path, + RoiFloatImage::Pointer referenceImage, + std::vector& nameVec, + std::vector& maskVec, + QString* error = nullptr); + +RoiMaskType::Pointer resampleMaskToImage( + RoiMaskType::Pointer mask, + RoiFloatImage::Pointer target); + +RoiFloatImage::Pointer applyMaskToDose( + RoiFloatImage::Pointer dose, + RoiMaskType::Pointer maskOnDoseGrid);