Feat: 增加dvh对比功能, 以主界面 app选择形式.
parent
f3543da90f
commit
c54de6fe64
|
|
@ -30,23 +30,34 @@ endif()
|
|||
|
||||
set(SOURCES
|
||||
src/main.cpp
|
||||
src/MainWindow.cpp
|
||||
src/AppShell.cpp
|
||||
src/LauncherHomeWidget.cpp
|
||||
src/ProfileComparePage.cpp
|
||||
src/DoseImage.cpp
|
||||
src/ImageManager.cpp
|
||||
src/SliceViewWidget.cpp
|
||||
src/ProfilePlotWidget.cpp
|
||||
src/ImageListPanel.cpp
|
||||
src/dvh/DvhComparePage.cpp
|
||||
src/dvh/DvhPlotWidget.cpp
|
||||
src/dvh/dcmIO.cpp
|
||||
resources/resources.qrc
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
src/types.h
|
||||
src/MainWindow.h
|
||||
src/AppShell.h
|
||||
src/LauncherHomeWidget.h
|
||||
src/ProfileComparePage.h
|
||||
src/DoseImage.h
|
||||
src/ImageManager.h
|
||||
src/SliceViewWidget.h
|
||||
src/ProfilePlotWidget.h
|
||||
src/ImageListPanel.h
|
||||
src/dvh/DvhComparePage.h
|
||||
src/dvh/DvhPlotWidget.h
|
||||
src/dvh/dvhTypes.h
|
||||
src/dvh/dcmIO.h
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
|
|
@ -55,7 +66,10 @@ if(WIN32)
|
|||
endif()
|
||||
|
||||
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
|
||||
Qt5::Core Qt5::Gui Qt5::Widgets
|
||||
${ITK_LIBRARIES}
|
||||
|
|
@ -68,7 +82,18 @@ target_link_libraries(smoke_slice_test PRIVATE Qt5::Core Qt5::Gui ${ITK_LIBRARIE
|
|||
|
||||
if(MSVC)
|
||||
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_definitions(smoke_slice_test PRIVATE _CRT_SECURE_NO_WARNINGS)
|
||||
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="/">
|
||||
<file>dosecompare.png</file>
|
||||
<file>dosecompare.ico</file>
|
||||
<file>app-profile.png</file>
|
||||
<file>app-dvh.png</file>
|
||||
</qresource>
|
||||
</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 "ImageListPanel.h"
|
||||
#include "SliceViewWidget.h"
|
||||
|
|
@ -10,14 +10,12 @@
|
|||
#include <QSlider>
|
||||
#include <QLabel>
|
||||
#include <QToolBar>
|
||||
#include <QStatusBar>
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QAction>
|
||||
#include <QMenuBar>
|
||||
#include <QMenu>
|
||||
#include <QPushButton>
|
||||
#include <QSettings>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
|
|
@ -30,29 +28,34 @@
|
|||
#include <algorithm>
|
||||
#include <initializer_list>
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent)
|
||||
: QMainWindow(parent)
|
||||
ProfileComparePage::ProfileComparePage(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
m_manager = new ImageManager(this);
|
||||
buildUi();
|
||||
setAcceptDrops(true);
|
||||
|
||||
connect(m_manager, &ImageManager::imageListChanged, this, &MainWindow::refreshImageSelectors);
|
||||
connect(m_manager, &ImageManager::profileSelectionChanged, this, &MainWindow::refreshProfiles);
|
||||
|
||||
resize(1400, 900);
|
||||
setWindowTitle(tr("DoseCompare — 剂量三切面对比"));
|
||||
connect(m_manager, &ImageManager::imageListChanged, this, &ProfileComparePage::refreshImageSelectors);
|
||||
connect(m_manager, &ImageManager::profileSelectionChanged, this, &ProfileComparePage::refreshProfiles);
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
ProfileComparePage::~ProfileComparePage() = default;
|
||||
|
||||
void MainWindow::buildUi()
|
||||
void ProfileComparePage::buildUi()
|
||||
{
|
||||
auto* importAct = new QAction(tr("导入剂量..."), this);
|
||||
connect(importAct, &QAction::triggered, this, &MainWindow::onImportDose);
|
||||
menuBar()->addMenu(tr("文件"))->addAction(importAct);
|
||||
auto* pageLay = new QVBoxLayout(this);
|
||||
pageLay->setContentsMargins(0, 0, 0, 0);
|
||||
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));
|
||||
m_bgCombo = new QComboBox(this);
|
||||
m_bgCombo->setMinimumWidth(160);
|
||||
|
|
@ -70,7 +73,6 @@ void MainWindow::buildUi()
|
|||
m_opacityLabel = new QLabel(tr("50%"), this);
|
||||
tb->addWidget(m_opacityLabel);
|
||||
|
||||
tb->addSeparator();
|
||||
tb->addWidget(new QLabel(tr(" Colormap "), this));
|
||||
m_colorMapCombo = new QComboBox(this);
|
||||
m_colorMapCombo->blockSignals(true);
|
||||
|
|
@ -81,22 +83,24 @@ void MainWindow::buildUi()
|
|||
m_colorMapCombo->setMinimumWidth(110);
|
||||
m_colorMapCombo->blockSignals(false);
|
||||
tb->addWidget(m_colorMapCombo);
|
||||
tb->addStretch(1);
|
||||
pageLay->addWidget(topBar);
|
||||
|
||||
connect(m_bgCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &MainWindow::onBackgroundChanged);
|
||||
this, &ProfileComparePage::onBackgroundChanged);
|
||||
connect(m_frontCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &MainWindow::onFrontChanged);
|
||||
connect(m_opacitySlider, &QSlider::valueChanged, this, &MainWindow::onOpacityChanged);
|
||||
this, &ProfileComparePage::onFrontChanged);
|
||||
connect(m_opacitySlider, &QSlider::valueChanged, this, &ProfileComparePage::onOpacityChanged);
|
||||
connect(m_colorMapCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &MainWindow::onColorMapChanged);
|
||||
this, &ProfileComparePage::onColorMapChanged);
|
||||
|
||||
auto* central = new QWidget(this);
|
||||
setCentralWidget(central);
|
||||
auto* root = new QHBoxLayout(central);
|
||||
pageLay->addWidget(central, 1);
|
||||
|
||||
m_listPanel = new ImageListPanel(m_manager, this);
|
||||
m_listPanel->setFixedWidth(220);
|
||||
connect(m_listPanel, &ImageListPanel::requestDelete, this, &MainWindow::onDeleteImage);
|
||||
connect(m_listPanel, &ImageListPanel::requestDelete, this, &ProfileComparePage::onDeleteImage);
|
||||
root->addWidget(m_listPanel);
|
||||
|
||||
auto* right = new QVBoxLayout();
|
||||
|
|
@ -113,19 +117,19 @@ void MainWindow::buildUi()
|
|||
slider->setRange(0, 0);
|
||||
col->addWidget(slider);
|
||||
sliceRow->addLayout(col, 1);
|
||||
connect(view, &SliceViewWidget::cursorChanged, this, &MainWindow::onCursorChanged);
|
||||
connect(view, &SliceViewWidget::viewExtentChanged, this, &MainWindow::refreshProfiles);
|
||||
connect(view, &SliceViewWidget::zoomScaleChanged, this, &MainWindow::onZoomScaleChanged);
|
||||
connect(view, &SliceViewWidget::panWorldDelta, this, &MainWindow::onPanWorldDelta);
|
||||
connect(view, &SliceViewWidget::cursorChanged, this, &ProfileComparePage::onCursorChanged);
|
||||
connect(view, &SliceViewWidget::viewExtentChanged, this, &ProfileComparePage::refreshProfiles);
|
||||
connect(view, &SliceViewWidget::zoomScaleChanged, this, &ProfileComparePage::onZoomScaleChanged);
|
||||
connect(view, &SliceViewWidget::panWorldDelta, this, &ProfileComparePage::onPanWorldDelta);
|
||||
};
|
||||
addSliceCol(ViewAxis::Axial, m_axial, m_sliderAxial, tr("1 轴位 (Axial)"));
|
||||
addSliceCol(ViewAxis::Sagittal, m_sagittal, m_sliderSagittal, tr("2 矢状 (Sagittal)"));
|
||||
addSliceCol(ViewAxis::Coronal, m_coronal, m_sliderCoronal, tr("3 冠状 (Coronal)"));
|
||||
right->addLayout(sliceRow, 3);
|
||||
|
||||
connect(m_sliderAxial, &QSlider::valueChanged, this, &MainWindow::onSliceSliderChanged);
|
||||
connect(m_sliderSagittal, &QSlider::valueChanged, this, &MainWindow::onSliceSliderChanged);
|
||||
connect(m_sliderCoronal, &QSlider::valueChanged, this, &MainWindow::onSliceSliderChanged);
|
||||
connect(m_sliderAxial, &QSlider::valueChanged, this, &ProfileComparePage::onSliceSliderChanged);
|
||||
connect(m_sliderSagittal, &QSlider::valueChanged, this, &ProfileComparePage::onSliceSliderChanged);
|
||||
connect(m_sliderCoronal, &QSlider::valueChanged, this, &ProfileComparePage::onSliceSliderChanged);
|
||||
|
||||
auto* profRow = new QHBoxLayout();
|
||||
m_profAxial = new ProfilePlotWidget(ViewAxis::Axial, this);
|
||||
|
|
@ -142,11 +146,13 @@ void MainWindow::buildUi()
|
|||
profRow->addWidget(m_profCoronal, 1);
|
||||
right->addLayout(profRow, 2);
|
||||
|
||||
statusBar()->showMessage(
|
||||
tr("可拖入 mhd/nii/dcm。Ctrl+在1/2/3上移动可重切另两面。"));
|
||||
m_statusLabel = new QLabel(
|
||||
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"));
|
||||
const QString last = settings.value(QStringLiteral("lastOpenDir")).toString();
|
||||
|
|
@ -155,7 +161,7 @@ QString MainWindow::defaultOpenDir() const
|
|||
return QCoreApplication::applicationDirPath();
|
||||
}
|
||||
|
||||
void MainWindow::rememberOpenPath(const QString& path)
|
||||
void ProfileComparePage::rememberOpenPath(const QString& path)
|
||||
{
|
||||
const QString dir = QFileInfo(path).absolutePath();
|
||||
if (dir.isEmpty())
|
||||
|
|
@ -164,7 +170,7 @@ void MainWindow::rememberOpenPath(const QString& path)
|
|||
settings.setValue(QStringLiteral("lastOpenDir"), dir);
|
||||
}
|
||||
|
||||
bool MainWindow::isSupportedDosePath(const QString& path) const
|
||||
bool ProfileComparePage::isSupportedDosePath(const QString& path) const
|
||||
{
|
||||
const QString lower = path.toLower();
|
||||
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"));
|
||||
}
|
||||
|
||||
void MainWindow::onImportDose()
|
||||
void ProfileComparePage::onImportDose()
|
||||
{
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this, tr("导入剂量图像"), defaultOpenDir(),
|
||||
|
|
@ -185,7 +191,7 @@ void MainWindow::onImportDose()
|
|||
QMessageBox::warning(this, tr("导入失败"), err.isEmpty() ? tr("未知错误") : err);
|
||||
}
|
||||
|
||||
void MainWindow::dragEnterEvent(QDragEnterEvent* event)
|
||||
void ProfileComparePage::dragEnterEvent(QDragEnterEvent* event)
|
||||
{
|
||||
if (!event->mimeData() || !event->mimeData()->hasUrls()) {
|
||||
event->ignore();
|
||||
|
|
@ -200,7 +206,7 @@ void MainWindow::dragEnterEvent(QDragEnterEvent* event)
|
|||
event->ignore();
|
||||
}
|
||||
|
||||
void MainWindow::dropEvent(QDropEvent* event)
|
||||
void ProfileComparePage::dropEvent(QDropEvent* event)
|
||||
{
|
||||
if (!event->mimeData() || !event->mimeData()->hasUrls()) {
|
||||
event->ignore();
|
||||
|
|
@ -229,7 +235,7 @@ void MainWindow::dropEvent(QDropEvent* event)
|
|||
event->ignore();
|
||||
}
|
||||
|
||||
void MainWindow::applyBgFgAfterLoad(const QString& newId, int countBefore)
|
||||
void ProfileComparePage::applyBgFgAfterLoad(const QString& newId, int countBefore)
|
||||
{
|
||||
m_pendingBgId.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)
|
||||
{
|
||||
// 在 remove 之前根据“删除后”的逻辑准备 pending;remove 时 list 仍含 deleted
|
||||
|
|
@ -287,7 +293,7 @@ void MainWindow::promoteBgFgAfterDelete(const QString& deletedId, const QString&
|
|||
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();
|
||||
QString err;
|
||||
|
|
@ -312,11 +318,12 @@ bool MainWindow::loadDoseFile(const QString& path, QString* error)
|
|||
refreshImageSelectors();
|
||||
refreshViews();
|
||||
refreshProfiles();
|
||||
statusBar()->showMessage(tr("已加载: %1").arg(path), 5000);
|
||||
if (m_statusLabel)
|
||||
m_statusLabel->setText(tr("已加载: %1").arg(path));
|
||||
return true;
|
||||
}
|
||||
|
||||
void MainWindow::onDeleteImage(const QString& id)
|
||||
void ProfileComparePage::onDeleteImage(const QString& id)
|
||||
{
|
||||
const QString oldBg = currentBgId();
|
||||
const QString oldFg = currentFgId();
|
||||
|
|
@ -327,22 +334,22 @@ void MainWindow::onDeleteImage(const QString& id)
|
|||
refreshProfiles();
|
||||
}
|
||||
|
||||
QString MainWindow::currentBgId() const
|
||||
QString ProfileComparePage::currentBgId() const
|
||||
{
|
||||
return m_bgCombo->currentData().toString();
|
||||
}
|
||||
|
||||
QString MainWindow::currentFgId() const
|
||||
QString ProfileComparePage::currentFgId() const
|
||||
{
|
||||
return m_frontCombo->currentData().toString();
|
||||
}
|
||||
|
||||
DoseImage::Pointer MainWindow::bgDose() const
|
||||
DoseImage::Pointer ProfileComparePage::bgDose() const
|
||||
{
|
||||
return m_manager->imageById(currentBgId());
|
||||
}
|
||||
|
||||
DoseImage::Pointer MainWindow::fgDose() const
|
||||
DoseImage::Pointer ProfileComparePage::fgDose() const
|
||||
{
|
||||
const QString id = currentFgId();
|
||||
if (id.isEmpty() || id == currentBgId())
|
||||
|
|
@ -350,7 +357,7 @@ DoseImage::Pointer MainWindow::fgDose() const
|
|||
return m_manager->imageById(id);
|
||||
}
|
||||
|
||||
void MainWindow::refreshImageSelectors()
|
||||
void ProfileComparePage::refreshImageSelectors()
|
||||
{
|
||||
QString oldBg = currentBgId();
|
||||
QString oldFg = currentFgId();
|
||||
|
|
@ -400,19 +407,19 @@ void MainWindow::refreshImageSelectors()
|
|||
m_frontCombo->blockSignals(false);
|
||||
}
|
||||
|
||||
void MainWindow::onFrontChanged(int)
|
||||
void ProfileComparePage::onFrontChanged(int)
|
||||
{
|
||||
refreshViews();
|
||||
}
|
||||
|
||||
void MainWindow::onBackgroundChanged(int)
|
||||
void ProfileComparePage::onBackgroundChanged(int)
|
||||
{
|
||||
refreshViews();
|
||||
syncSliceSliders();
|
||||
refreshProfiles();
|
||||
}
|
||||
|
||||
void MainWindow::onOpacityChanged(int value)
|
||||
void ProfileComparePage::onOpacityChanged(int value)
|
||||
{
|
||||
m_opacityLabel->setText(QStringLiteral("%1%").arg(value));
|
||||
const double op = value / 100.0;
|
||||
|
|
@ -421,7 +428,7 @@ void MainWindow::onOpacityChanged(int value)
|
|||
m_coronal->setOpacity(op);
|
||||
}
|
||||
|
||||
void MainWindow::onColorMapChanged(int)
|
||||
void ProfileComparePage::onColorMapChanged(int)
|
||||
{
|
||||
if (!m_colorMapCombo || !m_axial)
|
||||
return;
|
||||
|
|
@ -431,7 +438,7 @@ void MainWindow::onColorMapChanged(int)
|
|||
m_coronal->setColorMap(map);
|
||||
}
|
||||
|
||||
void MainWindow::refreshViews()
|
||||
void ProfileComparePage::refreshViews()
|
||||
{
|
||||
auto bg = bgDose();
|
||||
auto fg = fgDose();
|
||||
|
|
@ -455,7 +462,7 @@ void MainWindow::refreshViews()
|
|||
syncSliceSliders();
|
||||
}
|
||||
|
||||
void MainWindow::syncSharedZoomFromViews()
|
||||
void ProfileComparePage::syncSharedZoomFromViews()
|
||||
{
|
||||
if (!m_axial || !m_sagittal || !m_coronal)
|
||||
return;
|
||||
|
|
@ -468,7 +475,7 @@ void MainWindow::syncSharedZoomFromViews()
|
|||
m_blockViewSync = false;
|
||||
}
|
||||
|
||||
void MainWindow::onZoomScaleChanged(double parallelScale)
|
||||
void ProfileComparePage::onZoomScaleChanged(double parallelScale)
|
||||
{
|
||||
if (m_blockViewSync)
|
||||
return;
|
||||
|
|
@ -480,7 +487,7 @@ void MainWindow::onZoomScaleChanged(double parallelScale)
|
|||
m_blockViewSync = false;
|
||||
}
|
||||
|
||||
void MainWindow::onPanWorldDelta(double dx, double dy, double dz)
|
||||
void ProfileComparePage::onPanWorldDelta(double dx, double dy, double dz)
|
||||
{
|
||||
if (m_blockViewSync)
|
||||
return;
|
||||
|
|
@ -495,7 +502,7 @@ void MainWindow::onPanWorldDelta(double dx, double dy, double dz)
|
|||
m_blockViewSync = false;
|
||||
}
|
||||
|
||||
void MainWindow::syncSliceSliders()
|
||||
void ProfileComparePage::syncSliceSliders()
|
||||
{
|
||||
auto bg = bgDose();
|
||||
if (!bg) {
|
||||
|
|
@ -536,7 +543,7 @@ void MainWindow::syncSliceSliders()
|
|||
Q_UNUSED(origin);
|
||||
}
|
||||
|
||||
void MainWindow::onSliceSliderChanged(int)
|
||||
void ProfileComparePage::onSliceSliderChanged(int)
|
||||
{
|
||||
if (m_blockSliders)
|
||||
return;
|
||||
|
|
@ -563,12 +570,12 @@ void MainWindow::onSliceSliderChanged(int)
|
|||
Q_UNUSED(origin);
|
||||
}
|
||||
|
||||
void MainWindow::onCursorChanged(Vec3d world)
|
||||
void ProfileComparePage::onCursorChanged(Vec3d world)
|
||||
{
|
||||
setCursor(world, false);
|
||||
}
|
||||
|
||||
void MainWindow::setCursor(const Vec3d& world, bool fromSlider)
|
||||
void ProfileComparePage::setCursor(const Vec3d& world, bool fromSlider)
|
||||
{
|
||||
m_cursor = world;
|
||||
m_axial->setCursorWorld(m_cursor);
|
||||
|
|
@ -579,12 +586,12 @@ void MainWindow::setCursor(const Vec3d& world, bool fromSlider)
|
|||
refreshProfiles();
|
||||
}
|
||||
|
||||
void MainWindow::refreshProfiles()
|
||||
void ProfileComparePage::refreshProfiles()
|
||||
{
|
||||
extractProfiles();
|
||||
}
|
||||
|
||||
void MainWindow::extractProfiles()
|
||||
void ProfileComparePage::extractProfiles()
|
||||
{
|
||||
QVector<ProfileSeries> alongX, alongY, alongZ;
|
||||
auto bg = bgDose();
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include "types.h"
|
||||
#include "DoseImage.h"
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QWidget>
|
||||
#include <QString>
|
||||
|
||||
class ImageManager;
|
||||
|
|
@ -16,11 +16,11 @@ class QLabel;
|
|||
class QDragEnterEvent;
|
||||
class QDropEvent;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
class ProfileComparePage : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
~MainWindow() override;
|
||||
explicit ProfileComparePage(QWidget* parent = nullptr);
|
||||
~ProfileComparePage() override;
|
||||
|
||||
bool loadDoseFile(const QString& path, QString* error = nullptr);
|
||||
|
||||
|
|
@ -86,4 +86,5 @@ private:
|
|||
QString m_pendingFgId;
|
||||
QString m_pendingBgId;
|
||||
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 <QSurfaceFormat>
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
|
||||
#include <QVTKOpenGLWidget.h>
|
||||
|
||||
#include "ProfileComparePage.h"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
QSurfaceFormat::setDefaultFormat(QVTKOpenGLWidget::defaultFormat());
|
||||
|
|
@ -16,13 +18,14 @@ int main(int argc, char* argv[])
|
|||
QApplication::setOrganizationName(QStringLiteral("Manteia"));
|
||||
QApplication::setWindowIcon(QIcon(QStringLiteral(":/dosecompare.png")));
|
||||
|
||||
MainWindow w;
|
||||
AppShell w;
|
||||
w.setWindowIcon(QIcon(QStringLiteral(":/dosecompare.png")));
|
||||
w.show();
|
||||
|
||||
if (argc > 1) {
|
||||
w.openProfileApp();
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue