Feat: 增加dvh对比功能, 以主界面 app选择形式.
parent
f3543da90f
commit
c54de6fe64
|
|
@ -30,23 +30,34 @@ endif()
|
||||||
|
|
||||||
set(SOURCES
|
set(SOURCES
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/MainWindow.cpp
|
src/AppShell.cpp
|
||||||
|
src/LauncherHomeWidget.cpp
|
||||||
|
src/ProfileComparePage.cpp
|
||||||
src/DoseImage.cpp
|
src/DoseImage.cpp
|
||||||
src/ImageManager.cpp
|
src/ImageManager.cpp
|
||||||
src/SliceViewWidget.cpp
|
src/SliceViewWidget.cpp
|
||||||
src/ProfilePlotWidget.cpp
|
src/ProfilePlotWidget.cpp
|
||||||
src/ImageListPanel.cpp
|
src/ImageListPanel.cpp
|
||||||
|
src/dvh/DvhComparePage.cpp
|
||||||
|
src/dvh/DvhPlotWidget.cpp
|
||||||
|
src/dvh/dcmIO.cpp
|
||||||
resources/resources.qrc
|
resources/resources.qrc
|
||||||
)
|
)
|
||||||
|
|
||||||
set(HEADERS
|
set(HEADERS
|
||||||
src/types.h
|
src/types.h
|
||||||
src/MainWindow.h
|
src/AppShell.h
|
||||||
|
src/LauncherHomeWidget.h
|
||||||
|
src/ProfileComparePage.h
|
||||||
src/DoseImage.h
|
src/DoseImage.h
|
||||||
src/ImageManager.h
|
src/ImageManager.h
|
||||||
src/SliceViewWidget.h
|
src/SliceViewWidget.h
|
||||||
src/ProfilePlotWidget.h
|
src/ProfilePlotWidget.h
|
||||||
src/ImageListPanel.h
|
src/ImageListPanel.h
|
||||||
|
src/dvh/DvhComparePage.h
|
||||||
|
src/dvh/DvhPlotWidget.h
|
||||||
|
src/dvh/dvhTypes.h
|
||||||
|
src/dvh/dcmIO.h
|
||||||
)
|
)
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
|
|
@ -55,7 +66,10 @@ if(WIN32)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_executable(DoseCompare ${SOURCES} ${HEADERS} ${APP_ICON_RC})
|
add_executable(DoseCompare ${SOURCES} ${HEADERS} ${APP_ICON_RC})
|
||||||
target_include_directories(DoseCompare PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
target_include_directories(DoseCompare PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/dvh
|
||||||
|
)
|
||||||
target_link_libraries(DoseCompare PRIVATE
|
target_link_libraries(DoseCompare PRIVATE
|
||||||
Qt5::Core Qt5::Gui Qt5::Widgets
|
Qt5::Core Qt5::Gui Qt5::Widgets
|
||||||
${ITK_LIBRARIES}
|
${ITK_LIBRARIES}
|
||||||
|
|
@ -68,7 +82,18 @@ target_link_libraries(smoke_slice_test PRIVATE Qt5::Core Qt5::Gui ${ITK_LIBRARIE
|
||||||
|
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
target_compile_options(DoseCompare PRIVATE /utf-8 /W3 /EHsc)
|
target_compile_options(DoseCompare PRIVATE /utf-8 /W3 /EHsc)
|
||||||
target_compile_definitions(DoseCompare PRIVATE _CRT_SECURE_NO_WARNINGS)
|
target_compile_definitions(DoseCompare PRIVATE _CRT_SECURE_NO_WARNINGS NOMINMAX WIN32_LEAN_AND_MEAN)
|
||||||
target_compile_options(smoke_slice_test PRIVATE /utf-8 /W3 /EHsc)
|
target_compile_options(smoke_slice_test PRIVATE /utf-8 /W3 /EHsc)
|
||||||
target_compile_definitions(smoke_slice_test PRIVATE _CRT_SECURE_NO_WARNINGS)
|
target_compile_definitions(smoke_slice_test PRIVATE _CRT_SECURE_NO_WARNINGS)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Runtime algo DLL next to the executable
|
||||||
|
add_custom_command(TARGET DoseCompare POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:DoseCompare>/algo
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${CMAKE_SOURCE_DIR}/algo/calcDVHLibrary.dll
|
||||||
|
$<TARGET_FILE_DIR:DoseCompare>/algo/calcDVHLibrary.dll
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
${CMAKE_SOURCE_DIR}/algo/calcDVHLibraryD.dll
|
||||||
|
$<TARGET_FILE_DIR:DoseCompare>/algo/calcDVHLibraryD.dll
|
||||||
|
)
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 878 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 820 KiB |
|
|
@ -3,5 +3,7 @@
|
||||||
<qresource prefix="/">
|
<qresource prefix="/">
|
||||||
<file>dosecompare.png</file>
|
<file>dosecompare.png</file>
|
||||||
<file>dosecompare.ico</file>
|
<file>dosecompare.ico</file>
|
||||||
|
<file>app-profile.png</file>
|
||||||
|
<file>app-dvh.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
#include "AppShell.h"
|
||||||
|
#include "LauncherHomeWidget.h"
|
||||||
|
#include "ProfileComparePage.h"
|
||||||
|
#include "dvh/DvhComparePage.h"
|
||||||
|
|
||||||
|
#include <QStackedWidget>
|
||||||
|
#include <QToolBar>
|
||||||
|
#include <QAction>
|
||||||
|
#include <QLabel>
|
||||||
|
|
||||||
|
AppShell::AppShell(QWidget* parent)
|
||||||
|
: QMainWindow(parent)
|
||||||
|
{
|
||||||
|
setWindowTitle(tr("DoseCompare"));
|
||||||
|
resize(1400, 900);
|
||||||
|
|
||||||
|
auto* tb = addToolBar(tr("导航"));
|
||||||
|
tb->setMovable(false);
|
||||||
|
tb->setFloatable(false);
|
||||||
|
m_backAct = tb->addAction(tr("← 返回主页"));
|
||||||
|
connect(m_backAct, &QAction::triggered, this, &AppShell::showHome);
|
||||||
|
tb->addSeparator();
|
||||||
|
auto* title = new QLabel(tr(" Dose Comparison & Analysis Tools"), this);
|
||||||
|
QFont titleFont = title->font();
|
||||||
|
titleFont.setPointSize(10);
|
||||||
|
titleFont.setBold(false);
|
||||||
|
title->setFont(titleFont);
|
||||||
|
title->setStyleSheet(QStringLiteral("color:#555b66; font-weight:400;"));
|
||||||
|
tb->addWidget(title);
|
||||||
|
|
||||||
|
m_stack = new QStackedWidget(this);
|
||||||
|
setCentralWidget(m_stack);
|
||||||
|
|
||||||
|
m_home = new LauncherHomeWidget(m_stack);
|
||||||
|
m_profile = new ProfileComparePage(m_stack);
|
||||||
|
m_dvh = new DvhComparePage(m_stack);
|
||||||
|
|
||||||
|
m_stack->addWidget(m_home);
|
||||||
|
m_stack->addWidget(m_profile);
|
||||||
|
m_stack->addWidget(m_dvh);
|
||||||
|
|
||||||
|
connect(m_home, &LauncherHomeWidget::openProfileRequested, this, &AppShell::openProfile);
|
||||||
|
connect(m_home, &LauncherHomeWidget::openDvhRequested, this, &AppShell::openDvh);
|
||||||
|
|
||||||
|
showHome();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppShell::showHome()
|
||||||
|
{
|
||||||
|
m_stack->setCurrentWidget(m_home);
|
||||||
|
updateChrome();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppShell::openProfile()
|
||||||
|
{
|
||||||
|
m_stack->setCurrentWidget(m_profile);
|
||||||
|
updateChrome();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppShell::openProfileApp()
|
||||||
|
{
|
||||||
|
openProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppShell::openDvh()
|
||||||
|
{
|
||||||
|
m_stack->setCurrentWidget(m_dvh);
|
||||||
|
updateChrome();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppShell::openDvhApp()
|
||||||
|
{
|
||||||
|
openDvh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppShell::updateChrome()
|
||||||
|
{
|
||||||
|
const bool onHome = (m_stack->currentWidget() == m_home);
|
||||||
|
m_backAct->setVisible(!onHome);
|
||||||
|
if (onHome)
|
||||||
|
setWindowTitle(tr("DoseCompare"));
|
||||||
|
else if (m_stack->currentWidget() == m_profile)
|
||||||
|
setWindowTitle(tr("DoseCompare — Profile Comparison"));
|
||||||
|
else
|
||||||
|
setWindowTitle(tr("DoseCompare — DVH Comparison"));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QMainWindow>
|
||||||
|
|
||||||
|
class QStackedWidget;
|
||||||
|
class LauncherHomeWidget;
|
||||||
|
class ProfileComparePage;
|
||||||
|
class DvhComparePage;
|
||||||
|
|
||||||
|
class AppShell : public QMainWindow {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit AppShell(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
ProfileComparePage* profilePage() const { return m_profile; }
|
||||||
|
void openProfileApp();
|
||||||
|
void openDvhApp();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void showHome();
|
||||||
|
void openProfile();
|
||||||
|
void openDvh();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateChrome();
|
||||||
|
|
||||||
|
QStackedWidget* m_stack = nullptr;
|
||||||
|
LauncherHomeWidget* m_home = nullptr;
|
||||||
|
ProfileComparePage* m_profile = nullptr;
|
||||||
|
DvhComparePage* m_dvh = nullptr;
|
||||||
|
QAction* m_backAct = nullptr;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
#include "LauncherHomeWidget.h"
|
||||||
|
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QPixmap>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QColor>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
QPixmap prepareLauncherIcon(const QString& iconPath, int size, int radius)
|
||||||
|
{
|
||||||
|
QPixmap src(iconPath);
|
||||||
|
if (src.isNull())
|
||||||
|
return QPixmap();
|
||||||
|
|
||||||
|
QImage img = src.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)
|
||||||
|
.toImage()
|
||||||
|
.convertToFormat(QImage::Format_ARGB32);
|
||||||
|
|
||||||
|
for (int y = 0; y < img.height(); ++y) {
|
||||||
|
QRgb* line = reinterpret_cast<QRgb*>(img.scanLine(y));
|
||||||
|
for (int x = 0; x < img.width(); ++x) {
|
||||||
|
if (qRed(line[x]) > 235 && qGreen(line[x]) > 235 && qBlue(line[x]) > 235)
|
||||||
|
line[x] = qRgba(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QPixmap out(size, size);
|
||||||
|
out.fill(Qt::transparent);
|
||||||
|
QPainter p(&out);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||||
|
QPainterPath clip;
|
||||||
|
clip.addRoundedRect(QRectF(0.5, 0.5, size - 1.0, size - 1.0), radius, radius);
|
||||||
|
p.setClipPath(clip);
|
||||||
|
p.drawImage(0, 0, img);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QPushButton* makeAppCard(const QString& title,
|
||||||
|
const QString& subtitle,
|
||||||
|
const QString& iconPath,
|
||||||
|
QWidget* parent)
|
||||||
|
{
|
||||||
|
auto* card = new QPushButton(parent);
|
||||||
|
card->setCursor(Qt::PointingHandCursor);
|
||||||
|
card->setFlat(true);
|
||||||
|
card->setMinimumSize(280, 340);
|
||||||
|
card->setMaximumWidth(360);
|
||||||
|
card->setAttribute(Qt::WA_StyledBackground, true);
|
||||||
|
card->setStyleSheet(
|
||||||
|
QStringLiteral(
|
||||||
|
"QPushButton {"
|
||||||
|
" background-color: #f7f8fa;"
|
||||||
|
" border: 1px solid #d0d5de;"
|
||||||
|
" border-radius: 16px;"
|
||||||
|
" text-align: center;"
|
||||||
|
" padding: 20px 18px;"
|
||||||
|
"}"
|
||||||
|
"QPushButton:hover {"
|
||||||
|
" background-color: #eef2f7;"
|
||||||
|
" border: 1px solid #aeb6c4;"
|
||||||
|
"}"
|
||||||
|
"QPushButton:pressed {"
|
||||||
|
" background-color: #e4e9f0;"
|
||||||
|
"}"));
|
||||||
|
|
||||||
|
auto* lay = new QVBoxLayout(card);
|
||||||
|
lay->setSpacing(14);
|
||||||
|
lay->setContentsMargins(22, 26, 22, 26);
|
||||||
|
|
||||||
|
auto* icon = new QLabel(card);
|
||||||
|
icon->setAlignment(Qt::AlignCenter);
|
||||||
|
icon->setFixedSize(148, 148);
|
||||||
|
icon->setStyleSheet(QStringLiteral("background: transparent; border: none;"));
|
||||||
|
const QPixmap pm = prepareLauncherIcon(iconPath, 148, 28);
|
||||||
|
if (!pm.isNull())
|
||||||
|
icon->setPixmap(pm);
|
||||||
|
icon->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
lay->addWidget(icon, 0, Qt::AlignHCenter);
|
||||||
|
|
||||||
|
auto* t = new QLabel(title, card);
|
||||||
|
t->setAlignment(Qt::AlignCenter);
|
||||||
|
QFont tf = t->font();
|
||||||
|
tf.setPointSize(15);
|
||||||
|
tf.setBold(true);
|
||||||
|
t->setFont(tf);
|
||||||
|
t->setStyleSheet(QStringLiteral("color:#2a3140; background:transparent; border:none;"));
|
||||||
|
t->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
lay->addWidget(t);
|
||||||
|
|
||||||
|
auto* s = new QLabel(subtitle, card);
|
||||||
|
s->setAlignment(Qt::AlignCenter);
|
||||||
|
s->setWordWrap(true);
|
||||||
|
s->setStyleSheet(QStringLiteral("color:#6a7385; background:transparent; border:none; font-size:12px;"));
|
||||||
|
s->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||||
|
lay->addWidget(s);
|
||||||
|
lay->addStretch(1);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
LauncherHomeWidget::LauncherHomeWidget(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_StyledBackground, true);
|
||||||
|
setStyleSheet(QStringLiteral(
|
||||||
|
"LauncherHomeWidget {"
|
||||||
|
" background: #f0f2f5;"
|
||||||
|
"}"));
|
||||||
|
|
||||||
|
auto* root = new QVBoxLayout(this);
|
||||||
|
root->setContentsMargins(48, 40, 48, 48);
|
||||||
|
root->setSpacing(28);
|
||||||
|
|
||||||
|
auto* brand = new QLabel(tr("Dose Comparison & Analysis Tools"), this);
|
||||||
|
QFont bf = brand->font();
|
||||||
|
bf.setPointSize(22);
|
||||||
|
bf.setBold(false);
|
||||||
|
brand->setFont(bf);
|
||||||
|
brand->setStyleSheet(QStringLiteral("color:#333945; background:transparent;"));
|
||||||
|
brand->setAlignment(Qt::AlignCenter);
|
||||||
|
root->addWidget(brand);
|
||||||
|
|
||||||
|
auto* hint = new QLabel(tr("选择一个应用开始"), this);
|
||||||
|
hint->setAlignment(Qt::AlignCenter);
|
||||||
|
hint->setStyleSheet(QStringLiteral("color:#7a8496; font-size:14px; background:transparent;"));
|
||||||
|
root->addWidget(hint);
|
||||||
|
|
||||||
|
auto* row = new QHBoxLayout();
|
||||||
|
row->setSpacing(36);
|
||||||
|
row->addStretch(1);
|
||||||
|
|
||||||
|
auto* profileCard = makeAppCard(
|
||||||
|
tr("Profile Comparison"),
|
||||||
|
tr("剂量三切面叠加与交线 Profile 对比"),
|
||||||
|
QStringLiteral(":/app-profile.png"),
|
||||||
|
this);
|
||||||
|
connect(profileCard, &QPushButton::clicked, this, &LauncherHomeWidget::openProfileRequested);
|
||||||
|
row->addWidget(profileCard);
|
||||||
|
|
||||||
|
auto* dvhCard = makeAppCard(
|
||||||
|
tr("DVH Comparison"),
|
||||||
|
tr("多剂量计划与结构勾画的 DVH 对比"),
|
||||||
|
QStringLiteral(":/app-dvh.png"),
|
||||||
|
this);
|
||||||
|
connect(dvhCard, &QPushButton::clicked, this, &LauncherHomeWidget::openDvhRequested);
|
||||||
|
row->addWidget(dvhCard);
|
||||||
|
|
||||||
|
row->addStretch(1);
|
||||||
|
root->addLayout(row, 1);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
class LauncherHomeWidget : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit LauncherHomeWidget(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void openProfileRequested();
|
||||||
|
void openDvhRequested();
|
||||||
|
};
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#include "MainWindow.h"
|
#include "ProfileComparePage.h"
|
||||||
#include "ImageManager.h"
|
#include "ImageManager.h"
|
||||||
#include "ImageListPanel.h"
|
#include "ImageListPanel.h"
|
||||||
#include "SliceViewWidget.h"
|
#include "SliceViewWidget.h"
|
||||||
|
|
@ -10,14 +10,12 @@
|
||||||
#include <QSlider>
|
#include <QSlider>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QToolBar>
|
#include <QToolBar>
|
||||||
#include <QStatusBar>
|
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
#include <QGridLayout>
|
#include <QGridLayout>
|
||||||
#include <QGroupBox>
|
#include <QGroupBox>
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QMenuBar>
|
#include <QPushButton>
|
||||||
#include <QMenu>
|
|
||||||
#include <QSettings>
|
#include <QSettings>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
|
@ -30,29 +28,34 @@
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
|
||||||
MainWindow::MainWindow(QWidget* parent)
|
ProfileComparePage::ProfileComparePage(QWidget* parent)
|
||||||
: QMainWindow(parent)
|
: QWidget(parent)
|
||||||
{
|
{
|
||||||
m_manager = new ImageManager(this);
|
m_manager = new ImageManager(this);
|
||||||
buildUi();
|
buildUi();
|
||||||
setAcceptDrops(true);
|
setAcceptDrops(true);
|
||||||
|
|
||||||
connect(m_manager, &ImageManager::imageListChanged, this, &MainWindow::refreshImageSelectors);
|
connect(m_manager, &ImageManager::imageListChanged, this, &ProfileComparePage::refreshImageSelectors);
|
||||||
connect(m_manager, &ImageManager::profileSelectionChanged, this, &MainWindow::refreshProfiles);
|
connect(m_manager, &ImageManager::profileSelectionChanged, this, &ProfileComparePage::refreshProfiles);
|
||||||
|
|
||||||
resize(1400, 900);
|
|
||||||
setWindowTitle(tr("DoseCompare — 剂量三切面对比"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindow::~MainWindow() = default;
|
ProfileComparePage::~ProfileComparePage() = default;
|
||||||
|
|
||||||
void MainWindow::buildUi()
|
void ProfileComparePage::buildUi()
|
||||||
{
|
{
|
||||||
auto* importAct = new QAction(tr("导入剂量..."), this);
|
auto* pageLay = new QVBoxLayout(this);
|
||||||
connect(importAct, &QAction::triggered, this, &MainWindow::onImportDose);
|
pageLay->setContentsMargins(0, 0, 0, 0);
|
||||||
menuBar()->addMenu(tr("文件"))->addAction(importAct);
|
pageLay->setSpacing(0);
|
||||||
|
|
||||||
|
auto* topBar = new QWidget(this);
|
||||||
|
auto* tb = new QHBoxLayout(topBar);
|
||||||
|
tb->setContentsMargins(8, 6, 8, 6);
|
||||||
|
tb->setSpacing(8);
|
||||||
|
|
||||||
|
auto* importBtn = new QPushButton(tr("导入剂量..."), this);
|
||||||
|
connect(importBtn, &QPushButton::clicked, this, &ProfileComparePage::onImportDose);
|
||||||
|
tb->addWidget(importBtn);
|
||||||
|
|
||||||
auto* tb = addToolBar(tr("对比"));
|
|
||||||
tb->addWidget(new QLabel(tr(" Background "), this));
|
tb->addWidget(new QLabel(tr(" Background "), this));
|
||||||
m_bgCombo = new QComboBox(this);
|
m_bgCombo = new QComboBox(this);
|
||||||
m_bgCombo->setMinimumWidth(160);
|
m_bgCombo->setMinimumWidth(160);
|
||||||
|
|
@ -70,7 +73,6 @@ void MainWindow::buildUi()
|
||||||
m_opacityLabel = new QLabel(tr("50%"), this);
|
m_opacityLabel = new QLabel(tr("50%"), this);
|
||||||
tb->addWidget(m_opacityLabel);
|
tb->addWidget(m_opacityLabel);
|
||||||
|
|
||||||
tb->addSeparator();
|
|
||||||
tb->addWidget(new QLabel(tr(" Colormap "), this));
|
tb->addWidget(new QLabel(tr(" Colormap "), this));
|
||||||
m_colorMapCombo = new QComboBox(this);
|
m_colorMapCombo = new QComboBox(this);
|
||||||
m_colorMapCombo->blockSignals(true);
|
m_colorMapCombo->blockSignals(true);
|
||||||
|
|
@ -81,22 +83,24 @@ void MainWindow::buildUi()
|
||||||
m_colorMapCombo->setMinimumWidth(110);
|
m_colorMapCombo->setMinimumWidth(110);
|
||||||
m_colorMapCombo->blockSignals(false);
|
m_colorMapCombo->blockSignals(false);
|
||||||
tb->addWidget(m_colorMapCombo);
|
tb->addWidget(m_colorMapCombo);
|
||||||
|
tb->addStretch(1);
|
||||||
|
pageLay->addWidget(topBar);
|
||||||
|
|
||||||
connect(m_bgCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
connect(m_bgCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
this, &MainWindow::onBackgroundChanged);
|
this, &ProfileComparePage::onBackgroundChanged);
|
||||||
connect(m_frontCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
connect(m_frontCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
this, &MainWindow::onFrontChanged);
|
this, &ProfileComparePage::onFrontChanged);
|
||||||
connect(m_opacitySlider, &QSlider::valueChanged, this, &MainWindow::onOpacityChanged);
|
connect(m_opacitySlider, &QSlider::valueChanged, this, &ProfileComparePage::onOpacityChanged);
|
||||||
connect(m_colorMapCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
connect(m_colorMapCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||||
this, &MainWindow::onColorMapChanged);
|
this, &ProfileComparePage::onColorMapChanged);
|
||||||
|
|
||||||
auto* central = new QWidget(this);
|
auto* central = new QWidget(this);
|
||||||
setCentralWidget(central);
|
|
||||||
auto* root = new QHBoxLayout(central);
|
auto* root = new QHBoxLayout(central);
|
||||||
|
pageLay->addWidget(central, 1);
|
||||||
|
|
||||||
m_listPanel = new ImageListPanel(m_manager, this);
|
m_listPanel = new ImageListPanel(m_manager, this);
|
||||||
m_listPanel->setFixedWidth(220);
|
m_listPanel->setFixedWidth(220);
|
||||||
connect(m_listPanel, &ImageListPanel::requestDelete, this, &MainWindow::onDeleteImage);
|
connect(m_listPanel, &ImageListPanel::requestDelete, this, &ProfileComparePage::onDeleteImage);
|
||||||
root->addWidget(m_listPanel);
|
root->addWidget(m_listPanel);
|
||||||
|
|
||||||
auto* right = new QVBoxLayout();
|
auto* right = new QVBoxLayout();
|
||||||
|
|
@ -113,19 +117,19 @@ void MainWindow::buildUi()
|
||||||
slider->setRange(0, 0);
|
slider->setRange(0, 0);
|
||||||
col->addWidget(slider);
|
col->addWidget(slider);
|
||||||
sliceRow->addLayout(col, 1);
|
sliceRow->addLayout(col, 1);
|
||||||
connect(view, &SliceViewWidget::cursorChanged, this, &MainWindow::onCursorChanged);
|
connect(view, &SliceViewWidget::cursorChanged, this, &ProfileComparePage::onCursorChanged);
|
||||||
connect(view, &SliceViewWidget::viewExtentChanged, this, &MainWindow::refreshProfiles);
|
connect(view, &SliceViewWidget::viewExtentChanged, this, &ProfileComparePage::refreshProfiles);
|
||||||
connect(view, &SliceViewWidget::zoomScaleChanged, this, &MainWindow::onZoomScaleChanged);
|
connect(view, &SliceViewWidget::zoomScaleChanged, this, &ProfileComparePage::onZoomScaleChanged);
|
||||||
connect(view, &SliceViewWidget::panWorldDelta, this, &MainWindow::onPanWorldDelta);
|
connect(view, &SliceViewWidget::panWorldDelta, this, &ProfileComparePage::onPanWorldDelta);
|
||||||
};
|
};
|
||||||
addSliceCol(ViewAxis::Axial, m_axial, m_sliderAxial, tr("1 轴位 (Axial)"));
|
addSliceCol(ViewAxis::Axial, m_axial, m_sliderAxial, tr("1 轴位 (Axial)"));
|
||||||
addSliceCol(ViewAxis::Sagittal, m_sagittal, m_sliderSagittal, tr("2 矢状 (Sagittal)"));
|
addSliceCol(ViewAxis::Sagittal, m_sagittal, m_sliderSagittal, tr("2 矢状 (Sagittal)"));
|
||||||
addSliceCol(ViewAxis::Coronal, m_coronal, m_sliderCoronal, tr("3 冠状 (Coronal)"));
|
addSliceCol(ViewAxis::Coronal, m_coronal, m_sliderCoronal, tr("3 冠状 (Coronal)"));
|
||||||
right->addLayout(sliceRow, 3);
|
right->addLayout(sliceRow, 3);
|
||||||
|
|
||||||
connect(m_sliderAxial, &QSlider::valueChanged, this, &MainWindow::onSliceSliderChanged);
|
connect(m_sliderAxial, &QSlider::valueChanged, this, &ProfileComparePage::onSliceSliderChanged);
|
||||||
connect(m_sliderSagittal, &QSlider::valueChanged, this, &MainWindow::onSliceSliderChanged);
|
connect(m_sliderSagittal, &QSlider::valueChanged, this, &ProfileComparePage::onSliceSliderChanged);
|
||||||
connect(m_sliderCoronal, &QSlider::valueChanged, this, &MainWindow::onSliceSliderChanged);
|
connect(m_sliderCoronal, &QSlider::valueChanged, this, &ProfileComparePage::onSliceSliderChanged);
|
||||||
|
|
||||||
auto* profRow = new QHBoxLayout();
|
auto* profRow = new QHBoxLayout();
|
||||||
m_profAxial = new ProfilePlotWidget(ViewAxis::Axial, this);
|
m_profAxial = new ProfilePlotWidget(ViewAxis::Axial, this);
|
||||||
|
|
@ -142,11 +146,13 @@ void MainWindow::buildUi()
|
||||||
profRow->addWidget(m_profCoronal, 1);
|
profRow->addWidget(m_profCoronal, 1);
|
||||||
right->addLayout(profRow, 2);
|
right->addLayout(profRow, 2);
|
||||||
|
|
||||||
statusBar()->showMessage(
|
m_statusLabel = new QLabel(
|
||||||
tr("可拖入 mhd/nii/dcm。Ctrl+在1/2/3上移动可重切另两面。"));
|
tr("可拖入 mhd/nii/dcm。Ctrl+在1/2/3上移动可重切另两面。"), this);
|
||||||
|
m_statusLabel->setContentsMargins(8, 4, 8, 4);
|
||||||
|
pageLay->addWidget(m_statusLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MainWindow::defaultOpenDir() const
|
QString ProfileComparePage::defaultOpenDir() const
|
||||||
{
|
{
|
||||||
QSettings settings(QStringLiteral("Manteia"), QStringLiteral("DoseCompare"));
|
QSettings settings(QStringLiteral("Manteia"), QStringLiteral("DoseCompare"));
|
||||||
const QString last = settings.value(QStringLiteral("lastOpenDir")).toString();
|
const QString last = settings.value(QStringLiteral("lastOpenDir")).toString();
|
||||||
|
|
@ -155,7 +161,7 @@ QString MainWindow::defaultOpenDir() const
|
||||||
return QCoreApplication::applicationDirPath();
|
return QCoreApplication::applicationDirPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::rememberOpenPath(const QString& path)
|
void ProfileComparePage::rememberOpenPath(const QString& path)
|
||||||
{
|
{
|
||||||
const QString dir = QFileInfo(path).absolutePath();
|
const QString dir = QFileInfo(path).absolutePath();
|
||||||
if (dir.isEmpty())
|
if (dir.isEmpty())
|
||||||
|
|
@ -164,7 +170,7 @@ void MainWindow::rememberOpenPath(const QString& path)
|
||||||
settings.setValue(QStringLiteral("lastOpenDir"), dir);
|
settings.setValue(QStringLiteral("lastOpenDir"), dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MainWindow::isSupportedDosePath(const QString& path) const
|
bool ProfileComparePage::isSupportedDosePath(const QString& path) const
|
||||||
{
|
{
|
||||||
const QString lower = path.toLower();
|
const QString lower = path.toLower();
|
||||||
return lower.endsWith(QStringLiteral(".mhd")) || lower.endsWith(QStringLiteral(".mha"))
|
return lower.endsWith(QStringLiteral(".mhd")) || lower.endsWith(QStringLiteral(".mha"))
|
||||||
|
|
@ -172,7 +178,7 @@ bool MainWindow::isSupportedDosePath(const QString& path) const
|
||||||
|| lower.endsWith(QStringLiteral(".dcm")) || lower.endsWith(QStringLiteral(".dicom"));
|
|| lower.endsWith(QStringLiteral(".dcm")) || lower.endsWith(QStringLiteral(".dicom"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onImportDose()
|
void ProfileComparePage::onImportDose()
|
||||||
{
|
{
|
||||||
const QString path = QFileDialog::getOpenFileName(
|
const QString path = QFileDialog::getOpenFileName(
|
||||||
this, tr("导入剂量图像"), defaultOpenDir(),
|
this, tr("导入剂量图像"), defaultOpenDir(),
|
||||||
|
|
@ -185,7 +191,7 @@ void MainWindow::onImportDose()
|
||||||
QMessageBox::warning(this, tr("导入失败"), err.isEmpty() ? tr("未知错误") : err);
|
QMessageBox::warning(this, tr("导入失败"), err.isEmpty() ? tr("未知错误") : err);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::dragEnterEvent(QDragEnterEvent* event)
|
void ProfileComparePage::dragEnterEvent(QDragEnterEvent* event)
|
||||||
{
|
{
|
||||||
if (!event->mimeData() || !event->mimeData()->hasUrls()) {
|
if (!event->mimeData() || !event->mimeData()->hasUrls()) {
|
||||||
event->ignore();
|
event->ignore();
|
||||||
|
|
@ -200,7 +206,7 @@ void MainWindow::dragEnterEvent(QDragEnterEvent* event)
|
||||||
event->ignore();
|
event->ignore();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::dropEvent(QDropEvent* event)
|
void ProfileComparePage::dropEvent(QDropEvent* event)
|
||||||
{
|
{
|
||||||
if (!event->mimeData() || !event->mimeData()->hasUrls()) {
|
if (!event->mimeData() || !event->mimeData()->hasUrls()) {
|
||||||
event->ignore();
|
event->ignore();
|
||||||
|
|
@ -229,7 +235,7 @@ void MainWindow::dropEvent(QDropEvent* event)
|
||||||
event->ignore();
|
event->ignore();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::applyBgFgAfterLoad(const QString& newId, int countBefore)
|
void ProfileComparePage::applyBgFgAfterLoad(const QString& newId, int countBefore)
|
||||||
{
|
{
|
||||||
m_pendingBgId.clear();
|
m_pendingBgId.clear();
|
||||||
m_pendingFgId.clear();
|
m_pendingFgId.clear();
|
||||||
|
|
@ -243,7 +249,7 @@ void MainWindow::applyBgFgAfterLoad(const QString& newId, int countBefore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::promoteBgFgAfterDelete(const QString& deletedId, const QString& oldBg,
|
void ProfileComparePage::promoteBgFgAfterDelete(const QString& deletedId, const QString& oldBg,
|
||||||
const QString& oldFg)
|
const QString& oldFg)
|
||||||
{
|
{
|
||||||
// 在 remove 之前根据“删除后”的逻辑准备 pending;remove 时 list 仍含 deleted
|
// 在 remove 之前根据“删除后”的逻辑准备 pending;remove 时 list 仍含 deleted
|
||||||
|
|
@ -287,7 +293,7 @@ void MainWindow::promoteBgFgAfterDelete(const QString& deletedId, const QString&
|
||||||
m_hasPendingRoles = true;
|
m_hasPendingRoles = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MainWindow::loadDoseFile(const QString& path, QString* error)
|
bool ProfileComparePage::loadDoseFile(const QString& path, QString* error)
|
||||||
{
|
{
|
||||||
const int countBefore = m_manager->images().size();
|
const int countBefore = m_manager->images().size();
|
||||||
QString err;
|
QString err;
|
||||||
|
|
@ -312,11 +318,12 @@ bool MainWindow::loadDoseFile(const QString& path, QString* error)
|
||||||
refreshImageSelectors();
|
refreshImageSelectors();
|
||||||
refreshViews();
|
refreshViews();
|
||||||
refreshProfiles();
|
refreshProfiles();
|
||||||
statusBar()->showMessage(tr("已加载: %1").arg(path), 5000);
|
if (m_statusLabel)
|
||||||
|
m_statusLabel->setText(tr("已加载: %1").arg(path));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onDeleteImage(const QString& id)
|
void ProfileComparePage::onDeleteImage(const QString& id)
|
||||||
{
|
{
|
||||||
const QString oldBg = currentBgId();
|
const QString oldBg = currentBgId();
|
||||||
const QString oldFg = currentFgId();
|
const QString oldFg = currentFgId();
|
||||||
|
|
@ -327,22 +334,22 @@ void MainWindow::onDeleteImage(const QString& id)
|
||||||
refreshProfiles();
|
refreshProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MainWindow::currentBgId() const
|
QString ProfileComparePage::currentBgId() const
|
||||||
{
|
{
|
||||||
return m_bgCombo->currentData().toString();
|
return m_bgCombo->currentData().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString MainWindow::currentFgId() const
|
QString ProfileComparePage::currentFgId() const
|
||||||
{
|
{
|
||||||
return m_frontCombo->currentData().toString();
|
return m_frontCombo->currentData().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
DoseImage::Pointer MainWindow::bgDose() const
|
DoseImage::Pointer ProfileComparePage::bgDose() const
|
||||||
{
|
{
|
||||||
return m_manager->imageById(currentBgId());
|
return m_manager->imageById(currentBgId());
|
||||||
}
|
}
|
||||||
|
|
||||||
DoseImage::Pointer MainWindow::fgDose() const
|
DoseImage::Pointer ProfileComparePage::fgDose() const
|
||||||
{
|
{
|
||||||
const QString id = currentFgId();
|
const QString id = currentFgId();
|
||||||
if (id.isEmpty() || id == currentBgId())
|
if (id.isEmpty() || id == currentBgId())
|
||||||
|
|
@ -350,7 +357,7 @@ DoseImage::Pointer MainWindow::fgDose() const
|
||||||
return m_manager->imageById(id);
|
return m_manager->imageById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::refreshImageSelectors()
|
void ProfileComparePage::refreshImageSelectors()
|
||||||
{
|
{
|
||||||
QString oldBg = currentBgId();
|
QString oldBg = currentBgId();
|
||||||
QString oldFg = currentFgId();
|
QString oldFg = currentFgId();
|
||||||
|
|
@ -400,19 +407,19 @@ void MainWindow::refreshImageSelectors()
|
||||||
m_frontCombo->blockSignals(false);
|
m_frontCombo->blockSignals(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onFrontChanged(int)
|
void ProfileComparePage::onFrontChanged(int)
|
||||||
{
|
{
|
||||||
refreshViews();
|
refreshViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onBackgroundChanged(int)
|
void ProfileComparePage::onBackgroundChanged(int)
|
||||||
{
|
{
|
||||||
refreshViews();
|
refreshViews();
|
||||||
syncSliceSliders();
|
syncSliceSliders();
|
||||||
refreshProfiles();
|
refreshProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onOpacityChanged(int value)
|
void ProfileComparePage::onOpacityChanged(int value)
|
||||||
{
|
{
|
||||||
m_opacityLabel->setText(QStringLiteral("%1%").arg(value));
|
m_opacityLabel->setText(QStringLiteral("%1%").arg(value));
|
||||||
const double op = value / 100.0;
|
const double op = value / 100.0;
|
||||||
|
|
@ -421,7 +428,7 @@ void MainWindow::onOpacityChanged(int value)
|
||||||
m_coronal->setOpacity(op);
|
m_coronal->setOpacity(op);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onColorMapChanged(int)
|
void ProfileComparePage::onColorMapChanged(int)
|
||||||
{
|
{
|
||||||
if (!m_colorMapCombo || !m_axial)
|
if (!m_colorMapCombo || !m_axial)
|
||||||
return;
|
return;
|
||||||
|
|
@ -431,7 +438,7 @@ void MainWindow::onColorMapChanged(int)
|
||||||
m_coronal->setColorMap(map);
|
m_coronal->setColorMap(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::refreshViews()
|
void ProfileComparePage::refreshViews()
|
||||||
{
|
{
|
||||||
auto bg = bgDose();
|
auto bg = bgDose();
|
||||||
auto fg = fgDose();
|
auto fg = fgDose();
|
||||||
|
|
@ -455,7 +462,7 @@ void MainWindow::refreshViews()
|
||||||
syncSliceSliders();
|
syncSliceSliders();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::syncSharedZoomFromViews()
|
void ProfileComparePage::syncSharedZoomFromViews()
|
||||||
{
|
{
|
||||||
if (!m_axial || !m_sagittal || !m_coronal)
|
if (!m_axial || !m_sagittal || !m_coronal)
|
||||||
return;
|
return;
|
||||||
|
|
@ -468,7 +475,7 @@ void MainWindow::syncSharedZoomFromViews()
|
||||||
m_blockViewSync = false;
|
m_blockViewSync = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onZoomScaleChanged(double parallelScale)
|
void ProfileComparePage::onZoomScaleChanged(double parallelScale)
|
||||||
{
|
{
|
||||||
if (m_blockViewSync)
|
if (m_blockViewSync)
|
||||||
return;
|
return;
|
||||||
|
|
@ -480,7 +487,7 @@ void MainWindow::onZoomScaleChanged(double parallelScale)
|
||||||
m_blockViewSync = false;
|
m_blockViewSync = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onPanWorldDelta(double dx, double dy, double dz)
|
void ProfileComparePage::onPanWorldDelta(double dx, double dy, double dz)
|
||||||
{
|
{
|
||||||
if (m_blockViewSync)
|
if (m_blockViewSync)
|
||||||
return;
|
return;
|
||||||
|
|
@ -495,7 +502,7 @@ void MainWindow::onPanWorldDelta(double dx, double dy, double dz)
|
||||||
m_blockViewSync = false;
|
m_blockViewSync = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::syncSliceSliders()
|
void ProfileComparePage::syncSliceSliders()
|
||||||
{
|
{
|
||||||
auto bg = bgDose();
|
auto bg = bgDose();
|
||||||
if (!bg) {
|
if (!bg) {
|
||||||
|
|
@ -536,7 +543,7 @@ void MainWindow::syncSliceSliders()
|
||||||
Q_UNUSED(origin);
|
Q_UNUSED(origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onSliceSliderChanged(int)
|
void ProfileComparePage::onSliceSliderChanged(int)
|
||||||
{
|
{
|
||||||
if (m_blockSliders)
|
if (m_blockSliders)
|
||||||
return;
|
return;
|
||||||
|
|
@ -563,12 +570,12 @@ void MainWindow::onSliceSliderChanged(int)
|
||||||
Q_UNUSED(origin);
|
Q_UNUSED(origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::onCursorChanged(Vec3d world)
|
void ProfileComparePage::onCursorChanged(Vec3d world)
|
||||||
{
|
{
|
||||||
setCursor(world, false);
|
setCursor(world, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::setCursor(const Vec3d& world, bool fromSlider)
|
void ProfileComparePage::setCursor(const Vec3d& world, bool fromSlider)
|
||||||
{
|
{
|
||||||
m_cursor = world;
|
m_cursor = world;
|
||||||
m_axial->setCursorWorld(m_cursor);
|
m_axial->setCursorWorld(m_cursor);
|
||||||
|
|
@ -579,12 +586,12 @@ void MainWindow::setCursor(const Vec3d& world, bool fromSlider)
|
||||||
refreshProfiles();
|
refreshProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::refreshProfiles()
|
void ProfileComparePage::refreshProfiles()
|
||||||
{
|
{
|
||||||
extractProfiles();
|
extractProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::extractProfiles()
|
void ProfileComparePage::extractProfiles()
|
||||||
{
|
{
|
||||||
QVector<ProfileSeries> alongX, alongY, alongZ;
|
QVector<ProfileSeries> alongX, alongY, alongZ;
|
||||||
auto bg = bgDose();
|
auto bg = bgDose();
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "types.h"
|
#include "types.h"
|
||||||
#include "DoseImage.h"
|
#include "DoseImage.h"
|
||||||
|
|
||||||
#include <QMainWindow>
|
#include <QWidget>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
class ImageManager;
|
class ImageManager;
|
||||||
|
|
@ -16,11 +16,11 @@ class QLabel;
|
||||||
class QDragEnterEvent;
|
class QDragEnterEvent;
|
||||||
class QDropEvent;
|
class QDropEvent;
|
||||||
|
|
||||||
class MainWindow : public QMainWindow {
|
class ProfileComparePage : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit MainWindow(QWidget* parent = nullptr);
|
explicit ProfileComparePage(QWidget* parent = nullptr);
|
||||||
~MainWindow() override;
|
~ProfileComparePage() override;
|
||||||
|
|
||||||
bool loadDoseFile(const QString& path, QString* error = nullptr);
|
bool loadDoseFile(const QString& path, QString* error = nullptr);
|
||||||
|
|
||||||
|
|
@ -86,4 +86,5 @@ private:
|
||||||
QString m_pendingFgId;
|
QString m_pendingFgId;
|
||||||
QString m_pendingBgId;
|
QString m_pendingBgId;
|
||||||
bool m_hasPendingRoles = false;
|
bool m_hasPendingRoles = false;
|
||||||
|
QLabel* m_statusLabel = nullptr;
|
||||||
};
|
};
|
||||||
|
|
@ -0,0 +1,560 @@
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
#include "DvhComparePage.h"
|
||||||
|
#include "DvhPlotWidget.h"
|
||||||
|
#include "dcmIO.h"
|
||||||
|
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QListWidget>
|
||||||
|
#include <QTableWidget>
|
||||||
|
#include <QTextEdit>
|
||||||
|
#include <QProgressBar>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QHBoxLayout>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
#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 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;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
#include "DvhPlotWidget.h"
|
||||||
|
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <QPalette>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
DvhPlotWidget::DvhPlotWidget(QWidget* parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
setMinimumSize(400, 300);
|
||||||
|
setAutoFillBackground(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DvhPlotWidget::setCurves(const QVector<DvhCurve>& curves)
|
||||||
|
{
|
||||||
|
m_curves = curves;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DvhPlotWidget::setXMax(double xMax)
|
||||||
|
{
|
||||||
|
m_xMax = std::max(1e-3, xMax);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DvhPlotWidget::paintEvent(QPaintEvent*)
|
||||||
|
{
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::Antialiasing, true);
|
||||||
|
|
||||||
|
const QColor bg = palette().color(QPalette::Window);
|
||||||
|
const QColor base = palette().color(QPalette::Base);
|
||||||
|
const QColor text = palette().color(QPalette::WindowText);
|
||||||
|
const QColor mid = palette().color(QPalette::Mid);
|
||||||
|
p.fillRect(rect(), bg);
|
||||||
|
|
||||||
|
const QRectF plot = rect().adjusted(56, 28, -18, -42);
|
||||||
|
p.setPen(QPen(mid, 1));
|
||||||
|
p.setBrush(base);
|
||||||
|
p.drawRect(plot);
|
||||||
|
|
||||||
|
p.setPen(QPen(mid, 1, Qt::DotLine));
|
||||||
|
for (int i = 1; i < 5; ++i) {
|
||||||
|
const double y = plot.top() + plot.height() * i / 5.0;
|
||||||
|
p.drawLine(QPointF(plot.left(), y), QPointF(plot.right(), y));
|
||||||
|
}
|
||||||
|
for (int i = 1; i < 5; ++i) {
|
||||||
|
const double x = plot.left() + plot.width() * i / 5.0;
|
||||||
|
p.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mapX = [&](double x) {
|
||||||
|
return plot.left() + (x / m_xMax) * plot.width();
|
||||||
|
};
|
||||||
|
auto mapY = [&](double y) {
|
||||||
|
return plot.bottom() - y * plot.height();
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const DvhCurve& c : m_curves) {
|
||||||
|
if (!c.visible || c.points.size() < 2)
|
||||||
|
continue;
|
||||||
|
QPen pen(c.color, 2.0);
|
||||||
|
pen.setStyle(c.style);
|
||||||
|
p.setPen(pen);
|
||||||
|
QPainterPath path;
|
||||||
|
path.moveTo(mapX(c.points[0].x()), mapY(c.points[0].y()));
|
||||||
|
for (int i = 1; i < c.points.size(); ++i)
|
||||||
|
path.lineTo(mapX(c.points[i].x()), mapY(c.points[i].y()));
|
||||||
|
p.drawPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setPen(text);
|
||||||
|
QFont f = font();
|
||||||
|
f.setPointSize(9);
|
||||||
|
p.setFont(f);
|
||||||
|
p.drawText(QRectF(plot.left(), plot.bottom() + 6, plot.width(), 20),
|
||||||
|
Qt::AlignCenter, tr("Dose"));
|
||||||
|
p.save();
|
||||||
|
p.translate(14, plot.center().y());
|
||||||
|
p.rotate(-90);
|
||||||
|
p.drawText(QRectF(-60, -10, 120, 20), Qt::AlignCenter, tr("Volume"));
|
||||||
|
p.restore();
|
||||||
|
|
||||||
|
p.drawText(QPointF(plot.left(), plot.bottom() + 20), QStringLiteral("0"));
|
||||||
|
p.drawText(QPointF(plot.right() - 40, plot.bottom() + 20),
|
||||||
|
QString::number(m_xMax, 'f', 1));
|
||||||
|
p.drawText(QPointF(8, plot.top() + 12), QStringLiteral("1"));
|
||||||
|
p.drawText(QPointF(8, plot.bottom()), QStringLiteral("0"));
|
||||||
|
|
||||||
|
double lx = plot.left() + 8;
|
||||||
|
double ly = plot.top() + 10;
|
||||||
|
for (const DvhCurve& c : m_curves) {
|
||||||
|
if (!c.visible)
|
||||||
|
continue;
|
||||||
|
p.setPen(QPen(c.color, 2));
|
||||||
|
p.drawLine(QPointF(lx, ly), QPointF(lx + 18, ly));
|
||||||
|
p.setPen(text);
|
||||||
|
p.drawText(QPointF(lx + 24, ly + 4), c.name);
|
||||||
|
ly += 16;
|
||||||
|
if (ly > plot.top() + 120)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QVector>
|
||||||
|
#include <QColor>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
struct DvhCurve {
|
||||||
|
QString name;
|
||||||
|
QColor color;
|
||||||
|
Qt::PenStyle style = Qt::SolidLine;
|
||||||
|
QVector<QPointF> points; // x=dose, y=volume fraction [0,1]
|
||||||
|
bool visible = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DvhPlotWidget : public QWidget {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit DvhPlotWidget(QWidget* parent = nullptr);
|
||||||
|
|
||||||
|
void setCurves(const QVector<DvhCurve>& curves);
|
||||||
|
void setXMax(double xMax);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent* event) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVector<DvhCurve> m_curves;
|
||||||
|
double m_xMax = 1.0;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,529 @@
|
||||||
|
#include "dcmIO.h"
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
//.........................................................................
|
||||||
|
// Get a itk 3-dim float IMAGE from a given META data file
|
||||||
|
//.........................................................................
|
||||||
|
int itkMetaDataReader(
|
||||||
|
const std::string metaDataFile,
|
||||||
|
itk::Image<float, 3>::Pointer& image)
|
||||||
|
{
|
||||||
|
itk::GDCMImageIOFactory::RegisterOneFactory();
|
||||||
|
typedef itk::ImageFileReader<itk::Image<float, 3>> ReaderType;
|
||||||
|
ReaderType::Pointer reader = ReaderType::New();
|
||||||
|
reader->SetFileName(metaDataFile);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
reader->Update();
|
||||||
|
image = reader->GetOutput();
|
||||||
|
}
|
||||||
|
catch (itk::ExceptionObject& err)
|
||||||
|
{
|
||||||
|
std::cout << "Error: " << err.GetDescription() << " in itkImageLocationFunction" << std::endl;;
|
||||||
|
return ERR_ER_LOAD_IMGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//.........................................................................
|
||||||
|
// Get a itk 3-dim float IMAGE from a given META data file
|
||||||
|
//.........................................................................
|
||||||
|
int itkMetaMaskReader(
|
||||||
|
const std::string maskDataFile,
|
||||||
|
itk::Image<unsigned char, 3>::Pointer& mask)
|
||||||
|
{
|
||||||
|
itk::GDCMImageIOFactory::RegisterOneFactory();
|
||||||
|
typedef itk::ImageFileReader<itk::Image<unsigned char, 3>> ReaderType;
|
||||||
|
ReaderType::Pointer reader = ReaderType::New();
|
||||||
|
reader->SetFileName(maskDataFile);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
reader->Update();
|
||||||
|
mask = reader->GetOutput();
|
||||||
|
}
|
||||||
|
catch (itk::ExceptionObject& err)
|
||||||
|
{
|
||||||
|
std::cout << "Error: " << err.GetDescription() << " in itkImageLocationFunction" << std::endl;;
|
||||||
|
return ERR_ER_LOAD_IMGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//.........................................................................
|
||||||
|
// Get a itk 3-dim float dose from a given DCM data file
|
||||||
|
//.........................................................................
|
||||||
|
int itkDcmDoseReader(
|
||||||
|
const std::string dcmDoseFile,
|
||||||
|
itk::Image<float, 3>::Pointer& dose)
|
||||||
|
{
|
||||||
|
itk::GDCMImageIOFactory::RegisterOneFactory();
|
||||||
|
typedef itk::ImageFileReader<itk::Image<float, 3>> ReaderType;
|
||||||
|
ReaderType::Pointer reader = ReaderType::New();
|
||||||
|
reader->SetFileName(dcmDoseFile);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
reader->Update();
|
||||||
|
dose = reader->GetOutput();
|
||||||
|
}
|
||||||
|
catch (itk::ExceptionObject& err)
|
||||||
|
{
|
||||||
|
std::cout << "Error: " << err.GetDescription() << " in itkImageLocationFunction" << std::endl;;
|
||||||
|
return ERR_ER_LOAD_DOSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the ratio by gdcm
|
||||||
|
itk::GDCMImageIOFactory::RegisterOneFactory();
|
||||||
|
gdcm::Reader RTreader;
|
||||||
|
RTreader.SetFileName(dcmDoseFile.c_str());
|
||||||
|
|
||||||
|
if (!RTreader.Read())
|
||||||
|
{
|
||||||
|
std::cout << "Problem reading file: " << dcmDoseFile << std::endl;
|
||||||
|
return ERR_ER_LOAD_DOSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const gdcm::FileMetaInformation &h = RTreader.GetFile().GetHeader();
|
||||||
|
const gdcm::DataSet& ds = RTreader.GetFile().GetDataSet();
|
||||||
|
std::cout << "Parsing: " << dcmDoseFile << std::endl;
|
||||||
|
gdcm::MediaStorage ms;
|
||||||
|
ms.SetFromFile(RTreader.GetFile());
|
||||||
|
std::cout << "media storage: " << ms << std::endl;
|
||||||
|
gdcm::Tag tdoseScale(0x3004, 0x000e);
|
||||||
|
|
||||||
|
if (!ds.FindDataElement(tdoseScale))
|
||||||
|
{
|
||||||
|
std::cout << "Problem locating 0x3004, 0x000e - Is this a valid RT Plan file?" << std::endl;
|
||||||
|
return ERR_LOAD_DOSE_RT;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gdcm::DataElement& doseScalede = ds.GetDataElement(tdoseScale);
|
||||||
|
|
||||||
|
// check the fraction beam number
|
||||||
|
if (doseScalede.IsEmpty())
|
||||||
|
{
|
||||||
|
std::cout << "Sorry! Plan do not contain dose grid scaling information!" << std::endl;
|
||||||
|
return ERR_LOAD_DOSE_RT;
|
||||||
|
}
|
||||||
|
|
||||||
|
gdcm::Attribute<0x3004, 0x000e> doseScaleat;
|
||||||
|
doseScaleat.SetFromDataElement(doseScalede);
|
||||||
|
float doseScale = doseScaleat.GetValues()[0];
|
||||||
|
// loop for each pixel
|
||||||
|
float* dosePtr = dose->GetBufferPointer();
|
||||||
|
itk::Image<float, 3>::SizeType sz = dose->GetLargestPossibleRegion().GetSize();
|
||||||
|
#pragma omp parallel for num_threads(15)
|
||||||
|
|
||||||
|
for (int i = 0; i < sz[0] * sz[1] * sz[2]; ++i)
|
||||||
|
dosePtr[i] *= doseScale;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//.........................................................................
|
||||||
|
// Get a itk 3-dim float IMAGE from a given DICOM series folder.
|
||||||
|
//.........................................................................
|
||||||
|
int itkDicomSeriesReader(
|
||||||
|
const std::string dicomFilesPath,
|
||||||
|
itk::Image<float, 3>::Pointer& image)
|
||||||
|
{
|
||||||
|
//typedef
|
||||||
|
typedef itk::Image<float, 3> ImageType;
|
||||||
|
// initializing
|
||||||
|
image = ImageType::New();
|
||||||
|
|
||||||
|
// read DICOM files
|
||||||
|
if (DBG_DISP)
|
||||||
|
std::cout << "Begin read DICOM SERIES..................." << std::endl;
|
||||||
|
|
||||||
|
// typedef
|
||||||
|
typedef itk::ImageSeriesReader<ImageType> ReaderType;
|
||||||
|
ReaderType::Pointer reader = ReaderType::New();
|
||||||
|
typedef itk::GDCMImageIO ImageIOType;
|
||||||
|
ImageIOType::Pointer dicomIO = ImageIOType::New();
|
||||||
|
reader->SetImageIO(dicomIO);
|
||||||
|
typedef itk::GDCMSeriesFileNames NameGeneratorType;
|
||||||
|
NameGeneratorType::Pointer nameGenerator = NameGeneratorType::New();
|
||||||
|
nameGenerator->SetUseSeriesDetails(true);
|
||||||
|
//nameGenerator->AddSeriesRestriction(dicomChar); // restriction
|
||||||
|
nameGenerator->SetDirectory(dicomFilesPath);
|
||||||
|
typedef std::vector<std::string> seriesIdContainer;
|
||||||
|
const seriesIdContainer& seriesUID = nameGenerator->GetSeriesUIDs();
|
||||||
|
|
||||||
|
// check file number
|
||||||
|
if (seriesUID.size() < 1)
|
||||||
|
{
|
||||||
|
std::cout << "No Image in folder!" << std::endl;
|
||||||
|
return ERR_ER_LOAD_IMGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
seriesIdContainer::const_iterator seriesItr = seriesUID.begin();
|
||||||
|
seriesIdContainer::const_iterator seriesEnd = seriesUID.end();
|
||||||
|
|
||||||
|
while (seriesItr != seriesEnd)
|
||||||
|
{
|
||||||
|
if (DBG_DISP)
|
||||||
|
std::cout << seriesItr->c_str() << std::endl;
|
||||||
|
|
||||||
|
++seriesItr;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef std::vector<std::string> FilesNamesContainer;
|
||||||
|
FilesNamesContainer filesNames;
|
||||||
|
filesNames = nameGenerator->GetFileNames(seriesUID.begin()->c_str());
|
||||||
|
reader->SetFileNames(filesNames);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
reader->Update();
|
||||||
|
}
|
||||||
|
catch (itk::ExceptionObject& ex)
|
||||||
|
{
|
||||||
|
std::cout << ex << std::endl;
|
||||||
|
return ERR_ER_LOAD_IMGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
image = reader->GetOutput();
|
||||||
|
|
||||||
|
if (DBG_DISP)
|
||||||
|
std::cout << "End read DICOM SERIES....................." << std::endl;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
//.........................................................................
|
||||||
|
// linear transform
|
||||||
|
//.........................................................................
|
||||||
|
static void linearTransform(
|
||||||
|
const double* rotMat,
|
||||||
|
const double* origin,
|
||||||
|
double* coord)
|
||||||
|
{
|
||||||
|
// homo coordinates.
|
||||||
|
double coordHomo[3];
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; ++i)
|
||||||
|
coordHomo[i] = coord[i] - origin[i];
|
||||||
|
|
||||||
|
// to new system
|
||||||
|
for (int i = 0; i < 3; ++i)
|
||||||
|
{
|
||||||
|
coord[i] = origin[i];
|
||||||
|
|
||||||
|
for (int j = 0; j < 3; ++j)
|
||||||
|
coord[i] += rotMat[i * 3 + j] * coordHomo[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//.........................................................................
|
||||||
|
// Get a set of itk 3-dim unsigned short masks
|
||||||
|
//.........................................................................
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
nameVec.clear();
|
||||||
|
maskVec.clear();
|
||||||
|
//typedef
|
||||||
|
typedef itk::Image<float, 3> ImageType;
|
||||||
|
typedef itk::Image<unsigned char, 3> MaskType;
|
||||||
|
// information of image
|
||||||
|
ImageType::SpacingType spacing3d = image->GetSpacing();
|
||||||
|
ImageType::PointType origin3d = image->GetOrigin();
|
||||||
|
ImageType::RegionType region3d = image->GetLargestPossibleRegion();
|
||||||
|
ImageType::SizeType size3d = region3d.GetSize();
|
||||||
|
ImageType::IndexType index3d = region3d.GetIndex();
|
||||||
|
ImageType::DirectionType direction = image->GetDirection();
|
||||||
|
double rotMat[9] { 0 };
|
||||||
|
double rotMatInv[9] { 0 };
|
||||||
|
double rotBase[3] { 0 };
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; ++i)
|
||||||
|
{
|
||||||
|
rotBase[i] = origin3d[i];
|
||||||
|
|
||||||
|
for (int j = 0; j < 3; ++j)
|
||||||
|
{
|
||||||
|
rotMat[i * 3 + j] = direction.GetVnlMatrix()[i][j];
|
||||||
|
rotMatInv[j * 3 + i] = rotMat[i * 3 + j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read rt file
|
||||||
|
int iPointsOutsideBoundary = 0;
|
||||||
|
itk::GDCMImageIOFactory::RegisterOneFactory();
|
||||||
|
gdcm::Reader RTreader;
|
||||||
|
RTreader.SetFileName(rtsFilePath.c_str());
|
||||||
|
|
||||||
|
if (!RTreader.Read())
|
||||||
|
{
|
||||||
|
std::cout << "Problem reading file: " << rtsFilePath << std::endl;
|
||||||
|
return ERR_LOAD_RTSTRUC;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const gdcm::FileMetaInformation &h = RTreader.GetFile().GetHeader();
|
||||||
|
const gdcm::DataSet& ds = RTreader.GetFile().GetDataSet();
|
||||||
|
std::cout << "Parsing: " << rtsFilePath << std::endl;
|
||||||
|
gdcm::MediaStorage ms;
|
||||||
|
ms.SetFromFile(RTreader.GetFile());
|
||||||
|
std::cout << "media storage: " << ms << std::endl;
|
||||||
|
// (3006,0020) SQ (Sequence with explicit length #=4) # 370, 1 StructureSetROISequence
|
||||||
|
gdcm::Tag tssroisq(0x3006, 0x0020);
|
||||||
|
|
||||||
|
if (!ds.FindDataElement(tssroisq))
|
||||||
|
{
|
||||||
|
std::cout << "Problem locating 0x3006,0x0020 - Is this a valid RT Structure file?" << std::endl;
|
||||||
|
return ERR_LOAD_RTSTRUC;
|
||||||
|
}
|
||||||
|
|
||||||
|
gdcm::Tag troicsq(0x3006, 0x0039);
|
||||||
|
|
||||||
|
if (!ds.FindDataElement(troicsq))
|
||||||
|
{
|
||||||
|
std::cout << "Problem locating 0x3006,0x0039 - Is this a valid RT Structure file?" << std::endl;
|
||||||
|
return ERR_LOAD_RTSTRUC;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gdcm::DataElement& roicsq = ds.GetDataElement(troicsq);
|
||||||
|
gdcm::SmartPointer<gdcm::SequenceOfItems> sqi = roicsq.GetValueAsSQ();
|
||||||
|
|
||||||
|
if (!sqi || !sqi->GetNumberOfItems())
|
||||||
|
{
|
||||||
|
return ERR_LOAD_RTSTRUC;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gdcm::DataElement& ssroisq = ds.GetDataElement(tssroisq);
|
||||||
|
gdcm::SmartPointer<gdcm::SequenceOfItems> ssqi = ssroisq.GetValueAsSQ();
|
||||||
|
|
||||||
|
if (!ssqi || !ssqi->GetNumberOfItems())
|
||||||
|
{
|
||||||
|
return ERR_LOAD_RTSTRUC;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Number of structures found:" << sqi->GetNumberOfItems() << std::endl;
|
||||||
|
//loop through structures
|
||||||
|
nameVec.reserve(sqi->GetNumberOfItems());
|
||||||
|
maskVec.reserve(sqi->GetNumberOfItems());
|
||||||
|
|
||||||
|
for (unsigned int pd = 0; pd < sqi->GetNumberOfItems(); ++pd)
|
||||||
|
{
|
||||||
|
// for vtkPoly contours
|
||||||
|
vtkSmartPointer<vtkPolyData> contours = vtkSmartPointer<vtkPolyData>::New();
|
||||||
|
const gdcm::Item& item = sqi->GetItem(pd + 1); // Item start at #1
|
||||||
|
gdcm::Attribute<0x3006, 0x0084> roinumber;
|
||||||
|
const gdcm::DataSet& nestedds = item.GetNestedDataSet();
|
||||||
|
roinumber.SetFromDataElement(nestedds.GetDataElement(roinumber.GetTag()));
|
||||||
|
// find structure_set_roi_sequence corresponding to roi_contour_sequence (by comparing id numbers)
|
||||||
|
unsigned int spd = 0;
|
||||||
|
gdcm::Item& sitem = ssqi->GetItem(spd + 1);
|
||||||
|
gdcm::DataSet& snestedds = sitem.GetNestedDataSet();
|
||||||
|
gdcm::Attribute<0x3006, 0x0022> sroinumber;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
sitem = ssqi->GetItem(spd + 1);
|
||||||
|
snestedds = sitem.GetNestedDataSet();
|
||||||
|
sroinumber.SetFromDataElement(snestedds.GetDataElement(sroinumber.GetTag()));
|
||||||
|
spd++;
|
||||||
|
}
|
||||||
|
while (sroinumber.GetValue() != roinumber.GetValue());
|
||||||
|
|
||||||
|
gdcm::Tag stcsq(0x3006, 0x0026);
|
||||||
|
|
||||||
|
if (!snestedds.FindDataElement(stcsq))
|
||||||
|
{
|
||||||
|
std::cout << "Did not find sttsq data el " << stcsq << " continuing..." << std::endl;
|
||||||
|
continue; //return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gdcm::DataElement& sde = snestedds.GetDataElement(stcsq);
|
||||||
|
//(3006,002a) IS [255\192\96] # 10,3 ROI Display Color
|
||||||
|
gdcm::Tag troidc(0x3006, 0x002a);
|
||||||
|
gdcm::Attribute<0x3006, 0x002a> color = {};
|
||||||
|
|
||||||
|
if (nestedds.FindDataElement(troidc))
|
||||||
|
{
|
||||||
|
const gdcm::DataElement& decolor = nestedds.GetDataElement(troidc);
|
||||||
|
color.SetFromDataElement(decolor);
|
||||||
|
}
|
||||||
|
|
||||||
|
//(3006,0040) SQ (Sequence with explicit length #=8) # 4326, 1 ContourSequence
|
||||||
|
gdcm::Tag tcsq(0x3006, 0x0040);
|
||||||
|
|
||||||
|
if (!nestedds.FindDataElement(tcsq))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gdcm::DataElement& csq = nestedds.GetDataElement(tcsq);
|
||||||
|
gdcm::SmartPointer<gdcm::SequenceOfItems> sqi2 = csq.GetValueAsSQ();
|
||||||
|
|
||||||
|
if (!sqi2 || !sqi2->GetNumberOfItems())
|
||||||
|
{
|
||||||
|
std::cout << "csq: " << csq << std::endl;
|
||||||
|
std::cout << "sqi2: " << *sqi2 << std::endl;
|
||||||
|
std::cout << "Did not find sqi2 or no. items == 0 " << sqi2->GetNumberOfItems() << " continuing..." << std::endl;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int nitems = sqi2->GetNumberOfItems();
|
||||||
|
std::cout << "Structure " << pd << ". Number of regions: " << nitems << std::endl;
|
||||||
|
std::string str_currentOrgan(sde.GetByteValue()->GetPointer(), sde.GetByteValue()->GetLength());
|
||||||
|
/*!trim to remove spaces in organ name which can cause problems in scripts eg. "CBCT01__BULK BONE .nii" .
|
||||||
|
Might need to have this as parameter? */
|
||||||
|
std::cout << pd << ". Structure name: " << str_currentOrgan << std::endl;
|
||||||
|
//now loop through each item for this structure (eg one prostate region on a single slice is an item)
|
||||||
|
vtkSmartPointer<vtkCellArray> cell = vtkSmartPointer<vtkCellArray>::New();
|
||||||
|
vtkSmartPointer<vtkPoints> points = vtkSmartPointer<vtkPoints>::New();
|
||||||
|
int pointId = 0;
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < nitems; ++i)
|
||||||
|
{
|
||||||
|
vtkSmartPointer<vtkPolyLine> polyLine = vtkSmartPointer<vtkPolyLine>::New();
|
||||||
|
const gdcm::Item& item2 = sqi2->GetItem(i + 1); // Item start at #1
|
||||||
|
const gdcm::DataSet& nestedds2 = item2.GetNestedDataSet();
|
||||||
|
// (3006,0050) DS [43.57636\65.52504\-10.0\46.043102\62.564945\-10.0\49.126537\60.714... # 398,48 ContourData
|
||||||
|
gdcm::Tag tcontourdata(0x3006, 0x0050);
|
||||||
|
const gdcm::DataElement& contourdata = nestedds2.GetDataElement(tcontourdata);
|
||||||
|
//const gdcm::ByteValue *bv = contourdata.GetByteValue();
|
||||||
|
gdcm::Attribute<0x3006, 0x0050> at;
|
||||||
|
at.SetFromDataElement(contourdata);
|
||||||
|
const double* pts = at.GetValues();
|
||||||
|
unsigned int npts = at.GetNumberOfValues() / 3;
|
||||||
|
|
||||||
|
for (unsigned int j = 0; j < npts * 3; j += 3)
|
||||||
|
{
|
||||||
|
double point[3];
|
||||||
|
point[0] = pts[j + 0];
|
||||||
|
point[1] = pts[j + 1];
|
||||||
|
point[2] = pts[j + 2];
|
||||||
|
// add by HSS for direction
|
||||||
|
linearTransform(rotMat, rotBase, point);
|
||||||
|
pointId = points->InsertNextPoint(point);
|
||||||
|
polyLine->GetPointIds()->InsertNextId(pointId);
|
||||||
|
}
|
||||||
|
|
||||||
|
double point[3];
|
||||||
|
point[0] = pts[0];
|
||||||
|
point[1] = pts[1];
|
||||||
|
point[2] = pts[2];
|
||||||
|
// add by HSS for direction
|
||||||
|
linearTransform(rotMat, rotBase, point);
|
||||||
|
pointId = points->InsertNextPoint(point);
|
||||||
|
polyLine->GetPointIds()->InsertNextId(pointId);
|
||||||
|
cell->InsertNextCell(polyLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (iPointsOutsideBoundary > 0)
|
||||||
|
{
|
||||||
|
std::cout << " --" << iPointsOutsideBoundary
|
||||||
|
<< " contour points detected outside image boundary. Please check the output volume. ";
|
||||||
|
iPointsOutsideBoundary = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
contours->SetPoints(points);
|
||||||
|
contours->SetLines(cell);
|
||||||
|
contours->Modified();
|
||||||
|
|
||||||
|
// convert the contours to itkImage
|
||||||
|
if (contours->GetNumberOfPoints() == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get bounds
|
||||||
|
double bounds[6];
|
||||||
|
contours->GetBounds(bounds);
|
||||||
|
// base on bounds get spacing origin and dimension for calculation
|
||||||
|
int Xmin = floor((bounds[0] - origin3d[0]) / spacing3d[0]) - 1;
|
||||||
|
int Xmax = floor((bounds[1] - origin3d[0]) / spacing3d[0]) + 2;
|
||||||
|
int Xextent = Xmax - Xmin;
|
||||||
|
double originX = Xmin * spacing3d[0] + origin3d[0];
|
||||||
|
int Ymin = floor((bounds[2] - origin3d[1]) / spacing3d[1]) - 1;
|
||||||
|
int Ymax = floor((bounds[3] - origin3d[1]) / spacing3d[1]) + 2;
|
||||||
|
int Yextent = Ymax - Ymin;
|
||||||
|
double originY = Ymin * spacing3d[1] + origin3d[1];
|
||||||
|
int Zmin = floor((bounds[4] - origin3d[2]) / spacing3d[2]) - 1;
|
||||||
|
int Zmax = floor((bounds[5] - origin3d[2]) / spacing3d[2]) + 2;
|
||||||
|
int Zextent = Zmax - Zmin;
|
||||||
|
double originZ = Zmin * spacing3d[2] + origin3d[2];
|
||||||
|
double spacing[3] = { spacing3d[0], spacing3d[1], spacing3d[2] };
|
||||||
|
double origin[3] = { originX, originY, originZ };
|
||||||
|
int dimension[3] = { Xextent + 1, Yextent + 1, Zextent + 1 };
|
||||||
|
// prepare the binary image's voxel grid
|
||||||
|
vtkSmartPointer<vtkImageData> whiteImage = vtkSmartPointer<vtkImageData>::New();
|
||||||
|
whiteImage->SetSpacing(spacing);
|
||||||
|
whiteImage->SetExtent(0, dimension[0] - 1, 0, dimension[1] - 1, 0, dimension[2] - 1);
|
||||||
|
whiteImage->SetOrigin(origin);
|
||||||
|
whiteImage->AllocateScalars(VTK_UNSIGNED_CHAR, 1);
|
||||||
|
//vtkIdType count = whiteImage->GetNumberOfPoints();
|
||||||
|
whiteImage->GetPointData()->GetScalars()->Fill(255);
|
||||||
|
// polygonal data --> image stencil:
|
||||||
|
vtkSmartPointer<vtkPolyDataToImageStencil> pol2stenc = vtkSmartPointer<vtkPolyDataToImageStencil>::New();
|
||||||
|
pol2stenc->SetTolerance(0); // important if extruder->SetVector(0, 0, 1) !!!
|
||||||
|
pol2stenc->SetInputData(contours);
|
||||||
|
pol2stenc->SetOutputOrigin(origin);
|
||||||
|
pol2stenc->SetOutputSpacing(spacing);
|
||||||
|
pol2stenc->SetOutputWholeExtent(whiteImage->GetExtent());
|
||||||
|
pol2stenc->Update();
|
||||||
|
// cut the corresponding white image and set the background:
|
||||||
|
vtkSmartPointer<vtkImageStencil> imgstenc = vtkSmartPointer<vtkImageStencil>::New();
|
||||||
|
imgstenc->SetInputData(whiteImage);
|
||||||
|
imgstenc->SetStencilConnection(pol2stenc->GetOutputPort());
|
||||||
|
imgstenc->ReverseStencilOff();
|
||||||
|
imgstenc->SetBackgroundValue(0);
|
||||||
|
imgstenc->Update();
|
||||||
|
vtkSmartPointer<vtkImageData> imageOrganVtk = imgstenc->GetOutput();
|
||||||
|
// to itk
|
||||||
|
typedef itk::VTKImageToImageFilter<MaskType> VTKImageToImageType;
|
||||||
|
VTKImageToImageType::Pointer vtkImageToImageFilter = VTKImageToImageType::New();
|
||||||
|
vtkImageToImageFilter->SetInput(imageOrganVtk);
|
||||||
|
vtkImageToImageFilter->Update();
|
||||||
|
// deep copy image
|
||||||
|
MaskType::Pointer imageOrgan = vtkImageToImageFilter->GetOutput();
|
||||||
|
MaskType::Pointer imageOrganCopy = MaskType::New();
|
||||||
|
// imageOrganCopy->SetOrigin(imageOrgan->GetOrigin()); origion must be rotated back
|
||||||
|
imageOrganCopy->SetRegions(imageOrgan->GetLargestPossibleRegion());
|
||||||
|
imageOrganCopy->SetSpacing(imageOrgan->GetSpacing());
|
||||||
|
// direction consider
|
||||||
|
double originRoi[3] { 0 };
|
||||||
|
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
originRoi[k] = imageOrgan->GetOrigin()[k];
|
||||||
|
|
||||||
|
linearTransform(rotMatInv, rotBase, originRoi);
|
||||||
|
ImageType::PointType originRoiItk;
|
||||||
|
|
||||||
|
for (int k = 0; k < 3; ++k)
|
||||||
|
originRoiItk[k] = originRoi[k];
|
||||||
|
|
||||||
|
imageOrganCopy->SetOrigin(originRoiItk);
|
||||||
|
imageOrganCopy->SetDirection(direction);
|
||||||
|
imageOrganCopy->Allocate();
|
||||||
|
// end add by HSS
|
||||||
|
itk::ImageRegionConstIterator<MaskType> inputIterator(imageOrgan, imageOrgan->GetLargestPossibleRegion());
|
||||||
|
itk::ImageRegionIterator<MaskType> outputIterator(imageOrganCopy, imageOrganCopy->GetLargestPossibleRegion());
|
||||||
|
|
||||||
|
while (!inputIterator.IsAtEnd())
|
||||||
|
{
|
||||||
|
outputIterator.Set(inputIterator.Get());
|
||||||
|
++inputIterator;
|
||||||
|
++outputIterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
//push back
|
||||||
|
maskVec.push_back(imageOrganCopy);
|
||||||
|
nameVec.push_back(str_currentOrgan);
|
||||||
|
} // next structure name
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifndef NOMINMAX
|
||||||
|
#define NOMINMAX
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "itkStlCompat.h"
|
||||||
|
#include "dvhTypes.h"
|
||||||
|
|
||||||
|
#define DBG_DISP false
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <iostream>
|
||||||
|
#include <fstream>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <itkImage.h>
|
||||||
|
#include <itkGDCMImageIO.h>
|
||||||
|
#include <itkGDCMImageIOFactory.h>
|
||||||
|
#include <itkGDCMSeriesFileNames.h>
|
||||||
|
#include <itkImageSeriesReader.h>
|
||||||
|
#include <itkImageFileReader.h>
|
||||||
|
#include <itkImageFileWriter.h>
|
||||||
|
#include <itkPoint.h>
|
||||||
|
#include <itkImageSliceIteratorWithIndex.h>
|
||||||
|
#include <itkImageIteratorWithIndex.h>
|
||||||
|
#include <itkExtractImageFilter.h>
|
||||||
|
#include <itkGroupSpatialObject.h>
|
||||||
|
#include <itkSpatialObjectToImageFilter.h>
|
||||||
|
#include <itkPolygonSpatialObject.h>
|
||||||
|
#include <itkImageRegionIterator.h>
|
||||||
|
#include <itkResampleImageFilter.h>
|
||||||
|
#include <itkNearestNeighborInterpolateImageFunction.h>
|
||||||
|
#include <itkVersorRigid3DTransform.h>
|
||||||
|
#include <itkLinearInterpolateImageFunction.h>
|
||||||
|
#include <itkMinimumMaximumImageCalculator.h>
|
||||||
|
#include <itkVTKImageToImageFilter.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 <gdcmTypes.h>
|
||||||
|
#include <gdcmReader.h>
|
||||||
|
#include <gdcmSmartPointer.h>
|
||||||
|
#include <gdcmAttribute.h>
|
||||||
|
#include <gdcmSequenceOfItems.h>
|
||||||
|
#include <gdcmFileMetaInformation.h>
|
||||||
|
#include <gdcmMediaStorage.h>
|
||||||
|
|
||||||
|
int itkMetaDataReader(
|
||||||
|
const std::string metaDataFile,
|
||||||
|
itk::Image<float, 3>::Pointer& image);
|
||||||
|
|
||||||
|
int itkMetaMaskReader(
|
||||||
|
const std::string maskDataFile,
|
||||||
|
itk::Image<unsigned char, 3>::Pointer& mask);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#define DVH_MAX_SPN 2000
|
||||||
|
|
||||||
|
#define ERR_ER_LOAD_IMGE -10004
|
||||||
|
#define ERR_ER_LOAD_DOSE -10005
|
||||||
|
#define ERR_LOAD_DOSE_RT -10006
|
||||||
|
#define ERR_LOAD_RTSTRUC -10007
|
||||||
|
|
||||||
|
struct Struc {
|
||||||
|
std::string strucName;
|
||||||
|
int datSz = 0;
|
||||||
|
int innSz = 0;
|
||||||
|
int size[3] = {0, 0, 0};
|
||||||
|
float origin[3] = {0, 0, 0};
|
||||||
|
float spacing[3] = {0, 0, 0};
|
||||||
|
unsigned char* h_dat = nullptr;
|
||||||
|
unsigned char* d_dat = nullptr;
|
||||||
|
float* h_innDat = nullptr;
|
||||||
|
float* d_innDat = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StrucSet {
|
||||||
|
std::string tagName;
|
||||||
|
int strucNum = 0;
|
||||||
|
Struc* struc = nullptr;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// ITK 4.13 still uses std::unary_function / binary_function removed from modern MSVC STL.
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#ifndef _DOSECOMPARE_ITK_STL_COMPAT_
|
||||||
|
#define _DOSECOMPARE_ITK_STL_COMPAT_
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
namespace std {
|
||||||
|
#if !defined(_HAS_AUTO_PTR_ETC) || !_HAS_AUTO_PTR_ETC
|
||||||
|
template <class Arg, class Result>
|
||||||
|
struct unary_function {
|
||||||
|
typedef Arg argument_type;
|
||||||
|
typedef Result result_type;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class Arg1, class Arg2, class Result>
|
||||||
|
struct binary_function {
|
||||||
|
typedef Arg1 first_argument_type;
|
||||||
|
typedef Arg2 second_argument_type;
|
||||||
|
typedef Result result_type;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
} // namespace std
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#include "MainWindow.h"
|
#include "AppShell.h"
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QSurfaceFormat>
|
#include <QSurfaceFormat>
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
#include <QVTKOpenGLWidget.h>
|
#include <QVTKOpenGLWidget.h>
|
||||||
|
|
||||||
|
#include "ProfileComparePage.h"
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
QSurfaceFormat::setDefaultFormat(QVTKOpenGLWidget::defaultFormat());
|
QSurfaceFormat::setDefaultFormat(QVTKOpenGLWidget::defaultFormat());
|
||||||
|
|
@ -16,13 +18,14 @@ int main(int argc, char* argv[])
|
||||||
QApplication::setOrganizationName(QStringLiteral("Manteia"));
|
QApplication::setOrganizationName(QStringLiteral("Manteia"));
|
||||||
QApplication::setWindowIcon(QIcon(QStringLiteral(":/dosecompare.png")));
|
QApplication::setWindowIcon(QIcon(QStringLiteral(":/dosecompare.png")));
|
||||||
|
|
||||||
MainWindow w;
|
AppShell w;
|
||||||
w.setWindowIcon(QIcon(QStringLiteral(":/dosecompare.png")));
|
w.setWindowIcon(QIcon(QStringLiteral(":/dosecompare.png")));
|
||||||
w.show();
|
w.show();
|
||||||
|
|
||||||
if (argc > 1) {
|
if (argc > 1) {
|
||||||
|
w.openProfileApp();
|
||||||
QString err;
|
QString err;
|
||||||
if (!w.loadDoseFile(QString::fromLocal8Bit(argv[1]), &err))
|
if (!w.profilePage()->loadDoseFile(QString::fromLocal8Bit(argv[1]), &err))
|
||||||
QMessageBox::warning(&w, QObject::tr("导入失败"), err);
|
QMessageBox::warning(&w, QObject::tr("导入失败"), err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue