Skip to content

Commit

Permalink
ENH: Introduce Visual DICOM Browser with CTK Update
Browse files Browse the repository at this point in the history
This enhancement includes an update to CTK and introduces the `ctkDICOMVisualBrowserWidget`
to augment DICOM exploration, query, and retrieval capabilities within the DICOM
module. The widget is seamlessly integrated into the Slicer DICOM module and is
labeled as experimental.

For a comprehensive overview and future plans, refer to the roadmap at
commontk/CTK#1162.

By default, the widget is disabled, and users can access the familiar `ctkDICOMBrowser`
when opening the `DICOM` module. To enable the experimental feature, users can
toggle the option in the dropdown menu of the `Show DICOM database` pushbutton
in the `DICOM` module UI.

In `SlicerDICOMBrowser`, this introduces an instance of `ctkDICOMVisualBrowserWidget`
alongside the existing `ctkDICOMBrowser` instance. When users activate the
experimental feature, widget visibilities are adjusted accordingly. Both widgets
share the same DICOM folder set in the DICOM settings.

List of CTK changes:

```
$ git shortlog 45f33c81..88ff72b9 --group=author --group=trailer:co-authored-by --no-merges
Andras Lasso (1):
      ENH: Add Visual DICOM Browser (#1165)

Davide Punzo (1):
      ENH: Add Visual DICOM Browser (#1165)

Jean-Christophe Fillion-Robin (1):
      ENH: Add Visual DICOM Browser (#1165)
```

Co-authored-by: Andras Lasso <lasso@queensu.ca>
Co-authored-by: Jean-Christophe Fillion-Robin <jchris.fillionr@kitware.com>
  • Loading branch information
3 people committed Jan 19, 2024
1 parent e0326d4 commit f0138cd
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 14 deletions.
65 changes: 56 additions & 9 deletions Modules/Scripted/DICOM/DICOM.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from slicer.ScriptedLoadableModule import *

import DICOMLib
from DICOMLib import DICOMUtils
from slicer.i18n import tr as _
from slicer.i18n import translate

Expand Down Expand Up @@ -160,8 +161,6 @@ def onURLReceived(self, urlString):
slicer.util.selectModule("DICOM")
slicer.app.processEvents()

from DICOMLib import DICOMUtils

