diff --git a/CMakeLists.txt b/CMakeLists.txt index 06c2fbf..2f5be19 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 $/algo + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/algo/calcDVHLibrary.dll + $/algo/calcDVHLibrary.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CMAKE_SOURCE_DIR}/algo/calcDVHLibraryD.dll + $/algo/calcDVHLibraryD.dll +) diff --git a/algo/calcDVHLibrary.dll b/algo/calcDVHLibrary.dll new file mode 100644 index 0000000..64b8536 Binary files /dev/null and b/algo/calcDVHLibrary.dll differ diff --git a/algo/calcDVHLibraryD.dll b/algo/calcDVHLibraryD.dll new file mode 100644 index 0000000..ee1e7aa Binary files /dev/null and b/algo/calcDVHLibraryD.dll differ diff --git a/resources/app-dvh.png b/resources/app-dvh.png new file mode 100644 index 0000000..0302c66 Binary files /dev/null and b/resources/app-dvh.png differ diff --git a/resources/app-profile.png b/resources/app-profile.png new file mode 100644 index 0000000..84ac458 Binary files /dev/null and b/resources/app-profile.png differ diff --git a/resources/resources.qrc b/resources/resources.qrc index 75ee518..1afee1b 100644 --- a/resources/resources.qrc +++ b/resources/resources.qrc @@ -3,5 +3,7 @@ dosecompare.png dosecompare.ico + app-profile.png + app-dvh.png diff --git a/src/AppShell.cpp b/src/AppShell.cpp new file mode 100644 index 0000000..eda2d18 --- /dev/null +++ b/src/AppShell.cpp @@ -0,0 +1,86 @@ +#include "AppShell.h" +#include "LauncherHomeWidget.h" +#include "ProfileComparePage.h" +#include "dvh/DvhComparePage.h" + +#include +#include +#include +#include + +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")); +} diff --git a/src/AppShell.h b/src/AppShell.h new file mode 100644 index 0000000..b432fa9 --- /dev/null +++ b/src/AppShell.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +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; +}; diff --git a/src/LauncherHomeWidget.cpp b/src/LauncherHomeWidget.cpp new file mode 100644 index 0000000..b377850 --- /dev/null +++ b/src/LauncherHomeWidget.cpp @@ -0,0 +1,159 @@ +#include "LauncherHomeWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(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); +} diff --git a/src/LauncherHomeWidget.h b/src/LauncherHomeWidget.h new file mode 100644 index 0000000..24909a4 --- /dev/null +++ b/src/LauncherHomeWidget.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +class LauncherHomeWidget : public QWidget { + Q_OBJECT +public: + explicit LauncherHomeWidget(QWidget* parent = nullptr); + +signals: + void openProfileRequested(); + void openDvhRequested(); +}; diff --git a/src/MainWindow.cpp b/src/ProfileComparePage.cpp similarity index 81% rename from src/MainWindow.cpp rename to src/ProfileComparePage.cpp index 5e02317..779d6f5 100644 --- a/src/MainWindow.cpp +++ b/src/ProfileComparePage.cpp @@ -1,4 +1,4 @@ -#include "MainWindow.h" +#include "ProfileComparePage.h" #include "ImageManager.h" #include "ImageListPanel.h" #include "SliceViewWidget.h" @@ -10,14 +10,12 @@ #include #include #include -#include #include #include #include #include #include -#include -#include +#include #include #include #include @@ -30,29 +28,34 @@ #include #include -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::of(&QComboBox::currentIndexChanged), - this, &MainWindow::onBackgroundChanged); + this, &ProfileComparePage::onBackgroundChanged); connect(m_frontCombo, QOverload::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::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 alongX, alongY, alongZ; auto bg = bgDose(); diff --git a/src/MainWindow.h b/src/ProfileComparePage.h similarity index 91% rename from src/MainWindow.h rename to src/ProfileComparePage.h index 3ab9198..63d335a 100644 --- a/src/MainWindow.h +++ b/src/ProfileComparePage.h @@ -1,9 +1,9 @@ -#pragma once +#pragma once #include "types.h" #include "DoseImage.h" -#include +#include #include 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; }; diff --git a/src/dvh/DvhComparePage.cpp b/src/dvh/DvhComparePage.cpp new file mode 100644 index 0000000..e905d2d --- /dev/null +++ b/src/dvh/DvhComparePage.cpp @@ -0,0 +1,560 @@ +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include "DvhComparePage.h" +#include "DvhPlotWidget.h" +#include "dcmIO.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace { + +struct Rgb { + int r, g, b; +}; + +static QVector 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> 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(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::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::Pointer> doseGroup; + for (const auto& d : doses) { + itk::Image::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 nameVec; + std::vector::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(maskVec.size()))); + m_progress->setValue(7); + appendLog(tr("正在计算 DVH…")); + + const int doseGroupSz = static_cast(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(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(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(maskVec[sid]->GetOrigin()[i]); + struc->spacing[i] = static_cast(maskVec[sid]->GetSpacing()[i]); + struc->size[i] = static_cast(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(doseGroup[gid]->GetOrigin()[i]); + dsSpacing[i] = static_cast(doseGroup[gid]->GetSpacing()[i]); + dsSize[i] = static_cast(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 curves; + if (!m_strucSet || m_doseNum <= 0) { + m_plot->setCurves(curves); + return; + } + + const auto palette = dvhPalette(); + float maxDose = 0.f; + QVector 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(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); +} diff --git a/src/dvh/DvhComparePage.h b/src/dvh/DvhComparePage.h new file mode 100644 index 0000000..7acb2e3 --- /dev/null +++ b/src/dvh/DvhComparePage.h @@ -0,0 +1,63 @@ +#pragma once + +#include "dvhTypes.h" +#include "itkStlCompat.h" + +#include +#include +#include +#include + +#include + +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 m_roiChecks; + + int m_doseNum = 0; + StrucSet* m_strucSet = nullptr; + std::vector::Pointer> m_keptDoses; + std::vector::Pointer> m_keptMasks; +}; diff --git a/src/dvh/DvhPlotWidget.cpp b/src/dvh/DvhPlotWidget.cpp new file mode 100644 index 0000000..9e1f6ba --- /dev/null +++ b/src/dvh/DvhPlotWidget.cpp @@ -0,0 +1,106 @@ +#include "DvhPlotWidget.h" + +#include +#include +#include +#include +#include +#include + +DvhPlotWidget::DvhPlotWidget(QWidget* parent) + : QWidget(parent) +{ + setMinimumSize(400, 300); + setAutoFillBackground(true); +} + +void DvhPlotWidget::setCurves(const QVector& 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; + } +} diff --git a/src/dvh/DvhPlotWidget.h b/src/dvh/DvhPlotWidget.h new file mode 100644 index 0000000..0114766 --- /dev/null +++ b/src/dvh/DvhPlotWidget.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include + +struct DvhCurve { + QString name; + QColor color; + Qt::PenStyle style = Qt::SolidLine; + QVector 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& curves); + void setXMax(double xMax); + +protected: + void paintEvent(QPaintEvent* event) override; + +private: + QVector m_curves; + double m_xMax = 1.0; +}; diff --git a/src/dvh/dcmIO.cpp b/src/dvh/dcmIO.cpp new file mode 100644 index 0000000..fc146d6 --- /dev/null +++ b/src/dvh/dcmIO.cpp @@ -0,0 +1,529 @@ +#include "dcmIO.h" +#include +#include + +//......................................................................... +// Get a itk 3-dim float IMAGE from a given META data file +//......................................................................... +int itkMetaDataReader( + const std::string metaDataFile, + itk::Image::Pointer& image) +{ + itk::GDCMImageIOFactory::RegisterOneFactory(); + typedef itk::ImageFileReader> 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::Pointer& mask) +{ + itk::GDCMImageIOFactory::RegisterOneFactory(); + typedef itk::ImageFileReader> 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::Pointer& dose) +{ + itk::GDCMImageIOFactory::RegisterOneFactory(); + typedef itk::ImageFileReader> 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::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::Pointer& image) +{ + //typedef + typedef itk::Image ImageType; + // initializing + image = ImageType::New(); + + // read DICOM files + if (DBG_DISP) + std::cout << "Begin read DICOM SERIES..................." << std::endl; + + // typedef + typedef itk::ImageSeriesReader 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 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 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::Pointer image, + std::vector& nameVec, + std::vector::Pointer>& maskVec) +{ + nameVec.clear(); + maskVec.clear(); + //typedef + typedef itk::Image ImageType; + typedef itk::Image 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 sqi = roicsq.GetValueAsSQ(); + + if (!sqi || !sqi->GetNumberOfItems()) + { + return ERR_LOAD_RTSTRUC; + } + + const gdcm::DataElement& ssroisq = ds.GetDataElement(tssroisq); + gdcm::SmartPointer 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 contours = vtkSmartPointer::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 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 cell = vtkSmartPointer::New(); + vtkSmartPointer points = vtkSmartPointer::New(); + int pointId = 0; + + for (unsigned int i = 0; i < nitems; ++i) + { + vtkSmartPointer polyLine = vtkSmartPointer::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 whiteImage = vtkSmartPointer::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 pol2stenc = vtkSmartPointer::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 imgstenc = vtkSmartPointer::New(); + imgstenc->SetInputData(whiteImage); + imgstenc->SetStencilConnection(pol2stenc->GetOutputPort()); + imgstenc->ReverseStencilOff(); + imgstenc->SetBackgroundValue(0); + imgstenc->Update(); + vtkSmartPointer imageOrganVtk = imgstenc->GetOutput(); + // to itk + typedef itk::VTKImageToImageFilter 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 inputIterator(imageOrgan, imageOrgan->GetLargestPossibleRegion()); + itk::ImageRegionIterator 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; +} + + diff --git a/src/dvh/dcmIO.h b/src/dvh/dcmIO.h new file mode 100644 index 0000000..35d5d0a --- /dev/null +++ b/src/dvh/dcmIO.h @@ -0,0 +1,80 @@ +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include "itkStlCompat.h" +#include "dvhTypes.h" + +#define DBG_DISP false + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +int itkMetaDataReader( + const std::string metaDataFile, + itk::Image::Pointer& image); + +int itkMetaMaskReader( + const std::string maskDataFile, + itk::Image::Pointer& mask); + +int itkDcmDoseReader( + const std::string dcmDoseFile, + itk::Image::Pointer& dose); + +int itkDicomSeriesReader( + const std::string dicomFilesPath, + itk::Image::Pointer& image); + +int itkRtStructureReader( + std::string rtsFilePath, + itk::Image::Pointer image, + std::vector& nameVec, + std::vector::Pointer>& maskVec); diff --git a/src/dvh/dvhTypes.h b/src/dvh/dvhTypes.h new file mode 100644 index 0000000..41c4a73 --- /dev/null +++ b/src/dvh/dvhTypes.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#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; +}; diff --git a/src/dvh/itkStlCompat.h b/src/dvh/itkStlCompat.h new file mode 100644 index 0000000..170c9b6 --- /dev/null +++ b/src/dvh/itkStlCompat.h @@ -0,0 +1,26 @@ +#pragma once + +// ITK 4.13 still uses std::unary_function / binary_function removed from modern MSVC STL. +#include + +#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 +struct unary_function { + typedef Arg argument_type; + typedef Result result_type; +}; + +template +struct binary_function { + typedef Arg1 first_argument_type; + typedef Arg2 second_argument_type; + typedef Result result_type; +}; +#endif +} // namespace std +#endif +#endif diff --git a/src/main.cpp b/src/main.cpp index 157f69c..3877d5b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,4 @@ -#include "MainWindow.h" +#include "AppShell.h" #include #include @@ -7,6 +7,8 @@ #include +#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); }