diff --git a/CMakeLists.txt b/CMakeLists.txt index 87501e673..9d234230e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ SET( ${PROJECT_NAME}_Variant "Full" ) # the particular variant of CaPTk (Full/Ne SET( PROJECT_VERSION_MAJOR 1 ) SET( PROJECT_VERSION_MINOR 9 ) -SET( PROJECT_VERSION_PATCH 0 ) +SET( PROJECT_VERSION_PATCH 0.ACRExperimental ) SET( PROJECT_VERSION_TWEAK ) # check for the string "nonRelease" in the PROJECT_VERSION_PATCH variable diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f6b60f83a..8e9a4bbc6 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -4,17 +4,17 @@ schedules: displayName: Daily midnight build branches: include: - - master + - acr-mode-experimental trigger: batch: true branches: - include: ['master'] + include: ['acr-mode-experimental'] # 20200312: apparently this is now explicitly required for azure pr: branches: - include: ['master'] + include: ['acr-mode-experimental'] jobs: - job: 'WindowsBuild' diff --git a/src/CaPTk.cxx b/src/CaPTk.cxx index 7d4e42428..adc5c816b 100644 --- a/src/CaPTk.cxx +++ b/src/CaPTk.cxx @@ -56,6 +56,7 @@ int main(int argc, char** argv) std::string cmd_inputs, cmd_mask, cmd_tumor, cmd_tissue; float cmd_maskOpacity = 1; bool comparisonMode = false; + bool loadOnStartupWithStudyBrowser = false; // this is used to populate the available CWL files for the cli auto cwlFolderPath = @@ -86,6 +87,10 @@ int main(int argc, char** argv) parser.addOptionalParameter("ts", "tissuePt", cbica::Parameter::FILE, ".txt", "Tissue Point file for the image(s) being loaded"); parser.addOptionalParameter("a", "advanced", cbica::Parameter::BOOLEAN, "none", "Advanced visualizer which does *not* consider", "origin information during loading"); parser.addOptionalParameter("c", "comparisonMode", cbica::Parameter::BOOLEAN, "true or false", "Enable/Disable comparison mode", "comparison mode during loading"); + + // ACR collaboration options, although possibly usable in other contexts + parser.addOptionalParameter("acr", "acrExperimentalMode", cbica::Parameter::BOOLEAN, "none", "Enable the ACR collaboration mode, with fewer features and a modified GUI"); + parser.addOptionalParameter("bs", "browseStudy", cbica::Parameter::BOOLEAN, "none", "Open CaPTk with an experimental study-browser for previewing image information -- requires NIFTIs converted from DICOM and JSON data"); //parser.exampleUsage("-i C:/data/input1.nii.gz,C:/data/input2.nii.gz -m C:/data/inputMask.nii.gz -tu C:/data/init_seed.txt -ts C:/data/init_GLISTR.txt"); parser.addExampleUsage("-i C:/data/input1.nii.gz,C:/data/input2.nii.gz -m C:/data/inputMask.nii.gz -tu C:/data/init_seed.txt -ts C:/data/init_GLISTR.txt", @@ -169,6 +174,10 @@ int main(int argc, char** argv) { parser.getParameterValue("c", comparisonMode); } + if (parser.isPresent("bs")) + { + loadOnStartupWithStudyBrowser = true; + } #if defined(__linux__) //auto defaultFormat = QVTKOpenGLWidget::defaultFormat(); @@ -251,6 +260,10 @@ int main(int argc, char** argv) { window.EnableAdvancedVisualizer(); } + if (parser.isPresent("acr")) + { + window.EnableACRMode(); + } // get everything to QString std::vector< QString > inputFiles_QString; @@ -320,9 +333,10 @@ int main(int argc, char** argv) #endif } else - window.loadFromCommandLine(inputFiles_QString, comparisonMode, inputMask.toStdString(), - cmd_maskOpacity, cmd_tumor, cmd_tissue/*, true*/); // at this point, inputFiles_QString will have at least 1 value - + { + window.loadFromCommandLine(inputFiles_QString, comparisonMode, inputMask.toStdString(), + cmd_maskOpacity, cmd_tumor, cmd_tissue, loadOnStartupWithStudyBrowser /*, true*/); // at this point, inputFiles_QString will have at least 1 value + } //window.setFixedSize(QSize(2735, 1538)); // useful when doing video recording from SP4 and maintain 16x9 ratio diff --git a/src/applications/FeatureExtraction/src/FeatureExtraction.hxx b/src/applications/FeatureExtraction/src/FeatureExtraction.hxx index f0f99590b..8e12abdde 100644 --- a/src/applications/FeatureExtraction/src/FeatureExtraction.hxx +++ b/src/applications/FeatureExtraction/src/FeatureExtraction.hxx @@ -1251,6 +1251,15 @@ void FeatureExtraction< TImage >::SetSelectedROIsAndLabels(std::string roi, std: { m_roiLabels = tempstr2; } + if (m_roiLabels.size() < m_roi.size()) + { + auto labelsInitialSize = m_roiLabels.size(); + auto missingLabelsCount = m_roi.size() - m_roiLabels.size(); + for (unsigned int i = 0; i < missingLabelsCount; i++) + { + m_roiLabels.push_back("Label" + std::to_string(m_roi[labelsInitialSize + i])); + } + } } if (m_debug) diff --git a/src/applications/GeodesicTraining/src/GeodesicTrainingCaPTkApp.h b/src/applications/GeodesicTraining/src/GeodesicTrainingCaPTkApp.h index 7f1f81e38..64ad15b50 100644 --- a/src/applications/GeodesicTraining/src/GeodesicTrainingCaPTkApp.h +++ b/src/applications/GeodesicTraining/src/GeodesicTrainingCaPTkApp.h @@ -11,6 +11,9 @@ //#include "ApplicationBase.h" #include +// Do this to make sure we can connect signals/slots across threads with std::strings +Q_DECLARE_METATYPE(std::string) + class GeodesicTrainingApplicationBase : public QObject { Q_OBJECT @@ -30,7 +33,9 @@ public slots: //void GeodesicTrainingFinished3D(typename itk::Image::Pointer result); //void GeodesicTrainingFinished2D(typename itk::Image::Pointer result); void GeodesicTrainingFinishedWithError(QString errorMessage); + void GeodesicTrainingFinishedWithError(); // For when no error message is published void GeodesicTrainingProgressUpdate(int progress, std::string message, int max); + //void GeodesicTrainingProgressUpdate(int progress, QString message, int max); protected: std::thread m_Thread; @@ -47,7 +52,10 @@ class GeodesicTrainingCaPTkApp : typedef itk::Image LabelsImageType; typedef typename LabelsImageType::Pointer LabelsImagePointer; - explicit GeodesicTrainingCaPTkApp(QObject* parent = nullptr) : GeodesicTrainingApplicationBase(parent) {} + explicit GeodesicTrainingCaPTkApp(QObject* parent = nullptr) : GeodesicTrainingApplicationBase(parent) + { + qRegisterMetaType(); // Needs to be done at runtime to allow these connections + } virtual ~GeodesicTrainingCaPTkApp() {} @@ -61,6 +69,8 @@ class GeodesicTrainingCaPTkApp : /** Overriden from GeodesicTrainingSegmentation class */ void progressUpdate(std::string message, int progress) override { + // use QString to avoid argument queuing issues across threads + // (see https://stackoverflow.com/a/17083775) emit GeodesicTrainingProgressUpdate(progress, message, 100); } @@ -73,6 +83,8 @@ class GeodesicTrainingCaPTkApp : this->SetLabels(labelsImage); this->SetSaveAll(true); this->SetProcessing(true, 6, true, false, false, 5000000); // Disable pixel limit, all else default. + emit GeodesicTrainingProgressUpdate(10, "Geodesic Training Segmentation in progress...", 100); + auto executeResult = this->Execute(); if (executeResult->ok) { diff --git a/src/view/gui/CMakeLists.txt b/src/view/gui/CMakeLists.txt index 1a9366bc8..ab5eb4667 100644 --- a/src/view/gui/CMakeLists.txt +++ b/src/view/gui/CMakeLists.txt @@ -55,6 +55,7 @@ SET( GUI CaPTkDockWidget fBiasCorrectionDialog fBraTSSegmentation + fStudyBrowserDialog #fSegmentationPanel #fHelpAppDialog ) diff --git a/src/view/gui/fDrawingPanel.cpp b/src/view/gui/fDrawingPanel.cpp index 32924a732..d23a7d38c 100644 --- a/src/view/gui/fDrawingPanel.cpp +++ b/src/view/gui/fDrawingPanel.cpp @@ -42,6 +42,8 @@ fDrawingPanel::fDrawingPanel(QWidget * parent) : QWidget(parent) connect(HelpButton, SIGNAL(clicked()), this, SLOT(helpClicked())); connect(changeButton, SIGNAL(clicked()), this, SLOT(ChangeLabelValuesClicked())); connect(applyMaskButton, SIGNAL(clicked()), this, SLOT(OnApplyMaskButtonClicked())); + connect(runGeodesicTrainingSegmentationButton, SIGNAL(clicked()), this, SLOT(OnRunGTSButtonClicked())); + connect(finalizeSegmentationButton, SIGNAL(clicked()), this, SLOT(OnFinalizeSegmentationButtonClicked())); } void fDrawingPanel::helpClicked() @@ -137,4 +139,20 @@ void fDrawingPanel::FillButtonFunctionality() void fDrawingPanel::OnApplyMaskButtonClicked() { emit ApplyMask(); +} + +void fDrawingPanel::EnableACRMode() +{ + applyMaskButton->hide(); + algorithmGroup->setHidden(false); +} + +void fDrawingPanel::OnRunGTSButtonClicked() +{ + emit runGTSButtonClicked(); +} + +void fDrawingPanel::OnFinalizeSegmentationButtonClicked() +{ + emit finalizeSegmentationButtonClicked(); } \ No newline at end of file diff --git a/src/view/gui/fDrawingPanel.h b/src/view/gui/fDrawingPanel.h index f9a2b8a48..b3d049a55 100644 --- a/src/view/gui/fDrawingPanel.h +++ b/src/view/gui/fDrawingPanel.h @@ -65,6 +65,7 @@ class fDrawingPanel : public QWidget, private Ui::fDrawingPanel } int getCurrentOpacity() { return m_currentOpacity; }; + void EnableACRMode(); signals : void clearMask(int label=-1); @@ -76,8 +77,11 @@ signals : void CurrentMaskOpacityChanged(int); // multiLabel related change void helpClicked_Interaction(std::string); void sig_ChangeLabelValuesClicked(const std::string, const std::string); + void runGTSButtonClicked(); + void finalizeSegmentationButtonClicked(); void ApplyMask(); + public slots : //! Enable voxel based erase functionality @@ -118,6 +122,8 @@ public slots : //! Apply mask void OnApplyMaskButtonClicked(); + void OnRunGTSButtonClicked(); + void OnFinalizeSegmentationButtonClicked(); private: int m_currentOpacity; // set default to the tenth spinbox selection (1.0 mask opacity) diff --git a/src/view/gui/fFeaturePanel.cpp b/src/view/gui/fFeaturePanel.cpp index 5b4173823..a8e06abf6 100644 --- a/src/view/gui/fFeaturePanel.cpp +++ b/src/view/gui/fFeaturePanel.cpp @@ -272,7 +272,7 @@ void fFeaturePanel::computeFeature(int type) } } - ((fMainWindow*)m_listener)->updateProgress(30, "Calculating and exporting features", images.size()); + ((fMainWindow*)m_listener)->updateProgress(30, "Calculating and exporting features", 100); // last param used to be images.size()??? if (images[0]->GetLargestPossibleRegion().GetSize()[2] == 1) { @@ -378,6 +378,7 @@ void fFeaturePanel::computeFeature(int type) } m_btnCompute->setEnabled(true); + emit computationFinished(); return; } @@ -439,4 +440,119 @@ void fFeaturePanel::onIBSI2Toggled(bool checked) msgbox.setText(msg); msgbox.exec(); } +} + +void fFeaturePanel::EnableACRMode() +{ + m_doACRCallbackOnFinish = true; + // force Image Selection to All Images, hide those options + radio1->setChecked(false); + radio1->hide(); + radio2->setChecked(true); + + // For speed, uncheck these by default for now so that demos are done quickly + // TBD: Once we address long-running feature extraction, this can be changed back. + m_GLCM->setChecked(false); + m_GLSZM->setChecked(false); + m_GLRLM->setChecked(false); + m_NGTDM->setChecked(false); + m_LBP->setChecked(false); + m_Histogram->setChecked(false); + m_Collage->setChecked(false); + m_Lattice->setChecked(false); + + + // Add/replace Compute+save button, hide output file selector + // TODO switch to a temp dir and store this location for deletion later + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + QString outputDirRoot = env.value("DART_CAPTK_OUTPUT_DIR_ROOT", "NULL"); + cbica::createDirectory(cbica::normalizePath(outputDirRoot.toStdString() + "/temp/")); + QString tmpFeatureFilePath = outputDirRoot + "/temp/tmp_features.csv"; + m_txtSaveFileName->setText(QString::fromStdString(cbica::normPath(tmpFeatureFilePath.toStdString()))); // will be deleted after each run + m_txtSaveFileName->hide(); + m_btnBrowseSaveFile->hide(); + saveGroup_file->hide(); + instructionsGroup->setHidden(false); + //m_verticalConcat->hide(); // We may need to hide this if we want all output to be uniform + saveGroup->setTitle("Output"); + + // Ensure output uniformity in a consistent format for training module + m_verticalConcat->setChecked(false); + m_verticalConcat->setVisible(false); + m_btnCompute->setText("Compute + Save to DART"); +} + +std::string fFeaturePanel::GetCurrentOutputPath() +{ + return m_txtSaveFileName->text().toStdString(); +} + +bool fFeaturePanel::GetSelectedROIValuesAndLabelsVectors(std::vector& vals, std::vector& labels) +{ + vals.clear(); + labels.clear(); + auto roi = m_roi->text().toStdString(); + auto roi_labels = m_roi_label->text().toStdString(); + bool roivaluesfound = false; + bool roilabelsfound = false; + + if (!roi.empty() && roi != "all") + { + auto tempstr = cbica::stringSplit(roi, "|"); + auto tempstr2 = cbica::stringSplit(roi, ","); + if (tempstr2.size() > tempstr.size()) + { + tempstr = tempstr2; + } + for (size_t i = 0; i < tempstr.size(); i++) + { + vals.push_back(std::stoi(tempstr[i])); + if (roi_labels.empty()) + { + labels.push_back(tempstr[i]); + } + } + + roivaluesfound = true; + } + + if (!roi_labels.empty() && roi_labels != "all") + { + labels = cbica::stringSplit(roi_labels, "|"); + auto tempstr2 = cbica::stringSplit(roi_labels, ","); + if (tempstr2.size() > labels.size()) + { + labels = tempstr2; + } + if (labels.size() < vals.size()) + { + auto labelsInitialSize = labels.size(); + auto missingLabelsCount = vals.size() - labels.size(); + for (unsigned int i = 0; i < missingLabelsCount; i++) + { + labels.push_back("Label" + std::to_string(vals[labelsInitialSize + i])); + } + } + roilabelsfound = true; + } + + return (roivaluesfound && roilabelsfound); +} + +void fFeaturePanel::SetROIValuesAndLabelStrings(std::string valuesString, std::string labelsString) +{ + m_roi->setText(QString::fromStdString(valuesString)); + m_roi_label->setText(QString::fromStdString(labelsString)); +} + +bool fFeaturePanel::IBSIFeaturesEnabled() +{ + if (m_IBSI2->isChecked()) + { + return true; + } + else + { + return false; + } } \ No newline at end of file diff --git a/src/view/gui/fFeaturePanel.h b/src/view/gui/fFeaturePanel.h index 0fdf66b42..b56c8801c 100644 --- a/src/view/gui/fFeaturePanel.h +++ b/src/view/gui/fFeaturePanel.h @@ -55,12 +55,25 @@ class fFeaturePanel : public QWidget, private Ui::fFeaturePanel m_tempFolderLocation = input_tempFolder; } + void EnableACRMode(); + + //! Get the currently selected output filepath. + std::string GetCurrentOutputPath(); + + //! Populate the 2 given vectors with the ROI values (intensity in mask image) and labels (corresponding region names) + // returns true if this actually works, false if nothing is selected or if "all" is typed in + bool GetSelectedROIValuesAndLabelsVectors(std::vector& vals, std::vector& labels); + + // return true if IBSI features are toggled on. Useful for detecting when to pick up the artifacts. + bool IBSIFeaturesEnabled(); signals: void m_btnComputeClicked(); void helpClicked_FeaUsage(std::string); + void computationFinished(); public slots : + void SetROIValuesAndLabelStrings(std::string valuesString, std::string labelsString); void browseOutputFileName(); void ComputeFunctionality(); void CancelFunctionality(); @@ -115,7 +128,8 @@ class fFeaturePanel : public QWidget, private Ui::fFeaturePanel m_FeatureMaps.push_back(featureMap); } } - + // Flag for whether to run the ACR callback script (only works in an ACR containerized environment) + bool m_doACRCallbackOnFinish; }; diff --git a/src/view/gui/fImagesPanel.cpp b/src/view/gui/fImagesPanel.cpp index 00e2bea47..07dcd3d39 100644 --- a/src/view/gui/fImagesPanel.cpp +++ b/src/view/gui/fImagesPanel.cpp @@ -31,6 +31,7 @@ fImagesPanel::fImagesPanel(QWidget * parent) : QWidget(parent) connect(m_3dViz, SIGNAL(clicked()), this, SLOT(theiaClicked())); connect(m_CompareButton, SIGNAL(toggled(bool)), this, SIGNAL(CompareModeToggled(bool))); connect(HelpButton, SIGNAL(clicked()), this, SLOT(helpClicked())); + connect(m_clearImagesBtn, SIGNAL(clicked()), this, SLOT(OnClearImagesButtonClicked())); } void fImagesPanel::ImageTableSelectionChanged() @@ -130,7 +131,7 @@ void fImagesPanel::NewImageLoaded(QString idstr, const std::string &filename, in connect(cButton2, SIGNAL(clickedInto(QTableWidgetItem*)), caller, SLOT(CloseImage(QTableWidgetItem*))); connect(modalitySwitcher, SIGNAL(currentIndexChanged(int)), this, SLOT(imageModalityChanged(int))); connect(overlayRB, SIGNAL(clicked()), caller, SLOT(overlayChanged())); - connect(m_clearImagesBtn, SIGNAL(clicked()), caller, SLOT(CloseAllImages()));//TBD fix calling everytime + //connect(m_clearImagesBtn, SIGNAL(clicked()), this, SLOT(OnClearImagesButtonClicked()));//TBD fix calling everytime //! tell table to resize to contents after we add stuff into the table m_imagesTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); @@ -140,4 +141,17 @@ void fImagesPanel::NewImageLoaded(QString idstr, const std::string &filename, in void fImagesPanel::helpClicked() { emit helpClicked_Interaction("Getting_Started.html"); -} \ No newline at end of file +} + +void fImagesPanel::OnClearImagesButtonClicked() +{ + emit clearImagesButtonClicked(); +} + +void fImagesPanel::EnableACRMode() +{ + // make clear images button also open + // Update Clear Images button to be more clear about its new meaning in this context + m_clearImagesBtn->setText("Close All Images and Return to Study Browser"); + m_imagesTable->setColumnHidden(0, true); // Hide the image-close buttons so that users have to manage from the study browser +} \ No newline at end of file diff --git a/src/view/gui/fImagesPanel.h b/src/view/gui/fImagesPanel.h index 39e0a591e..f8f9ceb84 100644 --- a/src/view/gui/fImagesPanel.h +++ b/src/view/gui/fImagesPanel.h @@ -68,6 +68,8 @@ class fImagesPanel : public QWidget, private Ui::fImagesPanel } private: +public: + void EnableACRMode(); public slots: void ImageTableSelectionChanged(QTableWidgetItem*); @@ -78,6 +80,7 @@ public slots: void imageModalityChanged(int); void helpClicked(); void theiaClicked(); + void OnClearImagesButtonClicked(); QTableWidgetItem* getSelectedImage() { @@ -122,6 +125,7 @@ public slots: void helpClicked_Interaction(std::string); void sigTheiaClicked(); void CompareModeToggled(bool); + void clearImagesButtonClicked(); }; diff --git a/src/view/gui/fMainWindow.cpp b/src/view/gui/fMainWindow.cpp index 39dfafa31..b8247c3ac 100644 --- a/src/view/gui/fMainWindow.cpp +++ b/src/view/gui/fMainWindow.cpp @@ -173,7 +173,7 @@ fMainWindow::fMainWindow() m_downloadLinks = YAML::LoadFile(getCaPTkDataDir() + "/links.yaml"); this->m_DownloadManager = new DownloadManager(); - + studyBrowserDialog = new fStudyBrowserDialog(); //! load preferences ApplicationPreferences::GetInstance()->DeSerializePreferences(); @@ -842,6 +842,7 @@ fMainWindow::fMainWindow() connect(&fetalbrainpanel, SIGNAL(TrainNewFetalModel(std::string, std::string)), this, SLOT(FetalBrain_TrainNewModel(const std::string &, const std::string &))); connect(imagesPanel, SIGNAL(sigImageTableSelectionChanged()), this, SLOT(DisplayChanged())); + connect(imagesPanel, SIGNAL(clearImagesButtonClicked()), this, SLOT(CloseAllImages())); connect(windowSpinBox, SIGNAL(editingFinished()), this, SLOT(WindowLevelEdited())); connect(levelSpinBox, SIGNAL(editingFinished()), this, SLOT(WindowLevelEdited())); @@ -993,6 +994,7 @@ fMainWindow::fMainWindow() // Instantiate this last -- when instantiated, this restores appearance settings from saved preferences. // Doing this after the UI elements are set up ensures that the restored style is applied to everything. preferenceDialog = new PreferencesDialog(nullptr); + } fMainWindow::~fMainWindow() @@ -1049,10 +1051,17 @@ fMainWindow::~fMainWindow() } void fMainWindow::loadFromCommandLine(std::vector< QString > files, bool comparisonMode, const std::string &maskImage, const float maskOpacity, - const std::string &tumorPointFile, const std::string &tissuePointFile, bool firstRun) + const std::string &tumorPointFile, const std::string &tissuePointFile, bool useStudyBrowser, bool firstRun) { auto qvectorString = QVector< QString >::fromStdVector(files); auto lst = QStringList::fromVector(QVector< QString >::fromStdVector(files)); + if (useStudyBrowser) + { + studyBrowserDialog->LoadInfoFromFilepaths(lst); + studyBrowserDialog->exec(); + lst = studyBrowserDialog->GetFinalNiftiFilesAsQStringList(); + } + this->openImages(lst, true); if (!maskImage.empty()) { @@ -2813,17 +2822,18 @@ void fMainWindow::SetActiveLandmarksType(int type, int row, int col) } void fMainWindow::panelChanged(int current) { - if (drawingPanel) //Reset shape mode on every panle switch + QWidget* currentPanel = m_tabWidget->widget(current); + if (currentPanel == drawingPanel) //Reset shape mode on every panle switch { m_drawShapeMode = SHAPE_MODE_NONE; drawingPanel->shapesNoneButtonFunctionality(); } - if (current == TAB_IMAGES) + if (currentPanel == imagesPanel) { SetActiveLandmarksType(LANDMARK_TYPE::NONE, 0, 0); } - else if (current == TAB_TUMOR) + else if (currentPanel == tumorPanel) { SetActiveLandmarksType(LANDMARK_TYPE::NONE, 0, 0); tumorPanel->SetCurrentSelectedTissueType(); @@ -3033,29 +3043,29 @@ void fMainWindow::SaveDicomDrawing() } -void fMainWindow::SaveDrawing() +void fMainWindow::SaveDrawing(std::string destinationFile) { - auto items = m_imagesTable->selectedItems(); - if (items.empty()) - return; - int index = GetSlicerIndexFromItem(items[0]); + auto items = m_imagesTable->selectedItems(); + if (items.empty()) + return; + int index = GetSlicerIndexFromItem(items[0]); - typedef unsigned short MaskPixelType; - const unsigned int Dimensions = 3; + typedef unsigned short MaskPixelType; + const unsigned int Dimensions = 3; - typedef itk::Image ImageTypeMask; - ImageTypeMask::Pointer imageToWrite = convertVtkToItk(mSlicerManagers[index]->mMask); + typedef itk::Image ImageTypeMask; + ImageTypeMask::Pointer imageToWrite = convertVtkToItk(mSlicerManagers[index]->mMask); - typedef itk::MinimumMaximumImageCalculator< ImageTypeMask > CalculatorType; - CalculatorType::Pointer calculator = CalculatorType::New(); - calculator->SetImage(imageToWrite); - calculator->Compute(); + typedef itk::MinimumMaximumImageCalculator< ImageTypeMask > CalculatorType; + CalculatorType::Pointer calculator = CalculatorType::New(); + calculator->SetImage(imageToWrite); + calculator->Compute(); - if (calculator->GetMaximum() == 0) // this means that the mask image contains no values at all - { - ShowErrorMessage("There should be at least one region (near or far) for saving."); - return; - } + if (calculator->GetMaximum() == 0) // this means that the mask image contains no values at all + { + ShowErrorMessage("There should be at least one labeled region (near or far) for saving. Try drawing a segmentation."); + return; + } auto imageToWrite_wrap = imageToWrite; imageToWrite->DisconnectPipeline(); @@ -3079,8 +3089,8 @@ void fMainWindow::SaveDrawing() originalDirection[2][1] = mSlicerManagers[index]->mDirection(2, 1); originalDirection[2][2] = mSlicerManagers[index]->mDirection(2, 2); - ImageTypeMask::PointType originalOrigin; - originalOrigin = mSlicerManagers[index]->mOrigin; + ImageTypeMask::PointType originalOrigin; + originalOrigin = mSlicerManagers[index]->mOrigin; auto reorientedImage = cbica::GetImageOrientation< ImageTypeMask >(imageToWrite, originalOrientation); auto imageToWrite_wrap = reorientedImage.second; @@ -3095,22 +3105,27 @@ void fMainWindow::SaveDrawing() imageToWrite_wrap = infoChanger->GetOutput(); } - QString saveFileName = getSaveFile(this, mInputPathName, mInputPathName + "mask.nii.gz"); - if (!saveFileName.isEmpty()) - { - std::string filename = saveFileName.toStdString(); + cbica::WriteImage< ImageTypeMask >(imageToWrite_wrap, destinationFile); - cbica::WriteImage< ImageTypeMask >(imageToWrite_wrap, filename); + if (cbica::isFile(destinationFile)) + { + updateProgress(0, "ROI saved!(" + destinationFile + ")"); + } + else + { + ShowErrorMessage("Couldn't write to file: " + destinationFile); + } +} - if (cbica::isFile(filename)) - { - updateProgress(0, "ROI saved!(" + filename + ")"); - } - else +void fMainWindow::SaveDrawing() +{ + + QString saveFileName = getSaveFile(this, mInputPathName, mInputPathName + "mask.nii.gz"); + if (!saveFileName.isEmpty()) { - ShowErrorMessage("Couldn't write to file: " + filename); + std::string filename = saveFileName.toStdString(); + SaveDrawing(filename); } - } } void fMainWindow::SaveSeedDrawing() @@ -7143,6 +7158,23 @@ void fMainWindow::ApplicationGeodesicTraining() (mSlicerManagers[0]->mITKImage->GetLargestPossibleRegion().GetSize()[2] == 1) ? 2 : 3 ); + // The ROIs that are needed (most of them will be populated later if needed) + LabelsImagePointer3D currentROI = convertVtkToItk(mSlicerManagers[0]->mMask); + LabelsImagePointer2D currentROI2D; + LabelsImagePointer3D previousResult; + LabelsImagePointer2D previousResult2D; + LabelsImagePointer3D mask; + LabelsImagePointer2D mask2D; + + // Check if there are at least two different labels in the image (function in UtilImageToCvMatGTS.h) + auto labelsMap = GeodesicTrainingSegmentation::ParserGTS::CountsOfEachLabel(currentROI); + if (labelsMap.size() < 2) + { + ShowErrorMessage("Please draw using at least 2 different labels.", this); + m_IsGeodesicTrainingRunning = false; + return; + } + // Different operations happen if the user reruns it on the same images std::string firstFileName = mSlicerManagers[0]->mFileName; bool isRerun = (firstFileName == m_GeodesicTrainingFirstFileNameFromLastExec); @@ -7157,22 +7189,6 @@ void fMainWindow::ApplicationGeodesicTraining() updateProgress(0, "Geodesic Training segmentation started, please wait"); - // The ROIs that are needed (most of them will be populated later if needed) - LabelsImagePointer3D currentROI = convertVtkToItk(mSlicerManagers[0]->mMask); - LabelsImagePointer2D currentROI2D; - LabelsImagePointer3D previousResult; - LabelsImagePointer2D previousResult2D; - LabelsImagePointer3D mask; - LabelsImagePointer2D mask2D; - - // Check if there are at least two different labels in the image (function in UtilImageToCvMatGTS.h) - auto labelsMap = GeodesicTrainingSegmentation::ParserGTS::CountsOfEachLabel(currentROI); - if (labelsMap.size() < 2) - { - ShowErrorMessage("Please draw using at least 2 different labels.", this); - m_IsGeodesicTrainingRunning = false; - return; - } // Find the input images (always 3D at first) std::vector inputImages; @@ -7327,7 +7343,7 @@ void fMainWindow::ApplicationGeodesicTraining() this, SLOT(GeodesicTrainingFinishedHandler()) ); connect(m_GeodesicTrainingCaPTkApp3D, SIGNAL(GeodesicTrainingFinishedWithError(QString)), - this, SLOT(GeodesicTrainingSegmentationResultErrorHandler(QString)) + this, SLOT(GeodesicTrainingFinishedWithErrorHandler(QString)) ); auto test = connect(m_GeodesicTrainingCaPTkApp3D, SIGNAL(GeodesicTrainingProgressUpdate(int, std::string, int)), this, SLOT(updateProgress(int, std::string, int)) @@ -9946,11 +9962,18 @@ void fMainWindow::updateProgress(qint64 progress, std::string message, qint64 ma m_progressBar->setMaximum(max); m_progressBar->setValue(progress); m_messageLabel->setText(QString::fromStdString(message)); - QTimer::singleShot(10000.0, m_messageLabel, SLOT(clear())); + // TBD: Tune this so that an expiry is optional (for when algorithms finish, etc) + QTimer::singleShot(500000.0, m_messageLabel, SLOT(clear())); // Text time on progress bar can be tuned here qApp->processEvents(); #endif } + +void fMainWindow::updateProgress(int progress, std::string message, int max) +{ + updateProgress(qint64(progress), message, qint64(max)); +} + std::vector> fMainWindow::LoadQualifiedSubjectsFromGivenDirectoryForSurvival(const std::string directoryname) { std::map OneQualifiedSubject; @@ -10700,3 +10723,414 @@ void fMainWindow::OnPreferencesMenuClicked() { int result = this->preferenceDialog->exec(); } + +void fMainWindow::EnableACRMode() +{ + /* Need to: + hide the menu bar, (and any other menus that might expose too much) + disable the tumor panel, + add the button to clear and open the study browser to the imagePanel, + (add the 2D stack column to the image panel?), + add the finalize segmentation button to the drawing panel, + make the simplifying changes to the featurePanel, + finally add some other minor touch-ups + */ + menubar->hide(); + m_tabWidget->setTabText(2, "Segmentation"); + m_tabWidget->setTabText(3, "Feature Extraction (Radiomics)"); + // Below line causes drawing not to work if enabled... + //m_tabWidget->removeTab(1); // hide() doesn't work here, remove tumor panel + m_tabWidget->setTabEnabled(1, false); // disable it instead to prevent anything breaking for now + + // GUI functions to hide certain options and change from defaults in some cases + drawingPanel->EnableACRMode(); + featurePanel->EnableACRMode(); + imagesPanel->EnableACRMode(); + // Remove normal behavior to make sure we close first then open the study browser + disconnect(imagesPanel, SIGNAL(clearImagesButtonClicked()), this, SLOT(CloseAllImages())); + connect(imagesPanel, SIGNAL(clearImagesButtonClicked()), this, SLOT(OpenStudyBrowser())); + connect(drawingPanel, SIGNAL(runGTSButtonClicked()), this, SLOT(ApplicationGeodesicTraining())); + connect(drawingPanel, SIGNAL(finalizeSegmentationButtonClicked()), this, SLOT(FinalizeSegmentation())); + connect(featurePanel, SIGNAL(computationFinished()), this, SLOT(NotifyACROutputCreated())); +} + +void fMainWindow::OpenStudyBrowser() +{ + CloseAllImages(); + studyBrowserDialog->exec(); + auto list = studyBrowserDialog->GetFinalNiftiFilesAsQStringList(); + this->openImages(list, true); // Avoids file menu opening behavior with blank list +} + +void fMainWindow::FinalizeSegmentation() +{ + // Get the values in the mask to populate featurePanel with sane defaults + if (!this->isMaskDefined()) + { + ShowErrorMessage("Please draw an ROI before proceeding!"); + return; + } + auto maskImg = this->getMaskImage(); + auto uniqueValues = cbica::GetUniqueValuesInImage(maskImg); // vector of floats -- need to int-convert before display + std::string labelValString; + std::string defaultLabelNamesString; + bool foundFirstNonZero = false; + for (int i = 0; i < uniqueValues.size(); i++) + { + int integerUniqueVal = (int)uniqueValues[i]; + if (integerUniqueVal <= 0) + { + continue; + } + if (foundFirstNonZero) + { + labelValString += ","; + defaultLabelNamesString += ","; + } + foundFirstNonZero = true; + labelValString += std::to_string(integerUniqueVal); + switch (integerUniqueVal) // This is really ugly, we should refactor color selection into a mapping/enum? -AG + { + case 1: + defaultLabelNamesString += "RedLabel"; break; + case 2: + defaultLabelNamesString += "GreenLabel"; break; + case 3: + defaultLabelNamesString += "YellowLabel"; break; + case 4: + defaultLabelNamesString += "BlueLabel"; break; + case 5: + defaultLabelNamesString += "MagentaLabel"; break; + case 6: + defaultLabelNamesString += "CyanLabel"; break; + case 7: + defaultLabelNamesString += "LightRedLabel"; break; + case 8: + defaultLabelNamesString += "LightGreenLabel"; break; + case 9: + defaultLabelNamesString += "LightBlueLabel"; break; + default: + defaultLabelNamesString += "UnknownLabel"; + } + } + featurePanel->SetROIValuesAndLabelStrings(labelValString, defaultLabelNamesString); + // Assume feature extraction is the next tab over... TBD fix this to be flexible + m_tabWidget->setCurrentIndex(m_tabWidget->currentIndex() + 1); + // Now alert the user that they should extract features. + ShowMessage("Using your generated segmentation, radiomic feature extraction will be run as part of saving your work to DART. Note that your segmentation and extracted radiomics features will not be saved to DART until you complete this process. \n\nPlease edit the label values and label names under the mask selection section of the Feature Extraction tab to reflect your anatomical labels, if necessary.\n\nWhen you are finished, select \'Compute + Save to DART\' to save your work.", this, "Segmentation finalized"); +} + +void fMainWindow::NotifyACROutputCreated() +{ + // Get env vars and prepare for output creation + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + QString baseCallbackCommand = env.value("NOTIFY_DART_JOBDONE_SCRIPT_PATH", "NULL"); + QString studyInstanceUid = env.value("DART_STUDY_INSTANCE_UID", "NULL"); + QString transactionId = env.value("DART_TRANSACTION_ID", "NULL"); + QString numberOfAnnotationsStr = "1"; + QString userId = env.value("DART_USER_ID", "NULL"); + QString userName = env.value("DART_USER_NAME", "NULL"); + QString userOriginalAccessToken = env.value("DART_USER_ACCESSTOKEN", "NULL"); + QString outputDirRoot = env.value("DART_CAPTK_OUTPUT_DIR_ROOT", "NULL"); + QString outputMountPoint = env.value("DART_CAPTK_OUTPUT_MOUNTPOINT", "NULL"); + QString datetimestamp = QString::fromStdString(cbica::getCurrentLocalTimestamp()); + QString studyOutputDir = outputDirRoot + "/" + studyInstanceUid; + cbica::makeDirectory((outputDirRoot + "/" + studyInstanceUid).toStdString()); + QString thisJobOutputDir = studyOutputDir + "/" + "output_" + datetimestamp; + cbica::makeDirectory(thisJobOutputDir.toStdString()); + + + + // Save/copy input images, JSONs, segmentation to the correct output location + // (also metadata file, feature extraction csv) + // Output looks like (individual seg+feature things are saved under their timestamp for uniqueness) + // outputDirRoot/transactionID/studyInstanceUID/niftis/series1.nii.gz + series1.json + series2.nii.gz ... + // outputDirRoot/transactionID/studyInstanceUID/output_YYYYMMDDhhmmss/metadata.json + segmentation.nii.gz + features.csv + QStringList pre_inputNiftisPaths = studyBrowserDialog->GetFinalNiftiFilesAsQStringList(); + QStringList pre_niftiJsonPaths = studyBrowserDialog->GetFinalJsonFilesAsQStringList(); + QStringList relativeImagePathsToMetadata; // Will be populated with relative paths from the session output + QStringList relativeJsonPathsToMetadata; // Will be populated with relative paths from the session output + QString pre_RadiomicsCSVPath = outputDirRoot + "/temp/tmp_features.csv"; + QString roiOutputPath = thisJobOutputDir + "/segmentation.nii.gz"; + QString radiomicsCSVOutputPath = thisJobOutputDir + "/features.csv"; + QString metadataJsonOutputPath = thisJobOutputDir + "/metadata.json"; + QStringList niftiDestinationPathsRelativeToOutputMount; + QStringList jsonDestinationPathsRelativeToOutputMount; + QStringList seriesInstanceUIDs; + bool anyFileCopiesFailed = false; + cbica::createDir(cbica::normalizePath(studyOutputDir.toStdString() + "/niftis/")); + for (int i = 0; i < pre_inputNiftisPaths.size(); i++) + { + // copy NIFTI data + std::string imagePath = pre_inputNiftisPaths[i].toStdString(); + std::string path, base, ext; + cbica::splitFileName(imagePath, path, base, ext); + std::string destImagePath = studyOutputDir.toStdString() + "/niftis/" + base + ext; + bool imageCopySuccess = cbica::copyFile(imagePath, cbica::normalizePath(destImagePath)); + relativeImagePathsToMetadata.push_back(QString::fromStdString(cbica::relativePath(destImagePath, thisJobOutputDir.toStdString()))); + niftiDestinationPathsRelativeToOutputMount.push_back(QString::fromStdString(cbica::relativePath(destImagePath, outputMountPoint.toStdString()))); + + // copy JSON data + std::string jsonPath = pre_niftiJsonPaths[i].toStdString(); + std::string jsonBase, jsonExt; + cbica::splitFileName(jsonPath, path, jsonBase, jsonExt); + std::string destJsonPath = studyOutputDir.toStdString() + "/niftis/" + jsonBase + jsonExt; + bool jsonCopySuccess = cbica::copyFile(jsonPath, cbica::normalizePath(destJsonPath)); + relativeJsonPathsToMetadata.push_back(QString::fromStdString(cbica::relativePath(destJsonPath, thisJobOutputDir.toStdString()))); + jsonDestinationPathsRelativeToOutputMount.push_back(QString::fromStdString(cbica::relativePath(destJsonPath, outputMountPoint.toStdString()))); + + // Now read the JSON file and get the seriesInstanceUID + QFile jsonFile(pre_niftiJsonPaths[i]); + if (jsonFile.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QString text = jsonFile.readAll(); + jsonFile.close(); + QJsonDocument doc = QJsonDocument::fromJson(text.toUtf8()); + if (!doc.isNull()) + { + QJsonObject docObject = doc.object(); + QString currentSeriesInstanceUID = docObject["SeriesInstanceUID"].toString(); + seriesInstanceUIDs.push_back(currentSeriesInstanceUID); + } + else + { + std::cerr << "Malformed DICOM-header JSON file in ACR Callback." << std::endl; + anyFileCopiesFailed = true; + } + + } + else + { + std::cerr << "Failed to open JSON metadata for NIFTI series in ACR Callback." << std::endl; + anyFileCopiesFailed = true; + } + + + + + if (!imageCopySuccess || !jsonCopySuccess) + { + anyFileCopiesFailed = true; + } + } + + bool radiomicsFileCopySucceeded = cbica::copyFile(pre_RadiomicsCSVPath.toStdString(), radiomicsCSVOutputPath.toStdString()); + + bool ibsiFeaturesCopySucceeded = true; // default true for the non-IBSI case + QStringList artifactOutputDirs; + if (featurePanel->IBSIFeaturesEnabled()) + { + + std::string path, ext, base; + cbica::splitFileName(pre_RadiomicsCSVPath.toStdString(), path, base, ext); + std::string artifactIBSIDir = path + base + "_IBSI2/"; + std::string artifactIBSIOutputDir = thisJobOutputDir.toStdString() + "/features" + "_IBSI2/"; + + ibsiFeaturesCopySucceeded = cbica::copyDirectory(artifactIBSIDir, artifactIBSIOutputDir, true); + if (ibsiFeaturesCopySucceeded) + { + artifactOutputDirs.push_back(QString::fromStdString(artifactIBSIOutputDir)); + } + } + + // clean up any temporary files generated, recreate temp dir + cbica::removeDirectoryRecursively(outputDirRoot.toStdString() + "/temp/", true); + cbica::createDirectory(outputDirRoot.toStdString() + "/temp/"); + + if (anyFileCopiesFailed) + { + ShowErrorMessage("Couldn't copy NIFTI file data when sending data to DART. Please submit a bug report."); + return; + } + if (!radiomicsFileCopySucceeded) + { + ShowErrorMessage("Couldn't save radiomic feature extraction output when sending data to DART. Please double-check your label values, and if the error persists, please submit a bug report."); + return; + } + + if (!ibsiFeaturesCopySucceeded) + { + ShowErrorMessage("IBSI-2 features were selected but the resulting output could not be copied. Please submit a bug report and/or try again without IBSI features selected."); + return; + } + + SaveDrawing(roiOutputPath.toStdString()); + if (!cbica::fileExists(roiOutputPath.toStdString())) + { + ShowErrorMessage("Couldn't save segmentation when sending data to DART. Please submit a bug report."); + return; + } + + + + std::vector roiRegionValues; + std::vector roiRegionNames; + + // By the time we call this, the featurePanel should have already validated the roi label values/names + featurePanel->GetSelectedROIValuesAndLabelsVectors(roiRegionValues, roiRegionNames); + + // We need to pass paths relative to the job output dir to the metadata-writing function + // (DART has to be given relative paths to locate the metadata file inside the container we run CaPTk in) + // (And the the metadata file needs to correspond to the locations inside the study output) + QString roiRelativeToOutputMount = QString::fromStdString(cbica::relativePath(roiOutputPath.toStdString(), outputMountPoint.toStdString())); + QString radiomicsCSVRelativeToOutputMount = QString::fromStdString(cbica::relativePath(radiomicsCSVOutputPath.toStdString(), outputMountPoint.toStdString())); + QString metadataRelativeToOutputMount = QString::fromStdString(cbica::relativePath(metadataJsonOutputPath.toStdString(), outputMountPoint.toStdString())); + QString niftiFolderRelativeToOutputMount = QString::fromStdString(cbica::relativePath(studyOutputDir.toStdString() + "/niftis/", outputMountPoint.toStdString())); + QString segFileRelativeToMetadata = "./segmentation.nii.gz"; // an assumption, TBD clean this up + QString featureFileRelativeToMetadata = "./features.csv"; // an assumption, TBD clean this up + QStringList artifactDirsRelativeToMetadata; + for (int i = 0; i < artifactOutputDirs.size(); i++) + { + QString currentRelativePath = QString::fromStdString(cbica::relativePath(artifactOutputDirs[i].toStdString(), metadataJsonOutputPath.toStdString())); + artifactDirsRelativeToMetadata.push_back(currentRelativePath); + } + + + if (!WriteACRSessionMetadataToFile(segFileRelativeToMetadata, featureFileRelativeToMetadata, artifactDirsRelativeToMetadata, + relativeImagePathsToMetadata, relativeJsonPathsToMetadata, seriesInstanceUIDs, roiRegionValues, roiRegionNames, + transactionId, studyInstanceUid, metadataJsonOutputPath, userName)) + { + ShowErrorMessage("Couldn't save session metadata when sending data to DART. Please submit a bug report."); + return; + } + + + // Not used at the moment + /* + QString relativeNiftiPathsJoinedByComma = ""; + QString relativeJsonPathsJoinedByComma = ""; + for (int i = 0; i < niftiDestinationPathsRelativeToOutputRoot.size(); i++) + { + if (!relativeNiftiPathsJoinedByComma.isEmpty()) + { + relativeNiftiPathsJoinedByComma += ","; + } + if (!relativeJsonPathsJoinedByComma.isEmpty()) + { + relativeJsonPathsJoinedByComma += ","; + } + relativeNiftiPathsJoinedByComma += niftiDestinationPathsRelativeToOutputRoot[i]; + relativeJsonPathsJoinedByComma += jsonDestinationPathsRelativeToOutputRoot[i]; + } + */ + // Notify ACR DART about the output that's been created. + // Only works if environment vars are set properly and the corresponding script exists on-system + // Also, avoids doing any web request handling here in C++ ;) + QString fullCommand = baseCallbackCommand + " " + studyInstanceUid + " " + transactionId + " " + + roiRelativeToOutputMount + " " + niftiFolderRelativeToOutputMount + " " + radiomicsCSVRelativeToOutputMount + " " + + metadataRelativeToOutputMount + " " + numberOfAnnotationsStr + " " + + userId + " " + userOriginalAccessToken; + int exitCode = system(fullCommand.toStdString().c_str()); // Execute the command + // When done, check if OK (just by return code here), and alert the user that their work has been saved to DART + // They can exit the CaPTk session or return to the study browser to do more work + if (exitCode == 0) + { + ShowMessage("Your output (segmentation and extracted radiomics features) has been successfully sent to DART.\n\n You may now close the session, or access the study browser from the Images tab to segment another set of images.", this, "Success"); + m_tabWidget->setCurrentIndex(0); + } + else + { + ShowErrorMessage("An error occurred when communicating with the DART endpoint. Please submit a bug report."); + return; + } + + +} + +bool fMainWindow::WriteACRSessionMetadataToFile(QString segFile, QString radiomicFeaturesFile, QStringList additionalArtifactDirs, QStringList inputImageFiles, + QStringList inputJsonFiles, QStringList seriesInstanceUids, std::vector labelValues, std::vector labelNames, QString transactionId, + QString studyInstanceUid, QString destinationFile, QString userName) +{ + // Should create a JSON file for uploading to DART. + // metadata contains: + // created by CaPTk + // captk version + // link to captk homepage (https://www.med.upenn.edu/cbica/captk/) + // date/time created (GMT), cbica::getCurrentGMTDateAndTime() + // "pipeline": "GeodesicTrainingSegmentation+FeatureExtractionV1" (placeholder for later pipeline info as JSON/YAML object) + // [label values] + // [label names] + // (N) inputs: + // - path to nifti (all are relative to outputDirRoot/) + // - path to json + // - seriesInstanceUID + // + // path to output segmentation file + // path to output radiomic feature file + // dart transactionID + // dart studyInstanceUID + QJsonDocument jsonDocument; // used for formatting + validation + QJsonObject jsonObject; // actually do the insertion + jsonObject.insert("outputDescription", "Segmentation and Radiomics Features"); + jsonObject.insert("createdBy", "CaPTk"); + jsonObject.insert("captkVersion", PROJECT_VERSION); + jsonObject.insert("homepage", "https://www.med.upenn.edu/cbica/captk/"); + jsonObject.insert("createdAt", QString::fromStdString(cbica::getCurrentGMTDateAndTime())); + jsonObject.insert("pipeline", "InteractiveGeodesicTrainingSegmentation+FeatureExtraction"); + jsonObject.insert("user", userName); + jsonObject.insert("dartTransactionId", transactionId); + jsonObject.insert("studyInstanceUid", studyInstanceUid); + + QJsonObject outputFilesJsonObject; + outputFilesJsonObject.insert("segmentationFile", segFile); + outputFilesJsonObject.insert("radiomicsFile", radiomicFeaturesFile); + QJsonArray additionalArtifactDirectoriesJsonArray; + for (int i = 0; i < additionalArtifactDirs.size(); i++) + { + additionalArtifactDirectoriesJsonArray.push_back(additionalArtifactDirs[i]); + } + + outputFilesJsonObject.insert("additionalArtifactDirectories", additionalArtifactDirectoriesJsonArray); + jsonObject.insert("outputs", outputFilesJsonObject); + + QJsonObject segmentationInfoJsonObject; + // Order has to be preserved with these + QJsonArray labelValuesJsonArray; + QJsonArray correspondingLabelNamesJsonArray; + for (int i = 0; i < labelValues.size(); i++) + { + labelValuesJsonArray.push_back(labelValues[i]); + } + for (int i = 0; i < labelNames.size(); i++) + { + correspondingLabelNamesJsonArray.push_back(QString::fromStdString(labelNames[i])); + } + segmentationInfoJsonObject.insert("labelValues", labelValuesJsonArray); + segmentationInfoJsonObject.insert("labelNames", correspondingLabelNamesJsonArray); + jsonObject.insert("segmentationInfo", segmentationInfoJsonObject); + + QJsonArray inputsJsonArray; + for (int i = 0; i < inputImageFiles.size(); i++) + { + QJsonObject currentInputJsonObject; + currentInputJsonObject.insert("image", inputImageFiles[i]); + currentInputJsonObject.insert("metadataJson", inputJsonFiles[i]); + currentInputJsonObject.insert("seriesInstanceUid", seriesInstanceUids[i]); + inputsJsonArray.push_back(currentInputJsonObject); + } + jsonObject.insert("inputs", inputsJsonArray); + jsonDocument.setObject(jsonObject); + if (jsonDocument.isEmpty() || jsonDocument.isNull()) + { + return false; + } + + QFile file(destinationFile); + file.open(QIODevice::WriteOnly | QIODevice::Text); + bool fileWriteSuccess = file.write(jsonDocument.toJson(QJsonDocument::Indented)); + file.close(); + if (fileWriteSuccess) + { + return true; + } + else + { + return false; + } + + + + + + +} \ No newline at end of file diff --git a/src/view/gui/fMainWindow.h b/src/view/gui/fMainWindow.h index 768ea0e58..40b189dc9 100644 --- a/src/view/gui/fMainWindow.h +++ b/src/view/gui/fMainWindow.h @@ -64,6 +64,7 @@ See COPYING file or https://www.med.upenn.edu/cbica/software-agreement.html #include "fSBRTAnalysisDialog.h" #include "fBiasCorrectionDialog.h" #include "fBraTSSegmentation.h" +#include "fStudyBrowserDialog.h" #include @@ -270,6 +271,7 @@ class fMainWindow : public QMainWindow, private Ui::fMainWindow fDirectionalityDialog directionalityEstimator; PreferencesDialog *preferenceDialog; SystemInformationDisplayWidget *sysinfowidget; + fStudyBrowserDialog* studyBrowserDialog; fDrawingPanel *drawingPanel; @@ -527,7 +529,7 @@ class fMainWindow : public QMainWindow, private Ui::fMainWindow \param tissuePointFile The tissue points file (containing no radius information) */ void loadFromCommandLine(std::vector< QString > files, bool comparisonMode, const std::string &maskImage = "", const float maskOpacity = 1.0, - const std::string &tumorPointFile = "", const std::string &tissuePointFile = "", bool firstRun = false); + const std::string &tumorPointFile = "", const std::string &tissuePointFile = "", bool useStudyBrowser = false, bool firstRun = false); signals: void SelectedImageHasChanged(SlicerManager *); @@ -1054,7 +1056,8 @@ public slots: /** \brief Save near/far drawing in Nifti format */ - void SaveDrawing(); + void SaveDrawing(); // this one prompts for a save location first + void SaveDrawing(std::string filename); // this one just saves to the specified location /** \brief Save near/far drawing in DICOM format @@ -1408,12 +1411,14 @@ public slots: //! GUI control for Imaging Subtype void ApplicationImagingSubtype(); #endif +#ifdef BUILD_MSUBTYPE //! GUI control for Molecular Subtype void ApplicationMolecularSubtype(); #endif #ifdef BUILD_SURVIVAL //! GUI control for Survival void ApplicationSurvival(); +#endif #ifdef BUILD_EGFRvIIISVM //! GUI control for EGFRvIII SVM void ApplicationEGFRvIIISVM(); @@ -1511,6 +1516,9 @@ public slots: // Progress Update void updateProgress(qint64 progress, std::string message = "", qint64 max = 100); + // Wrap for the above to accept normal ints + void updateProgress(int progress, std::string message, int max = 100); + //void updateProgress(int progress, QString message, int max = 100); // causes ambiguity errors //! Enables "advanced mode" - no image checks are done - disabled by default void EnableAdvancedVisualizer() @@ -1518,6 +1526,24 @@ public slots: m_advancedVisualizer = true; } + //! Enables the minimalistic ACR mode where options besides interactive segmentation and feature extraction are suppressed. + void EnableACRMode(); + + //! Open the study browser dialog. When this is called, it should already be loaded with appropriate information. + void OpenStudyBrowser(); + + //! Gather final images + segmentation and proceed to feature extraction (for ACR mode) + void FinalizeSegmentation(); + + //! Notify DART that output has been created (for ACR mode) using an existing script on-system + // (won't work unless in containerized CaPTk session) + void NotifyACROutputCreated(); + + //! Writes the ACR mode session metadata when a user finishes segmentation+feature extraction. Returns true if success. + bool WriteACRSessionMetadataToFile(QString segFile, QString radiomicFeaturesFile, QStringList additionalArtifactDirs, QStringList inputImageFiles, + QStringList inputJsonFiles, QStringList seriesInstanceUids, std::vector labelValues, std::vector labelNames, QString transactionId, + QString studyInstanceUid, QString destinationFile, QString userName) ; + public: std::string currentPlatform; diff --git a/src/view/gui/fStudyBrowserDialog.cpp b/src/view/gui/fStudyBrowserDialog.cpp new file mode 100644 index 000000000..86fb136db --- /dev/null +++ b/src/view/gui/fStudyBrowserDialog.cpp @@ -0,0 +1,307 @@ +#include "fStudyBrowserDialog.h" +#include "CaPTkGUIUtils.h" +#include "cbicaLogging.h" +#include "CaPTkUtils.h" +#include +#include +#include "cbicaUtilities.h" +#include "cbicaITKUtilities.h" +#include + +fStudyBrowserDialog::fStudyBrowserDialog() +{ + setupUi(this); + this->setModal(true); + this->setWindowFlags(this->windowFlags() & ~Qt::WindowCloseButtonHint & ~Qt::WindowContextHelpButtonHint | Qt::Dialog ); + this->resize(QSize(800, 450)); + + connect(this->m_filesTable, SIGNAL(itemSelectionChanged()), this, SLOT(OnSelectionChanged())); + connect(this->m_confirmButton, SIGNAL(clicked()), this, SLOT(OnConfirmButtonPressed())); + m_confirmButton->setEnabled(false); // Initialize to false so users need to make a selection + +} +fStudyBrowserDialog::~fStudyBrowserDialog() +{ +} + +bool fStudyBrowserDialog::LoadInfoFromFilepaths(const QStringList& fileList) +{ + for (int i = 0; i < fileList.size(); i++) + { + QString currentFilepath = fileList.at(i); + auto normedFilepath = cbica::normPath(currentFilepath.toStdString()); + std::string path, base, ext; + cbica::splitFileName(normedFilepath, path, base, ext); + auto pathDir = QDir(QString::fromStdString(path)); + pathDir.setFilter(QDir::Files); + auto entryList = pathDir.entryList(); + std::string metadataString = ""; + // For NIfTIs converted using dcm2niix. + // Search in the path given for a file with the same base but with JSON extension (BIDS metadata). + // Does not search subdirectories. + + if (ext == ".nii.gz" || ext == ".nii") + { + std::string matchingJsonEntry = ""; + for (int j = 0; j < entryList.size(); j++) + { + QString otherFilepath = QString::fromStdString(path + "/") + entryList.at(j); + auto otherNormed = cbica::normPath(otherFilepath.toStdString()); + std::string otherPath, otherBase, otherExt; + cbica::splitFileName(otherNormed, otherPath, otherBase, otherExt); + if ((otherBase == base) && (otherExt == ".json")) + { + matchingJsonEntry = path+otherBase+otherExt; + // Assume this is the corresponding JSON BIDS info. Load that in. + // Note we don't actually need to handle a JSON node tree, just the text (at least for now). + QFile f(QString::fromStdString(otherNormed)); + if (!f.open(QFile::ReadOnly | QFile::Text)) + { + metadataString = "Could not read JSON metadata for this file.\n"; + break; + } + QTextStream input(&f); + metadataString = input.readAll().toStdString(); + f.close(); + + + } + } + if (matchingJsonEntry == "") + { + metadataString = "Could not find JSON metadata for this file.\n"; + } + + + auto inputImageInfo = cbica::ImageInfo(normedFilepath); + metadataString += "Calculated image information:\n"; + auto dims = inputImageInfo.GetImageDimensions(); + auto size = inputImageInfo.GetImageSize(); + auto origin = inputImageInfo.GetImageOrigins(); + auto spacing = inputImageInfo.GetImageSpacings(); + auto directions = inputImageInfo.GetImageDirections(); + auto size_string = std::to_string(size[0]); + auto origin_string = std::to_string(origin[0]); + auto spacing_string = std::to_string(spacing[0]); + auto directions_string = "[" + std::to_string(directions[0][0]) + "x" + std::to_string(directions[0][1]) + "x" + std::to_string(directions[0][2]); + size_t totalSize = size[0]; + for (size_t i = 1; i < dims; i++) + { + size_string += " x " + std::to_string(size[i]); + origin_string += " x " + std::to_string(origin[i]); + spacing_string += " x " + std::to_string(spacing[i]); + directions_string += ";" + std::to_string(directions[i][0]) + " x " + std::to_string(directions[i][1]) + " x " + std::to_string(directions[i][2]); + totalSize *= size[i]; + } + directions_string += "]"; + //metadataString << "Property,Value\n"; + metadataString += "\nExpected modality: " + std::string((CAPTK::ImageModalityString[guessImageType(normedFilepath, false)])) + "\n"; + metadataString += "Dimensions:" + std::to_string(dims) + "\n"; + metadataString += "Size:" + size_string + "\n"; + metadataString += "Total Voxels:" + std::to_string(totalSize) + "\n"; + metadataString += "Origin:" + origin_string + "\n"; + metadataString += "Spacing:" + spacing_string + "\n"; + metadataString += "Component Type:" + inputImageInfo.GetComponentTypeAsString() + "\n"; + metadataString += "Pixel Type:" + inputImageInfo.GetPixelTypeAsString() + "\n"; + metadataString += "Directions:" + directions_string + "\n"; + + auto fileInfo = std::make_tuple(normedFilepath, matchingJsonEntry, metadataString, inputImageInfo); + mFileListing.push_back(fileInfo); + } + else if (ext == ".dcm") + { + // Do nothing for now. We may handle this case eventually. + } + } + + GenerateFileCompatibilityMatrix(); + GenerateTable(); + + return true; +} + +void fStudyBrowserDialog::OnConfirmButtonPressed() +{ + // Invoke any necessary preprocessing, write to temp location + // Gather the final paths of files to use into mFinalFiles + QStringList filesResult; + QStringList jsonsResult; + + // Preprocessing like registration TBD. + // For now just get the selected files. + for (int i = 0; i < mFileListing.size(); i++) + { + QCheckBox* cb = (QCheckBox*)m_filesTable->cellWidget(i, 0); + if (cb->isChecked()) + { + filesResult.push_back(QString::fromStdString(std::get<0>(mFileListing[i]))); + jsonsResult.push_back(QString::fromStdString(std::get<1>(mFileListing[i]))); + } + } + + mFinalNiftiFiles = filesResult; + mFinalJsonFiles = jsonsResult; + this->close(); +} + +QStringList fStudyBrowserDialog::GetFinalNiftiFilesAsQStringList() +{ + return mFinalNiftiFiles; +} + +QStringList fStudyBrowserDialog::GetFinalJsonFilesAsQStringList() +{ + return mFinalJsonFiles; +} + +void fStudyBrowserDialog::GenerateFileCompatibilityMatrix() +{ + // Generate a compatibility matrix between the files so we don't need to reload and do comparisons every time + std::vector blankVec; + blankVec.resize(mFileListing.size()); + std::fill(blankVec.begin(), blankVec.end(), false); + //m_compatibilityWithoutRegistrationMatrix initialize to all false + for (int i = 0; i < mFileListing.size(); i++) + { + mCompatibilityWithoutRegistrationMatrix.push_back(blankVec); + } + // Check and fill in compatibility + for (int i = 0; i < mCompatibilityWithoutRegistrationMatrix.size(); i++) + { + for (int j = i; j < mCompatibilityWithoutRegistrationMatrix[i].size(); j++) + { + if (i == j) + { + mCompatibilityWithoutRegistrationMatrix[i][j] = true; // Same image, so of course it's compatible + mCompatibilityWithoutRegistrationMatrix[j][i] = true; + } + else // We need to check if the files pass the normal sanity checks + { + bool sanityCheckPassed; + bool fourDCheckNeeded = (std::get<3>(mFileListing[i]).GetImageDimensions() == 4 || std::get<3>(mFileListing[j]).GetImageDimensions() == 4); + sanityCheckPassed = cbica::ImageSanityCheck(std::get<0>(mFileListing[i]), std::get<0>(mFileListing[j]), fourDCheckNeeded); + + // Set both relevant elements of the matrix for easier comparison later + mCompatibilityWithoutRegistrationMatrix[i][j] = sanityCheckPassed; + mCompatibilityWithoutRegistrationMatrix[j][i] = sanityCheckPassed; + } + + } + } +} + +void fStudyBrowserDialog::GenerateTable() +{ + // clear table and re-add to table based on file listing + m_filesTable->clearContents(); + m_filesTable->setRowCount(mFileListing.size()); + + for (int i = 0; i < mFileListing.size(); i++) + { + std::string name = std::get<0>(mFileListing[i]); + std::string path, base, ext; + cbica::splitFileName(name, path, base, ext); + QLabel* nameItem = new QLabel(base.c_str()); + //CAPTK::ImageModalityType modality = (CAPTK::ImageModalityType)guessImageType(name, false); + + std::string modalityStr = CAPTK::ImageModalityString[guessImageType(name, false)]; + QLabel* modalityItem = new QLabel(modalityStr.c_str()); + + auto imageDims = std::get<3>(mFileListing[i]).GetImageDimensions(); + auto imageSize = std::get<3>(mFileListing[i]).GetImageSize(); + std::string sizeStr = " "; + for (int d = 0; d < imageDims; d++) + { + if (d != 0) + { + sizeStr += " x " + std::to_string(imageSize[d]); + } + else + { + sizeStr += std::to_string(imageSize[d]); + } + } + sizeStr += " (" + std::to_string(imageDims) + "D)"; + QLabel* dimsItem = new QLabel(sizeStr.c_str()); + QCheckBox* loadCheckbox = new QCheckBox; + loadCheckbox->setToolTip(QString("Load this series?")); + + m_filesTable->setCellWidget(i, 3, nameItem); + m_filesTable->setCellWidget(i, 1, modalityItem); + m_filesTable->setCellWidget(i, 2, dimsItem); + m_filesTable->setCellWidget(i, 0, loadCheckbox); + + connect(loadCheckbox, SIGNAL(toggled(bool)), this, SLOT(RecheckCompatibleFiles(bool))); + + /* // This is for allowing modality switching in the study browser + auto modalitySwitcher = new QComboBox; + modalitySwitcher->setTooltip(QString("Select the image modality")); + for (int i = 0; i < CAPTK::ImageModalityType::IMAGE_TYPE_FEATURES + 1; i++) + { + modalitySwitcher->insertItem(i, CAPTK::ImageModalityString[i]); + } + */ + + } +} + +void fStudyBrowserDialog::OnSelectionChanged() +{ + int selected = m_filesTable->currentRow(); + // Update metadata textbox with the corresponding data + std::string data = std::get<2>(mFileListing[selected]); + m_metadataDisplay->setPlainText(QString::fromStdString(data)); +} + +void fStudyBrowserDialog::RecheckCompatibleFiles(bool state) +{ + int firstCheckedItemIndex; // all images to be loaded must be compatible, so using the first checked is fine + int numChecked = 0; + for (int i = 0; i < mFileListing.size(); i++) + { + QCheckBox* cb = (QCheckBox*)m_filesTable->cellWidget(i, 0); + if (cb->isChecked()) + { + firstCheckedItemIndex = i; + numChecked++; + } + } + + if (numChecked == 0) // No items checked, no need to check compatibility + { + // Make sure all checkboxes are re-enabled just to be safe + for (int i = 0; i < mFileListing.size(); i++) + { + QCheckBox* cb = (QCheckBox*)m_filesTable->cellWidget(i, 0); + cb->setEnabled(true); + cb->setCheckable(true); + } + // And then disable the confirm button, since there's nothing to do! + m_confirmButton->setEnabled(false); + return; + } + else // Make sure confirm button is enabled if necessary + { + m_confirmButton->setEnabled(true); + } + + + // Now do the actual compatibility determination from the matrix + for (int i = 0; i < mFileListing.size(); i++) + { + QCheckBox* cb = (QCheckBox*)m_filesTable->cellWidget(i, 0); + if (mCompatibilityWithoutRegistrationMatrix[i][firstCheckedItemIndex]) + { + // Allow the user to check it, but don't enforce it + cb->setEnabled(true); + cb->setCheckable(true); + } + else + { + // Don't even allow a user to check these unless registration is enabled (TBD) + cb->setChecked(false); + cb->setCheckable(false); + cb->setEnabled(false); + } + } +} diff --git a/src/view/gui/fStudyBrowserDialog.h b/src/view/gui/fStudyBrowserDialog.h new file mode 100644 index 000000000..ab3914387 --- /dev/null +++ b/src/view/gui/fStudyBrowserDialog.h @@ -0,0 +1,71 @@ +/////////////////////////////////////////////////////////////////////////////////////// +// fStudyBrowserDialog.h +// +// Copyright (c) 2021. All rights reserved. +// Center for Biomedical Image Computing and Analytics +// Department of Radiology +// Perelman School of Medicine +// University of Pennsylvania +// +// Contact details: software@cbica.upenn.edu +// +// License Agreement: https://www.med.upenn.edu/cbica/software-agreement.html +/////////////////////////////////////////////////////////////////////////////////////// + +#ifndef _fStudyBrowserDialog_h_ +#define _fStudyBrowserDialog_h_ + + +//#include "CAPTk.h" +#include "ui_fStudyBrowserDialog.h" +#include +#include +#include +#include "cbicaITKImageInfo.h" + +/** +\class fStudyBrowserDialog + +\brief This class controls the elements in the study browser dialog used for displaying information on selected images. +The dialog should show files as a group, display information from JSON header files if available, and offer users the ability to register images if needed. + +*/ + +class fStudyBrowserDialog : public QDialog, private Ui::fStudyBrowserDialog +{ + Q_OBJECT + +public: + fStudyBrowserDialog(); + ~fStudyBrowserDialog(); + + std::string mLoggerFile; + std::vector> mFileListing; + std::vector> mCompatibilityWithoutRegistrationMatrix; + QStringList mFinalNiftiFiles; + QStringList mFinalJsonFiles; + + // tuples are filename, jsonfile, metadata string, image info structure + + /*! \brief Given a list of file paths, search for related JSON files and display image info. + + This is useful for finding DICOM metadata after dcm2niix is used (generating a NIfTI image and JSON BIDS header file). + \param fileList QStringList containing the list of filepaths. + \return bool : true if metadata was found for all files, false otherwise + */ + bool LoadInfoFromFilepaths(const QStringList& fileList); + void GenerateFileCompatibilityMatrix(); + void GenerateTable(); + QStringList GetFinalNiftiFilesAsQStringList(); + QStringList GetFinalJsonFilesAsQStringList(); + +public slots: + void OnConfirmButtonPressed(); + void OnSelectionChanged(); + void RecheckCompatibleFiles(bool state); + + + +}; + +#endif diff --git a/src/view/gui/ui_fDrawingPanel.h b/src/view/gui/ui_fDrawingPanel.h index c0c5d1488..a95975ef5 100644 --- a/src/view/gui/ui_fDrawingPanel.h +++ b/src/view/gui/ui_fDrawingPanel.h @@ -65,6 +65,11 @@ class Ui_fDrawingPanel QLineEdit* changeNewValues; QPushButton* changeButton; QPushButton* applyMaskButton; + QGroupBox* othersGroup; + + QGroupBox* algorithmGroup; + QPushButton* runGeodesicTrainingSegmentationButton; + QPushButton* finalizeSegmentationButton; void setupUi(QWidget *parent) { @@ -249,7 +254,7 @@ class Ui_fDrawingPanel shapesLayout->addLayout(shapesLayout2); shapesGroup->setLayout(shapesLayout); - QGroupBox* othersGroup = new QGroupBox("Operations"); + //QGroupBox* othersGroup = new QGroupBox("Operations"); QHBoxLayout* operationsHLayout = new QHBoxLayout(); QVBoxLayout* othersLayout = new QVBoxLayout(); QVBoxLayout* operationsV2Layout = new QVBoxLayout(); @@ -293,10 +298,26 @@ class Ui_fDrawingPanel operationsV2Layout->addWidget(changeGroup); //operations group box has a horizontal layout + othersGroup = new QGroupBox("Label + Mask Operations"); operationsHLayout->addLayout(othersLayout); operationsHLayout->addLayout((operationsV2Layout)); othersGroup->setLayout(operationsHLayout); + algorithmGroup = new QGroupBox("Interactive Segmentation"); + auto algorithmVLayout = new QVBoxLayout(algorithmGroup); + algorithmGroup->setLayout(algorithmVLayout); + + runGeodesicTrainingSegmentationButton = new QPushButton("Run Geodesic Training Segmentation"); + runGeodesicTrainingSegmentationButton->setToolTip("Iterate on the current segmentation and receive a new result."); + finalizeSegmentationButton = new QPushButton("Finalize Segmentation + Extract Features"); + finalizeSegmentationButton->setToolTip("Save the current segmentation and proceed to radiomic feature extraction."); + algorithmVLayout->addWidget(runGeodesicTrainingSegmentationButton); + algorithmVLayout->addWidget(finalizeSegmentationButton); + algorithmGroup->setHidden(true); // hide by default + + + + HelpButton = new QPushButton(); HelpButton->setIcon(QIcon((iconDir + "help.png").c_str())); HelpButton->setToolTip("Get Help"); @@ -310,6 +331,7 @@ class Ui_fDrawingPanel subLayout->addWidget(shapesGroup); subLayout->addWidget(drawPropertiesGroup); subLayout->addWidget(othersGroup); + subLayout->addWidget(algorithmGroup); subLayout->addStretch(); subLayout->addLayout(helpLayout); diff --git a/src/view/gui/ui_fFeaturePanel.h b/src/view/gui/ui_fFeaturePanel.h index 570e09b04..a16314bd3 100644 --- a/src/view/gui/ui_fFeaturePanel.h +++ b/src/view/gui/ui_fFeaturePanel.h @@ -80,13 +80,20 @@ class Ui_fFeaturePanel QRadioButton *radio2; QLineEdit* m_roi; + QLabel* m_roi_desc; QLineEdit* m_roi_label; + QLabel* m_roi_label_desc; + QLabel* m_maskSelectionInstructions; + QGroupBox* instructionsGroup; QCheckBox* m_verticalConcat; QPushButton* m_btnBrowseSaveFile; QCheckBox* csv_format; QCheckBox* xml_format; + QGroupBox* saveGroup_file; + QGroupBox* saveGroup; + //std::vector< std::string > m_featureFiles; std::string m_tempFolderLocation; @@ -288,20 +295,34 @@ class Ui_fFeaturePanel imageselect->setLayout(featureLayoutin); // QLabel* label1 = new QLabel("Mask Selector:"); - maskselect->setToolTip(QString("Allows user to select a particular mask value for which the features has to be exracted.ex:If mask has 3 labels and user wants label 2")); + maskselect->setToolTip(QString("Allows user to select a particular mask value for which the features has to be extracted.ex:If mask has 3 labels and user wants label 2 only.\ + \nLabel Color + Corresponding Values: \nRed = 1, Green = 2\nYellow = 3, Blue = 4\nMagenta = 5, Cyan = 6\nLight Red = 7, Light Green = 8\n Light Blue = 9")); m_roi= new QLineEdit(std::string("1").c_str()); - m_roi->setToolTip(QString("Value(s) of Label to extract features from")); + m_roi->setToolTip(QString("Value(s) of Label to extract features from.\n\ + Label Color + Corresponding Values: \nRed = 1, Green = 2\nYellow = 3, Blue = 4\nMagenta = 5, Cyan = 6\nLight Red = 7, Light Green = 8\n Light Blue = 9")); m_roi->setAlignment(Qt::AlignCenter | Qt::AlignVCenter); m_roi->setFixedWidth(buttonWidth + 25); + m_roi_desc = new QLabel("ROI Label Values"); m_roi_label = new QLineEdit(std::string("LabelText").c_str()); m_roi_label->setToolTip(QString("Name(s) of corresponding Labels")); m_roi_label->setAlignment(Qt::AlignCenter | Qt::AlignVCenter); m_roi_label->setFixedWidth(buttonWidth + buttonWidth / 5); + m_roi_label_desc = new QLabel("ROI Label Names"); + m_maskSelectionInstructions = new QLabel("The \"ROI Values\" field should reflect the labels in your segmentations for which you want to extract features, in the form of a comma-separated list.\nBy default, this field is populated with all labels present in your segmentation when you selected to finalize the segmentation. If you wish to make edits, you can use the following label color correspondence table:\n\nLabel Color + Corresponding Values: \nRed = 1, Green = 2\nYellow = 3, Blue = 4\nMagenta = 5, Cyan = 6\nLight Red = 7, Light Green = 8\n Light Blue = 9\n\nThe \"ROI Label Names\" field should reflect the anatomical meaning of the corresponding label (order matters).\nFor example, if you segment a tumor in Red (value 1) and the rest of the brain in Green (value 2), if ROI Values are \"1, 2\", then ROI Label Names may be \"Tumor, Brain\". You can use the default values to guide you. "); + m_maskSelectionInstructions->setWordWrap(true); + instructionsGroup = new QGroupBox("Instructions"); + QVBoxLayout* instructionsLayout = new QVBoxLayout(); + instructionsLayout->addWidget(m_maskSelectionInstructions); + instructionsGroup->setLayout(instructionsLayout); + instructionsGroup->setHidden(true); + instructionsGroup->setFixedWidth(400); QVBoxLayout* featureLayoutmask = new QVBoxLayout(); // featureLayoutmask->addWidget(label1); + featureLayoutmask->addWidget(m_roi_desc); featureLayoutmask->addWidget(m_roi); + featureLayoutmask->addWidget(m_roi_label_desc); featureLayoutmask->addWidget(m_roi_label); featureLayoutmask->addStretch(); maskselect->setLayout(featureLayoutmask); @@ -315,8 +336,8 @@ class Ui_fFeaturePanel // Browse output fileName - QGroupBox* saveGroup = new QGroupBox("Output"); - QGroupBox* saveGroup_file = new QGroupBox("File Selector"); + saveGroup = new QGroupBox("Output"); + saveGroup_file = new QGroupBox("File Selector"); QHBoxLayout* flLayout = new QHBoxLayout(); // QLabel* label = new QLabel("FileName:"); m_btnBrowseSaveFile = new QPushButton("..."); @@ -369,6 +390,7 @@ class Ui_fFeaturePanel subLayout->addWidget(featureGroup); subLayout->addWidget(selectionGroup); subLayout->addWidget(saveGroup); + subLayout->addWidget(instructionsGroup); subLayout->addStretch(); subLayout->addLayout(helpLayout); diff --git a/src/view/gui/ui_fStudyBrowserDialog.h b/src/view/gui/ui_fStudyBrowserDialog.h new file mode 100644 index 000000000..a15cf788e --- /dev/null +++ b/src/view/gui/ui_fStudyBrowserDialog.h @@ -0,0 +1,112 @@ +#ifndef UI_FSTUDYBROWSERDIALOG_H +#define UI_FSTUDYBROWSERDIALOG_H + +#include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// #include +// NEW CHANGES +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "CaPTkGUIUtils.h" + +QT_BEGIN_NAMESPACE + +class Ui_fStudyBrowserDialog +{ +public: + + QTableWidget * m_filesTable; + QTextEdit* m_metadataDisplay; + QPushButton* m_confirmButton; + + void setupUi(QWidget *fStudyBrowserDialog) + { + //Columns: + // File name, detected modality where possible, dimensions (N: XxYxZ), load(checkbox), + // register (radio button), 2D Stack?(checkbox) + const int pixelPad = 10; + m_filesTable = new QTableWidget(); + m_filesTable->setColumnCount(4); + m_filesTable->setSelectionBehavior(QAbstractItemView::SelectRows); + m_filesTable->setSelectionMode(QAbstractItemView::SingleSelection); + m_filesTable->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_filesTable->setHorizontalHeaderItem(3, new QTableWidgetItem("Name")); + m_filesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Image Type")); + m_filesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Dimensions")); + m_filesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Load?")); + m_filesTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + m_filesTable->horizontalHeader()->setStretchLastSection(true); + m_filesTable->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_filesTable->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + //m_filesTable->setHorizontalHeaderItem(4, new QTableWidgetItem("Register")); + //m_filesTable->setHorizontalHeaderItem(5, new QTableWidgetItem("2D Stack?")); + + m_metadataDisplay = new QTextEdit(); + m_metadataDisplay->setReadOnly(true); + m_metadataDisplay->setPlainText(""); + + m_confirmButton = new QPushButton("Load Selected Images"); + + + //--------------------- Layout------------------------------------------- + QVBoxLayout* visLayout = new QVBoxLayout(); + QHBoxLayout* visLayoutSub = new QHBoxLayout(); + visLayoutSub->addWidget(new QLabel("Images")); + visLayoutSub->addStretch(); + QHBoxLayout* buttonLayout = new QHBoxLayout(); + + buttonLayout->addWidget(m_confirmButton); + + + visLayout->addLayout(visLayoutSub); + visLayout->addWidget(m_filesTable); + QHBoxLayout* visLayoutSub2 = new QHBoxLayout(); + visLayoutSub2->addWidget(new QLabel("Selected Image Metadata")); + visLayoutSub2->addStretch(); + visLayoutSub2->addWidget(m_confirmButton); + visLayout->addLayout(visLayoutSub2); + visLayout->addWidget(m_metadataDisplay); + + + QHBoxLayout * mainLayout = new QHBoxLayout(fStudyBrowserDialog); + mainLayout->addLayout(visLayout); + } + +}; + +namespace Ui { + class fStudyBrowserDialog : public Ui_fStudyBrowserDialog {}; +} // namespace Ui + +QT_END_NAMESPACE + +#endif // UI_fStudyBrowserDialog_H