From 52a6b2d77e4caaede4a75f724dfe0281693e4cbf Mon Sep 17 00:00:00 2001 From: Shelly Belsky <71195502+sbelsk@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:16:41 -0500 Subject: [PATCH] ENH: Update Pre-Processing UI to improve BVR workflow (#141) Improves user workflow in the Pre-Processing module, and refactors the BVR configuration file generation UI. The changes include: * 'Volume Node' and 'Output Directory' at top of UI were swapped * 'Trial Name' moved to the config section as the ''.cfg field * Segmentation generation UI redesigned to better convey the two separate generation methods * 'Adavnced Options' renamed to 'Default Subdirectories', moved below the 'Segmentation Generation' section, and rendered as collapsed by default * 'VRG Resolution' fields from 'Advanced Options' moved to the config generation section * 'Generate Config' section: added file and path selectors to specify the camera calibration files, radiograph root directories, and volume files. Also added 'Voxel Size' fields that are automatically populated based on the selected input volume node. * Elements in .ui file were edited to be properly indexed and named Co-authored-by: Amy M Morton --- AutoscoperM/AutoscoperM.py | 332 +++++++- AutoscoperM/AutoscoperMLib/IO.py | 56 +- AutoscoperM/Resources/UI/AutoscoperM.ui | 1043 +++++++++++++++-------- 3 files changed, 1007 insertions(+), 424 deletions(-) diff --git a/AutoscoperM/AutoscoperM.py b/AutoscoperM/AutoscoperM.py index 80ca66e..4da7c31 100644 --- a/AutoscoperM/AutoscoperM.py +++ b/AutoscoperM/AutoscoperM.py @@ -211,11 +211,21 @@ def setup(self): self.ui.ankleSampleButton.connect("clicked(bool)", lambda: self.onSampleDataButtonClicked("2023-08-01-Ankle")) # Pre-processing Library Buttons + self.ui.volumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onCurrentNodeChanged) + # segmentation and PV generation self.ui.tiffGenButton.connect("clicked(bool)", self.onGeneratePartialVolumes) - self.ui.configGenButton.connect("clicked(bool)", self.onGenerateConfig) - self.ui.segmentationButton.connect("clicked(bool)", self.onSegmentation) - + self.ui.segGen_segmentationButton.connect("clicked(bool)", self.onSegmentation) + self.ui.segSTL_importModelsButton.connect("clicked(bool)", self.onImportModels) self.ui.loadPVButton.connect("clicked(bool)", self.onLoadPV) + # config generation + self.ui.populateCameraCalListButton.connect("clicked(bool)", self.onPopulateCameraCalList) + self.ui.stageCameraCalFileButton.setIcon(qt.QApplication.style().standardIcon(qt.QStyle.SP_ArrowRight)) + self.ui.stageCameraCalFileButton.connect("clicked(bool)", self.onStageCameraCalFile) + self.ui.populateTrialNameListButton.connect("clicked(bool)", self.onPopulateTrialNameList) + self.ui.stageTrialDirButton.setIcon(qt.QApplication.style().standardIcon(qt.QStyle.SP_ArrowRight)) + self.ui.stageTrialDirButton.connect("clicked(bool)", self.onStageTrialDir) + self.ui.populatePartialVolumeListButton.connect("clicked(bool)", self.onPopulatePartialVolumeList) + self.ui.configGenButton.connect("clicked(bool)", self.onGenerateConfig) # Default output directory self.ui.mainOutputSelector.setCurrentPath( @@ -225,6 +235,9 @@ def setup(self): # Make sure parameter node is initialized (needed for module reload) self.initializeParameterNode() + # Trigger any required UI updates based on the volume node selected by default + self.onCurrentNodeChanged() + def cleanup(self): """ Called when the application closes and the module widget is destroyed. @@ -410,6 +423,18 @@ def onSampleDataButtonClicked(self, dataType): for cam in range(numCams): self.logic.AutoscoperSocket.loadFilters(cam, filterSettings) + def onCurrentNodeChanged(self): + """ + Updates and UI components that correspond to the selected input volume node + """ + volumeNode = self.ui.volumeSelector.currentNode() + if volumeNode: + with slicer.util.tryWithErrorDisplay("Failed to grab volume node information", waitCursor=True): + vSizeX, vSizeY, vSizeZ = self.logic.GetVolumeSpacing(volumeNode) + self.ui.voxelSizeX.value = vSizeX + self.ui.voxelSizeY.value = vSizeY + self.ui.voxelSizeZ.value = vSizeZ + def onGeneratePartialVolumes(self): """ This function creates partial volumes for each segment in the segmentation node for the selected volume node. @@ -465,36 +490,90 @@ def onGenerateConfig(self): with slicer.util.tryWithErrorDisplay("Failed to compute results", waitCursor=True): volumeNode = self.ui.volumeSelector.currentNode() mainOutputDir = self.ui.mainOutputSelector.currentPath - trialName = self.ui.trialName.text - width = self.ui.vrgRes_width.value - height = self.ui.vrgRes_height.value + configFileName = self.ui.configFileName.text + + configPath = os.path.join(mainOutputDir, f"{configFileName}.cfg") tiffSubDir = self.ui.tiffSubDir.text - vrgSubDir = self.ui.vrgSubDir.text + radiographSubDir = self.ui.radiographSubDir.text calibrationSubDir = self.ui.cameraSubDir.text + trialList = self.ui.trialList + partialVolumeList = self.ui.partialVolumeList + camCalList = self.ui.camCalList + # Validate the inputs if not self.logic.validateInputs( volumeNode=volumeNode, mainOutputDir=mainOutputDir, - trialName=trialName, - width=width, - height=height, - volumeSubDir=tiffSubDir, - vrgSubDir=vrgSubDir, + configFileName=configFileName, + tiffSubDir=tiffSubDir, + radiographSubDir=radiographSubDir, calibrationSubDir=calibrationSubDir, + trialList=trialList, + partialVolumeList=partialVolumeList, + camCalList=camCalList, ): raise ValueError("Invalid inputs") return if not self.logic.validatePaths( mainOutputDir=mainOutputDir, tiffDir=os.path.join(mainOutputDir, tiffSubDir), - vrgDir=os.path.join(mainOutputDir, vrgSubDir), + radiographSubDir=os.path.join(mainOutputDir, radiographSubDir), calibDir=os.path.join(mainOutputDir, calibrationSubDir), ): raise ValueError("Invalid paths") return + def get_staged_items(listWidget): + staged_items = [] + for row in range(listWidget.count): + item = listWidget.item(row) + widget = listWidget.itemWidget(item) + + # try to find the label of this item + label = widget.findChild(qt.QLabel) if widget else None + if not label: + raise ValueError(f"Could not extract item label from list at index {row}") + staged_items.append(label.text) + + return staged_items + + # extract filenames from UI lists, and use them to construct the paths relative to mainOutputDir. + # NOTE: We rely here on the order of the files as constructed by the user in the UI. The order of items + # in the staged camera files list and the radiograph root dirs list are expected to match. + camCalFiles = [os.path.join(calibrationSubDir, item) for item in get_staged_items(camCalList)] + trialDirs = [os.path.join(radiographSubDir, item) for item in get_staged_items(trialList)] + + if len(camCalFiles) == 0: + raise ValueError( + "Invalid inputs: must select at least one camera calibration file, but zero were provided." + ) + + if len(trialDirs) == 0: + raise ValueError( + "Invalid inputs: must select at least one radiograph subdirectory, but zero were provided." + ) + + if len(camCalFiles) != len(trialDirs): + raise ValueError( + "Invalid inputs: number of selected trial directories must match the number " + f"of camera calibration files: {len(camCalFiles)} != {len(trialDirs)}" + ) + + def get_checked_items(listWidget): + checked_items = [] + for idx in range(listWidget.count): + item = listWidget.item(idx) + if item.checkState() == qt.Qt.Checked: + checked_items.append(item.text()) + return checked_items + + partialVolumeFiles = [os.path.join(tiffSubDir, item) for item in get_checked_items(partialVolumeList)] + + if len(partialVolumeFiles) == 0: + raise ValueError("Invalid inputs: at least one volume file must be selected!") + optimizationOffsets = [ self.ui.optOffX.value, self.ui.optOffY.value, @@ -503,30 +582,91 @@ def onGenerateConfig(self): self.ui.optOffPitch.value, self.ui.optOffRoll.value, ] + volumeFlip = [ int(self.ui.flipX.isChecked()), int(self.ui.flipY.isChecked()), int(self.ui.flipZ.isChecked()), ] - voxel_spacing = self.logic.GetVolumeSpacing(volumeNode) + renderResolution = [ + self.ui.configRes_width.value, + self.ui.configRes_height.value, + ] + + voxel_spacing = [ + self.ui.voxelSizeX.value, + self.ui.voxelSizeY.value, + self.ui.voxelSizeZ.value, + ] + + # Validate the inputs + if not self.logic.validateInputs( + *trialDirs, + *partialVolumeFiles, + *camCalFiles, + *optimizationOffsets, + *volumeFlip, + *renderResolution, + *voxel_spacing, + ): + raise ValueError("Invalid inputs") + # generate the config file - configFilePath = IO.generateConfigFile( - mainOutputDir, - [tiffSubDir, vrgSubDir, calibrationSubDir], - trialName, + IO.generateConfigFile( + outputConfigPath=configPath, + trialName=configFileName, + camCalFiles=camCalFiles, + camRootDirs=trialDirs, + volumeFiles=partialVolumeFiles, volumeFlip=volumeFlip, voxelSize=voxel_spacing, - renderResolution=[int(width / 2), int(height / 2)], + renderResolution=renderResolution, optimizationOffsets=optimizationOffsets, ) - self.ui.configSelector.setCurrentPath(configFilePath) + # Set the path to this newly created config file in the "Config File" field in the Autoscoper Control UI + self.ui.configSelector.setCurrentPath(configPath) + slicer.util.messageBox("Success!") + + def onImportModels(self): + """ + Imports Models from a directory- converts to Segmentation Nodes + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results", waitCursor=True): + self.ui.progressBar.setValue(0) + self.ui.progressBar.setMaximum(100) + + volumeNode = self.ui.volumeSelector.currentNode() + + if not self.logic.validateInputs(voluemNode=volumeNode): + raise ValueError("Invalid inputs") + return + + if self.ui.segSTL_loadRadioButton.isChecked(): + segmentationFileDir = self.ui.segSTL_modelsDir.currentPath + if not self.logic.validatePaths(segmentationFileDir=segmentationFileDir): + raise ValueError("Invalid paths") + return + segmentationFiles = glob.glob(os.path.join(segmentationFileDir, "*.*")) + segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") + segmentationNode.CreateDefaultDisplayNodes() + for idx, file in enumerate(segmentationFiles): + returnedNode = IO.loadSegmentation(segmentationNode, file) + if returnedNode: + # get the segment from the returned node and add it to the segmentation node + segment = returnedNode.GetSegmentation().GetNthSegment(0) + segmentationNode.GetSegmentation().AddSegment(segment) + 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") + return slicer.util.messageBox("Success!") def onSegmentation(self): """ - Either launches the automatic segmentation process or loads in a set of segmentations from a directory + Launches the automatic segmentation process """ with slicer.util.tryWithErrorDisplay("Failed to compute results", waitCursor=True): self.ui.progressBar.setValue(0) @@ -551,7 +691,7 @@ def onSegmentation(self): self.logic.cleanFilename(currentVolumeNode.GetName(), i) segmentationNode = SubVolumeExtraction.automaticSegmentation( currentVolumeNode, - self.ui.segGen_ThresholdSpinBox.value, + self.ui.segGen_thresholdSpinBox.value, self.ui.segGen_marginSizeSpin.value, progressCallback=self.updateProgressBar, ) @@ -561,24 +701,8 @@ def onSegmentation(self): segmentationSequenceNode.SetDataNodeAtValue(segmentationNode, str(i)) slicer.mrmlScene.RemoveNode(segmentationNode) currentVolumeNode = self.logic.getNextItemInSequence(volumeNode) - elif self.ui.segGen_fileRadioButton.isChecked(): - segmentationFileDir = self.ui.segGen_lineEdit.currentPath - if not self.logic.validatePaths(segmentationFileDir=segmentationFileDir): - raise ValueError("Invalid paths") - return - segmentationFiles = glob.glob(os.path.join(segmentationFileDir, "*.*")) - segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") - segmentationNode.CreateDefaultDisplayNodes() - for idx, file in enumerate(segmentationFiles): - returnedNode = IO.loadSegmentation(segmentationNode, file) - if returnedNode: - # get the segment from the returned node and add it to the segmentation node - segment = returnedNode.GetSegmentation().GetNthSegment(0) - segmentationNode.GetSegmentation().AddSegment(segment) - slicer.mrmlScene.RemoveNode(returnedNode) - self.ui.progressBar.setValue((idx + 1) / len(segmentationFiles) * 100) else: # Should never happen but just in case - raise Exception("No segmentation method selected") + raise Exception("Please select the 'Automatic Segmentation' option in order to generate segmentations") return slicer.util.messageBox("Success!") @@ -649,6 +773,136 @@ def onLoadPV(self): slicer.util.messageBox("Success!") + def onPopulateTrialNameList(self): + """ + Populates trial name UI list using files from the selected radiograph directory + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): + self.populateListFromOutputSubDir(self.ui.trialCandidateList, self.ui.radiographSubDir.text, itemType="dir") + + def onPopulatePartialVolumeList(self): + """ + Populates partial volumes UI list using files from the selected PV directory + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): + self.populateListFromOutputSubDir(self.ui.partialVolumeList, self.ui.tiffSubDir.text) + + def onPopulateCameraCalList(self): + """ + Populates camera calibration UI list using files from the selected camera directory + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): + self.populateListFromOutputSubDir(self.ui.camCalCandidateList, self.ui.cameraSubDir.text) + + def populateListFromOutputSubDir(self, listWidget, fileSubDir, itemType="file"): + """ + Populates input UI list with files/directories that exist in the given input directory + """ + listWidget.clear() + + mainOutputDir = self.ui.mainOutputSelector.currentPath + if not self.logic.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 + + if itemType == "file": + listFiles = [f.name for f in os.scandir(fileDir) if os.path.isfile(f)] + elif itemType == "dir": + listFiles = [f.name for f in os.scandir(fileDir) if os.path.isdir(f)] + else: + raise ValueError( + "Invalid input: can either search for type 'file' or 'dir' " + f"in specified path, but given itemType='{itemType}'" + ) + return + + for file in sorted(listFiles): + fileItem = qt.QListWidgetItem(file) + fileItem.setFlags(fileItem.flags() & ~qt.Qt.ItemIsSelectable) # Remove the selectable flag + fileItem.setCheckState(qt.Qt.Unchecked) + listWidget.addItem(fileItem) + + def onStageCameraCalFile(self): + """ + Adds selected items from the camera calibration list to the staged files list + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): + self.stageSelectedFiles(self.ui.camCalCandidateList, self.ui.camCalList) + + def onStageTrialDir(self): + """ + Adds selected items from the radiograph subdirectories list to the staged subdirs list + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): + self.stageSelectedFiles(self.ui.trialCandidateList, self.ui.trialList) + + def stageSelectedFiles(self, candidateListWidget, listWidget): + """ + Stages chosen files into listWidget based on the selected items + in candidateListWidget which contains all candidate file names + """ + # gether checked items from the input candidate list + checked_items = [] + for idx in range(candidateListWidget.count): + item = candidateListWidget.item(idx) + if item.checkState() == qt.Qt.Checked: + checked_items.append(item.text()) + item.setCheckState(qt.Qt.Unchecked) + + if len(checked_items) == 0: + raise ValueError("No items were selected.") + + def stagedItemExists(itemText): + # iterate over the list items and see if item with the given label already exists + for row in range(listWidget.count): + item = listWidget.item(row) + widget = listWidget.itemWidget(item) + if widget: + # extract label to compare the text in the item + label = widget.findChild(qt.QLabel) + if label and label.text == itemText: + return True + return False + + # stage all selected items if they're not already in the target list + for file in checked_items: + if not stagedItemExists(file): + # create item widget with text and a delete button + itemBaseWidget = qt.QWidget() + itemLayout = qt.QHBoxLayout() + itemLabel = qt.QLabel(file) + itemDeleteButton = qt.QPushButton("Delete") + + # set styling attributes to make it look nice in the UI + itemLayout.setContentsMargins(3, 1, 3, 1) + itemLayout.setSpacing(3) + itemDeleteButton.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Fixed)) + + itemLayout.addWidget(itemLabel) + # add spacing so that the delete button is always aligned to the right + itemLayout.addItem(qt.QSpacerItem(0, 0, qt.QSizePolicy.Expanding, qt.QSizePolicy.Minimum)) + itemLayout.addWidget(itemDeleteButton) + itemBaseWidget.setLayout(itemLayout) + itemWidget = qt.QListWidgetItem(listWidget) + itemWidget.setFlags(itemWidget.flags() & ~qt.Qt.ItemIsSelectable) + + # finally, add the composite widget as an item to the list + listWidget.setItemWidget(itemWidget, itemBaseWidget) + + # add delete functionality to the button + itemDeleteButton.clicked.connect(lambda _, item=itemWidget: listWidget.takeItem(listWidget.row(item))) + else: + logging.info(f"Skipped adding the item '{file}' as it already exists in the target list.") + # # AutoscoperMLogic diff --git a/AutoscoperM/AutoscoperMLib/IO.py b/AutoscoperM/AutoscoperMLib/IO.py index 01bf5c5..890e7ba 100644 --- a/AutoscoperM/AutoscoperMLib/IO.py +++ b/AutoscoperM/AutoscoperMLib/IO.py @@ -1,4 +1,3 @@ -import glob import logging import os from itertools import product @@ -34,53 +33,34 @@ def loadSegmentation(segmentationNode: slicer.vtkMRMLSegmentationNode, filename: def generateConfigFile( - mainDirectory: str, - subDirectories: list[str], + outputConfigPath: str, trialName: str, + camCalFiles: list[str], + camRootDirs: list[str], + volumeFiles: list[str], volumeFlip: list[int], voxelSize: list[float], renderResolution: list[int], optimizationOffsets: list[float], -) -> str: +) -> None: """ Generates the v1.1 config file for the trial - :param mainDirectory: Main directory - :param subDirectories: Sub directories + :param outputConfigPath: The absolute path to the config file to be generated :param trialName: Trial name - :param volumeFlip: Volume flip - :param voxelSize: Voxel size - :param renderResolution: Render resolution - :param optimizationOffsets: Optimization offsets + :param camCalFiles: The list of camera calibration file paths, relative to the main output dir + :param camRootDirs: The list of the radiograph directory paths, relative to the main output dir + :param volumeFiles: The list of tiff volume files, relative to the main output dir + :param volumeFlip: The flip settings for each of the volumes + :param voxelSize: The voxel size of each of the the volumes + :param renderResolution: The resolution of the 2D rendering of each of the volumes + :param optimizationOffsets: The offsets for the optimization :return: Path to the config file """ import datetime - # Get the camera calibration files, camera root directories, and volumes - volumes = glob.glob(os.path.join(mainDirectory, subDirectories[0], "*.tif")) - cameraRootDirs = glob.glob(os.path.join(mainDirectory, subDirectories[1], "*")) - calibrationFiles = glob.glob(os.path.join(mainDirectory, subDirectories[2], "*.json")) - - # Check that we have the same number of camera calibration files and camera root directories - if len(calibrationFiles) != len(cameraRootDirs): - logging.error( - "Number of camera calibration files and camera root directories do not match: " - " {len(calibrationFiles)} != {len(cameraRootDirs)}" - ) - return None - - # Check that we have at least one volume - if len(volumes) == 0: - logging.error("No volumes found!") - return None - - # Transform the paths to be relative to the main directory - calibrationFiles = [os.path.relpath(calibrationFile, mainDirectory) for calibrationFile in calibrationFiles] - cameraRootDirs = [os.path.relpath(cameraRootDir, mainDirectory) for cameraRootDir in cameraRootDirs] - volumes = [os.path.relpath(volume, mainDirectory) for volume in volumes] - - with open(os.path.join(mainDirectory, trialName + ".cfg"), "w+") as f: + with open(outputConfigPath, "w+") as f: # Trial Name as comment f.write(f"# {trialName} configuration file\n") f.write( @@ -94,19 +74,19 @@ def generateConfigFile( # Camera Calibration Files f.write("# Camera Calibration Files\n") - for calibrationFile in calibrationFiles: + for calibrationFile in camCalFiles: f.write("mayaCam_csv " + calibrationFile + "\n") f.write("\n") # Camera Root Directories f.write("# Camera Root Directories\n") - for cameraRootDir in cameraRootDirs: + for cameraRootDir in camRootDirs: f.write("CameraRootDir " + cameraRootDir + "\n") f.write("\n") # Volumes f.write("# Volumes\n") - for volume in volumes: + for volume in volumeFiles: f.write("VolumeFile " + volume + "\n") f.write("VolumeFlip " + " ".join([str(x) for x in volumeFlip]) + "\n") f.write("VoxelSize " + " ".join([str(x) for x in voxelSize]) + "\n") @@ -122,8 +102,6 @@ def generateConfigFile( f.write("OptimizationOffsets " + " ".join([str(x) for x in optimizationOffsets]) + "\n") f.write("\n") - return os.path.join(mainDirectory, trialName + ".cfg") - def writeVolume(volumeNode: slicer.vtkMRMLVolumeNode, filename: str): """ diff --git a/AutoscoperM/Resources/UI/AutoscoperM.ui b/AutoscoperM/Resources/UI/AutoscoperM.ui index 09d347a..6d8625a 100644 --- a/AutoscoperM/Resources/UI/AutoscoperM.ui +++ b/AutoscoperM/Resources/UI/AutoscoperM.ui @@ -9,8 +9,8 @@ 0 0 - 1018 - 860 + 659 + 1566 @@ -120,6 +120,13 @@ + + + + Qt::Vertical + + + @@ -128,254 +135,145 @@ - + + + 0 + + + false + + + + + + + + + Output Directory: + + + + + + + ctkPathLineEdit::Dirs|ctkPathLineEdit::Executable|ctkPathLineEdit::NoDot|ctkPathLineEdit::NoDotDot|ctkPathLineEdit::Writable + + + + + + + Volume Node: + + + + + + + true + + + + vtkMRMLScalarVolumeNode + vtkMRMLSequenceNode + + + + + + + + + + + + + + - General Input + Segmentation Generation true - + + false + + - + - Volume Node: - - - - - - - false - - - - vtkMRMLScalarVolumeNode - vtkMRMLSequenceNode - - - - + Automatic Segmentation - - + + true - - + + - Output Directory: - - - - - - - ctkPathLineEdit::Dirs|ctkPathLineEdit::Executable|ctkPathLineEdit::NoDot|ctkPathLineEdit::NoDotDot|ctkPathLineEdit::Writable + Threshold Value: - - - - true + + + + 10000 - - Trial Name: + + 700 - - - - true + + + + Generate Segmentations - - - - true - - - Advanced Options - - - true - - - true - - - false + + + + Margin Size: - - - - - Camera Subdirectory: - - - - - - - Radiograph Subdirectory: - - - - - - - true - - - Calibration - - - - - - - Tracking Subdirectory: - - - - - - - VRG Resolution: (width,height) - - - - - - - Transforms - - - - - - - Volumes - - - - - - - 999999999 - - - 1760 - - - - - - - Partial Volume Transforms Subdirectory: - - - - - - - RadiographImages - - - - - - - 999999999 - - - 1760 - - - - - - - Partial Volume Subdirectory: - - - - - - - Model Subdirectory: - - - - - - - Tracking - - - - - - - Models - - - - - - + + - 0 - - - false + 2.000000000000000 - - - - - - - Segmentation Generation - - - false - - - + - Threshold Value: + OR - - + + - Automatic Segmentation - - - true + Segmentation from Model - - + + + + false + - Batch Load from File + STL Models Directory: - - + + false @@ -384,41 +282,13 @@ - - - - 10000 - - - 700 - - - - - - - Segmentation File Directory: - - - - - - - Generate Segmentations + + + + false - - - - - Margin Size: - - - - - - - 2.000000000000000 + Import Models @@ -431,10 +301,20 @@ Partial Volume Generation + true + + false - + + + + Segmentation Node: + + + + true @@ -452,21 +332,14 @@ - - - - Segmentation Node: - - - - + Generate Partial Volumes - + Load Partial Volumes @@ -477,148 +350,557 @@ - + true - Generate Config + Default Subdirectories + + + true - false + true true - - - + + + - Flip Y - - - false + Partial Volume Subdirectory: - - - - 0.100000000000000 - - - 0.100000000000000 + + + + Volumes - - + + - Generate Config File + Partial Volume Transforms Subdirectory: - - - - 0.100000000000000 - - - 0.100000000000000 + + + + Transforms - - - - Flip X + + + + + 75 + true + - - false + + Radiograph Subdirectory: - - - - 0.100000000000000 + + + + + 75 + true + - - 0.100000000000000 + + RadiographImages - - - - 0.100000000000000 + + + + + 75 + true + - - 0.100000000000000 + + Camera Subdirectory: - - + + + + true + + + + 75 + true + + - Flip Z + Calibration - - - - 0.100000000000000 - - - 0.100000000000000 + + + + Tracking Subdirectory: - - - - 0.100000000000000 - - - 0.100000000000000 + + + + Tracking - - + + - Optimization Offsets: + Model Subdirectory: - - + + - Volume Flip: + Models + + + + true + + + Generate Config + + + true + + + false + + + + + + + + Config Trial Name: + + + + + + + true + + + + + + + .cfg + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 10 + + + + + + + + Select Paired Camera Calibrations: + + + + + + + + QListWidget::indicator:unchecked { + background-color: palette(alternate-base); + } + + + + + + + + + + + Add selected camera calibration file as next in order + + + + 40 + 16777215 + + + + + + + + + + + Populate From Camera Subdirectory + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 10 + + + + + + + + Select Paired Radiograph Subirectories: + + + + + + + + QListWidget::indicator:unchecked { + background-color: palette(alternate-base); + } + + + + + + + + + + + Add selected radiograph subdirectory as next in order + + + + 40 + 16777215 + + + + + + + + + + + Populate From Radiographs Subdirectory + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 10 + + + + + + + + Select Partial Volumes: + + + + + + + + QListWidget::indicator:unchecked { + background-color: palette(alternate-base); + } + + + + + + + + Populate From Volume Subdirectory + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 10 + + + + + + + + + + + + Optimization Offsets: + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + Volume Flip: + + + + + + + Flip X + + + false + + + + + + + Flip Y + + + false + + + + + + + Flip Z + + + + + + + Render Resolution: (width,height) + + + + + + + 999999999 + + + 1760 + + + + + + + 999999999 + + + 1760 + + + + + + + Voxel Size: + + + + + + + true + + + 3 + + + 1.000000000000000 + + + + + + + true + + + 3 + + + 1.000000000000000 + + + + + + + true + + + 3 + + + 1.000000000000000 + + + + + + + Generate Config File + + + + + + + + + + + + Qt::Vertical + + + - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -639,17 +921,6 @@
ctkCollapsibleButton.h
1 - - ctkCollapsibleGroupBox - QGroupBox -
ctkCollapsibleGroupBox.h
- 1 -
- - ctkDoubleRangeSlider - QWidget -
ctkDoubleRangeSlider.h
-
ctkPathLineEdit QWidget @@ -677,7 +948,7 @@ segGen_autoRadioButton toggled(bool) - segGen_ThresholdSpinBox + segGen_thresholdSpinBox setEnabled(bool) @@ -691,9 +962,9 @@ - segGen_fileRadioButton + segSTL_loadRadioButton toggled(bool) - segGen_lineEdit + segSTL_modelsDir setEnabled(bool) @@ -738,5 +1009,85 @@ + + segSTL_loadRadioButton + toggled(bool) + segSTL_importModelsButton + setEnabled(bool) + + + 122 + 276 + + + 698 + 204 + + + + + segGen_autoRadioButton + toggled(bool) + segGen_segmentationButton + setEnabled(bool) + + + 122 + 189 + + + 698 + 276 + + + + + segSTL_loadRadioButton + toggled(bool) + segSTL_modelsDirLabel + setEnabled(bool) + + + 122 + 276 + + + 279 + 276 + + + + + segGen_autoRadioButton + toggled(bool) + segGen_thresholdLabel + setEnabled(bool) + + + 122 + 189 + + + 279 + 189 + + + + + segGen_autoRadioButton + toggled(bool) + segGen_marginSizeLabel + setEnabled(bool) + + + 122 + 189 + + + 279 + 220 + + +