Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Improve pre-processing error handling for UI user friendliness #153

Merged
merged 2 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 22 additions & 96 deletions AutoscoperM/AutoscoperM.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
)
from slicer.util import VTKObservationMixin

from AutoscoperMLib import IO, SubVolumeExtraction
from AutoscoperMLib import IO, SubVolumeExtraction, Validation

#
# AutoscoperM
Expand Down Expand Up @@ -447,17 +447,16 @@ def onGeneratePartialVolumes(self):
trackingSubDir = self.ui.trackingSubDir.text
modelSubDir = self.ui.modelSubDir.text
segmentationNode = self.ui.pv_SegNodeComboBox.currentNode()
if not self.logic.validateInputs(

Validation.validateInputs(
volumeNode=volumeNode,
segmentationNode=segmentationNode,
mainOutputDir=mainOutputDir,
volumeSubDir=tiffSubDir,
transformSubDir=tfmSubDir,
trackingSubDir=trackingSubDir,
modelSubDir=modelSubDir,
):
raise ValueError("Invalid inputs")
return
)

self.logic.createPathsIfNotExists(
mainOutputDir,
Expand Down Expand Up @@ -503,7 +502,7 @@ def onGenerateConfig(self):
camCalList = self.ui.camCalList

# Validate the inputs
if not self.logic.validateInputs(
Validation.validateInputs(
volumeNode=volumeNode,
mainOutputDir=mainOutputDir,
configFileName=configFileName,
Expand All @@ -513,17 +512,13 @@ def onGenerateConfig(self):
trialList=trialList,
partialVolumeList=partialVolumeList,
camCalList=camCalList,
):
raise ValueError("Invalid inputs")
return
if not self.logic.validatePaths(
)
Validation.validatePaths(
mainOutputDir=mainOutputDir,
tiffDir=os.path.join(mainOutputDir, tiffSubDir),
radiographSubDir=os.path.join(mainOutputDir, radiographSubDir),
calibDir=os.path.join(mainOutputDir, calibrationSubDir),
):
raise ValueError("Invalid paths")
return
)

def get_staged_items(listWidget):
staged_items = []
Expand Down Expand Up @@ -600,17 +595,16 @@ def get_checked_items(listWidget):
self.ui.voxelSizeZ.value,
]

# Validate the inputs
if not self.logic.validateInputs(
# Validate the extracted parameters
Validation.validateInputs(
*trialDirs,
*partialVolumeFiles,
*camCalFiles,
*optimizationOffsets,
*volumeFlip,
*renderResolution,
*voxel_spacing,
):
raise ValueError("Invalid inputs")
)

