diff --git a/AutoscoperM/AutoscoperM.py b/AutoscoperM/AutoscoperM.py index 461a57f..2860b91 100644 --- a/AutoscoperM/AutoscoperM.py +++ b/AutoscoperM/AutoscoperM.py @@ -468,24 +468,29 @@ 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, configFileName, ".cfg") tiffSubDir = self.ui.tiffSubDir.text vrgSubDir = self.ui.vrgSubDir.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, + configFileName=configFileName, + tiffSubDir=tiffSubDir, vrgSubDir=vrgSubDir, calibrationSubDir=calibrationSubDir, + trialList=trialList, + partialVolumeList=partialVolumeList, + camCalList=camCalList, ): raise ValueError("Invalid inputs") return @@ -498,6 +503,32 @@ def onGenerateConfig(self): raise ValueError("Invalid paths") return + 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 + + # extract filenames from UI lists, and use them to construct the paths relative to mainOutputDir + # FIXME: don't assume the list of camera files is given in the same order as the list of radiograph root dir! + camCalFiles = [os.path.join(calibrationSubDir, item) for item in get_checked_items(camCalList)] + trialDirs = [os.path.join(vrgSubDir, item) for item in get_checked_items(trialList)] + + if not 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)}" + ) + return + + 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!") + return + optimizationOffsets = [ self.ui.optOffX.value, self.ui.optOffY.value, @@ -512,19 +543,42 @@ def onGenerateConfig(self): int(self.ui.flipZ.isChecked()), ] + renderResolution = [ + self.ui.configRes_width.value, + self.ui.configRes_height.value, + ] + + # TODO: automatically populate UI fields when input volume is selected voxel_spacing = self.logic.GetVolumeSpacing(volumeNode) + + # Validate the inputs + if not self.logic.validateInputs( + *trialDirs, + *partialVolumeFiles, + *camCalFiles, + *optimizationOffsets, + *volumeFlip, + *renderResolution, + *voxel_spacing, + ): + raise ValueError("Invalid inputs") + return + # 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)], # why divide by 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): @@ -676,31 +730,40 @@ def onPopulateTrialNameList(self): Populates trial name UI list using files from the selected radiograph directory """ radiographDir = os.path.join(self.ui.mainOutputSelector.currentPath, self.ui.vrgSubDir.text) - self.populateFileList(self.ui.trialList, radiographDir, itemType="dir") + try: + self.populateFileList(self.ui.trialList, radiographDir, itemType="dir") + except ValueError as errMsg: + slicer.util.errorDisplay(errMsg) def onPopulatePartialVolumeList(self): """ Populates partial volumes UI list using files from the selected PV directory """ - radiographDir = os.path.join(self.ui.mainOutputSelector.currentPath, self.ui.tiffSubDir.text) - self.populateFileList(self.ui.partialVolumeList, radiographDir) + partialVolumeshDir = os.path.join(self.ui.mainOutputSelector.currentPath, self.ui.tiffSubDir.text) + try: + self.populateFileList(self.ui.partialVolumeList, partialVolumeshDir) + except ValueError as errMsg: + slicer.util.errorDisplay(errMsg) def onPopulateCameraCalList(self): """ Populates camera calibration UI list using files from the selected camera directory """ - radiographDir = os.path.join(self.ui.mainOutputSelector.currentPath, self.ui.cameraSubDir.text) - self.populateFileList(self.ui.camCalList, radiographDir) + cameraDir = os.path.join(self.ui.mainOutputSelector.currentPath, self.ui.cameraSubDir.text) + try: + self.populateFileList(self.ui.camCalList, cameraDir) + except ValueError as errMsg: + slicer.util.errorDisplay(errMsg) def populateFileList(self, listWidget, fileDir, itemType="file"): """ - Populates trial name UI list using files from the selected radiograph directory + Populates input UI list with files/directories that exist in the given input directory """ listWidget.clear() if not self.logic.validatePaths( fileDir=fileDir, ): - raise ValueError("Invalid input: subdirectory does not exist") + raise ValueError(f"Invalid input: subdirectory '{fileDir}' does not exist.") return if itemType == "file": @@ -708,11 +771,12 @@ def populateFileList(self, listWidget, fileDir, itemType="file"): elif itemType == "dir": listFiles = [f.name for f in os.scandir(fileDir) if os.path.isdir(f)] else: - raise ValueError(f"Invalid input: can only search for either files or directories in specified path, but given itemType={itemType}") + raise ValueError(f"Invalid input: can either search for type 'file' or 'dir' in specified path, but given itemType='{itemType}'") return - for file in listFiles: + 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) diff --git a/AutoscoperM/AutoscoperMLib/IO.py b/AutoscoperM/AutoscoperMLib/IO.py index 01bf5c5..6cceef0 100644 --- a/AutoscoperM/AutoscoperMLib/IO.py +++ b/AutoscoperM/AutoscoperMLib/IO.py @@ -34,53 +34,35 @@ 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: + logging.info(f"generateConfigFile: writing to '{outputConfigPath}'") + with open(outputConfigPath, "w+") as f: # Trial Name as comment f.write(f"# {trialName} configuration file\n") f.write( @@ -94,19 +76,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,7 +104,7 @@ def generateConfigFile( f.write("OptimizationOffsets " + " ".join([str(x) for x in optimizationOffsets]) + "\n") f.write("\n") - return os.path.join(mainDirectory, trialName + ".cfg") + return True def writeVolume(volumeNode: slicer.vtkMRMLVolumeNode, filename: str): diff --git a/AutoscoperM/Resources/UI/AutoscoperM.ui b/AutoscoperM/Resources/UI/AutoscoperM.ui index c825079..1bfdf2b 100644 --- a/AutoscoperM/Resources/UI/AutoscoperM.ui +++ b/AutoscoperM/Resources/UI/AutoscoperM.ui @@ -364,7 +364,7 @@ true - false + true @@ -526,7 +526,7 @@ - + true @@ -686,7 +686,7 @@ - + 999999999 @@ -696,7 +696,7 @@ - + 999999999