From 9f5e88cd3a9474838d5e748241e8966eff838b8c Mon Sep 17 00:00:00 2001 From: Shelly Belsky Date: Sun, 10 Nov 2024 13:47:15 -0500 Subject: [PATCH] BUG: Remove obsolete VRG Generation functionality Removes the 'Manual Camera Placement' and 'Automatic Camera Placement' VRG Generation UI groups from the Pre-Processing modules, along with all of their associated backend code. This includes the removal of the CalculateDataIntensityDensity and VirtualRadiographGeneration modules. The support for camera placement is superseded by the hierarchical registration capability. --- AutoscoperM/AutoscoperM.py | 413 +----------------- AutoscoperM/AutoscoperMLib/IO.py | 61 --- .../AutoscoperMLib/RadiographGeneration.py | 239 ---------- AutoscoperM/CMakeLists.txt | 1 - AutoscoperM/Resources/UI/AutoscoperM.ui | 338 -------------- CMakeLists.txt | 2 - CalculateDataIntensityDensity/CMakeLists.txt | 6 - .../CalculateDataIntensityDensity.py | 95 ---- .../CalculateDataIntensityDensity.xml | 41 -- .../Hierarchical3DRegistration.py | 4 - VirtualRadiographGeneration/CMakeLists.txt | 6 - .../VirtualRadiographGeneration.py | 168 ------- .../VirtualRadiographGeneration.xml | 62 --- 13 files changed, 1 insertion(+), 1435 deletions(-) delete mode 100644 AutoscoperM/AutoscoperMLib/RadiographGeneration.py delete mode 100644 CalculateDataIntensityDensity/CMakeLists.txt delete mode 100644 CalculateDataIntensityDensity/CalculateDataIntensityDensity.py delete mode 100644 CalculateDataIntensityDensity/CalculateDataIntensityDensity.xml delete mode 100644 VirtualRadiographGeneration/CMakeLists.txt delete mode 100644 VirtualRadiographGeneration/VirtualRadiographGeneration.py delete mode 100644 VirtualRadiographGeneration/VirtualRadiographGeneration.xml diff --git a/AutoscoperM/AutoscoperM.py b/AutoscoperM/AutoscoperM.py index 71e63ff..4a82b84 100644 --- a/AutoscoperM/AutoscoperM.py +++ b/AutoscoperM/AutoscoperM.py @@ -18,7 +18,7 @@ ) from slicer.util import VTKObservationMixin -from AutoscoperMLib import IO, RadiographGeneration, SubVolumeExtraction +from AutoscoperMLib import IO, SubVolumeExtraction # # AutoscoperM @@ -36,10 +36,6 @@ def __init__(self, parent): self.parent.categories = [ "Tracking", ] - self.parent.dependencies = [ - "CalculateDataIntensityDensity", - "VirtualRadiographGeneration", - ] self.parent.contributors = [ "Anthony Lombardi (Kitware)", "Amy M Morton (Brown University)", @@ -216,8 +212,6 @@ def setup(self): # Pre-processing Library Buttons self.ui.tiffGenButton.connect("clicked(bool)", self.onGeneratePartialVolumes) - self.ui.vrgGenButton.connect("clicked(bool)", self.onGenerateVRG) - self.ui.manualVRGGenButton.connect("clicked(bool)", self.onManualVRGGen) self.ui.configGenButton.connect("clicked(bool)", self.onGenerateConfig) self.ui.segmentationButton.connect("clicked(bool)", self.onSegmentation) @@ -228,11 +222,6 @@ def setup(self): os.path.join(slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory(), "AutoscoperM-Pre-Processing") ) - # Dynamic camera frustum functions - self.ui.mVRG_markupSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onMarkupNodeChanged) - self.ui.mVRG_ClippingRangeSlider.connect("valuesChanged(double,double)", self.updateClippingRange) - self.ui.mVRG_viewAngleSpin.connect("valueChanged(int)", self.updateViewAngle) - # Make sure parameter node is initialized (needed for module reload) self.initializeParameterNode() @@ -279,8 +268,6 @@ def initializeParameterNode(self): # so that when the scene is saved and reloaded, these settings are restored. self.setParameterNode(self.logic.getParameterNode()) - if self.ui.mVRG_markupSelector.currentNode() is not None: - self.onMarkupNodeChanged(self.ui.mVRG_markupSelector.currentNode()) # Select default input nodes if nothing is selected yet to save a few clicks for the user # NA @@ -470,151 +457,6 @@ def onGeneratePartialVolumes(self): self.onLoadPV() # onLoadPV has a call to the "success" display, remove the one here so the user doesn't get two. - def onGenerateVRG(self): - """ - This function optimizes the camera positions for a given volume and then - generates a VRG file for each optimized camera. - """ - - with slicer.util.tryWithErrorDisplay("Failed to compute results", waitCursor=True): - self.updateProgressBar(0) - - # Set up and validate inputs - volumeNode = self.ui.volumeSelector.currentNode() - mainOutputDir = self.ui.mainOutputSelector.currentPath - width = self.ui.vrgRes_width.value - height = self.ui.vrgRes_height.value - nPossibleCameras = self.ui.posCamSpin.value - nOptimizedCameras = self.ui.optCamSpin.value - tmpDir = self.ui.vrgTempDir.text - tfmPath = self.ui.tfmSubDir.text - cameraSubDir = self.ui.cameraSubDir.text - vrgSubDir = self.ui.vrgSubDir.text - if not self.logic.validateInputs( - volumeNode=volumeNode, - mainOutputDir=mainOutputDir, - width=width, - height=height, - nPossibleCameras=nPossibleCameras, - nOptimizedCameras=nOptimizedCameras, - tmpDir=tmpDir, - tfmPath=tfmPath, - cameraSubDir=cameraSubDir, - vrgSubDir=vrgSubDir, - ): - raise ValueError("Invalid inputs") - return - if not self.logic.validatePaths(mainOutputDir=mainOutputDir): - raise ValueError("Invalid paths") - return - if nPossibleCameras < nOptimizedCameras: - raise Exception("Failed to generate VRG: more optimized cameras than possible cameras") - return - - # center the volume - self.logic.createPathsIfNotExists(os.path.join(mainOutputDir, tfmPath)) - if not self.logic.IsSequenceVolume(volumeNode): - volumeNode.AddCenteringTransform() - tfmNode = slicer.util.getNode(f"{volumeNode.GetName()} centering transform") - volumeNode.HardenTransform() - volumeNode.SetAndObserveTransformNodeID(None) - tfmPath = os.path.join(mainOutputDir, tfmPath, "Origin2Dicom.tfm") - tfmNode.Inverse() - slicer.util.saveNode(tfmNode, tfmPath) - slicer.mrmlScene.RemoveNode(tfmNode) - else: - # Just export the first frame - currentNode, _ = self.logic.getItemInSequence(volumeNode, 0) - currentNode.AddCenteringTransform() - tfmNode = currentNode.GetParentTransformNode() - currentNode.HardenTransform() - currentNode.SetAndObserveTransformNodeID(None) - tfmPath = os.path.join(mainOutputDir, tfmPath, "Origin2Dicom.tfm") - tfmNode.Inverse() - slicer.util.saveNode(tfmNode, tfmPath) - slicer.mrmlScene.RemoveNode(tfmNode) - - # Harden and remove the transform from the sequence - for i in range(1, volumeNode.GetNumberOfDataNodes()): - currentNode, _ = self.logic.getItemInSequence(volumeNode, i) - currentNode.AddCenteringTransform() - tfmNode = currentNode.GetParentTransformNode() - currentNode.HardenTransform() - currentNode.SetAndObserveTransformNodeID(None) - slicer.mrmlScene.RemoveNode(tfmNode) - - numFrames = 1 - currentNode = volumeNode - curName = volumeNode.GetName() - if self.logic.IsSequenceVolume(currentNode): - numFrames = volumeNode.GetNumberOfDataNodes() - currentNode, curName = self.logic.getItemInSequence(volumeNode, 0) - bounds = [0] * 6 - currentNode.GetBounds(bounds) - - # Generate all possible camera positions - camOffset = self.ui.camOffSetSpin.value - cameras = RadiographGeneration.generateNCameras( - nPossibleCameras, bounds, camOffset, [width, height], self.ui.camDebugCheckbox.isChecked() - ) - - # TODO: Validate that both the tmp directory and the camera calibration directory are empty before starting - - cameraDir = os.path.join(mainOutputDir, cameraSubDir) - if not os.path.exists(cameraDir): - os.mkdir(cameraDir) - for cam in cameras: # Generate Camera Calib files - camFName = os.path.join(cameraDir, f"cam{cam.id}.json") - IO.generateCameraCalibrationFile(cam, camFName) - - self.updateProgressBar(10) - - # Generate initial VRG for each camera - for i in range(numFrames): - filename = self.logic.cleanFilename(curName, i) if not self.ui.idxOnly.isChecked() else i - self.logic.generateVRGForCameras( - os.path.join(mainOutputDir, cameraSubDir), - currentNode, - os.path.join(mainOutputDir, tmpDir), - [width, height], - filename=filename, - ) - progress = (i + 1) / numFrames * 40 + 10 - self.updateProgressBar(progress) - - if self.logic.IsSequenceVolume(volumeNode): - currentNode, curName = self.logic.getNextItemInSequence(volumeNode) - - # Optimize the camera positions - bestCameras = RadiographGeneration.optimizeCameras( - cameras, os.path.join(mainOutputDir, tmpDir), nOptimizedCameras, progressCallback=self.updateProgressBar - ) - - shutil.rmtree(os.path.join(mainOutputDir, cameraSubDir)) - - # Move the optimized VRGs to the final directory and generate the camera calibration files - self.logic.generateCameraCalibrationFiles( - bestCameras, - os.path.join(mainOutputDir, tmpDir), - os.path.join(mainOutputDir, vrgSubDir), - os.path.join(mainOutputDir, cameraSubDir), - progressCallback=self.updateProgressBar, - ) - - # Clean Up - if self.ui.removeVrgTmp.isChecked(): - shutil.rmtree(os.path.join(mainOutputDir, tmpDir)) - - slicer.util.messageBox("Success!") - - if not self.logic.IsSequenceVolume(volumeNode): - firstNode = volumeNode - else: - firstNode, _ = self.logic.getItemInSequence(volumeNode, 0) - firstNode.SetAndObserveTransformNodeID(tfmNode.GetID()) - firstNode.HardenTransform() - slicer.mrmlScene.RemoveNode(tfmNode) - def onGenerateConfig(self): """ Generates a complete config file (including all partial volumes, radiographs, @@ -792,113 +634,6 @@ def onLoadPV(self): slicer.util.messageBox("Success!") - def onManualVRGGen(self): - with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): - markupsNode = self.ui.mVRG_markupSelector.currentNode() - volumeNode = self.ui.volumeSelector.currentNode() - mainOutputDir = self.ui.mainOutputSelector.currentPath - viewAngle = self.ui.mVRG_viewAngleSpin.value - clippingRange = ( - self.ui.mVRG_ClippingRangeSlider.minimumValue, - self.ui.mVRG_ClippingRangeSlider.maximumValue, - ) - width = self.ui.vrgRes_width.value - height = self.ui.vrgRes_height.value - vrgDir = self.ui.vrgSubDir.text - cameraDir = self.ui.cameraSubDir.text - if not self.logic.validateInputs( - markupsNode=markupsNode, - volumeNode=volumeNode, - mainOutputDir=mainOutputDir, - viewAngle=viewAngle, - clippingRange=clippingRange, - width=width, - height=height, - vrgDir=vrgDir, - cameraDir=cameraDir, - ): - raise Exception("Failed to generate VRG: invalid inputs") - return - if not self.logic.validatePaths(mainOutputDir=mainOutputDir): - raise Exception("Failed to generate VRG: invalid output directory") - return - self.logic.createPathsIfNotExists( - os.path.join(mainOutputDir, vrgDir), os.path.join(mainOutputDir, cameraDir) - ) - - if self.logic.vrgManualCameras is None: - self.onMarkupNodeChanged(markupsNode) # create the cameras - - if not self.logic.IsVolumeCentered(volumeNode): - logging.warning("Volume is not centered at the origin. This may cause issues with Autoscoper.") - - for cam in self.logic.vrgManualCameras: - IO.generateCameraCalibrationFile(cam, os.path.join(mainOutputDir, cameraDir, f"cam{cam.id}.json")) - - self.updateProgressBar(0) - - numFrames = 1 - currentNode = volumeNode - curName = currentNode.GetName() - if self.logic.IsSequenceVolume(currentNode): - numFrames = volumeNode.GetNumberOfDataNodes() - currentNode, curName = self.logic.getItemInSequence(volumeNode, 0) - - for i in range(numFrames): - filename = self.logic.cleanFilename(curName, i) - self.logic.generateVRGForCameras( - os.path.join(mainOutputDir, cameraDir), - currentNode, - os.path.join(mainOutputDir, vrgDir), - [width, height], - filename=filename, - ) - - progress = ((i + 1) / numFrames) * 100 - self.updateProgressBar(progress) - - if self.logic.IsSequenceVolume(volumeNode): - currentNode, curName = self.logic.getNextItemInSequence(volumeNode) - - self.updateProgressBar(100) - slicer.util.messageBox("Success!") - - def onMarkupNodeChanged(self, node): - if node is None: - if self.logic.vrgManualCameras is not None: - # clean up - for cam in self.logic.vrgManualCameras: - slicer.mrmlScene.RemoveNode(cam.FrustumModel) - self.logic.vrgManualCameras = None - return - if self.logic.vrgManualCameras is not None: - # clean up - for cam in self.logic.vrgManualCameras: - slicer.mrmlScene.RemoveNode(cam.FrustumModel) - self.logic.vrgManualCameras = None - # get the volume nodes - volumeNode = self.ui.volumeSelector.currentNode() - self.logic.validateInputs(volumeNode=volumeNode) - bounds = self.logic.GetRASBounds(volumeNode) - self.logic.vrgManualCameras = RadiographGeneration.generateCamerasFromMarkups( - node, - bounds, - (self.ui.mVRG_ClippingRangeSlider.minimumValue, self.ui.mVRG_ClippingRangeSlider.maximumValue), - self.ui.mVRG_viewAngleSpin.value, - [self.ui.vrgRes_width.value, self.ui.vrgRes_height.value], - True, - ) - - def updateClippingRange(self, min, max): - for cam in self.logic.vrgManualCameras: - cam.vtkCamera.SetClippingRange(min, max) - RadiographGeneration._updateFrustumModel(cam) - - def updateViewAngle(self, value): - for cam in self.logic.vrgManualCameras: - cam.vtkCamera.SetViewAngle(value) - RadiographGeneration._updateFrustumModel(cam) - # # AutoscoperMLogic @@ -924,7 +659,6 @@ def __init__(self): self.AutoscoperProcess = qt.QProcess() self.AutoscoperProcess.setProcessChannelMode(qt.QProcess.ForwardedChannels) self.AutoscoperSocket = None - self.vrgManualCameras = None @staticmethod def IsSequenceVolume(node: Union[slicer.vtkMRMLNode, None]) -> bool: @@ -1300,136 +1034,6 @@ def extractSubVolumeForVRG( return newVolumeImageData, bounds - def generateVRGForCameras( - self, - cameraDir: str, - volumeNode: slicer.vtkMRMLVolumeNode, - outputDir: str, - size: list[int], - filename: str, - ) -> None: - """ - Generates VRG files for each camera in the cameras list - - :param cameraDir: Directory containing the camera JSON files - :param volumeNode: volume node - :param outputDir: output directory - :param size: size of the VRG - :param filename: filename of the VRG - """ - self.createPathsIfNotExists(outputDir) - # Apply a thresh of 0 to the volume to remove air from the volume - thresholdScalarVolume = slicer.modules.thresholdscalarvolume - parameters = { - "InputVolume": volumeNode.GetID(), - "OutputVolume": volumeNode.GetID(), - "ThresholdValue": 0, - "ThresholdType": "Below", - "Lower": 0, - } - slicer.cli.runSync(thresholdScalarVolume, None, parameters) - - # write a temporary volume to disk - volumeFileName = "AutoscoperM_VRG_GEN_TEMP.mhd" - IO.writeTemporyFile(volumeFileName, self.convertNodeToData(volumeNode)) - - # Execute CLI for each camera - cliModule = slicer.modules.virtualradiographgeneration - parameters = { - "inputVolumeFileName": os.path.join(slicer.app.temporaryPath, volumeFileName), - "cameraDir": cameraDir, - "radiographMainOutDir": outputDir, - "outputFileName": f"{filename}.tif", - "outputWidth": size[0], - "outputHeight": size[1], - } - cliNode = slicer.cli.run(cliModule, None, parameters) # run asynchronously - - # Note: CLI nodes are currently not executed in parallel. See https://github.com/Slicer/Slicer/pull/6723 - # This just allows the UI to remain responsive while the CLI nodes are running for now. - - # Wait for all the CLI nodes to finish - while cliNode.GetStatusString() != "Completed": - slicer.app.processEvents() - if cliNode.GetStatus() & cliNode.ErrorsMask: - # error - errorText = cliNode.GetErrorText() - slicer.mrmlScene.RemoveNode(cliNode) - raise ValueError("CLI execution failed: " + errorText) - slicer.mrmlScene.RemoveNode(cliNode) - - def generateCameraCalibrationFiles( - self, - bestCameras: list[RadiographGeneration.Camera], - tmpDir: str, - finalDir: str, - calibDir: str, - progressCallback: Optional[callable] = None, - ) -> None: - """ - Copies the optimized VRGs from the temporary directory to the final directory - and generates the camera calibration files - - :param bestCameras: list of optimized cameras - :param tmpDir: temporary directory - :param finalDir: final directory - :param calibDir: calibration directory - :param progressCallback: progress callback, defaults to None - """ - self.validatePaths(tmpDir=tmpDir) - self.createPathsIfNotExists(finalDir, calibDir) - if not progressCallback: - logging.warning( - "[AutoscoperM.logic.generateCameraCalibrationFiles] " - "No progress callback provided, progress bar will not be updated" - ) - - def progressCallback(x): - return x - - for idx, cam in enumerate(bestCameras): - IO.generateCameraCalibrationFile(cam, os.path.join(calibDir, f"cam{cam.id}.json")) - cameraDir = os.path.join(finalDir, f"cam{cam.id}") - self.createPathsIfNotExists(cameraDir) - # Copy all tif files from the tmp to the final directory - for file in glob.glob(os.path.join(tmpDir, f"cam{cam.id}", "*.tif")): - shutil.copy(file, cameraDir) - - progress = ((idx + 1) / len(bestCameras)) * 10 + 90 - progressCallback(progress) - - @staticmethod - def convertNodeToData(volumeNode: slicer.vtkMRMLVolumeNode) -> vtk.vtkImageData: - """ - Converts a volume node to a vtkImageData object - """ - imageData = vtk.vtkImageData() - imageData.DeepCopy(volumeNode.GetImageData()) - imageData.SetSpacing(volumeNode.GetSpacing()) - origin = list(volumeNode.GetOrigin()) - imageData.SetOrigin(origin) - - mat = vtk.vtkMatrix4x4() - volumeNode.GetIJKToRASMatrix(mat) - if mat.GetElement(0, 0) < 0 and mat.GetElement(1, 1) < 0: - origin[0:2] = [x * -1 for x in origin[0:2]] - imageData.SetOrigin(origin) - - # Ensure we are in the correct orientation (RAS vs LPS) - imageReslice = vtk.vtkImageReslice() - imageReslice.SetInputData(imageData) - - axes = vtk.vtkMatrix4x4() - axes.Identity() - axes.SetElement(0, 0, -1) - axes.SetElement(1, 1, -1) - - imageReslice.SetResliceAxes(axes) - imageReslice.Update() - imageData = imageReslice.GetOutput() - - return imageData - @staticmethod def getItemInSequence(sequenceNode: slicer.vtkMRMLSequenceNode, idx: int) -> slicer.vtkMRMLNode: """ @@ -1492,21 +1096,6 @@ def GetVolumeSpacing(node: Union[slicer.vtkMRMLVolumeNode, slicer.vtkMRMLSequenc return AutoscoperMLogic.getItemInSequence(node, 0)[0].GetSpacing() return node.GetSpacing() - @staticmethod - def GetRASBounds(node: Union[slicer.vtkMRMLVolumeNode, slicer.vtkMRMLSequenceNode]) -> list[float]: - bounds = [0] * 6 - if AutoscoperMLogic.IsSequenceVolume(node): - AutoscoperMLogic.getItemInSequence(node, 0)[0].GetRASBounds(bounds) - else: - node.GetRASBounds(bounds) - return bounds - - @staticmethod - def IsVolumeCentered(node: Union[slicer.vtkMRMLVolumeNode, slicer.vtkMRMLSequenceNode]) -> bool: - if AutoscoperMLogic.IsSequenceVolume(node): - return AutoscoperMLogic.getItemInSequence(node, 0)[0].IsCentered() - return node.IsCentered() - @staticmethod def loadTransformFromFile(transformFileName: str) -> slicer.vtkMRMLLinearTransformNode: return slicer.util.loadNodeFromFile(transformFileName) diff --git a/AutoscoperM/AutoscoperMLib/IO.py b/AutoscoperM/AutoscoperMLib/IO.py index b8ccd52..01bf5c5 100644 --- a/AutoscoperM/AutoscoperMLib/IO.py +++ b/AutoscoperM/AutoscoperMLib/IO.py @@ -7,8 +7,6 @@ import slicer import vtk -from .RadiographGeneration import Camera - def loadSegmentation(segmentationNode: slicer.vtkMRMLSegmentationNode, filename: str): """ @@ -35,34 +33,6 @@ def loadSegmentation(segmentationNode: slicer.vtkMRMLSegmentationNode, filename: return None -def generateCameraCalibrationFile(camera: Camera, filename: str): - """ - Generates a VTK camera calibration json file from the given camera. - - :param camera: Camera - :param filename: Output file name - """ - import json - - contents = {} - contents["@schema"] = "https://autoscoperm.slicer.org/vtkCamera-schema-1.0.json" - contents["version"] = 1.0 - contents["focal-point"] = camera.vtkCamera.GetFocalPoint() - contents["camera-position"] = camera.vtkCamera.GetPosition() - contents["view-up"] = camera.vtkCamera.GetViewUp() - contents["view-angle"] = camera.vtkCamera.GetViewAngle() - contents["image-width"] = camera.imageSize[0] - contents["image-height"] = camera.imageSize[1] - # The clipping-range field is not used by Autoscoper, it is used to communicate - # information between AutoscoperM and VirtualRadiographGeneration modules within Slicer. - contents["clipping-range"] = camera.vtkCamera.GetClippingRange() - - contents_json = json.dumps(contents, indent=4) - - with open(filename, "w+") as f: - f.write(contents_json) - - def generateConfigFile( mainDirectory: str, subDirectories: list[str], @@ -263,34 +233,3 @@ def writeTRA(fileName: str, transforms: list[vtk.vtkMatrix4x4]) -> None: with open(fileName, "w+") as traFile: for row in rowWiseStrings: traFile.write(",".join(row) + "\n") - - -def writeTemporyFile(filename: str, data: vtk.vtkImageData) -> str: - """ - Writes a temporary file to the slicer temp directory - - :param filename: Output file name - :param data: data - - :return: Path to the file - """ - - slicerTempDirectory = slicer.app.temporaryPath - - # write vtk image data as a vtk file - writer = vtk.vtkMetaImageWriter() - writer.SetFileName(os.path.join(slicerTempDirectory, filename)) - writer.SetInputData(data) - writer.Write() - return writer.GetFileName() - - -def removeTemporyFile(filename: str): - """ - Removes a temporary file from the slicer temp directory - - :param filename: Output file name - """ - - slicerTempDirectory = slicer.app.temporaryPath - os.remove(os.path.join(slicerTempDirectory, filename)) diff --git a/AutoscoperM/AutoscoperMLib/RadiographGeneration.py b/AutoscoperM/AutoscoperMLib/RadiographGeneration.py deleted file mode 100644 index 86566a6..0000000 --- a/AutoscoperM/AutoscoperMLib/RadiographGeneration.py +++ /dev/null @@ -1,239 +0,0 @@ -import math -from typing import Optional - -import slicer -import vtk - - -class Camera: - def __init__(self) -> None: - self.DID = 0 - self.vtkCamera = vtk.vtkCamera() - self.imageSize = [512, 512] - self.id = -1 - self.FrustumModel = None - - def __str__(self) -> str: - return "\n".join( - [ - f"Camera {self.id}", - f"Position: {self.vtkCamera.GetPosition()}", - f"Focal Point: {self.vtkCamera.GetFocalPoint()}", - f"View Angle: {self.vtkCamera.GetViewAngle()}", - f"Clipping Range: {self.vtkCamera.GetClippingRange()}", - f"View Up: {self.vtkCamera.GetViewUp()}", - f"Direction of Projection: {self.vtkCamera.GetDirectionOfProjection()}", - f"Distance: {self.vtkCamera.GetDistance()}", - f"Image Size: {self.imageSize}", - f"DID: {self.DID}", - "~" * 20, - ] - ) - - -def _createFrustumModel(cam: Camera) -> None: - model = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") - model.CreateDefaultDisplayNodes() - model.GetDisplayNode().SetColor(1, 0, 0) # Red - model.GetDisplayNode().SetOpacity(0.3) - model.GetDisplayNode().SetVisibility(True) - - model.SetName(f"cam{cam.id}-frustum") - - cam.FrustumModel = model - - _updateFrustumModel(cam) - - -def _updateFrustumModel(cam: Camera) -> None: - if cam.FrustumModel is None: - _createFrustumModel(cam) - return - # The equations of the six planes of the frustum in the order: left, right, bottom, top, far, near - # Given as A, B, C, D where Ax + By + Cz + D = 0 for each plane - planesArray = [0] * 24 - aspectRatio = cam.vtkCamera.GetExplicitAspectRatio() - - cam.vtkCamera.GetFrustumPlanes(aspectRatio, planesArray) - - planes = vtk.vtkPlanes() - planes.SetFrustumPlanes(planesArray) - - hull = vtk.vtkHull() - hull.SetPlanes(planes) - pd = vtk.vtkPolyData() - hull.GenerateHull(pd, [-1000, 1000, -1000, 1000, -1000, 1000]) - - cam.FrustumModel.SetAndObservePolyData(pd) - - -def generateNCameras( - N: int, bounds: list[int], offset: int = 100, imageSize: tuple[int] = (512, 512), camDebugMode: bool = False -) -> list[Camera]: - """ - Generate N cameras - - :param N: Number of cameras to generate - :param bounds: Bounds of the volume - :param offset: Offset from the volume. Defaults to 100. - :param imageSize: Image size. Defaults to [512,512]. - :param camDebugMode: Whether or not to show the cameras in the scene. Defaults to False. - - :return: List of cameras - """ - # find the center of the bounds - center = [(bounds[0] + bounds[1]) / 2, (bounds[2] + bounds[3]) / 2, (bounds[4] + bounds[5]) / 2] - - # find the largest dimension of the bounds - largestDimension = max([bounds[1] - bounds[0], bounds[3] - bounds[2], bounds[5] - bounds[4]]) - - # find the distance from the center to the bounds - r = largestDimension / 2 + offset - - points = vtk.vtkPoints() - points.SetNumberOfPoints(N) - points.SetDataTypeToDouble() - points.Allocate(N) - - # use the spherical fibonacci algorithm to generate the points - goldenRatio = (1 + math.sqrt(5)) / 2 - i = range(0, N) - theta = [2 * math.pi * i / goldenRatio for i in i] - phi = [math.acos(1 - 2 * (i + 0.5) / N) for i in i] - x = [math.cos(theta[i]) * math.sin(phi[i]) for i in i] - y = [math.sin(theta[i]) * math.sin(phi[i]) for i in i] - z = [math.cos(phi[i]) for i in i] - # scale the points to the radius - x = [r * x[i] for i in i] - y = [r * y[i] for i in i] - z = [r * z[i] for i in i] - for px, py, pz in zip(x, y, z): - points.InsertNextPoint(px + center[0], py + center[1], pz + center[2]) - - # create the cameras - cameras = [] - for i in range(N): - camera = Camera() - camera.vtkCamera.SetPosition(points.GetPoint(i)) - camera.vtkCamera.SetFocalPoint(center) - camera.vtkCamera.SetViewAngle(30) - camera.vtkCamera.SetClippingRange(0.1, r + largestDimension) - camera.vtkCamera.SetViewUp(0, 1, 0) - camera.id = i - camera.imageSize = imageSize - cameras.append(camera) - - if camDebugMode: - # Visualize the cameras - markups = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") - for cam in cameras: - print(cam) - # add a point to the markups node - markups.AddControlPoint( - cam.vtkCamera.GetPosition()[0], - cam.vtkCamera.GetPosition()[1], - cam.vtkCamera.GetPosition()[2], - f"cam{cam.id}", - ) - # lock the point - markups.SetNthControlPointLocked(markups.GetNumberOfControlPoints() - 1, True) - - _createFrustumModel(cam) - - return cameras - - -def generateCamerasFromMarkups( - fiduaicalNode: slicer.vtkMRMLMarkupsFiducialNode, - volumeBounds: list[int], - clippingRange: tuple[int], - viewAngle: int, - imageSize: tuple[int] = (512, 512), - cameraDebug: bool = False, -) -> list[Camera]: - """ - Generate cameras from a markups fiducial node - - :param fiduaicalNode: Markups fiducial node - :param volumeBounds: Bounds of the volume - :param clippingRange: Clipping range - :param viewAngle: View angle - :param imageSize: Image size. Defaults to [512,512]. - :param cameraDebug: Whether or not to show the cameras in the scene. Defaults to False. - - :return: List of cameras - """ - center = [ - (volumeBounds[0] + volumeBounds[1]) / 2, - (volumeBounds[2] + volumeBounds[3]) / 2, - (volumeBounds[4] + volumeBounds[5]) / 2, - ] - n = fiduaicalNode.GetNumberOfControlPoints() - cameras = [] - for i in range(n): - camera = Camera() - camera.vtkCamera.SetPosition(fiduaicalNode.GetNthControlPointPosition(i)) - camera.vtkCamera.SetFocalPoint(center) - camera.vtkCamera.SetViewAngle(viewAngle) - camera.vtkCamera.SetClippingRange(clippingRange[0], clippingRange[1]) - camera.vtkCamera.SetViewUp(0, 1, 0) - camera.id = fiduaicalNode.GetNthControlPointLabel(i) - camera.imageSize = imageSize - if cameraDebug: - _createFrustumModel(camera) - cameras.append(camera) - return cameras - - -def optimizeCameras( - cameras: list[Camera], - cameraDir: str, - nOptimizedCameras: int, - progressCallback: Optional[callable] = None, -) -> list[Camera]: - """ - Optimize the cameras by finding the N cameras with the best data intensity density. - - :param cameras: Cameras - :param cameraDir: Camera directory - :param nOptimizedCameras: Number of optimized cameras to find - - :return: Optimized cameras - """ - import os - - if not progressCallback: - - def progressCallback(_x): - return None - - # Parallel calls to cliModule - cliModule = slicer.modules.calculatedataintensitydensity - cliNodes = [] - for i in range(len(cameras)): - camera = cameras[i] - vrgDirName = os.path.join(cameraDir, f"cam{camera.id}") - cliNode = slicer.cli.run( - cliModule, - None, - {"whiteRadiographDirName": vrgDirName}, - wait_for_completion=False, - ) - cliNodes.append(cliNode) - - for i in range(len(cameras)): - while cliNodes[i].GetStatusString() != "Completed": - slicer.app.processEvents() - if cliNode.GetStatus() & cliNode.ErrorsMask: - # error - errorText = cliNode.GetErrorText() - slicer.mrmlScene.RemoveNode(cliNode) - raise ValueError("CLI execution failed: " + errorText) - cameras[i].DID = float(cliNodes[i].GetOutputText()) # cliNodes[i].GetParameterAsString("dataIntensityDensity") - progress = ((i + 1) / len(cameras)) * 40 + 50 - progressCallback(progress) - slicer.mrmlScene.RemoveNode(cliNodes[i]) - - cameras.sort(key=lambda x: x.DID) - - return cameras[:nOptimizedCameras] diff --git a/AutoscoperM/CMakeLists.txt b/AutoscoperM/CMakeLists.txt index 9af705f..f752650 100644 --- a/AutoscoperM/CMakeLists.txt +++ b/AutoscoperM/CMakeLists.txt @@ -6,7 +6,6 @@ set(MODULE_PYTHON_SCRIPTS ${MODULE_NAME}.py ${MODULE_NAME}Lib/__init__.py ${MODULE_NAME}Lib/IO.py - ${MODULE_NAME}Lib/RadiographGeneration.py ${MODULE_NAME}Lib/SubVolumeExtraction.py ) diff --git a/AutoscoperM/Resources/UI/AutoscoperM.ui b/AutoscoperM/Resources/UI/AutoscoperM.ui index 9dbcde4..09d347a 100644 --- a/AutoscoperM/Resources/UI/AutoscoperM.ui +++ b/AutoscoperM/Resources/UI/AutoscoperM.ui @@ -218,36 +218,6 @@ - - - - true - - - Delete Temporary VRG Files - - - true - - - true - - - - - - - Only use indices for radiograph filename - - - - - - - VRG Temp Subdirectory: - - - @@ -286,16 +256,6 @@ - - - - Camera Debug Mode - - - false - - - @@ -337,13 +297,6 @@ - - - - VRG-Temp - - - @@ -523,217 +476,6 @@ - - - - VRG Generation - Manual Camera Placement - - - false - - - - - - Camera Positions - - - - - - - 0.100000000000000 - - - 2000.000000000000000 - - - - - - - Clipping Range - - - - - - - View Angle - - - - - - - true - - - - vtkMRMLMarkupsFiducialNode - - - - - - - - - - - - - - Generate VRGs from Markups - - - - - - - 0.100000000000000 - - - 2000.000000000000000 - - - 0.100000000000000 - - - 0.100000000000000 - - - 300.000000000000000 - - - Qt::Horizontal - - - - - - - 360 - - - 30 - - - - - - - 0.100000000000000 - - - 2000.000000000000000 - - - 300.000000000000000 - - - - - - - - - - true - - - VRG Generation - Automatic Camera Placement - - - false - - - true - - - - - - 0 - - - 1000 - - - 400 - - - - - - - Camera Offset: - - - - - - - # of Optimized Cameras: - - - - - - - 10 - - - 50 - - - - - - - # of Possible Cameras: - - - - - - - false - - - 0 - - - 1000 - - - 400 - - - Qt::Horizontal - - - - - - - Generate VRGs - - - - - - - 2 - - - 2 - - - - - - @@ -996,85 +738,5 @@ - - camOffSetSpin - valueChanged(int) - camOffSetSlider - setValue(int) - - - 953 - 667 - - - 553 - 667 - - - - - camOffSetSlider - valueChanged(int) - camOffSetSpin - setValue(int) - - - 553 - 667 - - - 953 - 667 - - - - - AutoscoperM - mrmlSceneChanged(vtkMRMLScene*) - mVRG_markupSelector - setMRMLScene(vtkMRMLScene*) - - - 508 - 429 - - - 507 - 409 - - - - - mVRG_ClippingRangeSlider - minimumValueChanged(double) - mVRG_clippingRangeMinBox - setValue(double) - - - 567 - 499 - - - 189 - 499 - - - - - mVRG_ClippingRangeSlider - maximumValueChanged(double) - mVRG_clippingRangeMaxBox - setValue(double) - - - 567 - 499 - - - 944 - 499 - - - diff --git a/CMakeLists.txt b/CMakeLists.txt index 22c84bd..d5efaa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,8 +42,6 @@ endif() # Extension modules add_subdirectory(AutoscoperM) add_subdirectory(TrackingEvaluation) -add_subdirectory(CalculateDataIntensityDensity) -add_subdirectory(VirtualRadiographGeneration) add_subdirectory(Hierarchical3DRegistration) ## NEXT_MODULE diff --git a/CalculateDataIntensityDensity/CMakeLists.txt b/CalculateDataIntensityDensity/CMakeLists.txt deleted file mode 100644 index 1588932..0000000 --- a/CalculateDataIntensityDensity/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -#----------------------------------------------------------------------------- -set(MODULE_NAME CalculateDataIntensityDensity) - -SlicerMacroBuildScriptedCLI( - NAME ${MODULE_NAME} - ) diff --git a/CalculateDataIntensityDensity/CalculateDataIntensityDensity.py b/CalculateDataIntensityDensity/CalculateDataIntensityDensity.py deleted file mode 100644 index ca123ee..0000000 --- a/CalculateDataIntensityDensity/CalculateDataIntensityDensity.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python-real - -import concurrent.futures as cf -import glob -import os -import sys - -import numpy as np -import SimpleITK as sitk - - -def calcDID(whiteRadiographFName): - MEAN_COMPARISON = 185 - # Read in the white radiograph - whiteRadiograph = sitk.ReadImage(whiteRadiographFName) - - # Superpixel Segmentation - slicImageFilter = sitk.SLICImageFilter() - slicImageFilter.SetSuperGridSize([85, 85, 85]) - labelImage = slicImageFilter.Execute(whiteRadiograph) - - # Get the mean pixel value for each label - labelStatsFilter = sitk.LabelStatisticsImageFilter() - labelStatsFilter.Execute(whiteRadiograph, labelImage) - N = labelStatsFilter.GetNumberOfLabels() - labelMeanColors = np.zeros((N, 1)) - labelWidth, labelHeight = labelImage.GetSize() - labels = list(labelStatsFilter.GetLabels()) - labels.sort() - for labelIdx, labelValue in enumerate(labels): - labelMeanColors[labelIdx, 0] = labelStatsFilter.GetMean(labelValue) - - # Create a binary label from the labelImage where all '1' are labels whose meanColor are < MEAN_COMPARISON - labelShapeFilter = sitk.LabelShapeStatisticsImageFilter() - labelShapeFilter.Execute(labelImage) - binaryLabels = np.zeros((labelWidth, labelHeight)) - for labelIdx, labelValue in enumerate(labels): - if labelValue == 0: - continue - if labelMeanColors[labelIdx, 0] < MEAN_COMPARISON: - pixels = list(labelShapeFilter.GetIndexes(labelValue)) - for j in range(0, len(pixels), 2): - y = pixels[j] - x = pixels[j + 1] - binaryLabels[x, y] = 1 - - # Calculate the Data Intensity Density - # Largest Region based off of https://discourse.itk.org/t/simpleitk-extract-largest-connected-component-from-binary-image/4958/2 - binaryImage = sitk.Cast(sitk.GetImageFromArray(binaryLabels), sitk.sitkUInt8) - componentImage = sitk.ConnectedComponent(binaryImage) - sortedComponentImage = sitk.RelabelComponent(componentImage, sortByObjectSize=True) - largest = sortedComponentImage == 1 - - return np.sum(sitk.GetArrayFromImage(largest)) - - -def main(whiteRadiographDirName: str) -> float: - """ - Calculates the data intensity density of the given camera on its corresponding white radiograph. - Internal function used by :func:`optimizeCameras`. - - :param whiteRadiographFName: White radiograph file name - - return Data intensity density - """ - whiteRadiographFiles = glob.glob(os.path.join(whiteRadiographDirName, "*.tif")) - - if not isinstance(whiteRadiographDirName, str): - raise TypeError(f"whiteRadiographDirName must be a string, not {type(whiteRadiographDirName)}") - if not os.path.isdir(whiteRadiographDirName): - raise FileNotFoundError(f"Directory {whiteRadiographDirName} not found.") - if len(whiteRadiographFiles) == 0: - raise FileNotFoundError(f"No white radiographs found in {whiteRadiographDirName}") - - if len(whiteRadiographFiles) > 1: # Avoid overhead in the single 3DCT case - dids = [] - with cf.ThreadPoolExecutor() as executor: - futures = [executor.submit(calcDID, wrFName) for wrFName in whiteRadiographFiles] - for future in cf.as_completed(futures): - dids.append(future.result()) - return np.mean(dids) - return calcDID(whiteRadiographFiles[0]) - - -if __name__ == "__main__": - expected_args = [ - "whiteRadiographFileName", - # Value reported on standard output - "DID", - ] - expected_args = [f"<{arg}>" for arg in expected_args] - if len(sys.argv) < len(expected_args): - print(f"Usage: {sys.argv[0]} {' '.join(expected_args)}") - sys.exit(1) - print(main(sys.argv[1])) diff --git a/CalculateDataIntensityDensity/CalculateDataIntensityDensity.xml b/CalculateDataIntensityDensity/CalculateDataIntensityDensity.xml deleted file mode 100644 index eaaac5e..0000000 --- a/CalculateDataIntensityDensity/CalculateDataIntensityDensity.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - Tracking.Advanced - 0 - CalculateDataIntensityDensity - - 0.1.0. - https://autoscoperm.slicer.org/ - - Anthony Lombardi (Kitware) - Amy M Morton (Brown University) - Bardiya Akhbari (Brown University) - Beatriz Paniagua (Kitware) - Jean-Christophe Fillion-Robin (Kitware) - - - - whiteRadiographDirName - - 1 - - input - - - DID - - 2 - - output - - - outliers - - 3 - - output - - - - - diff --git a/Hierarchical3DRegistration/Hierarchical3DRegistration.py b/Hierarchical3DRegistration/Hierarchical3DRegistration.py index 9e03a04..5317e2f 100644 --- a/Hierarchical3DRegistration/Hierarchical3DRegistration.py +++ b/Hierarchical3DRegistration/Hierarchical3DRegistration.py @@ -30,10 +30,6 @@ def __init__(self, parent): self.parent.categories = [ "Tracking", ] - self.parent.dependencies = [ - "CalculateDataIntensityDensity", - "VirtualRadiographGeneration", - ] self.parent.contributors = [ "Anthony Lombardi (Kitware)", "Amy M Morton (Brown University)", diff --git a/VirtualRadiographGeneration/CMakeLists.txt b/VirtualRadiographGeneration/CMakeLists.txt deleted file mode 100644 index 02872d6..0000000 --- a/VirtualRadiographGeneration/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -#----------------------------------------------------------------------------- -set(MODULE_NAME VirtualRadiographGeneration) - -SlicerMacroBuildScriptedCLI( - NAME ${MODULE_NAME} - ) diff --git a/VirtualRadiographGeneration/VirtualRadiographGeneration.py b/VirtualRadiographGeneration/VirtualRadiographGeneration.py deleted file mode 100644 index 721155c..0000000 --- a/VirtualRadiographGeneration/VirtualRadiographGeneration.py +++ /dev/null @@ -1,168 +0,0 @@ -#!/usr/bin/env python-real - -import concurrent.futures as cf -import glob -import json -import os -import sys - -import vtk - - -def generateVRG( - camera: vtk.vtkCamera, - volumeImageData: vtk.vtkImageData, - outputFileName: str, - width: int, - height: int, -) -> None: - """ - Generate a virtual radiograph from the given camera and volume node - - :param camera: Camera - :param volumeImageData: Volume image data - :param outputFileName: Output file name - :param width: Width of the output image - :param height: Height of the output image - """ - - # find the min and max scalar values - hist = vtk.vtkImageHistogramStatistics() - hist.SetInputData(volumeImageData) - hist.Update() - minVal = hist.GetMinimum() - maxVal = hist.GetMaximum() - - # create the renderer - renderer = vtk.vtkRenderer() - renderer.SetBackground(1, 1, 1) # Set background to white - renderer.SetUseDepthPeeling(True) - - # create the render window - renderWindow = vtk.vtkRenderWindow() - renderWindow.SetOffScreenRendering(1) - renderWindow.SetSize(width, height) - renderWindow.AddRenderer(renderer) - - # create the volume mapper - volumeMapper = vtk.vtkGPUVolumeRayCastMapper() - volumeMapper.SetInputData(volumeImageData) - volumeMapper.SetBlendModeToComposite() - volumeMapper.SetBlendModeToComposite() - volumeMapper.SetUseJittering(False) - - # Set the transfer functions for opacity, gradient and color - opacityTransferFunction = vtk.vtkPiecewiseFunction() # From the Slicer CT XRay preset - opacityTransferFunction.AddPoint(minVal, 0.0) - opacityTransferFunction.AddPoint(1500, 0.05) - opacityTransferFunction.AddPoint(maxVal, 0.05) - - gradTransferFunction = vtk.vtkPiecewiseFunction() # From the Slicer CT XRay preset - gradTransferFunction.AddPoint(0, 1) - gradTransferFunction.AddPoint(255, 1) - - colorTransferFunction = vtk.vtkColorTransferFunction() - colorTransferFunction.AddRGBPoint(maxVal, 1, 1, 1) - colorTransferFunction.AddRGBPoint(minVal, 0, 0, 0) - - volumeProperty = vtk.vtkVolumeProperty() - volumeProperty.SetInterpolationTypeToLinear() - volumeProperty.ShadeOff() - volumeProperty.SetScalarOpacity(opacityTransferFunction) - volumeProperty.SetGradientOpacity(gradTransferFunction) - volumeProperty.SetColor(colorTransferFunction) - - # create the volume - volume = vtk.vtkVolume() - volume.SetMapper(volumeMapper) - volume.SetProperty(volumeProperty) - - # add the volume to the renderer - renderer.AddVolume(volume) - renderer.SetActiveCamera(camera) - - # render the image - renderWindow.Render() - - # save the image - writer = vtk.vtkTIFFWriter() - writer.SetFileName(outputFileName) - - windowToImageFilter = vtk.vtkWindowToImageFilter() - windowToImageFilter.SetInput(renderWindow) - windowToImageFilter.SetScale(1) - windowToImageFilter.SetInputBufferTypeToRGB() - - # convert the image to grayscale - luminance = vtk.vtkImageLuminance() - luminance.SetInputConnection(windowToImageFilter.GetOutputPort()) - - writer.SetInputConnection(luminance.GetOutputPort()) - writer.Write() - - -def _createVTKCameras(cameraDir: str, radiographMainDir: str): - """ - Generates a vtkCamera object from the given parameters - """ - cameras = {} - for camFName in glob.glob(os.path.join(cameraDir, "*.json")): - with open(camFName) as f: - camJSON = json.load(f) - - cam = vtk.vtkCamera() - cam.SetPosition(camJSON["camera-position"]) - cam.SetFocalPoint(camJSON["focal-point"]) - cam.SetViewUp(camJSON["view-up"]) - cam.SetViewAngle(camJSON["view-angle"]) - cam.SetClippingRange(camJSON["clipping-range"]) - - cameraSubDirName = os.path.basename(camFName).split(".")[0] - cameraDirName = os.path.join(radiographMainDir, cameraSubDirName) - if not os.path.exists(cameraDirName): - os.mkdir(cameraDirName) - - cameras[cam] = cameraDirName - - return cameras - - -if __name__ == "__main__": - expected_args = [ - "inputVolumeFileName", - "cameraDir", - "radiographMainOutDir", - "outputFileName", - "outputWidth", - "outputHeight", - ] - expected_args = [f"<{arg}>" for arg in expected_args] - if len(sys.argv[1:]) != len(expected_args): - print(f"Usage: {sys.argv[0]} {' '.join(expected_args)}") - sys.exit(1) - - volumeData = sys.argv[1] - cameraDir = sys.argv[2] - radiographMainOutDir = sys.argv[3] - outputFileName = sys.argv[4] - outputWidth = int(sys.argv[5]) - outputHeight = int(sys.argv[6]) - - # create the camera - cameras = _createVTKCameras(cameraDir, radiographMainOutDir) - - # Read the mhd file - reader = vtk.vtkMetaImageReader() - reader.SetFileName(volumeData) - reader.Update() - - # generate the virtual radiograph - with cf.ThreadPoolExecutor() as executor: - futures = [ - executor.submit( - generateVRG, cam, reader.GetOutput(), os.path.join(camDir, outputFileName), outputWidth, outputHeight - ) - for cam, camDir in cameras.items() - ] - for future in cf.as_completed(futures): - future.result() diff --git a/VirtualRadiographGeneration/VirtualRadiographGeneration.xml b/VirtualRadiographGeneration/VirtualRadiographGeneration.xml deleted file mode 100644 index 79a2209..0000000 --- a/VirtualRadiographGeneration/VirtualRadiographGeneration.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - Tracking.Advanced - 0 - VirtualRadiographGeneration - - 0.1.0. - https://autoscoperm.slicer.org/ - - Anthony Lombardi (Kitware) - Amy M Morton (Brown University) - Bardiya Akhbari (Brown University) - Beatriz Paniagua (Kitware) - Jean-Christophe Fillion-Robin (Kitware) - - - - inputVolumeFileName - - 0 - - input - - - cameraDir - - 2 - - input - - - radiographMainOutDir - - 3 - - input - - - outputFileName - - 4 - - input - - - outputWidth - - 5 - - input - - - outputHeight - - 6 - - input - - - - -