# generate the config file
IO.generateConfigFile(
Expand Down Expand Up @@ -639,15 +633,13 @@ def onImportModels(self):

volumeNode = self.ui.volumeSelector.currentNode()

if not self.logic.validateInputs(voluemNode=volumeNode):
raise ValueError("Invalid inputs")
return
Validation.validateInputs(volumeNode=volumeNode)

if self.ui.segSTL_loadRadioButton.isChecked():
segmentationFileDir = self.ui.segSTL_modelsDir.currentPath
if not self.logic.validatePaths(segmentationFileDir=segmentationFileDir):
raise ValueError("Invalid paths")
return

Validation.validatePaths(segmentationFileDir=segmentationFileDir)

segmentationFiles = glob.glob(os.path.join(segmentationFileDir, "*.*"))
segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode")
segmentationNode.CreateDefaultDisplayNodes()
Expand All @@ -660,7 +652,7 @@ def onImportModels(self):
slicer.mrmlScene.RemoveNode(returnedNode)
self.ui.progressBar.setValue((idx + 1) / len(segmentationFiles) * 100)
else: # Should never happen but just in case
raise Exception("Please select the 'Segmentation From Model' option in order to import models")
raise ValueError("Please select the 'Segmentation From Model' option in order to import models")
return
slicer.util.messageBox("Success!")

Expand All @@ -674,9 +666,7 @@ def onSegmentation(self):

volumeNode = self.ui.volumeSelector.currentNode()

if not self.logic.validateInputs(voluemNode=volumeNode):
raise ValueError("Invalid inputs")
return
Validation.validateInputs(volumeNode=volumeNode)

if self.ui.segGen_autoRadioButton.isChecked():
currentVolumeNode = volumeNode
Expand All @@ -702,7 +692,7 @@ def onSegmentation(self):
slicer.mrmlScene.RemoveNode(segmentationNode)
currentVolumeNode = self.logic.getNextItemInSequence(volumeNode)
else: # Should never happen but just in case
raise Exception("Please select the 'Automatic Segmentation' option in order to generate segmentations")
raise ValueError("Please select the 'Automatic Segmentation' option in order to generate segmentations")
return
slicer.util.messageBox("Success!")

Expand All @@ -725,7 +715,7 @@ def onLoadPV(self):
tfms_scale = glob.glob(os.path.join(mainOutputDir, transformSubDir, "*_scale.tfm"))

if len(vols) == 0:
raise Exception("No data found")
raise ValueError("No data found")
return

if len(vols) != len(tfms_t) != len(tfms_scale):
Expand Down Expand Up @@ -801,18 +791,14 @@ def populateListFromOutputSubDir(self, listWidget, fileSubDir, itemType="file"):
listWidget.clear()

mainOutputDir = self.ui.mainOutputSelector.currentPath
if not self.logic.validateInputs(
Validation.validateInputs(
listWidget=listWidget,
mainOutputDir=mainOutputDir,
fileSubDir=fileSubDir,
):
raise ValueError("Invalid inputs")
return
)

fileDir = os.path.join(mainOutputDir, fileSubDir)
if not self.logic.validatePaths(fileDir=fileDir):
raise ValueError(f"Invalid input: subdirectory '{fileDir}' does not exist.")
return
Validation.validatePaths(fileDir=fileDir)

if itemType == "file":
listFiles = [f.name for f in os.scandir(fileDir) if os.path.isfile(f)]
Expand Down Expand Up @@ -1181,66 +1167,6 @@ def showVolumeIn3D(volumeNode: slicer.vtkMRMLVolumeNode):
logic.UpdateDisplayNodeFromVolumeNode(displayNode, volumeNode)
slicer.mrmlScene.RemoveNode(slicer.util.getNode("Volume rendering ROI"))

@staticmethod
def validateInputs(*args: tuple, **kwargs: dict) -> bool:
"""
Validates that the provided inputs are not None.

:param args: list of inputs to validate
:param kwargs: list of inputs to validate

:return: True if all inputs are valid, False otherwise
"""
statuses = []
for arg in args:
status = True
if arg is None:
logging.error(f"{arg} is None")
status = False
if isinstance(arg, str) and arg == "":
logging.error(f"{arg} is an empty string")
status = False
statuses.append(status)

for name, arg in kwargs.items():
status = True
if arg is None:
logging.error(f"{name} is None")
status = False
if isinstance(arg, str) and arg == "":
logging.error(f"{name} is an empty string")
status = False
statuses.append(status)

return all(statuses)

@staticmethod
def validatePaths(*args: tuple, **kwargs: dict) -> bool:
"""
Checks that the provided paths exist.

:param args: list of paths to validate
:param kwargs: list of paths to validate

:return: True if all paths exist, False otherwise
"""
statuses = []
for arg in args:
status = True
if not os.path.exists(arg):
logging.error(f"{arg} does not exist")
status = False
statuses.append(status)

for name, path in kwargs.items():
status = True
if not os.path.exists(path):
logging.error(f"{name} ({path}) does not exist")
status = False
statuses.append(status)

return all(statuses)

@staticmethod
def createPathsIfNotExists(*args: tuple) -> None:
"""
Expand Down
64 changes: 64 additions & 0 deletions AutoscoperM/AutoscoperMLib/Validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import os


class ValueErrorsException(Exception):
"""A custom exception class that accepts a list of errors."""

def __init__(self, errors):
if not isinstance(errors, list):
raise ValueError("The errors input must be a list")
if len(errors) < 1:
raise ValueError("The errors list must contain at least one error")
self.errors = errors
super().__init__("\n".join(errors))

def __str__(self):
err_str = "Invalid value{}".format("" if len(self.errors) == 0 else "s")

return err_str + ":\n" + "\n".join(self.errors)


def validateInputs(*args: tuple, **kwargs: dict) -> None:
"""
Validates that the provided inputs are not None or empty.

:param args: list of inputs to validate
:param kwargs: dictionary of inputs to validate
:raises: ValueErrorsException
"""
errors = []
for arg in args:
if arg is None:
errors.append("Input argument is None")
if isinstance(arg, str) and arg == "":
errors.append("Input argument is an empty string")

for name, arg in kwargs.items():
if arg is None:
errors.append(f"Input '{name}' is None")
if isinstance(arg, str) and arg == "":
errors.append(f"Input '{name}' is an empty string")

if len(errors) > 0:
raise ValueErrorsException(errors)


def validatePaths(*args: tuple, **kwargs: dict) -> None:
"""
Checks that the provided paths exist.

:param args: list of paths to validate
:param kwargs: list of paths to validate
:raises: ValueErrorsException
"""
errors = []
for arg in args:
if not os.path.exists(arg):
errors.append(f"Input path '{arg}' does not exist")

for name, path in kwargs.items():
if not os.path.exists(path):
errors.append(f"Input path '{name}' ({path}) does not exist")

if len(errors) > 0:
raise ValueErrorsException(errors)
1 change: 1 addition & 0 deletions AutoscoperM/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ set(MODULE_PYTHON_SCRIPTS
${MODULE_NAME}Lib/__init__.py
${MODULE_NAME}Lib/IO.py
${MODULE_NAME}Lib/SubVolumeExtraction.py
${MODULE_NAME}Lib/Validation.py
)

set(MODULE_PYTHON_RESOURCES
Expand Down
Loading