Feat: 增加dvh对比功能, 以主界面 app选择形式.

master
hssheng 2026-07-20 14:46:41 +08:00
parent f3543da90f
commit c54de6fe64
21 changed files with 1826 additions and 75 deletions

View File

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

BIN
algo/calcDVHLibrary.dll Normal file

Binary file not shown.

BIN
algo/calcDVHLibraryD.dll Normal file

Binary file not shown.

BIN
resources/app-dvh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 KiB

BIN
resources/app-profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 KiB

View File

@ -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>

86
src/AppShell.cpp Normal file
View File

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

32
src/AppShell.h Normal file
View File

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

159
src/LauncherHomeWidget.cpp Normal file
View File

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

13
src/LauncherHomeWidget.h Normal file
View File

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

View File

@ -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 之前根据“删除后”的逻辑准备 pendingremove 时 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();

View File

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

560
src/dvh/DvhComparePage.cpp Normal file
View File

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

63
src/dvh/DvhComparePage.h Normal file
View File

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

106
src/dvh/DvhPlotWidget.cpp Normal file
View File

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

30
src/dvh/DvhPlotWidget.h Normal file
View File

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

529
src/dvh/dcmIO.cpp Normal file
View File

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

80
src/dvh/dcmIO.h Normal file
View File

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

29
src/dvh/dvhTypes.h Normal file
View File

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

26
src/dvh/itkStlCompat.h Normal file
View File

@ -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

View File

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