Feat:添加第三个 gamma map(可以限定勾画) APP
parent
c54de6fe64
commit
6cd6e39632
|
|
@ -17,12 +17,16 @@ endif()
|
|||
if(NOT DEFINED VTK_DIR AND EXISTS "C:/Toolkits/VTK/lib/cmake/vtk-8.1")
|
||||
set(VTK_DIR "C:/Toolkits/VTK/lib/cmake/vtk-8.1" CACHE PATH "")
|
||||
endif()
|
||||
if(NOT DEFINED JSONCPP_DIR AND EXISTS "C:/Toolkits/jsoncpp/lib/cmake/jsoncpp")
|
||||
set(JSONCPP_DIR "C:/Toolkits/jsoncpp/lib/cmake/jsoncpp" CACHE PATH "")
|
||||
endif()
|
||||
|
||||
find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets)
|
||||
find_package(ITK REQUIRED)
|
||||
include(${ITK_USE_FILE})
|
||||
find_package(VTK REQUIRED)
|
||||
include(${VTK_USE_FILE})
|
||||
find_package(jsoncpp REQUIRED)
|
||||
|
||||
if("${VTK_QT_VERSION}" STREQUAL "")
|
||||
message(FATAL_ERROR "VTK was not built with Qt support")
|
||||
|
|
@ -39,8 +43,12 @@ set(SOURCES
|
|||
src/ProfilePlotWidget.cpp
|
||||
src/ImageListPanel.cpp
|
||||
src/dvh/DvhComparePage.cpp
|
||||
src/dvh/DvhPlotWidget.cpp
|
||||
src/dvh/DvhCenterPart_ui.cpp
|
||||
src/dvh/DvhCenterPart_slot.cpp
|
||||
src/dvh/dcmIO.cpp
|
||||
src/gamma/GammaComparePage.cpp
|
||||
src/gamma/RoiLoader.cpp
|
||||
src/gamma/HotLegendWidget.cpp
|
||||
resources/resources.qrc
|
||||
)
|
||||
|
||||
|
|
@ -55,9 +63,12 @@ set(HEADERS
|
|||
src/ProfilePlotWidget.h
|
||||
src/ImageListPanel.h
|
||||
src/dvh/DvhComparePage.h
|
||||
src/dvh/DvhPlotWidget.h
|
||||
src/dvh/DvhCenterPart.h
|
||||
src/dvh/dvhTypes.h
|
||||
src/dvh/dcmIO.h
|
||||
src/gamma/GammaComparePage.h
|
||||
src/gamma/RoiLoader.h
|
||||
src/gamma/HotLegendWidget.h
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
|
|
@ -69,11 +80,17 @@ add_executable(DoseCompare ${SOURCES} ${HEADERS} ${APP_ICON_RC})
|
|||
target_include_directories(DoseCompare PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/dvh
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/gamma
|
||||
)
|
||||
get_target_property(JSON_INC_PATH jsoncpp_lib INTERFACE_INCLUDE_DIRECTORIES)
|
||||
if(JSON_INC_PATH)
|
||||
target_include_directories(DoseCompare PRIVATE ${JSON_INC_PATH})
|
||||
endif()
|
||||
target_link_libraries(DoseCompare PRIVATE
|
||||
Qt5::Core Qt5::Gui Qt5::Widgets
|
||||
${ITK_LIBRARIES}
|
||||
${VTK_LIBRARIES}
|
||||
jsoncpp_lib
|
||||
)
|
||||
|
||||
add_executable(smoke_slice_test src/smoke_slice_test.cpp src/DoseImage.cpp)
|
||||
|
|
@ -96,4 +113,10 @@ add_custom_command(TARGET DoseCompare POST_BUILD
|
|||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_SOURCE_DIR}/algo/calcDVHLibraryD.dll
|
||||
$<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
|
||||
)
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 814 KiB |
|
|
@ -5,5 +5,6 @@
|
|||
<file>dosecompare.ico</file>
|
||||
<file>app-profile.png</file>
|
||||
<file>app-dvh.png</file>
|
||||
<file>app-gamma.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@
|
|||
#include "LauncherHomeWidget.h"
|
||||
#include "ProfileComparePage.h"
|
||||
#include "dvh/DvhComparePage.h"
|
||||
#include "gamma/GammaComparePage.h"
|
||||
|
||||
#include <QStackedWidget>
|
||||
#include <QToolBar>
|
||||
#include <QAction>
|
||||
#include <QLabel>
|
||||
#include <QFont>
|
||||
|
||||
AppShell::AppShell(QWidget* parent)
|
||||
: QMainWindow(parent)
|
||||
|
|
@ -34,13 +36,16 @@ AppShell::AppShell(QWidget* parent)
|
|||
m_home = new LauncherHomeWidget(m_stack);
|
||||
m_profile = new ProfileComparePage(m_stack);
|
||||
m_dvh = new DvhComparePage(m_stack);
|
||||
m_gamma = new GammaComparePage(m_stack);
|
||||
|
||||
m_stack->addWidget(m_home);
|
||||
m_stack->addWidget(m_profile);
|
||||
m_stack->addWidget(m_dvh);
|
||||
m_stack->addWidget(m_gamma);
|
||||
|
||||
connect(m_home, &LauncherHomeWidget::openProfileRequested, this, &AppShell::openProfile);
|
||||
connect(m_home, &LauncherHomeWidget::openDvhRequested, this, &AppShell::openDvh);
|
||||
connect(m_home, &LauncherHomeWidget::openGammaRequested, this, &AppShell::openGamma);
|
||||
|
||||
showHome();
|
||||
}
|
||||
|
|
@ -73,6 +78,17 @@ void AppShell::openDvhApp()
|
|||
openDvh();
|
||||
}
|
||||
|
||||
void AppShell::openGamma()
|
||||
{
|
||||
m_stack->setCurrentWidget(m_gamma);
|
||||
updateChrome();
|
||||
}
|
||||
|
||||
void AppShell::openGammaApp()
|
||||
{
|
||||
openGamma();
|
||||
}
|
||||
|
||||
void AppShell::updateChrome()
|
||||
{
|
||||
const bool onHome = (m_stack->currentWidget() == m_home);
|
||||
|
|
@ -81,6 +97,8 @@ void AppShell::updateChrome()
|
|||
setWindowTitle(tr("DoseCompare"));
|
||||
else if (m_stack->currentWidget() == m_profile)
|
||||
setWindowTitle(tr("DoseCompare — Profile Comparison"));
|
||||
else
|
||||
else if (m_stack->currentWidget() == m_dvh)
|
||||
setWindowTitle(tr("DoseCompare — DVH Comparison"));
|
||||
else
|
||||
setWindowTitle(tr("DoseCompare — Gamma Analysis"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ class QStackedWidget;
|
|||
class LauncherHomeWidget;
|
||||
class ProfileComparePage;
|
||||
class DvhComparePage;
|
||||
class GammaComparePage;
|
||||
|
||||
class AppShell : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
|
@ -15,11 +16,13 @@ public:
|
|||
ProfileComparePage* profilePage() const { return m_profile; }
|
||||
void openProfileApp();
|
||||
void openDvhApp();
|
||||
void openGammaApp();
|
||||
|
||||
private slots:
|
||||
void showHome();
|
||||
void openProfile();
|
||||
void openDvh();
|
||||
void openGamma();
|
||||
|
||||
private:
|
||||
void updateChrome();
|
||||
|
|
@ -28,5 +31,6 @@ private:
|
|||
LauncherHomeWidget* m_home = nullptr;
|
||||
ProfileComparePage* m_profile = nullptr;
|
||||
DvhComparePage* m_dvh = nullptr;
|
||||
GammaComparePage* m_gamma = nullptr;
|
||||
QAction* m_backAct = nullptr;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -100,6 +100,18 @@ DoseImage::Pointer DoseImage::loadFromFile(const QString& path, QString* error)
|
|||
return dose;
|
||||
}
|
||||
|
||||
DoseImage::Pointer DoseImage::fromItkImage(DoseImageType::Pointer image, const QString& name)
|
||||
{
|
||||
if (!image)
|
||||
return nullptr;
|
||||
auto dose = Pointer(new DoseImage());
|
||||
dose->m_name = name;
|
||||
dose->m_image = image;
|
||||
dose->computeStats();
|
||||
dose->ensureInterpolator();
|
||||
return dose;
|
||||
}
|
||||
|
||||
void DoseImage::computeStats()
|
||||
{
|
||||
using Calc = itk::MinimumMaximumImageCalculator<DoseImageType>;
|
||||
|
|
|
|||
|
|
@ -15,11 +15,13 @@ public:
|
|||
using Pointer = std::shared_ptr<DoseImage>;
|
||||
|
||||
static Pointer loadFromFile(const QString& path, QString* error = nullptr);
|
||||
static Pointer fromItkImage(DoseImageType::Pointer image, const QString& name);
|
||||
|
||||
const QString& name() const { return m_name; }
|
||||
const QString& path() const { return m_path; }
|
||||
QColor color() const { return m_color; }
|
||||
void setColor(const QColor& c) { m_color = c; }
|
||||
void setName(const QString& name) { m_name = name; }
|
||||
|
||||
DoseImageType::Pointer image() const { return m_image; }
|
||||
|
||||
|
|
|
|||
|
|
@ -25,14 +25,34 @@ ImageListPanel::ImageListPanel(ImageManager* manager, QWidget* parent)
|
|||
connect(m_manager, &ImageManager::profileSelectionChanged, this, &ImageListPanel::refresh);
|
||||
}
|
||||
|
||||
void ImageListPanel::setCheckMode(CheckMode mode)
|
||||
{
|
||||
m_checkMode = mode;
|
||||
refresh();
|
||||
}
|
||||
|
||||
void ImageListPanel::setRoleIds(const QString& backgroundId, const QString& frontId)
|
||||
{
|
||||
m_bgId = backgroundId;
|
||||
m_fgId = frontId;
|
||||
if (m_checkMode == CheckMode::RoleIndicator)
|
||||
refresh();
|
||||
}
|
||||
|
||||
void ImageListPanel::refresh()
|
||||
{
|
||||
m_block = true;
|
||||
m_list->clear();
|
||||
for (const auto& img : m_manager->images()) {
|
||||
auto* item = new QListWidgetItem(img.dose->name(), m_list);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(img.profileVisible ? Qt::Checked : Qt::Unchecked);
|
||||
item->setFlags((item->flags() | Qt::ItemIsUserCheckable) & ~Qt::ItemIsEditable);
|
||||
if (m_checkMode == CheckMode::RoleIndicator) {
|
||||
const bool on = (img.id == m_bgId || (!m_fgId.isEmpty() && img.id == m_fgId));
|
||||
item->setCheckState(on ? Qt::Checked : Qt::Unchecked);
|
||||
// Keep checkable for display, but RoleIndicator ignores user edits.
|
||||
} else {
|
||||
item->setCheckState(img.profileVisible ? Qt::Checked : Qt::Unchecked);
|
||||
}
|
||||
item->setData(Qt::UserRole, img.id);
|
||||
item->setForeground(img.dose->color());
|
||||
item->setToolTip(img.dose->path());
|
||||
|
|
@ -44,6 +64,17 @@ void ImageListPanel::onItemChanged(QListWidgetItem* item)
|
|||
{
|
||||
if (m_block || !item)
|
||||
return;
|
||||
|
||||
if (m_checkMode == CheckMode::RoleIndicator) {
|
||||
// Display-only: revert any user toggle.
|
||||
const QString id = item->data(Qt::UserRole).toString();
|
||||
const bool on = (id == m_bgId || (!m_fgId.isEmpty() && id == m_fgId));
|
||||
m_block = true;
|
||||
item->setCheckState(on ? Qt::Checked : Qt::Unchecked);
|
||||
m_block = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const QString id = item->data(Qt::UserRole).toString();
|
||||
m_manager->setProfileVisible(id, item->checkState() == Qt::Checked);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
#include <QWidget>
|
||||
#include <QListWidget>
|
||||
#include <QString>
|
||||
|
||||
class ImageManager;
|
||||
class QListWidgetItem;
|
||||
|
|
@ -9,8 +10,16 @@ class QListWidgetItem;
|
|||
class ImageListPanel : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class CheckMode {
|
||||
ProfileToggle, // Profile app: user toggles profile visibility
|
||||
RoleIndicator // Gamma app: checkbox mirrors Background/Front selection
|
||||
};
|
||||
|
||||
explicit ImageListPanel(ImageManager* manager, QWidget* parent = nullptr);
|
||||
|
||||
void setCheckMode(CheckMode mode);
|
||||
void setRoleIds(const QString& backgroundId, const QString& frontId);
|
||||
|
||||
public slots:
|
||||
void refresh();
|
||||
|
||||
|
|
@ -24,4 +33,7 @@ private:
|
|||
ImageManager* m_manager = nullptr;
|
||||
QListWidget* m_list = nullptr;
|
||||
bool m_block = false;
|
||||
CheckMode m_checkMode = CheckMode::ProfileToggle;
|
||||
QString m_bgId;
|
||||
QString m_fgId;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ QPushButton* makeAppCard(const QString& title,
|
|||
auto* card = new QPushButton(parent);
|
||||
card->setCursor(Qt::PointingHandCursor);
|
||||
card->setFlat(true);
|
||||
card->setMinimumSize(280, 340);
|
||||
card->setMaximumWidth(360);
|
||||
card->setMinimumSize(240, 320);
|
||||
card->setMaximumWidth(300);
|
||||
card->setAttribute(Qt::WA_StyledBackground, true);
|
||||
card->setStyleSheet(
|
||||
QStringLiteral(
|
||||
|
|
@ -154,6 +154,14 @@ LauncherHomeWidget::LauncherHomeWidget(QWidget* parent)
|
|||
connect(dvhCard, &QPushButton::clicked, this, &LauncherHomeWidget::openDvhRequested);
|
||||
row->addWidget(dvhCard);
|
||||
|
||||
auto* gammaCard = makeAppCard(
|
||||
tr("Gamma Analysis"),
|
||||
tr("Background / Front 三维 Gamma 通过率与 Gamma Map"),
|
||||
QStringLiteral(":/app-gamma.png"),
|
||||
this);
|
||||
connect(gammaCard, &QPushButton::clicked, this, &LauncherHomeWidget::openGammaRequested);
|
||||
row->addWidget(gammaCard);
|
||||
|
||||
row->addStretch(1);
|
||||
root->addLayout(row, 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ public:
|
|||
signals:
|
||||
void openProfileRequested();
|
||||
void openDvhRequested();
|
||||
void openGammaRequested();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "SliceViewWidget.h"
|
||||
#include "HotLegendWidget.h"
|
||||
|
||||
#include <QVTKOpenGLWidget.h>
|
||||
#include <QVBoxLayout>
|
||||
|
|
@ -153,6 +154,9 @@ SliceViewWidget::SliceViewWidget(ViewAxis axis, QWidget* parent)
|
|||
m_pickLabel->hide();
|
||||
m_pickLabel->raise();
|
||||
|
||||
m_colorBar = new HotLegendWidget(this);
|
||||
m_colorBar->hide();
|
||||
|
||||
auto style = vtkSmartPointer<vtkInteractorStyleUser>::New();
|
||||
m_vtkWidget->GetInteractor()->SetInteractorStyle(style);
|
||||
|
||||
|
|
@ -351,9 +355,32 @@ void SliceViewWidget::updateOrientationLabels()
|
|||
m_labelBottom->raise();
|
||||
m_labelLeft->raise();
|
||||
m_labelRight->raise();
|
||||
if (m_colorBar && m_colorBarVisible) {
|
||||
const int barW = m_colorBar->width();
|
||||
const int barH = std::max(80, height() - 36);
|
||||
m_colorBar->setGeometry(width() - barW - 8, (height() - barH) / 2, barW, barH);
|
||||
m_colorBar->show();
|
||||
m_colorBar->raise();
|
||||
}
|
||||
m_pickLabel->raise();
|
||||
}
|
||||
|
||||
void SliceViewWidget::setPickLabelMode(PickLabelMode mode)
|
||||
{
|
||||
m_pickLabelMode = mode;
|
||||
}
|
||||
|
||||
void SliceViewWidget::setColorBarVisible(bool visible, double vmin, double vmax)
|
||||
{
|
||||
m_colorBarVisible = visible;
|
||||
if (m_colorBar) {
|
||||
m_colorBar->setRange(vmin, vmax);
|
||||
if (!visible)
|
||||
m_colorBar->hide();
|
||||
}
|
||||
updateOrientationLabels();
|
||||
}
|
||||
|
||||
void SliceViewWidget::setBackground(DoseImage::Pointer bg)
|
||||
{
|
||||
m_bg = std::move(bg);
|
||||
|
|
@ -808,7 +835,9 @@ void SliceViewWidget::updatePickOverlay(const QPoint& pos)
|
|||
}
|
||||
const float bVal = m_bg->sampleWorld(world.x, world.y, world.z);
|
||||
QString text;
|
||||
if (m_fg) {
|
||||
if (m_pickLabelMode == PickLabelMode::ValueOnly) {
|
||||
text = QStringLiteral("%1").arg(bVal, 0, 'f', 3);
|
||||
} else if (m_fg) {
|
||||
const float fVal = m_fg->sampleWorld(world.x, world.y, world.z);
|
||||
text = QStringLiteral("F: %1\nB: %2").arg(fVal, 0, 'f', 3).arg(bVal, 0, 'f', 3);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ public:
|
|||
void setColorMap(DoseColorMap map);
|
||||
DoseColorMap colorMap() const { return m_colorMap; }
|
||||
|
||||
enum class PickLabelMode { FrontBackground = 0, ValueOnly = 1 };
|
||||
void setPickLabelMode(PickLabelMode mode);
|
||||
|
||||
// Optional in-view Hot colorbar (e.g. gamma 0–2).
|
||||
void setColorBarVisible(bool visible, double vmin = 0.0, double vmax = 2.0);
|
||||
|
||||
bool visibleRangeForWorldAxis(int worldAxis, double& rmin, double& rmax) const;
|
||||
|
||||
double parallelScale() const;
|
||||
|
|
@ -86,6 +92,7 @@ private:
|
|||
double m_window = 1.0;
|
||||
double m_level = 0.5;
|
||||
DoseColorMap m_colorMap = DoseColorMap::Gray;
|
||||
PickLabelMode m_pickLabelMode = PickLabelMode::FrontBackground;
|
||||
bool m_hasCursor = false;
|
||||
bool m_cameraReady = false;
|
||||
double m_halfU = 50.0;
|
||||
|
|
@ -99,6 +106,8 @@ private:
|
|||
QLabel* m_labelLeft = nullptr;
|
||||
QLabel* m_labelRight = nullptr;
|
||||
QLabel* m_pickLabel = nullptr;
|
||||
class HotLegendWidget* m_colorBar = nullptr;
|
||||
bool m_colorBarVisible = false;
|
||||
|
||||
vtkSmartPointer<vtkRenderer> m_renderer;
|
||||
vtkSmartPointer<vtkImageData> m_bgSlice;
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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()));
|
||||
}
|
||||
|
||||
|
|
@ -1,560 +1,15 @@
|
|||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include "DvhComparePage.h"
|
||||
#include "DvhPlotWidget.h"
|
||||
#include "dcmIO.h"
|
||||
#include "DvhCenterPart.h"
|
||||
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QTextEdit>
|
||||
#include <QProgressBar>
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QHBoxLayout>
|
||||
#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)
|
||||
: QWidget(parent)
|
||||
{
|
||||
buildUi();
|
||||
auto* lay = new QVBoxLayout(this);
|
||||
lay->setContentsMargins(0, 0, 0, 0);
|
||||
m_center = new DvhCenterPart(this);
|
||||
lay->addWidget(m_center);
|
||||
}
|
||||
|
||||
DvhComparePage::~DvhComparePage()
|
||||
{
|
||||
freeResults();
|
||||
}
|
||||
|
||||
void DvhComparePage::buildUi()
|
||||
{
|
||||
// Original dvhViewer layout:
|
||||
// [ ROI LISTS | DVH RESULTS | PATHS / CONTROL / OUTPUT ]
|
||||
auto* root = new QHBoxLayout(this);
|
||||
root->setContentsMargins(8, 8, 8, 8);
|
||||
root->setSpacing(8);
|
||||
|
||||
// ---- Left: ROI LISTS ----
|
||||
auto* roiBox = new QGroupBox(tr("ROI LISTS"), this);
|
||||
auto* roiLay = new QGridLayout(roiBox);
|
||||
auto* allBtn = new QPushButton(tr("ALL"), this);
|
||||
auto* noneBtn = new QPushButton(tr("CLC"), this);
|
||||
auto* refreshBtn = new QPushButton(tr("DSP"), this);
|
||||
connect(allBtn, &QPushButton::clicked, this, &DvhComparePage::onSelectAllRoi);
|
||||
connect(noneBtn, &QPushButton::clicked, this, &DvhComparePage::onClearAllRoi);
|
||||
connect(refreshBtn, &QPushButton::clicked, this, &DvhComparePage::onRoiVisibilityChanged);
|
||||
roiLay->addWidget(allBtn, 0, 0);
|
||||
roiLay->addWidget(noneBtn, 0, 1);
|
||||
roiLay->addWidget(refreshBtn, 0, 2);
|
||||
m_roiList = new QListWidget(this);
|
||||
roiLay->addWidget(m_roiList, 1, 0, 5, 3);
|
||||
root->addWidget(roiBox, 1);
|
||||
|
||||
// ---- Center: DVH RESULTS ----
|
||||
auto* plotBox = new QGroupBox(tr("DVH RESULTs"), this);
|
||||
auto* plotLay = new QVBoxLayout(plotBox);
|
||||
plotLay->setContentsMargins(4, 8, 4, 4);
|
||||
m_plot = new DvhPlotWidget(this);
|
||||
plotLay->addWidget(m_plot, 1);
|
||||
root->addWidget(plotBox, 7);
|
||||
|
||||
// ---- Right: PATHS + CONTROL + OUTPUT (~20% narrower than before) ----
|
||||
auto* right = new QVBoxLayout();
|
||||
root->addLayout(right, 3);
|
||||
|
||||
auto* pathBox = new QGroupBox(tr("PATHS"), this);
|
||||
auto* pathLay = new QGridLayout(pathBox);
|
||||
|
||||
auto* imgLab = new QLabel(tr("Image :"), this);
|
||||
imgLab->setFixedWidth(70);
|
||||
m_imgEdit = new QLineEdit(this);
|
||||
m_imgEdit->setPlaceholderText(tr("CT/参考图像:文件夹、.mhd、.nii"));
|
||||
auto* imgBtn = new QPushButton(tr("..."), this);
|
||||
imgBtn->setFixedSize(24, 24);
|
||||
connect(imgBtn, &QPushButton::clicked, this, &DvhComparePage::onBrowseImage);
|
||||
pathLay->addWidget(imgLab, 0, 0);
|
||||
pathLay->addWidget(m_imgEdit, 0, 1, 1, 10);
|
||||
pathLay->addWidget(imgBtn, 0, 11);
|
||||
|
||||
auto* rtsLab = new QLabel(tr("Struc :"), this);
|
||||
rtsLab->setFixedWidth(70);
|
||||
m_rtsEdit = new QLineEdit(this);
|
||||
m_rtsEdit->setPlaceholderText(tr("RTSTRUCT .dcm"));
|
||||
auto* rtsBtn = new QPushButton(tr("..."), this);
|
||||
rtsBtn->setFixedSize(24, 24);
|
||||
connect(rtsBtn, &QPushButton::clicked, this, &DvhComparePage::onBrowseRts);
|
||||
pathLay->addWidget(rtsLab, 1, 0);
|
||||
pathLay->addWidget(m_rtsEdit, 1, 1, 1, 10);
|
||||
pathLay->addWidget(rtsBtn, 1, 11);
|
||||
|
||||
auto* addDose = new QPushButton(tr("+"), this);
|
||||
auto* rmDose = new QPushButton(tr("-"), this);
|
||||
addDose->setFixedWidth(36);
|
||||
rmDose->setFixedWidth(36);
|
||||
connect(addDose, &QPushButton::clicked, this, &DvhComparePage::onAddDose);
|
||||
connect(rmDose, &QPushButton::clicked, this, &DvhComparePage::onRemoveDose);
|
||||
pathLay->addWidget(addDose, 2, 9);
|
||||
pathLay->addWidget(rmDose, 2, 10);
|
||||
|
||||
m_doseTable = new QTableWidget(0, 2, this);
|
||||
m_doseTable->setHorizontalHeaderLabels({tr("Name"), tr("Dose Path")});
|
||||
m_doseTable->horizontalHeader()->setStretchLastSection(true);
|
||||
m_doseTable->verticalHeader()->setVisible(false);
|
||||
m_doseTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_doseTable->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_doseTable->setShowGrid(false);
|
||||
pathLay->addWidget(m_doseTable, 3, 0, 8, 12);
|
||||
right->addWidget(pathBox, 5);
|
||||
|
||||
auto* ctrlBox = new QGroupBox(tr("CONTROL"), this);
|
||||
auto* ctrlLay = new QHBoxLayout(ctrlBox);
|
||||
m_progress = new QProgressBar(this);
|
||||
m_progress->setRange(0, 10);
|
||||
m_progress->setValue(0);
|
||||
m_calcBtn = new QPushButton(tr("CALCULATE"), this);
|
||||
m_calcBtn->setMinimumWidth(100);
|
||||
connect(m_calcBtn, &QPushButton::clicked, this, &DvhComparePage::onCalculate);
|
||||
ctrlLay->addWidget(m_progress, 8);
|
||||
ctrlLay->addWidget(m_calcBtn, 1);
|
||||
right->addWidget(ctrlBox, 1);
|
||||
|
||||
auto* logBox = new QGroupBox(tr("OUT PUT INFO."), this);
|
||||
auto* logLay = new QVBoxLayout(logBox);
|
||||
m_log = new QTextEdit(this);
|
||||
m_log->setReadOnly(true);
|
||||
m_log->setFont(QFont(QStringLiteral("Consolas"), 9));
|
||||
logLay->addWidget(m_log);
|
||||
right->addWidget(logBox, 4);
|
||||
}
|
||||
|
||||
QString DvhComparePage::defaultOpenDir() const
|
||||
{
|
||||
QSettings settings(QStringLiteral("Manteia"), QStringLiteral("DoseCompare"));
|
||||
const QString last = settings.value(QStringLiteral("dvhLastOpenDir")).toString();
|
||||
if (!last.isEmpty() && QDir(last).exists())
|
||||
return last;
|
||||
return QCoreApplication::applicationDirPath();
|
||||
}
|
||||
|
||||
void DvhComparePage::rememberOpenPath(const QString& path)
|
||||
{
|
||||
const QString dir = QFileInfo(path).absolutePath();
|
||||
if (dir.isEmpty())
|
||||
return;
|
||||
QSettings settings(QStringLiteral("Manteia"), QStringLiteral("DoseCompare"));
|
||||
settings.setValue(QStringLiteral("dvhLastOpenDir"), dir);
|
||||
}
|
||||
|
||||
QString DvhComparePage::resolveAlgoDllPath() const
|
||||
{
|
||||
const QString base = QCoreApplication::applicationDirPath() + QStringLiteral("/algo/");
|
||||
#ifdef _DEBUG
|
||||
const QString name = QStringLiteral("calcDVHLibraryD.dll");
|
||||
#else
|
||||
const QString name = QStringLiteral("calcDVHLibrary.dll");
|
||||
#endif
|
||||
return base + name;
|
||||
}
|
||||
|
||||
void DvhComparePage::appendLog(const QString& msg)
|
||||
{
|
||||
m_log->append(msg);
|
||||
}
|
||||
|
||||
void DvhComparePage::onBrowseImage()
|
||||
{
|
||||
const QString path = QFileDialog::getExistingDirectory(
|
||||
this, tr("选择图像目录(DICOM 序列)"), defaultOpenDir());
|
||||
if (!path.isEmpty()) {
|
||||
m_imgEdit->setText(QDir::toNativeSeparators(path));
|
||||
rememberOpenPath(path);
|
||||
return;
|
||||
}
|
||||
const QString file = QFileDialog::getOpenFileName(
|
||||
this, tr("选择图像文件"), defaultOpenDir(),
|
||||
tr("Image (*.mhd *.mha *.nii *.nii.gz);;All (*.*)"));
|
||||
if (file.isEmpty())
|
||||
return;
|
||||
m_imgEdit->setText(QDir::toNativeSeparators(file));
|
||||
rememberOpenPath(file);
|
||||
}
|
||||
|
||||
void DvhComparePage::onBrowseRts()
|
||||
{
|
||||
const QString file = QFileDialog::getOpenFileName(
|
||||
this, tr("选择 RT Structure"), defaultOpenDir(),
|
||||
tr("DICOM (*.dcm);;All (*.*)"));
|
||||
if (file.isEmpty())
|
||||
return;
|
||||
m_rtsEdit->setText(QDir::toNativeSeparators(file));
|
||||
rememberOpenPath(file);
|
||||
}
|
||||
|
||||
void DvhComparePage::onAddDose()
|
||||
{
|
||||
if (m_doseTable->rowCount() >= 5) {
|
||||
QMessageBox::information(this, tr("提示"), tr("最多支持 5 个剂量计划。"));
|
||||
return;
|
||||
}
|
||||
const QString file = QFileDialog::getOpenFileName(
|
||||
this, tr("选择剂量文件"), defaultOpenDir(),
|
||||
tr("Dose (*.dcm *.mhd *.mha *.nii *.nii.gz);;All (*.*)"));
|
||||
if (file.isEmpty())
|
||||
return;
|
||||
rememberOpenPath(file);
|
||||
const int row = m_doseTable->rowCount();
|
||||
m_doseTable->insertRow(row);
|
||||
m_doseTable->setItem(row, 0, new QTableWidgetItem(QStringLiteral("Plan%1").arg(row + 1)));
|
||||
m_doseTable->setItem(row, 1, new QTableWidgetItem(QDir::toNativeSeparators(file)));
|
||||
}
|
||||
|
||||
void DvhComparePage::onRemoveDose()
|
||||
{
|
||||
const auto rows = m_doseTable->selectionModel()->selectedRows();
|
||||
for (int i = rows.size() - 1; i >= 0; --i)
|
||||
m_doseTable->removeRow(rows[i].row());
|
||||
}
|
||||
|
||||
void DvhComparePage::freeResults()
|
||||
{
|
||||
if (!m_strucSet) {
|
||||
m_doseNum = 0;
|
||||
return;
|
||||
}
|
||||
for (int gid = 0; gid < m_doseNum; ++gid) {
|
||||
if (!m_strucSet[gid].struc)
|
||||
continue;
|
||||
for (int sid = 0; sid < m_strucSet[gid].strucNum; ++sid) {
|
||||
Struc* s = m_strucSet[gid].struc + sid;
|
||||
if (s->h_innDat) {
|
||||
free(s->h_innDat);
|
||||
s->h_innDat = nullptr;
|
||||
}
|
||||
}
|
||||
delete[] m_strucSet[gid].struc;
|
||||
m_strucSet[gid].struc = nullptr;
|
||||
}
|
||||
delete[] m_strucSet;
|
||||
m_strucSet = nullptr;
|
||||
m_doseNum = 0;
|
||||
m_keptDoses.clear();
|
||||
m_keptMasks.clear();
|
||||
}
|
||||
|
||||
void DvhComparePage::onCalculate()
|
||||
{
|
||||
m_log->clear();
|
||||
m_plot->setCurves({});
|
||||
freeResults();
|
||||
m_progress->setValue(0);
|
||||
|
||||
const QString imgPath = m_imgEdit->text().trimmed();
|
||||
const QString rtsPath = m_rtsEdit->text().trimmed();
|
||||
if (imgPath.isEmpty() || rtsPath.isEmpty()) {
|
||||
QMessageBox::warning(this, tr("警告"), tr("请先指定图像与 RT Structure 路径。"));
|
||||
return;
|
||||
}
|
||||
|
||||
QVector<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);
|
||||
}
|
||||
DvhComparePage::~DvhComparePage() = default;
|
||||
|
|
|
|||
|
|
@ -1,63 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include "dvhTypes.h"
|
||||
#include "itkStlCompat.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <vector>
|
||||
|
||||
#include <itkImage.h>
|
||||
|
||||
class QLineEdit;
|
||||
class QListWidget;
|
||||
class QTableWidget;
|
||||
class QTextEdit;
|
||||
class QProgressBar;
|
||||
class QCheckBox;
|
||||
class QPushButton;
|
||||
class DvhPlotWidget;
|
||||
class DvhCenterPart;
|
||||
|
||||
// Thin shell page that hosts the original dvhViewer CenterPart UI.
|
||||
class DvhComparePage : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DvhComparePage(QWidget* parent = nullptr);
|
||||
~DvhComparePage() override;
|
||||
|
||||
private slots:
|
||||
void onBrowseImage();
|
||||
void onBrowseRts();
|
||||
void onAddDose();
|
||||
void onRemoveDose();
|
||||
void onCalculate();
|
||||
void onSelectAllRoi();
|
||||
void onClearAllRoi();
|
||||
void onRoiVisibilityChanged();
|
||||
|
||||
private:
|
||||
void buildUi();
|
||||
void freeResults();
|
||||
void rebuildRoiList();
|
||||
void refreshPlot();
|
||||
QString defaultOpenDir() const;
|
||||
void rememberOpenPath(const QString& path);
|
||||
QString resolveAlgoDllPath() const;
|
||||
void appendLog(const QString& msg);
|
||||
|
||||
QLineEdit* m_imgEdit = nullptr;
|
||||
QLineEdit* m_rtsEdit = nullptr;
|
||||
QTableWidget* m_doseTable = nullptr;
|
||||
QListWidget* m_roiList = nullptr;
|
||||
QTextEdit* m_log = nullptr;
|
||||
QProgressBar* m_progress = nullptr;
|
||||
DvhPlotWidget* m_plot = nullptr;
|
||||
QPushButton* m_calcBtn = nullptr;
|
||||
|
||||
QVector<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;
|
||||
DvhCenterPart* m_center = nullptr;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#include "dcmIO.h"
|
||||
#include <json/json.h>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
//.........................................................................
|
||||
// Get a itk 3-dim float IMAGE from a given META data file
|
||||
|
|
@ -526,4 +526,86 @@ int itkRtStructureReader(
|
|||
return 0;
|
||||
}
|
||||
|
||||
//.........................................................................
|
||||
// Get a set of itk 3-dim unsigned short masks by json format
|
||||
//.........................................................................
|
||||
int itkRtJsonStructReader(
|
||||
std::string rtsJsonFilePath,
|
||||
itk::Image<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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
#include "itkStlCompat.h"
|
||||
#include "dvhTypes.h"
|
||||
|
||||
#define DBG_DISP false
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
|
|
@ -57,6 +55,8 @@
|
|||
#include <gdcmFileMetaInformation.h>
|
||||
#include <gdcmMediaStorage.h>
|
||||
|
||||
#define DBG_DISP false
|
||||
|
||||
int itkMetaDataReader(
|
||||
const std::string metaDataFile,
|
||||
itk::Image<float, 3>::Pointer& image);
|
||||
|
|
@ -78,3 +78,9 @@ int itkRtStructureReader(
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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__
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
// Vertical Hot colorbar with numeric range labels (e.g. 0–2 for gamma).
|
||||
class HotLegendWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit HotLegendWidget(QWidget* parent = nullptr);
|
||||
|
||||
void setRange(double vmin, double vmax);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
private:
|
||||
double m_min = 0.0;
|
||||
double m_max = 2.0;
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
Loading…
Reference in New Issue