Feat:添加第三个 gamma map(可以限定勾画) APP

master
hssheng 2026-07-20 16:39:10 +08:00
parent c54de6fe64
commit 6cd6e39632
30 changed files with 3370 additions and 612 deletions

View File

@ -17,12 +17,16 @@ endif()
if(NOT DEFINED VTK_DIR AND EXISTS "C:/Toolkits/VTK/lib/cmake/vtk-8.1") 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 "") set(VTK_DIR "C:/Toolkits/VTK/lib/cmake/vtk-8.1" CACHE PATH "")
endif() 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(Qt5 REQUIRED COMPONENTS Core Gui Widgets)
find_package(ITK REQUIRED) find_package(ITK REQUIRED)
include(${ITK_USE_FILE}) include(${ITK_USE_FILE})
find_package(VTK REQUIRED) find_package(VTK REQUIRED)
include(${VTK_USE_FILE}) include(${VTK_USE_FILE})
find_package(jsoncpp REQUIRED)
if("${VTK_QT_VERSION}" STREQUAL "") if("${VTK_QT_VERSION}" STREQUAL "")
message(FATAL_ERROR "VTK was not built with Qt support") message(FATAL_ERROR "VTK was not built with Qt support")
@ -39,8 +43,12 @@ set(SOURCES
src/ProfilePlotWidget.cpp src/ProfilePlotWidget.cpp
src/ImageListPanel.cpp src/ImageListPanel.cpp
src/dvh/DvhComparePage.cpp src/dvh/DvhComparePage.cpp
src/dvh/DvhPlotWidget.cpp src/dvh/DvhCenterPart_ui.cpp
src/dvh/DvhCenterPart_slot.cpp
src/dvh/dcmIO.cpp src/dvh/dcmIO.cpp
src/gamma/GammaComparePage.cpp
src/gamma/RoiLoader.cpp
src/gamma/HotLegendWidget.cpp
resources/resources.qrc resources/resources.qrc
) )
@ -55,9 +63,12 @@ set(HEADERS
src/ProfilePlotWidget.h src/ProfilePlotWidget.h
src/ImageListPanel.h src/ImageListPanel.h
src/dvh/DvhComparePage.h src/dvh/DvhComparePage.h
src/dvh/DvhPlotWidget.h src/dvh/DvhCenterPart.h
src/dvh/dvhTypes.h src/dvh/dvhTypes.h
src/dvh/dcmIO.h src/dvh/dcmIO.h
src/gamma/GammaComparePage.h
src/gamma/RoiLoader.h
src/gamma/HotLegendWidget.h
) )
if(WIN32) if(WIN32)
@ -69,11 +80,17 @@ add_executable(DoseCompare ${SOURCES} ${HEADERS} ${APP_ICON_RC})
target_include_directories(DoseCompare PRIVATE target_include_directories(DoseCompare PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/dvh ${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 target_link_libraries(DoseCompare PRIVATE
Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Core Qt5::Gui Qt5::Widgets
${ITK_LIBRARIES} ${ITK_LIBRARIES}
${VTK_LIBRARIES} ${VTK_LIBRARIES}
jsoncpp_lib
) )
add_executable(smoke_slice_test src/smoke_slice_test.cpp src/DoseImage.cpp) 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 COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/algo/calcDVHLibraryD.dll ${CMAKE_SOURCE_DIR}/algo/calcDVHLibraryD.dll
$<TARGET_FILE_DIR:DoseCompare>/algo/calcDVHLibraryD.dll $<TARGET_FILE_DIR:DoseCompare>/algo/calcDVHLibraryD.dll
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/algo/GammaMapLib.dll
$<TARGET_FILE_DIR:DoseCompare>/algo/GammaMapLib.dll
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_SOURCE_DIR}/algo/param.json
$<TARGET_FILE_DIR:DoseCompare>/algo/param.json
) )

BIN
algo/GammaMapLib.dll Normal file

Binary file not shown.

101
algo/gammaExample.txt Normal file
View File

@ -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));
}

11
algo/param.json Normal file
View File

@ -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"
}
]
}

BIN
resources/app-gamma.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

View File

@ -5,5 +5,6 @@
<file>dosecompare.ico</file> <file>dosecompare.ico</file>
<file>app-profile.png</file> <file>app-profile.png</file>
<file>app-dvh.png</file> <file>app-dvh.png</file>
<file>app-gamma.png</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -2,11 +2,13 @@
#include "LauncherHomeWidget.h" #include "LauncherHomeWidget.h"
#include "ProfileComparePage.h" #include "ProfileComparePage.h"
#include "dvh/DvhComparePage.h" #include "dvh/DvhComparePage.h"
#include "gamma/GammaComparePage.h"
#include <QStackedWidget> #include <QStackedWidget>
#include <QToolBar> #include <QToolBar>
#include <QAction> #include <QAction>
#include <QLabel> #include <QLabel>
#include <QFont>
AppShell::AppShell(QWidget* parent) AppShell::AppShell(QWidget* parent)
: QMainWindow(parent) : QMainWindow(parent)
@ -34,13 +36,16 @@ AppShell::AppShell(QWidget* parent)
m_home = new LauncherHomeWidget(m_stack); m_home = new LauncherHomeWidget(m_stack);
m_profile = new ProfileComparePage(m_stack); m_profile = new ProfileComparePage(m_stack);
m_dvh = new DvhComparePage(m_stack); m_dvh = new DvhComparePage(m_stack);
m_gamma = new GammaComparePage(m_stack);
m_stack->addWidget(m_home); m_stack->addWidget(m_home);
m_stack->addWidget(m_profile); m_stack->addWidget(m_profile);
m_stack->addWidget(m_dvh); m_stack->addWidget(m_dvh);
m_stack->addWidget(m_gamma);
connect(m_home, &LauncherHomeWidget::openProfileRequested, this, &AppShell::openProfile); connect(m_home, &LauncherHomeWidget::openProfileRequested, this, &AppShell::openProfile);
connect(m_home, &LauncherHomeWidget::openDvhRequested, this, &AppShell::openDvh); connect(m_home, &LauncherHomeWidget::openDvhRequested, this, &AppShell::openDvh);
connect(m_home, &LauncherHomeWidget::openGammaRequested, this, &AppShell::openGamma);
showHome(); showHome();
} }
@ -73,6 +78,17 @@ void AppShell::openDvhApp()
openDvh(); openDvh();
} }
void AppShell::openGamma()
{
m_stack->setCurrentWidget(m_gamma);
updateChrome();
}
void AppShell::openGammaApp()
{
openGamma();
}
void AppShell::updateChrome() void AppShell::updateChrome()
{ {
const bool onHome = (m_stack->currentWidget() == m_home); const bool onHome = (m_stack->currentWidget() == m_home);
@ -81,6 +97,8 @@ void AppShell::updateChrome()
setWindowTitle(tr("DoseCompare")); setWindowTitle(tr("DoseCompare"));
else if (m_stack->currentWidget() == m_profile) else if (m_stack->currentWidget() == m_profile)
setWindowTitle(tr("DoseCompare — Profile Comparison")); setWindowTitle(tr("DoseCompare — Profile Comparison"));
else else if (m_stack->currentWidget() == m_dvh)
setWindowTitle(tr("DoseCompare — DVH Comparison")); setWindowTitle(tr("DoseCompare — DVH Comparison"));
else
setWindowTitle(tr("DoseCompare — Gamma Analysis"));
} }

View File

@ -6,6 +6,7 @@ class QStackedWidget;
class LauncherHomeWidget; class LauncherHomeWidget;
class ProfileComparePage; class ProfileComparePage;
class DvhComparePage; class DvhComparePage;
class GammaComparePage;
class AppShell : public QMainWindow { class AppShell : public QMainWindow {
Q_OBJECT Q_OBJECT
@ -15,11 +16,13 @@ public:
ProfileComparePage* profilePage() const { return m_profile; } ProfileComparePage* profilePage() const { return m_profile; }
void openProfileApp(); void openProfileApp();
void openDvhApp(); void openDvhApp();
void openGammaApp();
private slots: private slots:
void showHome(); void showHome();
void openProfile(); void openProfile();
void openDvh(); void openDvh();
void openGamma();
private: private:
void updateChrome(); void updateChrome();
@ -28,5 +31,6 @@ private:
LauncherHomeWidget* m_home = nullptr; LauncherHomeWidget* m_home = nullptr;
ProfileComparePage* m_profile = nullptr; ProfileComparePage* m_profile = nullptr;
DvhComparePage* m_dvh = nullptr; DvhComparePage* m_dvh = nullptr;
GammaComparePage* m_gamma = nullptr;
QAction* m_backAct = nullptr; QAction* m_backAct = nullptr;
}; };

