diff --git a/Modules/SegmentationUI/Qmitk/QmitkSetupVirtualEnvUtil.cpp b/Modules/SegmentationUI/Qmitk/QmitkSetupVirtualEnvUtil.cpp index 9fd02ade30..326bbc522f 100644 --- a/Modules/SegmentationUI/Qmitk/QmitkSetupVirtualEnvUtil.cpp +++ b/Modules/SegmentationUI/Qmitk/QmitkSetupVirtualEnvUtil.cpp @@ -1,183 +1,274 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file.s ============================================================================*/ #include "QmitkSetupVirtualEnvUtil.h" #include "mitkLog.h" #include <QStandardPaths> #include <itkCommand.h> +#include <regex> +#include <sstream> +#include <QDir> +#include <QApplication> +#include <mutex> QmitkSetupVirtualEnvUtil::QmitkSetupVirtualEnvUtil() { m_BaseDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QDir::separator() + qApp->organizationName() + QDir::separator(); } QmitkSetupVirtualEnvUtil::QmitkSetupVirtualEnvUtil(const QString &baseDir) { m_BaseDir = baseDir; } QString& QmitkSetupVirtualEnvUtil::GetBaseDir() { return m_BaseDir; } QString QmitkSetupVirtualEnvUtil::GetVirtualEnvPath() { return m_venvPath; } QString& QmitkSetupVirtualEnvUtil::GetSystemPythonPath() { return m_SysPythonPath; } QString& QmitkSetupVirtualEnvUtil::GetPythonPath() { return m_PythonPath; } QString& QmitkSetupVirtualEnvUtil::GetPipPath() { return m_PipPath; } void QmitkSetupVirtualEnvUtil::SetVirtualEnvPath(const QString &path) { m_venvPath = path; } void QmitkSetupVirtualEnvUtil::SetPipPath(const QString &path) { m_PipPath = path; } void QmitkSetupVirtualEnvUtil::SetPythonPath(const QString &path) { if (this->IsPythonPath(path)) { m_PythonPath = path; } else { MITK_INFO << "Python was not detected in " + path.toStdString(); } } void QmitkSetupVirtualEnvUtil::SetSystemPythonPath(const QString &path) { if (this->IsPythonPath(path)) { m_SysPythonPath = path; } else { MITK_INFO << "Python was not detected in " + path.toStdString(); } } void QmitkSetupVirtualEnvUtil::PrintProcessEvent(itk::Object * /*pCaller*/, const itk::EventObject &e, void *) { std::string testCOUT; std::string testCERR; const auto *pEvent = dynamic_cast<const mitk::ExternalProcessStdOutEvent *>(&e); if (pEvent) { testCOUT = testCOUT + pEvent->GetOutput(); MITK_INFO << testCOUT; } const auto *pErrEvent = dynamic_cast<const mitk::ExternalProcessStdErrEvent *>(&e); if (pErrEvent) { testCERR = testCERR + pErrEvent->GetOutput(); MITK_ERROR << testCERR; } } void QmitkSetupVirtualEnvUtil::InstallPytorch(const std::string &workingDir, void (*callback)(itk::Object *, const itk::EventObject &, void *)) { mitk::ProcessExecutor::ArgumentListType args; auto spExec = mitk::ProcessExecutor::New(); auto spCommand = itk::CStyleCommand::New(); spCommand->SetCallback(callback); spExec->AddObserver(mitk::ExternalProcessOutputEvent(), spCommand); args.push_back("-m"); args.push_back("pip"); args.push_back("install"); args.push_back("light-the-torch==0.7.5"); spExec->Execute(workingDir, "python", args); PipInstall("torch==2.0.0", workingDir, callback, "ltt"); PipInstall("torchvision==0.15.0", workingDir, callback, "ltt"); } void QmitkSetupVirtualEnvUtil::InstallPytorch() { this->InstallPytorch(GetPythonPath().toStdString(), &PrintProcessEvent); } void QmitkSetupVirtualEnvUtil::PipInstall(const std::string &library, const std::string &workingDir, void (*callback)(itk::Object *, const itk::EventObject &, void *), const std::string &command) { mitk::ProcessExecutor::ArgumentListType args; auto spExec = mitk::ProcessExecutor::New(); auto spCommand = itk::CStyleCommand::New(); spCommand->SetCallback(callback); spExec->AddObserver(mitk::ExternalProcessOutputEvent(), spCommand); args.push_back("install"); args.push_back(library); spExec->Execute(workingDir, command, args); } void QmitkSetupVirtualEnvUtil::PipInstall(const std::string &library, void (*callback)(itk::Object*, const itk::EventObject&, void*), const std::string& command) { this->PipInstall(library, this->GetPipPath().toStdString(), callback, command); } void QmitkSetupVirtualEnvUtil::ExecutePython(const std::string &pythonCode, const std::string &workingDir, void (*callback)(itk::Object *, const itk::EventObject &, void *), const std::string &command) { mitk::ProcessExecutor::ArgumentListType args; auto spExec = mitk::ProcessExecutor::New(); auto spCommand = itk::CStyleCommand::New(); spCommand->SetCallback(callback); spExec->AddObserver(mitk::ExternalProcessOutputEvent(), spCommand); args.push_back("-c"); args.push_back(pythonCode); spExec->Execute(workingDir, command, args); } void QmitkSetupVirtualEnvUtil::ExecutePython(const std::string &args, void (*callback)(itk::Object *, const itk::EventObject &, void *), const std::string &command) { this->ExecutePython(args, this->GetPythonPath().toStdString(), callback, command); } bool QmitkSetupVirtualEnvUtil::IsPythonPath(const QString &pythonPath) { QString fullPath = pythonPath; bool isExists = #ifdef _WIN32 QFile::exists(fullPath + QDir::separator() + QString("python.exe")); #else QFile::exists(fullPath + QDir::separator() + QString("python3")); #endif return isExists; } + +namespace +{ + std::mutex mutex; + std::string pyVersionCaptured; + + void CapturePyVersion(itk::Object * /*pCaller*/, const itk::EventObject &e, void *) + { + std::string testCOUT; + const auto *pEvent = dynamic_cast<const mitk::ExternalProcessStdOutEvent *>(&e); + if (pEvent) + { + pyVersionCaptured = pEvent->GetOutput(); + } + } + + std::vector<int> SplitVersionString(const std::string& version) + { + std::vector<int> splits; + std::string part; + std::istringstream tokenStream(version); + while (std::getline(tokenStream, part, '.')) + { + splits.push_back(std::stoi(part)); + } + return splits; + } + + bool IsSupported(const std::string& version, const std::string& low, const std::string& high) + { + std::vector<int> inHandVersion = SplitVersionString(version); + std::vector<int> targetLowVersion = SplitVersionString(low); + std::vector<int> targetHighVersion = SplitVersionString(high); + if (inHandVersion.size() > 1 && targetLowVersion.size() > 1 && targetHighVersion.size() > 1) + { // comparing second part of the version + return (inHandVersion[1] > targetLowVersion[1] && inHandVersion[1] < targetHighVersion[1]); + } + return false; + } +} + +std::pair<QString, QString> QmitkSetupVirtualEnvUtil::GetExactPythonPath(const QString &pyEnv) +{ + QString fullPath = pyEnv; + bool pythonDoesExist = false; + bool isSupportedVersion = false; +#ifdef _WIN32 + const std::string PYTHON_EXE = "python.exe"; + // check if python exist in given folder. + pythonDoesExist = QFile::exists(fullPath + QDir::separator() + QString::fromStdString(PYTHON_EXE)); + if (!pythonDoesExist && // check if in Scripts already, if not go there + !(fullPath.endsWith("Scripts", Qt::CaseInsensitive) || fullPath.endsWith("Scripts/", Qt::CaseInsensitive))) + { + fullPath += QDir::separator() + QString("Scripts"); + pythonDoesExist = QFile::exists(fullPath + QDir::separator() + QString("python.exe")); + } +#else + const std::string PYTHON_EXE = "python3"; + pythonDoesExist = QFile::exists(fullPath + QDir::separator() + QString::fromStdString(PYTHON_EXE)); + if (!pythonDoesExist && + !(fullPath.endsWith("bin", Qt::CaseInsensitive) || fullPath.endsWith("bin/", Qt::CaseInsensitive))) + { + fullPath += QDir::separator() + QString("bin"); + pythonDoesExist = QFile::exists(fullPath + QDir::separator() + QString("python3")); + } +#endif + std::pair<QString, QString> pythonPath; + if (pythonDoesExist) + { + ::mutex.lock(); + std::regex sanitizer(R"([^3\.(\d+)])"); + mitk::ProcessExecutor::ArgumentListType args; + auto spExec = mitk::ProcessExecutor::New(); + auto spCommand = itk::CStyleCommand::New(); + spCommand->SetCallback(&::CapturePyVersion); + spExec->AddObserver(mitk::ExternalProcessOutputEvent(), spCommand); + args.push_back("--version"); + spExec->Execute(fullPath.toStdString(), PYTHON_EXE, args); + std::string pyVersionNumber = std::regex_replace(::pyVersionCaptured, sanitizer, ""); + isSupportedVersion = ::IsSupported(pyVersionNumber, "3.8", "3.13"); + pythonPath.second = QString::fromStdString(pyVersionNumber); + ::mutex.unlock(); + } + pythonPath.first = pythonDoesExist &&isSupportedVersion ? fullPath : ""; + return pythonPath; +} diff --git a/Modules/SegmentationUI/Qmitk/QmitkSetupVirtualEnvUtil.h b/Modules/SegmentationUI/Qmitk/QmitkSetupVirtualEnvUtil.h index 3f822b66dc..518fc5addc 100644 --- a/Modules/SegmentationUI/Qmitk/QmitkSetupVirtualEnvUtil.h +++ b/Modules/SegmentationUI/Qmitk/QmitkSetupVirtualEnvUtil.h @@ -1,195 +1,200 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file.s ============================================================================*/ #ifndef QmitkSetupVirtualEnvUtil_h_Included #define QmitkSetupVirtualEnvUtil_h_Included #include "mitkLog.h" #include "mitkProcessExecutor.h" #include <MitkSegmentationUIExports.h> #include <QString> -#include <QDir> -#include <QApplication> /** * @brief Abstract Class to Setup a python virtual environment and pip install required packages. * Derive this class for creating installer for the respective tool. */ class MITKSEGMENTATIONUI_EXPORT QmitkSetupVirtualEnvUtil { public: QmitkSetupVirtualEnvUtil(const QString& baseDir); QmitkSetupVirtualEnvUtil(); /** * @brief Implement the method in child class * to setup the virtual environment. */ virtual bool SetupVirtualEnv(const QString& venvName) = 0; /** * @brief Get the Virtual Env Path object. Override this method in the respective * tool installer class. * * @return QString */ virtual QString GetVirtualEnvPath(); /** * @brief Function to Pip install a library package given the location of * pip3 executable. * Any callback function can be passed to process the output. * * @param library * @param workingDir * @param callback * @param command */ void PipInstall(const std::string &library, const std::string &workingDir, void (*callback)(itk::Object *, const itk::EventObject &, void *), const std::string &command = "pip3"); /** * @brief Overloaded function to Pip install a library function. * * @param library * @param callback * @param command */ void PipInstall(const std::string &library, void (*callback)(itk::Object *, const itk::EventObject &, void *), const std::string &command = "pip3"); /** * @brief Function to execute any python code given a python path. * Any callback function can be passed to process the output. * * @param args * @param pythonPath * @param callback * @param command */ void ExecutePython(const std::string &args, const std::string &pythonPath, void (*callback)(itk::Object *, const itk::EventObject &, void *), const std::string &command = "python"); /** * @brief Overloaded function to Execute Python code. * Any callback function can be passed to process the output. * * @param args * @param callback * @param command */ void ExecutePython(const std::string &args, void (*callback)(itk::Object *, const itk::EventObject &, void *), const std::string &command = "python"); /** * @brief Installs pytorch using light-the-torch package, correctly identifying cuda version. * Requires location of pip3 executable. * Any callback function can be passed to process the output. * * @param workingDir * @param callback */ void InstallPytorch(const std::string &workingDir, void (*callback)(itk::Object *, const itk::EventObject &, void *)); /** * @brief Overloaded function to install pytorch using light-the-torch package, correctly * identifying cuda version. */ void InstallPytorch(); /** * @brief Get the Base Dir object * * @return QString& */ QString& GetBaseDir(); /** * @brief Get the System Python Path object * * @return QString& */ QString& GetSystemPythonPath(); /** * @brief Get the Python Path object * * @return QString& */ QString& GetPythonPath(); /** * @brief Get the Pip Path object * * @return QString& */ QString& GetPipPath(); /** * @brief Set the System Python Path object * * @param path */ void SetSystemPythonPath(const QString& path); /** * @brief Set the Python Path object * * @param path */ void SetPythonPath(const QString& path); /** * @brief Set the Pip Path object * * @param path */ void SetPipPath(const QString& path); /** * @brief Set the Virtual Env Path object * * @param path */ void SetVirtualEnvPath(const QString &path); /** * @brief Check if the path provide has python executable or not. * * @param pythonPath * @return true * @return false */ bool IsPythonPath(const QString &pythonPath); /** * @brief Function can be used as callback to simply print out all the process execution output * parsed out from itk::EventObject. * */ static void PrintProcessEvent(itk::Object *, const itk::EventObject &e, void *); + /** + * @brief Get the exact Python path and version for any OS from the virtual environment path. + * @return A pair of the exact python path and its Python version or empty, if an supported + * version of Python could not be found. + */ + static std::pair<QString, QString> GetExactPythonPath(const QString &pyEnv); + private: QString m_PythonPath; QString m_PipPath; QString m_BaseDir; QString m_venvPath; QString m_SysPythonPath; }; #endif diff --git a/Modules/SegmentationUI/Qmitk/QmitkTotalSegmentatorToolGUI.cpp b/Modules/SegmentationUI/Qmitk/QmitkTotalSegmentatorToolGUI.cpp index 8b24a854dc..a78bef468a 100644 --- a/Modules/SegmentationUI/Qmitk/QmitkTotalSegmentatorToolGUI.cpp +++ b/Modules/SegmentationUI/Qmitk/QmitkTotalSegmentatorToolGUI.cpp @@ -1,513 +1,497 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "QmitkTotalSegmentatorToolGUI.h" #include "mitkProcessExecutor.h" #include "mitkTotalSegmentatorTool.h" #include <QApplication> #include <QDir> #include <QDirIterator> #include <QFileDialog> #include <QIcon> #include <QmitkStyleManager.h> #include <QMessageBox> MITK_TOOL_GUI_MACRO(MITKSEGMENTATIONUI_EXPORT, QmitkTotalSegmentatorToolGUI, "") QmitkTotalSegmentatorToolGUI::QmitkTotalSegmentatorToolGUI() : QmitkMultiLabelSegWithPreviewToolGUIBase(), m_SuperclassEnableConfirmSegBtnFnc(m_EnableConfirmSegBtnFnc) { // Nvidia-smi command returning zero doesn't always imply lack of GPUs. // Pytorch uses its own libraries to communicate to the GPUs. Hence, only a warning can be given. if (m_GpuLoader.GetGPUCount() == 0) { std::string warning = "WARNING: No GPUs were detected on your machine. The TotalSegmentator tool can be very slow."; this->ShowErrorMessage(warning); } m_EnableConfirmSegBtnFnc = [this](bool enabled) { return !m_FirstPreviewComputation ? m_SuperclassEnableConfirmSegBtnFnc(enabled) : false; }; } void QmitkTotalSegmentatorToolGUI::ConnectNewTool(mitk::SegWithPreviewTool *newTool) { Superclass::ConnectNewTool(newTool); m_FirstPreviewComputation = true; } void QmitkTotalSegmentatorToolGUI::InitializeUI(QBoxLayout *mainLayout) { m_Controls.setupUi(this); #ifndef _WIN32 m_Controls.sysPythonComboBox->addItem("/usr/bin"); #endif this->AutoParsePythonPaths(); m_Controls.sysPythonComboBox->addItem("Select"); m_Controls.sysPythonComboBox->setCurrentIndex(0); m_Controls.pythonEnvComboBox->addItem("Select"); m_Controls.pythonEnvComboBox->setDuplicatesEnabled(false); m_Controls.pythonEnvComboBox->setDisabled(true); m_Controls.previewButton->setDisabled(true); m_Controls.statusLabel->setTextFormat(Qt::RichText); m_Controls.subtaskComboBox->addItems(VALID_TASKS); QString welcomeText; this->SetGPUInfo(); if (m_GpuLoader.GetGPUCount() != 0) { welcomeText = "<b>STATUS: </b><i>Welcome to TotalSegmentator tool. You're in luck: " + QString::number(m_GpuLoader.GetGPUCount()) + " GPU(s) were detected.</i>"; } else { welcomeText = "<b>STATUS: </b><i>Welcome to TotalSegmentator tool. Sorry, " + QString::number(m_GpuLoader.GetGPUCount()) + " GPUs were detected.</i>"; } connect(m_Controls.previewButton, SIGNAL(clicked()), this, SLOT(OnPreviewBtnClicked())); connect(m_Controls.clearButton, SIGNAL(clicked()), this, SLOT(OnClearInstall())); connect(m_Controls.installButton, SIGNAL(clicked()), this, SLOT(OnInstallBtnClicked())); connect(m_Controls.overrideBox, SIGNAL(stateChanged(int)), this, SLOT(OnOverrideChecked(int))); connect(m_Controls.pythonEnvComboBox, QOverload<int>::of(&QComboBox::activated), [=](int index) { OnPythonPathChanged(m_Controls.pythonEnvComboBox->itemText(index)); }); connect(m_Controls.sysPythonComboBox, QOverload<int>::of(&QComboBox::activated), [=](int index) { OnSystemPythonChanged(m_Controls.sysPythonComboBox->itemText(index)); }); QString lastSelectedPyEnv = m_Settings.value("TotalSeg/LastCustomPythonPath").toString(); if (!lastSelectedPyEnv.isEmpty() && lastSelectedPyEnv!= "Select") { m_Controls.pythonEnvComboBox->insertItem(0, lastSelectedPyEnv); } const QString storageDir = m_Installer.GetVirtualEnvPath(); m_IsInstalled = this->IsTotalSegmentatorInstalled(storageDir); if (m_IsInstalled) { - m_PythonPath = GetExactPythonPath(storageDir); + m_PythonPath = QmitkSetupVirtualEnvUtil::GetExactPythonPath(storageDir).first; m_Installer.SetVirtualEnvPath(m_PythonPath); this->EnableAll(m_IsInstalled); welcomeText += " TotalSegmentator is already found installed."; } else { welcomeText += " TotalSegmentator is not installed. Please click on \"Install TotalSegmentator\" above."; } this->WriteStatusMessage(welcomeText); QIcon deleteIcon = QmitkStyleManager::ThemeIcon(QStringLiteral(":/org_mitk_icons/icons/awesome/scalable/actions/edit-delete.svg")); QIcon arrowIcon = QmitkStyleManager::ThemeIcon(QStringLiteral(":/org_mitk_icons/icons/tango/scalable/actions/media-playback-start.svg")); m_Controls.clearButton->setIcon(deleteIcon); m_Controls.previewButton->setIcon(arrowIcon); mainLayout->addLayout(m_Controls.verticalLayout); Superclass::InitializeUI(mainLayout); } void QmitkTotalSegmentatorToolGUI::SetGPUInfo() { std::vector<QmitkGPUSpec> specs = m_GpuLoader.GetAllGPUSpecs(); for (const QmitkGPUSpec &gpuSpec : specs) { m_Controls.gpuComboBox->addItem(QString::number(gpuSpec.id) + ": " + gpuSpec.name + " (" + gpuSpec.memory + ")"); } if (specs.empty()) { m_Controls.gpuComboBox->setEditable(true); m_Controls.gpuComboBox->addItem(QString::number(0)); m_Controls.gpuComboBox->setValidator(new QIntValidator(0, 999, this)); } } unsigned int QmitkTotalSegmentatorToolGUI::FetchSelectedGPUFromUI() const { QString gpuInfo = m_Controls.gpuComboBox->currentText(); if (m_GpuLoader.GetGPUCount() == 0) { return static_cast<unsigned int>(gpuInfo.toInt()); } else { QString gpuId = gpuInfo.split(":", QString::SplitBehavior::SkipEmptyParts).first(); return static_cast<unsigned int>(gpuId.toInt()); } } void QmitkTotalSegmentatorToolGUI::EnableAll(bool isEnable) { m_Controls.previewButton->setEnabled(isEnable); m_Controls.subtaskComboBox->setEnabled(isEnable); m_Controls.installButton->setEnabled((!isEnable)); } void QmitkTotalSegmentatorToolGUI::OnInstallBtnClicked() { bool isInstalled = false; - QString systemPython = OnSystemPythonChanged(m_Controls.sysPythonComboBox->currentText()); - if (systemPython.isEmpty()) + const auto [path, version] = OnSystemPythonChanged(m_Controls.sysPythonComboBox->currentText()); + if (path.isEmpty()) { - this->WriteErrorMessage("<b>ERROR: </b>Couldn't find Python."); + this->WriteErrorMessage("<b>ERROR: </b>Couldn't find compatible Python."); + return; + } + // check if python 3.12 and ask for confirmation + if (version.startsWith("3.12") && + QMessageBox::No == QMessageBox::question( + nullptr, + "Installing TotalSegmentator", + QString("WARNING: This is an unsupported version of Python that may not work. " + "We recommend using a supported Python version between 3.9 and 3.11.\n\n" + "Continue anyway?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No)) + { + return; + } + this->WriteStatusMessage("<b>STATUS: </b>Installing TotalSegmentator..."); + m_Installer.SetSystemPythonPath(path); + isInstalled = m_Installer.SetupVirtualEnv(m_Installer.VENV_NAME); + if (isInstalled) + { + m_PythonPath = QmitkSetupVirtualEnvUtil::GetExactPythonPath(m_Installer.GetVirtualEnvPath()).first; + this->WriteStatusMessage("<b>STATUS: </b>Successfully installed TotalSegmentator."); } else { - this->WriteStatusMessage("<b>STATUS: </b>Installing TotalSegmentator..."); - m_Installer.SetSystemPythonPath(systemPython); - isInstalled = m_Installer.SetupVirtualEnv(m_Installer.VENV_NAME); - if (isInstalled) - { - m_PythonPath = this->GetExactPythonPath(m_Installer.GetVirtualEnvPath()); - this->WriteStatusMessage("<b>STATUS: </b>Successfully installed TotalSegmentator."); - } - else - { - this->WriteErrorMessage("<b>ERROR: </b>Couldn't install TotalSegmentator."); - } + this->WriteErrorMessage("<b>ERROR: </b>Couldn't install TotalSegmentator."); } this->EnableAll(isInstalled); } void QmitkTotalSegmentatorToolGUI::OnPreviewBtnClicked() { auto tool = this->GetConnectedToolAs<mitk::TotalSegmentatorTool>(); if (nullptr == tool) { return; } try { m_Controls.previewButton->setEnabled(false); qApp->processEvents(); if (!this->IsTotalSegmentatorInstalled(m_PythonPath)) { throw std::runtime_error(WARNING_TOTALSEG_NOT_FOUND); } bool isFast = m_Controls.fastBox->isChecked(); QString subTask = m_Controls.subtaskComboBox->currentText(); if (subTask != VALID_TASKS[0]) { isFast = true; } tool->SetPythonPath(m_PythonPath.toStdString()); tool->SetGpuId(FetchSelectedGPUFromUI()); tool->SetFast(isFast); tool->SetSubTask(subTask.toStdString()); this->WriteStatusMessage(QString("<b>STATUS: </b><i>Starting Segmentation task... This might take a while.</i>")); tool->UpdatePreview(); m_Controls.previewButton->setEnabled(true); m_FirstPreviewComputation = false; } catch (const std::exception &e) { std::stringstream errorMsg; errorMsg << "<b>STATUS: </b>Error while processing parameters for TotalSegmentator segmentation. Reason: " << e.what(); this->ShowErrorMessage(errorMsg.str()); this->WriteErrorMessage(QString::fromStdString(errorMsg.str())); m_Controls.previewButton->setEnabled(true); return; } catch (...) { std::string errorMsg = "Unkown error occured while generation TotalSegmentator segmentation."; this->ShowErrorMessage(errorMsg); m_Controls.previewButton->setEnabled(true); return; } this->SetLabelSetPreview(tool->GetPreviewSegmentation()); this->ActualizePreviewLabelVisibility(); this->WriteStatusMessage("<b>STATUS: </b><i>Segmentation task finished successfully.</i>"); QString pythonPathTextItem = m_Controls.pythonEnvComboBox->currentText(); if (!pythonPathTextItem.isEmpty() && pythonPathTextItem != "Select") // only cache if the prediction ended without errors. { QString lastSelectedPyEnv = m_Settings.value("TotalSeg/LastCustomPythonPath").toString(); if (lastSelectedPyEnv != pythonPathTextItem) { m_Settings.setValue("TotalSeg/LastCustomPythonPath", pythonPathTextItem); } } } void QmitkTotalSegmentatorToolGUI::ShowErrorMessage(const std::string &message, QMessageBox::Icon icon) { this->setCursor(Qt::ArrowCursor); QMessageBox *messageBox = new QMessageBox(icon, nullptr, message.c_str()); messageBox->exec(); delete messageBox; MITK_WARN << message; } void QmitkTotalSegmentatorToolGUI::WriteStatusMessage(const QString &message) { m_Controls.statusLabel->setText(message); m_Controls.statusLabel->setStyleSheet("font-weight: bold; color: white"); qApp->processEvents(); } void QmitkTotalSegmentatorToolGUI::WriteErrorMessage(const QString &message) { m_Controls.statusLabel->setText(message); m_Controls.statusLabel->setStyleSheet("font-weight: bold; color: red"); qApp->processEvents(); } bool QmitkTotalSegmentatorToolGUI::IsTotalSegmentatorInstalled(const QString &pythonPath) { QString fullPath = pythonPath; bool isPythonExists = false, isExists = false; #ifdef _WIN32 isPythonExists = QFile::exists(fullPath + QDir::separator() + QString("python.exe")); if (!(fullPath.endsWith("Scripts", Qt::CaseInsensitive) || fullPath.endsWith("Scripts/", Qt::CaseInsensitive))) { fullPath += QDir::separator() + QString("Scripts"); isPythonExists = (!isPythonExists) ? QFile::exists(fullPath + QDir::separator() + QString("python.exe")) : isPythonExists; } isExists = QFile::exists(fullPath + QDir::separator() + QString("TotalSegmentator.exe")) && isPythonExists; #else isPythonExists = QFile::exists(fullPath + QDir::separator() + QString("python3")); if (!(fullPath.endsWith("bin", Qt::CaseInsensitive) || fullPath.endsWith("bin/", Qt::CaseInsensitive))) { fullPath += QDir::separator() + QString("bin"); isPythonExists = (!isPythonExists) ? QFile::exists(fullPath + QDir::separator() + QString("python3")) : isPythonExists; } isExists = QFile::exists(fullPath + QDir::separator() + QString("TotalSegmentator")) && isPythonExists; #endif return isExists; } void QmitkTotalSegmentatorToolGUI::AutoParsePythonPaths() { QString homeDir = QDir::homePath(); std::vector<QString> searchDirs; #ifdef _WIN32 searchDirs.push_back(QString("C:") + QDir::separator() + QString("ProgramData") + QDir::separator() + QString("anaconda3")); #else // Add search locations for possible standard python paths here searchDirs.push_back(homeDir + QDir::separator() + "environments"); searchDirs.push_back(homeDir + QDir::separator() + "anaconda3"); searchDirs.push_back(homeDir + QDir::separator() + "miniconda3"); searchDirs.push_back(homeDir + QDir::separator() + "opt" + QDir::separator() + "miniconda3"); searchDirs.push_back(homeDir + QDir::separator() + "opt" + QDir::separator() + "anaconda3"); #endif for (QString searchDir : searchDirs) { if (searchDir.endsWith("anaconda3", Qt::CaseInsensitive)) { if (QDir(searchDir).exists()) { m_Controls.sysPythonComboBox->addItem("(base): " + searchDir); searchDir.append((QDir::separator() + QString("envs"))); } } for (QDirIterator subIt(searchDir, QDir::AllDirs, QDirIterator::NoIteratorFlags); subIt.hasNext();) { subIt.next(); QString envName = subIt.fileName(); if (!envName.startsWith('.')) // Filter out irrelevent hidden folders, if any. { - m_Controls.sysPythonComboBox->addItem("(" + envName + "): " + subIt.filePath()); + m_Controls.pythonEnvComboBox->addItem("(" + envName + "): " + subIt.filePath()); } } } } -QString QmitkTotalSegmentatorToolGUI::OnSystemPythonChanged(const QString &pyEnv) +std::pair<QString, QString> QmitkTotalSegmentatorToolGUI::OnSystemPythonChanged(const QString &pyEnv) { - QString pyPath; + std::pair<QString, QString> pyPath; if (pyEnv == QString("Select")) { m_Controls.previewButton->setDisabled(true); QString path = QFileDialog::getExistingDirectory(m_Controls.sysPythonComboBox->parentWidget(), "Python Path", "dir"); if (!path.isEmpty()) { this->OnSystemPythonChanged(path); // recall same function for new path validation bool oldState = m_Controls.sysPythonComboBox->blockSignals(true); // block signal firing while inserting item m_Controls.sysPythonComboBox->insertItem(0, path); m_Controls.sysPythonComboBox->setCurrentIndex(0); m_Controls.sysPythonComboBox->blockSignals( oldState); // unblock signal firing after inserting item. Remove this after Qt6 migration } } else { QString uiPyPath = this->GetPythonPathFromUI(pyEnv); - pyPath = this->GetExactPythonPath(uiPyPath); + pyPath = QmitkSetupVirtualEnvUtil::GetExactPythonPath(uiPyPath); } return pyPath; } void QmitkTotalSegmentatorToolGUI::OnPythonPathChanged(const QString &pyEnv) { if (pyEnv == QString("Select")) { m_Controls.previewButton->setDisabled(true); QString path = QFileDialog::getExistingDirectory(m_Controls.pythonEnvComboBox->parentWidget(), "Python Path", "dir"); if (!path.isEmpty()) { this->OnPythonPathChanged(path); // recall same function for new path validation bool oldState = m_Controls.pythonEnvComboBox->blockSignals(true); // block signal firing while inserting item m_Controls.pythonEnvComboBox->insertItem(0, path); m_Controls.pythonEnvComboBox->setCurrentIndex(0); m_Controls.pythonEnvComboBox->blockSignals( oldState); // unblock signal firing after inserting item. Remove this after Qt6 migration } } - else if (!this->IsTotalSegmentatorInstalled(pyEnv)) + else if (!this->IsTotalSegmentatorInstalled(this->GetPythonPathFromUI(pyEnv))) { this->ShowErrorMessage(WARNING_TOTALSEG_NOT_FOUND); m_Controls.previewButton->setDisabled(true); } else {// Show positive status meeage m_Controls.previewButton->setDisabled(false); QString uiPyPath = this->GetPythonPathFromUI(pyEnv); - m_PythonPath = this->GetExactPythonPath(uiPyPath); + m_PythonPath = QmitkSetupVirtualEnvUtil::GetExactPythonPath(uiPyPath).first; } } QString QmitkTotalSegmentatorToolGUI::GetPythonPathFromUI(const QString &pyUI) const { QString fullPath = pyUI; if (-1 != fullPath.indexOf(")")) { fullPath = fullPath.mid(fullPath.indexOf(")") + 2); } return fullPath.simplified(); } -QString QmitkTotalSegmentatorToolGUI::GetExactPythonPath(const QString &pyEnv) const -{ - QString fullPath = pyEnv; - bool isPythonExists = false; -#ifdef _WIN32 - isPythonExists = QFile::exists(fullPath + QDir::separator() + QString("python.exe")); - if (!isPythonExists && - !(fullPath.endsWith("Scripts", Qt::CaseInsensitive) || fullPath.endsWith("Scripts/", Qt::CaseInsensitive))) - { - fullPath += QDir::separator() + QString("Scripts"); - isPythonExists = QFile::exists(fullPath + QDir::separator() + QString("python.exe")); - } -#else - isPythonExists = QFile::exists(fullPath + QDir::separator() + QString("python3")); - if (!isPythonExists && !(fullPath.endsWith("bin", Qt::CaseInsensitive) || - fullPath.endsWith("bin/", Qt::CaseInsensitive))) - { - fullPath += QDir::separator() + QString("bin"); - isPythonExists = QFile::exists(fullPath + QDir::separator() + QString("python3")); - } -#endif - if (!isPythonExists) - { - fullPath.clear(); - } - return fullPath; -} - void QmitkTotalSegmentatorToolGUI::OnOverrideChecked(int state) { bool isEnabled = false; if (state == Qt::Checked) { isEnabled = true; m_Controls.previewButton->setDisabled(true); m_PythonPath.clear(); } else { m_PythonPath.clear(); m_Controls.previewButton->setDisabled(true); if (m_IsInstalled) { const QString pythonPath = m_Installer.GetVirtualEnvPath(); - m_PythonPath = this->GetExactPythonPath(pythonPath); + auto pathObject = QmitkSetupVirtualEnvUtil::GetExactPythonPath(pythonPath); + m_PythonPath = pathObject.first; this->EnableAll(m_IsInstalled); } } m_Controls.pythonEnvComboBox->setEnabled(isEnabled); } void QmitkTotalSegmentatorToolGUI::OnClearInstall() { QDir folderPath(m_Installer.GetVirtualEnvPath()); if (folderPath.removeRecursively()) { m_Controls.installButton->setEnabled(true); m_IsInstalled = false; if (!m_Controls.overrideBox->isChecked()) { m_Controls.previewButton->setEnabled(false); } } else { MITK_ERROR << "The virtual environment couldn't be removed. Please check if you have the required access privileges or, some other process is accessing the folders."; } } bool QmitkTotalSegmentatorToolInstaller::SetupVirtualEnv(const QString& venvName) { if (GetSystemPythonPath().isEmpty()) { return false; } QDir folderPath(GetBaseDir()); folderPath.mkdir(venvName); if (!folderPath.cd(venvName)) { return false; // Check if directory creation was successful. } mitk::ProcessExecutor::ArgumentListType args; auto spExec = mitk::ProcessExecutor::New(); auto spCommand = itk::CStyleCommand::New(); spCommand->SetCallback(&PrintProcessEvent); spExec->AddObserver(mitk::ExternalProcessOutputEvent(), spCommand); args.push_back("-m"); args.push_back("venv"); args.push_back(venvName.toStdString()); #ifdef _WIN32 QString pythonFile = GetSystemPythonPath() + QDir::separator() + "python.exe"; QString pythonExeFolder = "Scripts"; #else QString pythonFile = GetSystemPythonPath() + QDir::separator() + "python3"; QString pythonExeFolder = "bin"; #endif spExec->Execute(GetBaseDir().toStdString(), pythonFile.toStdString(), args); // Setup local virtual environment if (folderPath.cd(pythonExeFolder)) { this->SetPythonPath(folderPath.absolutePath()); this->SetPipPath(folderPath.absolutePath()); this->InstallPytorch(); for (auto &package : PACKAGES) { this->PipInstall(package.toStdString(), &PrintProcessEvent); } std::string pythonCode; // python syntax to check if torch is installed with CUDA. pythonCode.append("import torch;"); pythonCode.append("print('Pytorch was installed with CUDA') if torch.cuda.is_available() else print('PyTorch was " "installed WITHOUT CUDA');"); this->ExecutePython(pythonCode, &PrintProcessEvent); return true; } return false; } QString QmitkTotalSegmentatorToolInstaller::GetVirtualEnvPath() { return STORAGE_DIR + VENV_NAME; } diff --git a/Modules/SegmentationUI/Qmitk/QmitkTotalSegmentatorToolGUI.h b/Modules/SegmentationUI/Qmitk/QmitkTotalSegmentatorToolGUI.h index fb1442dfaa..ac0c787f4c 100644 --- a/Modules/SegmentationUI/Qmitk/QmitkTotalSegmentatorToolGUI.h +++ b/Modules/SegmentationUI/Qmitk/QmitkTotalSegmentatorToolGUI.h @@ -1,187 +1,181 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef QmitkTotalSegmentatorToolGUI_h_Included #define QmitkTotalSegmentatorToolGUI_h_Included #include "QmitkMultiLabelSegWithPreviewToolGUIBase.h" #include "QmitkSetupVirtualEnvUtil.h" #include "QmitknnUNetGPU.h" #include "ui_QmitkTotalSegmentatorGUIControls.h" #include <MitkSegmentationUIExports.h> #include <QMessageBox> #include <QSettings> #include <QStandardPaths> +#include <QDir> /** * @brief Installer class for TotalSegmentator Tool. * Class specifies the virtual environment name, install version, packages required to pip install * and implements SetupVirtualEnv method. * */ class QmitkTotalSegmentatorToolInstaller : public QmitkSetupVirtualEnvUtil { public: const QString VENV_NAME = ".totalsegmentator_v2"; const QString TOTALSEGMENTATOR_VERSION = "2.0.5"; const std::vector<QString> PACKAGES = {QString("Totalsegmentator==") + TOTALSEGMENTATOR_VERSION}; const QString STORAGE_DIR; inline QmitkTotalSegmentatorToolInstaller( const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QDir::separator() + qApp->organizationName() + QDir::separator()) : QmitkSetupVirtualEnvUtil(baseDir), STORAGE_DIR(baseDir){}; bool SetupVirtualEnv(const QString &) override; QString GetVirtualEnvPath() override; }; /** \ingroup org_mitk_gui_qt_interactivesegmentation_internal \brief GUI for mitk::TotalSegmentatorTool. \sa mitk:: */ class MITKSEGMENTATIONUI_EXPORT QmitkTotalSegmentatorToolGUI : public QmitkMultiLabelSegWithPreviewToolGUIBase { Q_OBJECT public: mitkClassMacro(QmitkTotalSegmentatorToolGUI, QmitkMultiLabelSegWithPreviewToolGUIBase); itkFactorylessNewMacro(Self); itkCloneMacro(Self); protected slots: /** * @brief Qt Slot */ void OnPreviewBtnClicked(); /** * @brief Qt Slot */ void OnPythonPathChanged(const QString &); /** * @brief Qt Slot */ - QString OnSystemPythonChanged(const QString &); + std::pair<QString, QString> OnSystemPythonChanged(const QString &); /** * @brief Qt Slot */ void OnInstallBtnClicked(); /** * @brief Qt Slot */ void OnOverrideChecked(int); /** * @brief Qt Slot */ void OnClearInstall(); protected: QmitkTotalSegmentatorToolGUI(); ~QmitkTotalSegmentatorToolGUI() = default; void ConnectNewTool(mitk::SegWithPreviewTool *newTool) override; void InitializeUI(QBoxLayout *mainLayout) override; /** * @brief Enable (or Disable) GUI elements. */ void EnableAll(bool); /** * @brief Searches and parses paths of python virtual enviroments * from predefined lookout locations */ void AutoParsePythonPaths(); /** * @brief Checks if TotalSegmentator command is valid in the selected python virtual environment. * * @return bool */ bool IsTotalSegmentatorInstalled(const QString &); /** * @brief Creates a QMessage object and shows on screen. */ void ShowErrorMessage(const std::string &, QMessageBox::Icon = QMessageBox::Critical); /** * @brief Writes any message in white on the tool pane. */ void WriteStatusMessage(const QString &); /** * @brief Writes any message in red on the tool pane. */ void WriteErrorMessage(const QString &); /** * @brief Adds GPU information to the gpu combo box. * In case, there aren't any GPUs avaialble, the combo box will be * rendered editable. */ void SetGPUInfo(); /** * @brief Returns GPU id of the selected GPU from the Combo box. * * @return unsigned int */ unsigned int FetchSelectedGPUFromUI() const; /** * @brief Get the virtual env path from UI combobox removing any * extra special characters. * * @return QString */ QString GetPythonPathFromUI(const QString &) const; - /** - * @brief Get the Exact Python Path for any OS - * from the virtual environment path. - * @return QString - */ - QString GetExactPythonPath(const QString &) const; - /** * @brief For storing values like Python path across sessions. */ QSettings m_Settings; QString m_PythonPath; QmitkGPULoader m_GpuLoader; Ui_QmitkTotalSegmentatorToolGUIControls m_Controls; bool m_FirstPreviewComputation = true; bool m_IsInstalled = false; EnableConfirmSegBtnFunctionType m_SuperclassEnableConfirmSegBtnFnc; const std::string WARNING_TOTALSEG_NOT_FOUND = "TotalSegmentator is not detected in the selected python environment.Please select a valid " "python environment or install TotalSegmentator."; const QStringList VALID_TASKS = { "total", "cerebral_bleed", "hip_implant", "coronary_arteries", "body", "lung_vessels", "pleural_pericard_effusion" }; QmitkTotalSegmentatorToolInstaller m_Installer; }; #endif diff --git a/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.cpp b/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.cpp index 3229b83a62..66bbb404e1 100644 --- a/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.cpp +++ b/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.cpp @@ -1,368 +1,356 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #include "QmitkSegmentAnythingPreferencePage.h" #include <mitkCoreServices.h> #include <mitkIPreferencesService.h> #include <mitkIPreferences.h> #include <mitkProcessExecutor.h> #include <itkCommand.h> #include <QmitkSegmentAnythingToolGUI.h> #include <QFileDialog> #include <QmitkStyleManager.h> #include <QDir> #include <QDirIterator> namespace { mitk::IPreferences* GetPreferences() { auto* preferencesService = mitk::CoreServices::GetPreferencesService(); return preferencesService->GetSystemPreferences()->Node("org.mitk.views.segmentation"); } } QmitkSegmentAnythingPreferencePage::QmitkSegmentAnythingPreferencePage() : m_Ui(new Ui::QmitkSegmentAnythingPreferencePage), m_Control(nullptr){} QmitkSegmentAnythingPreferencePage::~QmitkSegmentAnythingPreferencePage(){} void QmitkSegmentAnythingPreferencePage::Init(berry::IWorkbench::Pointer){} void QmitkSegmentAnythingPreferencePage::CreateQtControl(QWidget* parent) { m_Control = new QWidget(parent); m_Ui->setupUi(m_Control); + + m_Ui->samModelTipLabel->hide(); // TODO: All models except for vit_b seem to be unsupported by SAM? + #ifndef _WIN32 m_Ui->sysPythonComboBox->addItem("/usr/bin"); #endif this->AutoParsePythonPaths(); m_Ui->timeoutEdit->setValidator(new QIntValidator(0, 1000, this)); m_Ui->sysPythonComboBox->addItem("Select..."); m_Ui->sysPythonComboBox->setCurrentIndex(0); connect(m_Ui->installSAMButton, SIGNAL(clicked()), this, SLOT(OnInstallBtnClicked())); connect(m_Ui->clearSAMButton, SIGNAL(clicked()), this, SLOT(OnClearInstall())); connect(m_Ui->sysPythonComboBox, QOverload<int>::of(&QComboBox::activated), [=](int index) { OnSystemPythonChanged(m_Ui->sysPythonComboBox->itemText(index)); }); QIcon deleteIcon = QmitkStyleManager::ThemeIcon(QStringLiteral(":/org_mitk_icons/icons/awesome/scalable/actions/edit-delete.svg")); m_Ui->clearSAMButton->setIcon(deleteIcon); const QString storageDir = m_Installer.GetVirtualEnvPath(); bool isInstalled = QmitkSegmentAnythingToolGUI::IsSAMInstalled(storageDir); QString welcomeText; if (isInstalled) { - m_PythonPath = GetExactPythonPath(storageDir); + m_PythonPath = QmitkSetupVirtualEnvUtil::GetExactPythonPath(storageDir).first; m_Installer.SetVirtualEnvPath(m_PythonPath); welcomeText += " Segment Anything tool is already found installed."; m_Ui->installSAMButton->setEnabled(false); } else { welcomeText += " Segment Anything tool not installed. Please click on \"Install SAM\" above. \ The installation will create a new virtual environment using the System Python selected above."; m_Ui->installSAMButton->setEnabled(true); } this->WriteStatusMessage(welcomeText); m_Ui->samModelTypeComboBox->addItems(VALID_MODELS); m_Ui->gpuComboBox->addItem(CPU_ID); this->SetGPUInfo(); this->Update(); } QWidget* QmitkSegmentAnythingPreferencePage::GetQtControl() const { return m_Control; } bool QmitkSegmentAnythingPreferencePage::PerformOk() { auto* prefs = GetPreferences(); prefs->Put("sam parent path", m_Installer.STORAGE_DIR.toStdString()); prefs->Put("sam python path", m_PythonPath.toStdString()); prefs->Put("sam modeltype", m_Ui->samModelTypeComboBox->currentText().toStdString()); prefs->PutInt("sam gpuid", FetchSelectedGPUFromUI()); prefs->PutInt("sam timeout", std::stoi(m_Ui->timeoutEdit->text().toStdString())); return true; } void QmitkSegmentAnythingPreferencePage::PerformCancel(){} void QmitkSegmentAnythingPreferencePage::Update() { auto* prefs = GetPreferences(); m_Ui->samModelTypeComboBox->setCurrentText(QString::fromStdString(prefs->Get("sam modeltype", "vit_b"))); m_Ui->timeoutEdit->setText(QString::number(prefs->GetInt("sam timeout", 300))); int gpuId = prefs->GetInt("sam gpuid", -1); if (gpuId == -1) { m_Ui->gpuComboBox->setCurrentText(CPU_ID); } else if (m_GpuLoader.GetGPUCount() == 0) { m_Ui->gpuComboBox->setCurrentText(QString::number(gpuId)); } else { std::vector<QmitkGPUSpec> specs = m_GpuLoader.GetAllGPUSpecs(); QmitkGPUSpec gpuSpec = specs[gpuId]; m_Ui->gpuComboBox->setCurrentText(QString::number(gpuSpec.id) + ": " + gpuSpec.name + " (" + gpuSpec.memory + ")"); } } -QString QmitkSegmentAnythingPreferencePage::OnSystemPythonChanged(const QString &pyEnv) +std::pair<QString, QString> QmitkSegmentAnythingPreferencePage::OnSystemPythonChanged(const QString &pyEnv) { - QString pyPath; + std::pair<QString, QString> pyPath; if (pyEnv == QString("Select...")) { QString path = QFileDialog::getExistingDirectory(m_Ui->sysPythonComboBox->parentWidget(), "Python Path", "dir"); if (!path.isEmpty()) { this->OnSystemPythonChanged(path); // recall same function for new path validation bool oldState = m_Ui->sysPythonComboBox->blockSignals(true); // block signal firing while inserting item m_Ui->sysPythonComboBox->insertItem(0, path); m_Ui->sysPythonComboBox->setCurrentIndex(0); m_Ui->sysPythonComboBox->blockSignals(oldState); // unblock signal firing after inserting item. Remove this after Qt6 migration } } else { QString uiPyPath = this->GetPythonPathFromUI(pyEnv); - pyPath = this->GetExactPythonPath(uiPyPath); + pyPath = QmitkSetupVirtualEnvUtil::GetExactPythonPath(uiPyPath); } return pyPath; } QString QmitkSegmentAnythingPreferencePage::GetPythonPathFromUI(const QString &pyUI) const { QString fullPath = pyUI; if (-1 != fullPath.indexOf(")")) { fullPath = fullPath.mid(fullPath.indexOf(")") + 2); } return fullPath.simplified(); } -QString QmitkSegmentAnythingPreferencePage::GetExactPythonPath(const QString &pyEnv) const -{ - QString fullPath = pyEnv; - bool isPythonExists = false; -#ifdef _WIN32 - isPythonExists = QFile::exists(fullPath + QDir::separator() + QString("python.exe")); - if (!isPythonExists && - !(fullPath.endsWith("Scripts", Qt::CaseInsensitive) || fullPath.endsWith("Scripts/", Qt::CaseInsensitive))) - { - fullPath += QDir::separator() + QString("Scripts"); - isPythonExists = QFile::exists(fullPath + QDir::separator() + QString("python.exe")); - } -#else - isPythonExists = QFile::exists(fullPath + QDir::separator() + QString("python3")); - if (!isPythonExists && - !(fullPath.endsWith("bin", Qt::CaseInsensitive) || fullPath.endsWith("bin/", Qt::CaseInsensitive))) - { - fullPath += QDir::separator() + QString("bin"); - isPythonExists = QFile::exists(fullPath + QDir::separator() + QString("python3")); - } -#endif - if (!isPythonExists) - { - fullPath.clear(); - } - return fullPath; -} - void QmitkSegmentAnythingPreferencePage::AutoParsePythonPaths() { QString homeDir = QDir::homePath(); std::vector<QString> searchDirs; #ifdef _WIN32 searchDirs.push_back(QString("C:") + QDir::separator() + QString("ProgramData") + QDir::separator() + QString("anaconda3")); #else // Add search locations for possible standard python paths here searchDirs.push_back(homeDir + QDir::separator() + "anaconda3"); searchDirs.push_back(homeDir + QDir::separator() + "miniconda3"); searchDirs.push_back(homeDir + QDir::separator() + "opt" + QDir::separator() + "miniconda3"); searchDirs.push_back(homeDir + QDir::separator() + "opt" + QDir::separator() + "anaconda3"); #endif for (QString searchDir : searchDirs) { if (searchDir.endsWith("anaconda3", Qt::CaseInsensitive)) { if (QDir(searchDir).exists()) { m_Ui->sysPythonComboBox->addItem("(base): " + searchDir); searchDir.append((QDir::separator() + QString("envs"))); } } for (QDirIterator subIt(searchDir, QDir::AllDirs, QDirIterator::NoIteratorFlags); subIt.hasNext();) { subIt.next(); QString envName = subIt.fileName(); if (!envName.startsWith('.')) // Filter out irrelevent hidden folders, if any. { m_Ui->sysPythonComboBox->addItem("(" + envName + "): " + subIt.filePath()); } } } } void QmitkSegmentAnythingPreferencePage::SetGPUInfo() { std::vector<QmitkGPUSpec> specs = m_GpuLoader.GetAllGPUSpecs(); for (const QmitkGPUSpec &gpuSpec : specs) { m_Ui->gpuComboBox->addItem(QString::number(gpuSpec.id) + ": " + gpuSpec.name + " (" + gpuSpec.memory + ")"); } if (specs.empty()) { m_Ui->gpuComboBox->setCurrentIndex(m_Ui->gpuComboBox->findText("cpu")); } else { m_Ui->gpuComboBox->setCurrentIndex(m_Ui->gpuComboBox->count()-1); } } int QmitkSegmentAnythingPreferencePage::FetchSelectedGPUFromUI() const { QString gpuInfo = m_Ui->gpuComboBox->currentText(); if ("cpu" == gpuInfo) { return -1; } else if(m_GpuLoader.GetGPUCount() == 0) { return static_cast<int>(gpuInfo.toInt()); } else { QString gpuId = gpuInfo.split(":", QString::SplitBehavior::SkipEmptyParts).first(); return static_cast<int>(gpuId.toInt()); } } void QmitkSegmentAnythingPreferencePage::OnInstallBtnClicked() { - QString systemPython = OnSystemPythonChanged(m_Ui->sysPythonComboBox->currentText()); - if (!systemPython.isEmpty()) + const auto [path, version] = OnSystemPythonChanged(m_Ui->sysPythonComboBox->currentText()); + if (path.isEmpty()) { - this->WriteStatusMessage("<b>STATUS: </b>Installing SAM..."); - m_Ui->installSAMButton->setEnabled(false); - m_Installer.SetSystemPythonPath(systemPython); - bool isInstalled = false; - bool isFinished = m_Installer.SetupVirtualEnv(m_Installer.VENV_NAME); - if (isFinished) - { - isInstalled = QmitkSegmentAnythingToolGUI::IsSAMInstalled(m_Installer.GetVirtualEnvPath()); - } - if (isInstalled) - { - m_PythonPath = this->GetExactPythonPath(m_Installer.GetVirtualEnvPath()); - this->WriteStatusMessage("<b>STATUS: </b>Successfully installed SAM."); - } - else - { - this->WriteErrorMessage("<b>ERROR: </b>Couldn't install SAM."); - m_Ui->installSAMButton->setEnabled(true); - } + this->WriteErrorMessage("<b>ERROR: </b>Couldn't find compatible Python."); + return; + } + //check if python 3.12 and ask for confirmation + if (version.startsWith("3.12") && + QMessageBox::No == QMessageBox::question(nullptr, + "Installing Segment Anything", + QString("WARNING: This is an unsupported version of Python that may not work. " + "We recommend using a supported Python version between 3.9 and 3.11.\n\n" + "Continue anyway?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No)) + { + return; + } + this->WriteStatusMessage("<b>STATUS: </b>Installing SAM..."); + m_Ui->installSAMButton->setEnabled(false); + m_Installer.SetSystemPythonPath(path); + bool isInstalled = false; + if (m_Installer.SetupVirtualEnv(m_Installer.VENV_NAME)) + { + isInstalled = QmitkSegmentAnythingToolGUI::IsSAMInstalled(m_Installer.GetVirtualEnvPath()); + } + if (isInstalled) + { + m_PythonPath = QmitkSetupVirtualEnvUtil::GetExactPythonPath(m_Installer.GetVirtualEnvPath()).first; + this->WriteStatusMessage("<b>STATUS: </b>Successfully installed SAM."); + } + else + { + this->WriteErrorMessage("<b>ERROR: </b>Couldn't install SAM."); + m_Ui->installSAMButton->setEnabled(true); } } void QmitkSegmentAnythingPreferencePage::OnClearInstall() { QDir folderPath(m_Installer.GetVirtualEnvPath()); bool isDeleted = folderPath.removeRecursively(); if (isDeleted) { this->WriteStatusMessage("Deleted SAM installation."); m_Ui->installSAMButton->setEnabled(true); m_PythonPath.clear(); } else { MITK_ERROR << "The virtual environment couldn't be removed. Please check if you have the required access " "privileges or, some other process is accessing the folders."; } } void QmitkSegmentAnythingPreferencePage::WriteStatusMessage(const QString &message) { m_Ui->samInstallStatusLabel->setText(message); m_Ui->samInstallStatusLabel->setStyleSheet("font-weight: bold; color: white"); qApp->processEvents(); } void QmitkSegmentAnythingPreferencePage::WriteErrorMessage(const QString &message) { m_Ui->samInstallStatusLabel->setText(message); m_Ui->samInstallStatusLabel->setStyleSheet("font-weight: bold; color: red"); qApp->processEvents(); } QString QmitkSAMInstaller::GetVirtualEnvPath() { return STORAGE_DIR + VENV_NAME; } bool QmitkSAMInstaller::SetupVirtualEnv(const QString &venvName) { if (GetSystemPythonPath().isEmpty()) { return false; } QDir folderPath(GetBaseDir()); folderPath.mkdir(venvName); if (!folderPath.cd(venvName)) { return false; // Check if directory creation was successful. } mitk::ProcessExecutor::ArgumentListType args; auto spExec = mitk::ProcessExecutor::New(); auto spCommand = itk::CStyleCommand::New(); spCommand->SetCallback(&PrintProcessEvent); spExec->AddObserver(mitk::ExternalProcessOutputEvent(), spCommand); args.push_back("-m"); args.push_back("venv"); args.push_back(venvName.toStdString()); #ifdef _WIN32 QString pythonFile = GetSystemPythonPath() + QDir::separator() + "python.exe"; QString pythonExeFolder = "Scripts"; #else QString pythonFile = GetSystemPythonPath() + QDir::separator() + "python3"; QString pythonExeFolder = "bin"; #endif spExec->Execute(GetBaseDir().toStdString(), pythonFile.toStdString(), args); // Setup local virtual environment if (folderPath.cd(pythonExeFolder)) { this->SetPythonPath(folderPath.absolutePath()); this->SetPipPath(folderPath.absolutePath()); this->InstallPytorch(); for (auto &package : PACKAGES) { this->PipInstall(package.toStdString(), &PrintProcessEvent); } std::string pythonCode; // python syntax to check if torch is installed with CUDA. pythonCode.append("import torch;"); pythonCode.append("print('Pytorch was installed with CUDA') if torch.cuda.is_available() else print('PyTorch was " "installed WITHOUT CUDA');"); this->ExecutePython(pythonCode, &PrintProcessEvent); return true; } return false; } diff --git a/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.h b/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.h index 3a4118eaec..cbb758eed5 100644 --- a/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.h +++ b/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.h @@ -1,114 +1,108 @@ /*============================================================================ The Medical Imaging Interaction Toolkit (MITK) Copyright (c) German Cancer Research Center (DKFZ) All rights reserved. Use of this source code is governed by a 3-clause BSD license that can be found in the LICENSE file. ============================================================================*/ #ifndef QmitkSegmentAnythingPreferencePage_h #define QmitkSegmentAnythingPreferencePage_h #include <berryIQtPreferencePage.h> #include <QmitknnUNetGPU.h> #include <QString> #include <QmitkSetupVirtualEnvUtil.h> #include <QStandardPaths> #include <ui_QmitkSegmentAnythingPreferencePage.h> +#include <QDir> class QWidget; namespace Ui { class QmitkSegmentAnythingPreferencePage; } class QmitkSAMInstaller : public QmitkSetupVirtualEnvUtil { public: const QString VENV_NAME = ".sam"; const QString SAM_VERSION = "1.0"; // currently, unused const std::vector<QString> PACKAGES = {QString("git+https://github.com/MIC-DKFZ/agent-sam.git@v0.1")}; const QString STORAGE_DIR; inline QmitkSAMInstaller( const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QDir::separator() + qApp->organizationName() + QDir::separator()) : QmitkSetupVirtualEnvUtil(baseDir), STORAGE_DIR(baseDir){}; bool SetupVirtualEnv(const QString &) override; QString GetVirtualEnvPath() override; }; class QmitkSegmentAnythingPreferencePage : public QObject, public berry::IQtPreferencePage { Q_OBJECT Q_INTERFACES(berry::IPreferencePage) public: QmitkSegmentAnythingPreferencePage(); ~QmitkSegmentAnythingPreferencePage() override; void Init(berry::IWorkbench::Pointer workbench) override; void CreateQtControl(QWidget* parent) override; QWidget* GetQtControl() const override; bool PerformOk() override; void PerformCancel() override; void Update() override; private slots: void OnInstallBtnClicked(); void OnClearInstall(); - QString OnSystemPythonChanged(const QString&); + std::pair<QString, QString> OnSystemPythonChanged(const QString &); protected: /** * @brief Searches and parses paths of python virtual enviroments * from predefined lookout locations */ void AutoParsePythonPaths(); /** * @brief Get the virtual env path from UI combobox removing any * extra special characters. * * @return QString */ QString GetPythonPathFromUI(const QString &) const; - /** - * @brief Get the Exact Python Path for any OS - * from the virtual environment path. - * @return QString - */ - QString GetExactPythonPath(const QString &) const; - /** * @brief Adds GPU information to the gpu combo box. * In case, there aren't any GPUs avaialble, the combo box will be * rendered editable. */ void SetGPUInfo(); /** * @brief Returns GPU id of the selected GPU from the Combo box. * @return int */ int FetchSelectedGPUFromUI() const; void WriteStatusMessage(const QString &); void WriteErrorMessage(const QString &); private: Ui::QmitkSegmentAnythingPreferencePage* m_Ui; QmitkSAMInstaller m_Installer; QWidget* m_Control; QmitkGPULoader m_GpuLoader; QString m_PythonPath; const QString CPU_ID = "cpu"; const QStringList VALID_MODELS = {"vit_b", "vit_l", "vit_h"}; }; #endif diff --git a/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.ui b/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.ui index b68da828a3..647fec49e6 100644 --- a/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.ui +++ b/Plugins/org.mitk.gui.qt.segmentation/src/QmitkSegmentAnythingPreferencePage.ui @@ -1,153 +1,153 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>QmitkSegmentAnythingPreferencePage</class> <widget class="QWidget" name="QmitkSegmentAnythingPreferencePage"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>656</width> <height>779</height> </rect> </property> <property name="windowTitle"> <string>Form</string> </property> <layout class="QFormLayout" name="formLayout"> <item row="0" column="0"> <widget class="QLabel" name="invisibleLabel"/> </item> <item row="0" column="1"> <layout class="QGridLayout" name="gridLayout"> <item row="0" column="0" colspan="2"> <widget class="QPushButton" name="installSAMButton"> <property name="sizePolicy"> <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="text"> <string>Install Segment Anything</string> </property> </widget> </item> <item row="0" column="2" colspan="2"> <widget class="QPushButton" name="clearSAMButton"> <property name="sizePolicy"> <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="text"> <string>Clear Install</string> </property> </widget> </item> <item row="1" column="0"> <widget class="QLabel" name="pythonLabel"> <property name="text"> - <string>System Python</string> + <string>System Python (v3.9 - v3.11)</string> </property> </widget> </item> <item row="1" column="1" colspan="3"> <widget class="ctkComboBox" name="sysPythonComboBox" native="true"/> </item> <item row="2" column="0"> <widget class="QLabel" name="samModelTypeLabel"> <property name="text"> <string>Model Type</string> </property> </widget> </item> <item row="2" column="1" colspan="3"> <widget class="ctkComboBox" name="samModelTypeComboBox" native="true"/> </item> <item row="3" column="1" colspan="3"> <widget class="QLabel" name="samModelTipLabel"> <property name="text"> <string><html><head/><body><p><span style=" font-style:italic; color:#808080;">Tip: Select vit_b for VRAM &lt; 4GB, vit_l for VRAM &lt; 6GB or vit_h for VRAM &gt; 6GB.</span></p></body></html></string> </property> <property name="textFormat"> <enum>Qt::RichText</enum> </property> <property name="wordWrap"> <bool>true</bool> </property> </widget> </item> <item row="4" column="1" colspan="3"> <widget class="ctkComboBox" name="gpuComboBox" native="true"/> </item> <item row="4" column="0"> <widget class="QLabel" name="gpuSpinLabel"> <property name="sizePolicy"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="text"> <string>Device Id:</string> </property> </widget> </item> <item row="5" column="1" colspan="3"> <widget class="QLineEdit" name="timeoutEdit"> <property name="text"> <string>300</string> </property> </widget> </item> <item row="5" column="0"> <widget class="QLabel" name="timeoutLabel"> <property name="sizePolicy"> <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> </property> <property name="text"> <string>Time Out (s):</string> </property> </widget> </item> <item row="6" column="0" colspan="4"> <widget class="QLabel" name="samInstallStatusLabel"> <property name="textFormat"> <enum>Qt::RichText</enum> </property> <property name="wordWrap"> <bool>true</bool> </property> </widget> </item> </layout> </item> <item row="1" column="0"> <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> </property> <property name="sizeHint" stdset="0"> <size> <width>20</width> <height>40</height> </size> </property> </spacer> </item> </layout> </widget> <customwidgets> <customwidget> <class>ctkComboBox</class> <extends>QWidget</extends> <header>ctkComboBox.h</header> </customwidget> </customwidgets> <resources/> <connections/> </ui>