importedSeriesInstanceUIDs = DICOMUtils.importFromDICOMWeb(
dicomWebEndpoint=queryMap["dicomweb_endpoint"],
studyInstanceUID=queryMap["studyUID"],
Expand Down Expand Up @@ -307,7 +306,9 @@ def onLayoutChanged(self, viewArrangement):
dataProbe = mw.findChild("QWidget", "DataProbeCollapsibleWidget") if mw else None
if self.currentViewArrangement == slicer.vtkMRMLLayoutNode.SlicerLayoutDicomBrowserView:
# View has been changed to the DICOM browser view
useExpertimentalVisualDICOMBrowser = settingsValue("DICOM/UseExpertimentalVisualDICOMBrowser", False, converter=toBool)
self.browserWidget.show()
self.browserWidget.toggleBrowsers(useExpertimentalVisualDICOMBrowser)
# If we are in DICOM module, hide the Data Probe to have more space for the module
try:
inDicomModule = slicer.modules.dicom.widgetRepresentation().isEntered
Expand Down Expand Up @@ -391,6 +392,20 @@ def __init__(self, parent):
parent.registerProperty(
"DICOM/detailedLogging", detailedLoggingMapper,
"valueAsInt", str(qt.SIGNAL("valueAsIntChanged(int)")))
detailedLoggingCheckBox.stateChanged.connect(self.onDetailedLoggingStateChanged)

thumbnailsSizeComboBox = ctk.ctkComboBox()
thumbnailsSizeComboBox.toolTip = _(
"Determines the relative size of the thumbnails when using the visual DICOM browser")
thumbnailsSizeComboBox.addItem(_("Small"), "small")
thumbnailsSizeComboBox.addItem(_("Medium"), "medium")
thumbnailsSizeComboBox.addItem(_("Large"), "large")
thumbnailsSizeComboBox.currentIndex = 1
genericGroupBoxFormLayout.addRow(_("Thumbnails size:"), thumbnailsSizeComboBox)
parent.registerProperty(
"DICOM/thumbnailsSize", thumbnailsSizeComboBox,
"currentUserDataAsString", str(qt.SIGNAL("currentIndexChanged(int)")),
_("DICOM settings"), ctk.ctkSettingsPanel.OptionRequireRestart)

vBoxLayout.addWidget(genericGroupBox)

Expand All @@ -404,6 +419,12 @@ def __init__(self, parent):
plugins[pluginName].settingsPanelEntry(parent, pluginGroupBox)
vBoxLayout.addStretch(1)

def onDetailedLoggingStateChanged(self, detailedLoggingState):
if detailedLoggingState == qt.Qt.Checked:
ctk.ctk.setDICOMLogLevel(ctk.ctkErrorLogLevel.Debug)
else:
ctk.ctk.setDICOMLogLevel(ctk.ctkErrorLogLevel.Warning)


class DICOMSettingsPanel(ctk.ctkSettingsPanel):
def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -571,11 +592,12 @@ def setup(self):
self.layout.addWidget(uiWidget)
self.ui = slicer.util.childWidgetVariables(uiWidget)

# Add SlicerDICOMBrowser
self.browserWidget = DICOMLib.SlicerDICOMBrowser()
self.browserWidget.objectName = "SlicerDICOMBrowser"

slicer.modules.DICOMInstance.setBrowserWidgetInDICOMLayout(self.browserWidget)

# Setup layout manager
layoutManager = slicer.app.layoutManager()
if layoutManager is not None:
layoutManager.layoutChanged.connect(self.onLayoutChanged)
Expand All @@ -600,6 +622,22 @@ def setup(self):
importButtonMenu.addAction(self.copyOnImportAction)
self.copyOnImportAction.connect("toggled(bool)", self.copyOnImportToggled)

# Add show options menu to showBrowser button

showBrowserButtonMenu = qt.QMenu(_("Show options"), self.ui.showBrowserButton)
showBrowserButtonMenu.toolTipsVisible = True
self.ui.showBrowserButton.setMenu(showBrowserButtonMenu)

self.toggleVisualBrowserAction = qt.QAction(_("Show experimental visual DICOM browser"), showBrowserButtonMenu)
self.toggleVisualBrowserAction.setToolTip(_("If enabled, the DICOM browser widget will be substituted with new experimental visual browser."))
self.toggleVisualBrowserAction.setCheckable(True)
self.toggleVisualBrowserAction.checked = settingsValue("DICOM/UseExpertimentalVisualDICOMBrowser", False, converter=toBool)
showBrowserButtonMenu.addAction(self.toggleVisualBrowserAction)
self.toggleVisualBrowserAction.connect("toggled(bool)", self.onShowBrowser)
self.onShowBrowser()

# Connect subjectHierarchyTree

self.ui.subjectHierarchyTree.setMRMLScene(slicer.mrmlScene)
self.ui.subjectHierarchyTree.currentItemChanged.connect(self.onCurrentItemChanged)
self.ui.subjectHierarchyTree.currentItemModified.connect(self.onCurrentItemModified)
Expand All @@ -615,7 +653,6 @@ def setup(self):

self.ui.toggleListener.connect("toggled(bool)", self.onToggleListener)

settings = qt.QSettings()
self.ui.runListenerAtStart.checked = settingsValue("DICOM/RunListenerAtStart", False, converter=toBool)
self.ui.runListenerAtStart.connect("toggled(bool)", self.onRunListenerAtStart)

Expand Down Expand Up @@ -703,7 +740,7 @@ def enter(self):

def exit(self):
self.removeListenerObservers()
self.browserWidget.close()
self.closeBrowser()

def addListenerObservers(self):
if not hasattr(slicer, "dicomListener"):
Expand Down Expand Up @@ -747,14 +784,18 @@ def onCurrentItemModified(self, id):
return

if oldSubjectHierarchyCurrentVisibility != self.subjectHierarchyCurrentVisibility and self.subjectHierarchyCurrentVisibility:
self.browserWidget.close()
self.closeBrowser()

def toggleBrowserWidget(self):
if self.ui.showBrowserButton.checked:
self.onOpenBrowserWidget()
self.onShowBrowser()
else:
if self.browserWidget:
self.browserWidget.close()
self.closeBrowser()

def closeBrowser(self):
if self.browserWidget:
self.browserWidget.close()

def aboutToShowImportOptionsMenu(self):
self.copyOnImportAction.checked = self.browserWidget.dicomBrowser.ImportDirectoryMode == ctk.ctkDICOMBrowser.ImportDirectoryCopy
Expand All @@ -765,6 +806,12 @@ def copyOnImportToggled(self, copyOnImport):
else:
self.browserWidget.dicomBrowser.ImportDirectoryMode = ctk.ctkDICOMBrowser.ImportDirectoryAddLink

def onShowBrowser(self):
useExpertimentalVisualDICOMBrowser = self.toggleVisualBrowserAction.checked
settings = qt.QSettings()
settings.setValue("DICOM/UseExpertimentalVisualDICOMBrowser", useExpertimentalVisualDICOMBrowser)
self.browserWidget.toggleBrowsers(useExpertimentalVisualDICOMBrowser)

def importFolder(self):
if not DICOMFileDialog.createDefaultDatabase():
return
Expand Down Expand Up @@ -870,7 +917,7 @@ def onRunListenerAtStart(self, toggled):
settings.setValue("DICOM/RunListenerAtStart", toggled)

def updateDatabaseDirectoryFromWidget(self, databaseDirectory):
self.browserWidget.dicomBrowser.databaseDirectory = databaseDirectory
self.browserWidget.setDatabaseDirectory(databaseDirectory)

def updateDatabaseDirectoryFromBrowser(self, databaseDirectory):
wasBlocked = self.ui.directoryButton.blockSignals(True)
Expand Down
8 changes: 4 additions & 4 deletions Modules/Scripted/DICOM/Resources/UI/DICOM.ui
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>372</width>
<width>622</width>
<height>663</height>
</rect>
</property>
Expand Down Expand Up @@ -46,9 +46,9 @@
</widget>
</item>
<item>
<widget class="QPushButton" name="showBrowserButton">
<widget class="ctkMenuButton" name="showBrowserButton">
<property name="toolTip">
<string>Show DICOM database browser window</string>
<string>Import files into DICOM database</string>
</property>
<property name="text">
<string> Show DICOM database</string>
Expand All @@ -59,7 +59,7 @@
</property>
<property name="iconSize">
<size>
<width>32</width>
<width>64</width>
<height>32</height>
</size>
</property>
Expand Down
46 changes: 46 additions & 0 deletions Modules/Scripted/DICOMLib/DICOMBrowser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import os

import ctk
import qt

import slicer
Expand All @@ -11,6 +12,7 @@
from slicer.i18n import tr as _

import DICOMLib
from DICOMLib import DICOMUtils


#########################################################
Expand Down Expand Up @@ -51,6 +53,19 @@ def __init__(self, dicomBrowser=None, parent="mainWindow"):

self.dicomBrowser = dicomBrowser if dicomBrowser is not None else slicer.app.createDICOMBrowserForMainDatabase()

# Add ctkVisualDICOMBrowser
self.visualBrowserWidget = ctk.ctkDICOMVisualBrowserWidget()
self.visualBrowserWidget.findChild(ctk.ctkCollapsibleGroupBox, "ActionsCollapsibleGroupBox").hide()
if settingsValue("DICOM/thumbnailsSize", False) == "large":
self.visualBrowserWidget.thumbnailSize = ctk.ctkDICOMStudyItemWidget.Large
elif settingsValue("DICOM/thumbnailsSize", False) == "medium":
self.visualBrowserWidget.thumbnailSize = ctk.ctkDICOMStudyItemWidget.Medium
elif settingsValue("DICOM/thumbnailsSize", False) == "small":
self.visualBrowserWidget.thumbnailSize = ctk.ctkDICOMStudyItemWidget.Small

if settingsValue("DICOM/detailedLogging", False, converter=toBool):
ctk.ctk.setDICOMLogLevel(ctk.ctkErrorLogLevel.Debug)

self.browserPersistent = settingsValue("DICOM/BrowserPersistent", False, converter=toBool)
self.advancedView = settingsValue("DICOM/advancedView", 0, converter=int)
self.horizontalTables = settingsValue("DICOM/horizontalTables", 0, converter=int)
Expand All @@ -65,6 +80,18 @@ def __init__(self, dicomBrowser=None, parent="mainWindow"):
self.dicomBrowser.dicomTableManager().connect("studiesDoubleClicked(QModelIndex)", self.patientStudySeriesDoubleClicked)
self.dicomBrowser.dicomTableManager().connect("seriesDoubleClicked(QModelIndex)", self.patientStudySeriesDoubleClicked)

self.visualBrowserWidget.setDatabaseDirectory(self.dicomBrowser.databaseDirectory)
self.visualBrowserWidget.seriesRetrieved.connect(self.onSeriesRetrieved)
self.visualBrowserWidget.connect("sendRequested(QStringList)", self.onSend)

def onSeriesRetrieved(self, seriesInstanceUIDs):
seriesList = [str(seriesInstanceUID) for seriesInstanceUID in seriesInstanceUIDs]
if seriesList is None or not seriesList:
return
nodes = DICOMUtils.loadSeriesByUID(seriesList)
if len(nodes) > 0 and not settingsValue("DICOM/BrowserPersistent", False, converter=toBool):
self.close()

def open(self):
self.show()

Expand All @@ -76,6 +103,18 @@ def onSend(self, fileList):
if len(fileList):
sendDialog = DICOMLib.DICOMSendDialog(fileList, self)

def setDatabaseDirectory(self, databaseDirectory):
self.dicomBrowser.databaseDirectory = databaseDirectory
self.visualBrowserWidget.setDatabaseDirectory(databaseDirectory)

def toggleBrowsers(self, useExpertimentalVisualDICOMBrowser):
self.visualBrowserWidget.visible = useExpertimentalVisualDICOMBrowser
self.dicomBrowser.visible = not useExpertimentalVisualDICOMBrowser
self.loadableTableFrame.visible = not useExpertimentalVisualDICOMBrowser
self.actionButtonsFrame.visible = not useExpertimentalVisualDICOMBrowser
if useExpertimentalVisualDICOMBrowser:
self.visualBrowserWidget.onShowPatients()

def setup(self, showPreview=False):
"""
main window is a frame with widgets from the app
Expand All @@ -95,6 +134,13 @@ def setup(self, showPreview=False):
self.dicomBrowser.dicomTableManager().tableOrientation = qt.Qt.Horizontal if horizontal else qt.Qt.Vertical
self.layout().addWidget(self.dicomBrowser)

self.visualBrowserWidget.sendActionVisible = True
# Fix rendering groupbox
self.visualBrowserWidget.serverSettingsGroupBox().setChecked(True)
self.visualBrowserWidget.serverSettingsGroupBox().setChecked(False)
self.visualBrowserWidget.databaseDirectorySettingsKey = slicer.dicomDatabaseDirectorySettingsKey
self.layout().addWidget(self.visualBrowserWidget)

self.userFrame = qt.QWidget()
self.preview = qt.QWidget()

Expand Down
2 changes: 1 addition & 1 deletion SuperBuild/External_CTK.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ if(NOT DEFINED CTK_DIR AND NOT Slicer_USE_SYSTEM_${proj})

ExternalProject_SetIfNotDefined(
Slicer_${proj}_GIT_TAG
"45f33c81d8f60d2ac4b749cb21828974a73a4f76"
"88ff72b9c2b1e57cf4f822ea2e547be5b9a32b98"
QUIET
)

Expand Down

0 comments on commit f0138cd

Please sign in to comment.