View File

@ -100,6 +100,18 @@ DoseImage::Pointer DoseImage::loadFromFile(const QString& path, QString* error)
return dose; 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() void DoseImage::computeStats()
{ {
using Calc = itk::MinimumMaximumImageCalculator<DoseImageType>; using Calc = itk::MinimumMaximumImageCalculator<DoseImageType>;

View File

@ -15,11 +15,13 @@ public:
using Pointer = std::shared_ptr<DoseImage>; using Pointer = std::shared_ptr<DoseImage>;
static Pointer loadFromFile(const QString& path, QString* error = nullptr); 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& name() const { return m_name; }
const QString& path() const { return m_path; } const QString& path() const { return m_path; }
QColor color() const { return m_color; } QColor color() const { return m_color; }
void setColor(const QColor& c) { m_color = c; } void setColor(const QColor& c) { m_color = c; }
void setName(const QString& name) { m_name = name; }
DoseImageType::Pointer image() const { return m_image; } DoseImageType::Pointer image() const { return m_image; }

View File

@ -25,14 +25,34 @@ ImageListPanel::ImageListPanel(ImageManager* manager, QWidget* parent)
connect(m_manager, &ImageManager::profileSelectionChanged, this, &ImageListPanel::refresh); 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() void ImageListPanel::refresh()
{ {
m_block = true; m_block = true;
m_list->clear(); m_list->clear();
for (const auto& img : m_manager->images()) { for (const auto& img : m_manager->images()) {
auto* item = new QListWidgetItem(img.dose->name(), m_list); auto* item = new QListWidgetItem(img.dose->name(), m_list);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable); 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->setCheckState(img.profileVisible ? Qt::Checked : Qt::Unchecked);
}
item->setData(Qt::UserRole, img.id); item->setData(Qt::UserRole, img.id);
item->setForeground(img.dose->color()); item->setForeground(img.dose->color());
item->setToolTip(img.dose->path()); item->setToolTip(img.dose->path());
@ -44,6 +64,17 @@ void ImageListPanel::onItemChanged(QListWidgetItem* item)
{ {
if (m_block || !item) if (m_block || !item)
return; 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(); const QString id = item->data(Qt::UserRole).toString();
m_manager->setProfileVisible(id, item->checkState() == Qt::Checked); m_manager->setProfileVisible(id, item->checkState() == Qt::Checked);
} }

View File

@ -2,6 +2,7 @@
#include <QWidget> #include <QWidget>
#include <QListWidget> #include <QListWidget>
#include <QString>
class ImageManager; class ImageManager;
class QListWidgetItem; class QListWidgetItem;
@ -9,8 +10,16 @@ class QListWidgetItem;
class ImageListPanel : public QWidget { class ImageListPanel : public QWidget {
Q_OBJECT Q_OBJECT
public: 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); explicit ImageListPanel(ImageManager* manager, QWidget* parent = nullptr);
void setCheckMode(CheckMode mode);
void setRoleIds(const QString& backgroundId, const QString& frontId);
public slots: public slots:
void refresh(); void refresh();
@ -24,4 +33,7 @@ private:
ImageManager* m_manager = nullptr; ImageManager* m_manager = nullptr;
QListWidget* m_list = nullptr; QListWidget* m_list = nullptr;
bool m_block = false; bool m_block = false;
CheckMode m_checkMode = CheckMode::ProfileToggle;
QString m_bgId;
QString m_fgId;
}; };

View File

@ -51,8 +51,8 @@ QPushButton* makeAppCard(const QString& title,
auto* card = new QPushButton(parent); auto* card = new QPushButton(parent);
card->setCursor(Qt::PointingHandCursor); card->setCursor(Qt::PointingHandCursor);
card->setFlat(true); card->setFlat(true);
card->setMinimumSize(280, 340); card->setMinimumSize(240, 320);
card->setMaximumWidth(360); card->setMaximumWidth(300);
card->setAttribute(Qt::WA_StyledBackground, true); card->setAttribute(Qt::WA_StyledBackground, true);
card->setStyleSheet( card->setStyleSheet(
QStringLiteral( QStringLiteral(
@ -154,6 +154,14 @@ LauncherHomeWidget::LauncherHomeWidget(QWidget* parent)
connect(dvhCard, &QPushButton::clicked, this, &LauncherHomeWidget::openDvhRequested); connect(dvhCard, &QPushButton::clicked, this, &LauncherHomeWidget::openDvhRequested);
row->addWidget(dvhCard); 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); row->addStretch(1);
root->addLayout(row, 1); root->addLayout(row, 1);
} }

View File

@ -10,4 +10,5 @@ public:
signals: signals:
void openProfileRequested(); void openProfileRequested();
void openDvhRequested(); void openDvhRequested();
void openGammaRequested();
}; };

View File

@ -1,4 +1,5 @@
#include "SliceViewWidget.h" #include "SliceViewWidget.h"
#include "HotLegendWidget.h"
#include <QVTKOpenGLWidget.h> #include <QVTKOpenGLWidget.h>
#include <QVBoxLayout> #include <QVBoxLayout>
@ -153,6 +154,9 @@ SliceViewWidget::SliceViewWidget(ViewAxis axis, QWidget* parent)
m_pickLabel->hide(); m_pickLabel->hide();
m_pickLabel->raise(); m_pickLabel->raise();
m_colorBar = new HotLegendWidget(this);
m_colorBar->hide();
auto style = vtkSmartPointer<vtkInteractorStyleUser>::New(); auto style = vtkSmartPointer<vtkInteractorStyleUser>::New();
m_vtkWidget->GetInteractor()->SetInteractorStyle(style); m_vtkWidget->GetInteractor()->SetInteractorStyle(style);
@ -351,9 +355,32 @@ void SliceViewWidget::updateOrientationLabels()
m_labelBottom->raise(); m_labelBottom->raise();
m_labelLeft->raise(); m_labelLeft->raise();
m_labelRight->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(); 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) void SliceViewWidget::setBackground(DoseImage::Pointer bg)
{ {
m_bg = std::move(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); const float bVal = m_bg->sampleWorld(world.x, world.y, world.z);
QString text; 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); 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); text = QStringLiteral("F: %1\nB: %2").arg(fVal, 0, 'f', 3).arg(bVal, 0, 'f', 3);
} else { } else {

View File

@ -36,6 +36,12 @@ public:
void setColorMap(DoseColorMap map); void setColorMap(DoseColorMap map);
DoseColorMap colorMap() const { return m_colorMap; } 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 02).
void setColorBarVisible(bool visible, double vmin = 0.0, double vmax = 2.0);
bool visibleRangeForWorldAxis(int worldAxis, double& rmin, double& rmax) const; bool visibleRangeForWorldAxis(int worldAxis, double& rmin, double& rmax) const;
double parallelScale() const; double parallelScale() const;
@ -86,6 +92,7 @@ private:
double m_window = 1.0; double m_window = 1.0;
double m_level = 0.5; double m_level = 0.5;
DoseColorMap m_colorMap = DoseColorMap::Gray; DoseColorMap m_colorMap = DoseColorMap::Gray;
PickLabelMode m_pickLabelMode = PickLabelMode::FrontBackground;
bool m_hasCursor = false; bool m_hasCursor = false;
bool m_cameraReady = false; bool m_cameraReady = false;
double m_halfU = 50.0; double m_halfU = 50.0;
@ -99,6 +106,8 @@ private:
QLabel* m_labelLeft = nullptr; QLabel* m_labelLeft = nullptr;
QLabel* m_labelRight = nullptr; QLabel* m_labelRight = nullptr;
QLabel* m_pickLabel = nullptr; QLabel* m_pickLabel = nullptr;
class HotLegendWidget* m_colorBar = nullptr;
bool m_colorBarVisible = false;
vtkSmartPointer<vtkRenderer> m_renderer; vtkSmartPointer<vtkRenderer> m_renderer;
vtkSmartPointer<vtkImageData> m_bgSlice; vtkSmartPointer<vtkImageData> m_bgSlice;

122
src/dvh/DvhCenterPart.h Normal file
View File

@ -0,0 +1,122 @@
#pragma once
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "dvhTypes.h"
#include "itkStlCompat.h"
#include <QtWidgets>
#include <QVTKOpenGLWidget.h>
#include <vtkSmartPointer.h>
#include <vtkContextView.h>
#include <vtkChartXY.h>
#include <vtkChartLegend.h>
#include <vtkTable.h>
#include <vtkFloatArray.h>
#include <vtkPlot.h>
#include <vtkContextScene.h>
#include <vtkRenderer.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkAxis.h>
#include <vtkBrush.h>
#include <vtkPen.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <string>
#include <vector>
#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<vtkContextView> vtkView;
vtkSmartPointer<vtkChartXY> vtkChart;
int doseNum = 0;
StrucSet* strucSet = nullptr;
std::string imgPath;
std::string rtsPath;
std::vector<std::string> dseNameVec;
std::vector<std::string> rtdPathVec;
std::vector<QCheckBox*> chckBoxVec;
std::vector<QCheckBox*> chckDseVec;
std::vector<QLineEdit*> nameDseVec;
std::vector<QLineEdit*> lnEdDseVec;
std::vector<QPushButton*> 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;
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,273 @@
#include "DvhCenterPart.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QTextCodec>
#include <QTextStream>
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<vtkGenericOpenGLRenderWindow>::New();
renWin->SetMultiSamples(0);
this->vtkWidget->SetRenderWindow(renWin);
this->vtkView = vtkSmartPointer<vtkContextView>::New();
this->vtkChart = vtkSmartPointer<vtkChartXY>::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<int>(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<size_t>(bytes.size()));
}

