Skip to content

Commit

Permalink
ENH: Implement config generation from updated UI
Browse files Browse the repository at this point in the history
  • Loading branch information
sbelsk committed Nov 20, 2024
1 parent c72bb35 commit 8af2265
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 63 deletions.
108 changes: 86 additions & 22 deletions AutoscoperM/AutoscoperM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -676,43 +730,53 @@ 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":
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(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)

Expand Down
56 changes: 19 additions & 37 deletions AutoscoperM/AutoscoperMLib/IO.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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")
Expand All @@ -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):
Expand Down
8 changes: 4 additions & 4 deletions AutoscoperM/Resources/UI/AutoscoperM.ui
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@
<bool>true</bool>
</property>
<property name="collapsed">
<bool>false</bool>
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
Expand Down Expand Up @@ -526,7 +526,7 @@
</widget>
</item>
<item row="0" column="3" rowspan="2">
<widget class="QLineEdit" name="trialName">
<widget class="QLineEdit" name="configFileName">
<property name="enabled">
<bool>true</bool>
</property>
Expand Down Expand Up @@ -686,7 +686,7 @@
</widget>
</item>
<item row="2" column="1" colspan="3">
<widget class="QSpinBox" name="vrgRes_width">
<widget class="QSpinBox" name="configRes_width">
<property name="maximum">
<number>999999999</number>
</property>
Expand All @@ -696,7 +696,7 @@
</widget>
</item>
<item row="2" column="4" colspan="3">
<widget class="QSpinBox" name="vrgRes_height">
<widget class="QSpinBox" name="configRes_height">
<property name="maximum">
<number>999999999</number>
</property>
Expand Down

0 comments on commit 8af2265

Please sign in to comment.