View File

@ -1,560 +1,15 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "DvhComparePage.h" #include "DvhComparePage.h"
#include "DvhPlotWidget.h" #include "DvhCenterPart.h"
#include "dcmIO.h"
#include <QLineEdit>
#include <QListWidget>
#include <QTableWidget>
#include <QTextEdit>
#include <QProgressBar>
#include <QCheckBox>
#include <QPushButton>
#include <QLabel>
#include <QHBoxLayout>
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QGridLayout>
#include <QGroupBox>
#include <QHeaderView>
#include <QAbstractItemView>
#include <QFileDialog>
#include <QMessageBox>
#include <QSettings>
#include <QFileInfo>
#include <QDir>
#include <QCoreApplication>
#include <QLibrary>
#include <QColor>
#include <QPair>
#include <QFont>
#include <cmath>
#include <cstring>
namespace {
struct Rgb {
int r, g, b;
};
static QVector<Rgb> 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) DvhComparePage::DvhComparePage(QWidget* parent)
: 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() DvhComparePage::~DvhComparePage() = default;
{
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<QPair<QString, QString>> 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<DvhAlgoFunc>(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<float, 3>::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<itk::Image<float, 3>::Pointer> doseGroup;
for (const auto& d : doses) {
itk::Image<float, 3>::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<std::string> nameVec;
std::vector<itk::Image<unsigned char, 3>::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<int>(maskVec.size())));
m_progress->setValue(7);
appendLog(tr("正在计算 DVH…"));
const int doseGroupSz = static_cast<int>(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<int>(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<int>(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<float>(maskVec[sid]->GetOrigin()[i]);
struc->spacing[i] = static_cast<float>(maskVec[sid]->GetSpacing()[i]);
struc->size[i] = static_cast<int>(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<float>(doseGroup[gid]->GetOrigin()[i]);
dsSpacing[i] = static_cast<float>(doseGroup[gid]->GetSpacing()[i]);
dsSize[i] = static_cast<int>(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<DvhCurve> curves;
if (!m_strucSet || m_doseNum <= 0) {
m_plot->setCurves(curves);
return;
}
const auto palette = dvhPalette();
float maxDose = 0.f;
QVector<float> 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<int>(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);
}

View File

@ -1,63 +1,16 @@
#pragma once #pragma once
#include "dvhTypes.h"
#include "itkStlCompat.h"
#include <QWidget> #include <QWidget>
#include <QString>
#include <QVector>
#include <vector>
#include <itkImage.h> class DvhCenterPart;
class QLineEdit;
class QListWidget;
class QTableWidget;
class QTextEdit;
class QProgressBar;
class QCheckBox;
class QPushButton;
class DvhPlotWidget;
// Thin shell page that hosts the original dvhViewer CenterPart UI.
class DvhComparePage : public QWidget { class DvhComparePage : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit DvhComparePage(QWidget* parent = nullptr); explicit DvhComparePage(QWidget* parent = nullptr);
~DvhComparePage() override; ~DvhComparePage() override;
private slots:
void onBrowseImage();
void onBrowseRts();
void onAddDose();
void onRemoveDose();
void onCalculate();
void onSelectAllRoi();
void onClearAllRoi();
void onRoiVisibilityChanged();
private: private:
void buildUi(); DvhCenterPart* m_center = nullptr;
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<QCheckBox*> m_roiChecks;
int m_doseNum = 0;
StrucSet* m_strucSet = nullptr;
std::vector<itk::Image<float, 3>::Pointer> m_keptDoses;
std::vector<itk::Image<unsigned char, 3>::Pointer> m_keptMasks;
}; };

View File

@ -1,6 +1,6 @@
#include "dcmIO.h" #include "dcmIO.h"
#include <json/json.h>
#include <fstream> #include <fstream>
#include <iostream>
//......................................................................... //.........................................................................
// Get a itk 3-dim float IMAGE from a given META data file // Get a itk 3-dim float IMAGE from a given META data file
@ -526,4 +526,86 @@ int itkRtStructureReader(
return 0; return 0;
} }
//.........................................................................
// Get a set of itk 3-dim unsigned short masks by json format
//.........................................................................
int itkRtJsonStructReader(
std::string rtsJsonFilePath,
itk::Image<float, 3>::Pointer image,
std::vector<std::string>& nameVec,
std::vector<itk::Image<unsigned char, 3>::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<unsigned char, 3>::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;
}

View File

@ -7,8 +7,6 @@
#include "itkStlCompat.h" #include "itkStlCompat.h"
#include "dvhTypes.h" #include "dvhTypes.h"
#define DBG_DISP false
#include <string> #include <string>
#include <vector> #include <vector>
#include <iostream> #include <iostream>
@ -57,6 +55,8 @@
#include <gdcmFileMetaInformation.h> #include <gdcmFileMetaInformation.h>
#include <gdcmMediaStorage.h> #include <gdcmMediaStorage.h>
#define DBG_DISP false
int itkMetaDataReader( int itkMetaDataReader(
const std::string metaDataFile, const std::string metaDataFile,
itk::Image<float, 3>::Pointer& image); itk::Image<float, 3>::Pointer& image);
@ -78,3 +78,9 @@ int itkRtStructureReader(
itk::Image<float, 3>::Pointer image, itk::Image<float, 3>::Pointer image,
std::vector<std::string>& nameVec, std::vector<std::string>& nameVec,
std::vector<itk::Image<unsigned char, 3>::Pointer>& maskVec); std::vector<itk::Image<unsigned char, 3>::Pointer>& maskVec);
int itkRtJsonStructReader(
std::string rtsJsonFilePath,
itk::Image<float, 3>::Pointer image,
std::vector<std::string>& nameVec,
std::vector<itk::Image<unsigned char, 3>::Pointer>& maskVec);

103
src/dvh/dcmIO_orig.h Normal file
View File

@ -0,0 +1,103 @@
#ifndef __DCMIO_H__
#define __DCMIO_H__
#include "viewer.h"
// system
#include <string>
#include <cstdlib>
#include <vector>
#include <map>
#include <algorithm>
#include <iostream>
#include <list>
#include <numeric>
#include <string.h>
#include <math.h>
#include <time.h>
// for itk image reader
#include <itkImage.h>
#include <itkGDCMImageIO.h>
#include <itkGDCMImageIOFactory.h>
#include <itkGDCMSeriesFileNames.h>
#include <itkImageSeriesReader.h>
// for radiotherapy structure file reader
#include <itkImage.h>
#include <itkPoint.h>
#include <itkImageSliceIteratorWithIndex.h>
#include <itkImageIteratorWithIndex.h>
#include <itkPointSetToImageFilter.h>
#include <itkExtractImageFilter.h>
#include <itkGroupSpatialObject.h>
#include <itkSpatialObjectToImageFilter.h>
#include <itkPolygonSpatialObject.h>
#include <itkImageFileWriter.h>
#include <itkImageFileReader.h>
#include <itkImageRegionIterator.h>
#include <itkResampleImageFilter.h>
#include <itkNearestNeighborInterpolateImageFunction.h>
#include <itkImageFileWriter.h>
#include <itkImageFileReader.h>
#include <itkVersorRigid3DTransform.h>
#include <itkLinearInterpolateImageFunction.h>
#include <itkImageFileWriter.h>
#include <itkMinimumMaximumImageCalculator.h>
#include <vtkPolyData.h>
#include <vtkSmartPointer.h>
#include <vtkCellArray.h>
#include <vtkPoints.h>
#include <vtkPointData.h>
#include <vtkPolyLine.h>
#include <vtkImageData.h>
#include <vtkPolyDataToImageStencil.h>
#include <vtkImageStencil.h>
#include <itkVTKImageToImageFilter.h>
// GDCM
#include <gdcmTypes.h>
#include <gdcmReader.h>
#include <gdcmSmartPointer.h>
#include <gdcmAttribute.h>
#include <gdcmSequenceOfItems.h>
#include <gdcmFileMetaInformation.h>
#define DBG_DISP true
int itkMetaDataReader(
const std::string metaDataFile,
itk::Image<float, 3>::Pointer& image);
int itkDcmDoseReader(
const std::string dcmDoseFile,
itk::Image<float, 3>::Pointer& dose);
int itkDicomSeriesReader(
const std::string dicomFilesPath,
itk::Image<float, 3>::Pointer& image);
int itkRtStructureReader(
std::string rtsFilePath,
itk::Image<float, 3>::Pointer image,
std::vector<std::string>& nameVec,
std::vector<itk::Image<unsigned char, 3>::Pointer>& maskVec);
int itkRtJsonStructReader(
std::string rtsJsonFilePath,
itk::Image<float, 3>::Pointer image,
std::vector<std::string>& nameVec,
std::vector<itk::Image<unsigned char, 3>::Pointer>& maskVec);
#endif //__DCMIO_H__

View File

@ -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 <QFileDialog>
#include <QMessageBox>
#include <QComboBox>
#include <QSlider>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QListWidgetItem>
#include <QPushButton>
#include <QProgressBar>
#include <QSpinBox>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QFrame>
#include <QSettings>
#include <QFileInfo>
#include <QDir>
#include <QCoreApplication>
#include <QMimeData>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QUrl>
#include <QLibrary>
#include <QApplication>
#include <QFont>
#include <QAbstractItemView>
#include <QFile>
#include <algorithm>
#include <cstring>
#include <cstdlib>
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<int>(DoseColorMap::Gray));
m_colorMapCombo->addItem(tr("Rainbow"), static_cast<int>(DoseColorMap::Rainbow));
m_colorMapCombo->addItem(tr("Jet"), static_cast<int>(DoseColorMap::Jet));
m_colorMapCombo->addItem(tr("Hot"), static_cast<int>(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<int>::of(&QComboBox::currentIndexChanged),
this, &GammaComparePage::onBackgroundChanged);
connect(m_frontCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &GammaComparePage::onFrontChanged);
connect(m_opacitySlider, &QSlider::valueChanged, this, &GammaComparePage::onOpacityChanged);
connect(m_colorMapCombo, QOverload<int>::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<int>(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<int>(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<int>(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<DoseColorMap>(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<int>(std::round((bmax[worldAxis] - bmin[worldAxis]) / std::max(sp, 1e-6))));
slider->setRange(0, n);
const int idx = static_cast<int>(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<QSlider*>(sender()),
sender() == m_gSliderAxial ? m_gammaMap : bg, 2);
else if (sender() == m_sliderSagittal || sender() == m_gSliderSagittal)
c.x = worldFromSlider(qobject_cast<QSlider*>(sender()),
sender() == m_gSliderSagittal ? m_gammaMap : bg, 0);
else if (sender() == m_sliderCoronal || sender() == m_gSliderCoronal)
c.y = worldFromSlider(qobject_cast<QSlider*>(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<Gamma3DFn>(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<int>(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<float>(doseR->GetOrigin()[i]);
spacingR[i] = static_cast<float>(doseR->GetSpacing()[i]);
sizeR[i] = static_cast<int>(doseR->GetLargestPossibleRegion().GetSize()[i]);
originE[i] = static_cast<float>(doseE->GetOrigin()[i]);
spacingE[i] = static_cast<float>(doseE->GetSpacing()[i]);
sizeE[i] = static_cast<int>(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<size_t>(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();
}

View File

@ -0,0 +1,125 @@
#pragma once
#include "types.h"
#include "DoseImage.h"
#include "RoiLoader.h"
#include <QWidget>
#include <QString>
#include <vector>
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<std::string> m_roiNames;
std::vector<RoiMaskType::Pointer> m_roiMasks;
DoseImage::Pointer m_gammaMap;
};

View File

@ -0,0 +1,52 @@
#include "HotLegendWidget.h"
#include <QPainter>
#include <QPaintEvent>
#include <QLinearGradient>
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));
}

View File

@ -0,0 +1,19 @@
#pragma once
#include <QWidget>
// Vertical Hot colorbar with numeric range labels (e.g. 02 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;
};

195
src/gamma/RoiLoader.cpp Normal file
View File

@ -0,0 +1,195 @@
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "RoiLoader.h"
#include "dcmIO.h"
#include <QFileInfo>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDir>
#include <itkResampleImageFilter.h>
#include <itkNearestNeighborInterpolateImageFunction.h>
#include <itkImageRegionIterator.h>
#include <itkImageDuplicator.h>
#include <gdcmReader.h>
#include <gdcmMediaStorage.h>
#include <gdcmTag.h>
#include <gdcmDataSet.h>
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<std::string>& nameVec,
std::vector<RoiMaskType::Pointer>& 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<RoiMaskType, RoiMaskType>;
using Interp = itk::NearestNeighborInterpolateImageFunction<RoiMaskType, double>;
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<RoiFloatImage>;
auto dup = Dup::New();
dup->SetInputImage(dose);
dup->Update();
auto out = dup->GetOutput();
if (!maskOnDoseGrid)
return out;
itk::ImageRegionIterator<RoiFloatImage> dit(out, out->GetLargestPossibleRegion());
itk::ImageRegionConstIterator<RoiMaskType> 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;
}

35
src/gamma/RoiLoader.h Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "itkStlCompat.h"
#include <QString>
#include <string>
#include <vector>
#include <itkImage.h>
using RoiMaskType = itk::Image<unsigned char, 3>;
using RoiFloatImage = itk::Image<float, 3>;
enum class DropContentKind { Dose, Structure, Unknown };
DropContentKind classifyMedicalPath(const QString& path);
int loadRoiStructures(
const QString& path,
RoiFloatImage::Pointer referenceImage,
std::vector<std::string>& nameVec,
std::vector<RoiMaskType::Pointer>& maskVec,
QString* error = nullptr);
RoiMaskType::Pointer resampleMaskToImage(
RoiMaskType::Pointer mask,
RoiFloatImage::Pointer target);
RoiFloatImage::Pointer applyMaskToDose(
RoiFloatImage::Pointer dose,
RoiMaskType::Pointer maskOnDoseGrid);