Skip to content

Commit

Permalink
ENH: Major update to handle image/mask orientation and more
Browse files Browse the repository at this point in the history
  • Loading branch information
aylward committed Jul 3, 2024
1 parent 0b6838d commit f258a6b
Show file tree
Hide file tree
Showing 25 changed files with 518 additions and 275 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ __pycache__
*.swp
*~
*.zip
itk-wheels
*.tre
*.mha
*.nii
*.code-workspace
7 changes: 7 additions & 0 deletions README_Dev.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
Install the developer build of ITK, ITKMinimalPathExtraction ITKTubetk:

python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ itk itk-minimalpathextraction itk-tubetk

Setup for editable installation. Changes to code in src are
immediatelly reflected in the installed version. Also installs
all dependencies needed for development and testing:

pip install -e .[dev]

Prior to making a commit, please verify your code is compliant:

pre-commit run --all-files

HINT:
QT Designer was used to define the layout. On Windows, to launch
designer, use this command:

qt6-tools.exe designer pytubeview.ui
4 changes: 4 additions & 0 deletions data/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@
*.nii.gz
*.mhd
*.raw
test*
CH*
PCD*
UNC*
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ dependencies = [
'vtk',
'thedicomsort',
#'itk', # Do not install until ITK pypi is updated
#'itk-tubetk', # Do not install until ITK pypi is updated
]

[project.scripts]
Expand Down
12 changes: 12 additions & 0 deletions src/minder3d/lib/sovImageTablePanelWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,22 @@ def remove_all(self):
self.settings.clear_data()
self.fill_table()

def close_expanded_table(self):
self.enlarged_table.close()
self.enlarged_table = None

def expand_table(self):
if self.enlarged_table is None:
self.enlarged_table = ImageTablePanelWidget(self.gui, self.state)
self.enlarged_table.setWindowTitle('Image Table')
self.enlarged_table.imageTableExpandButton.setText("CLOSE")
self.enlarged_table.imageTableExpandButton.setMinimumWidth(75)
self.enlarged_table.imageTableExpandButton.clicked.disconnect()
self.enlarged_table.imageTableExpandButton.clicked.connect(
self.close_expanded_table
)
self.enlarged_table.show()
else:
self.enlarged_table.show()

@time_and_log
Expand Down
65 changes: 53 additions & 12 deletions src/minder3d/lib/sovInfoTablePanelWidget.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import numpy as np

from PySide6.QtWidgets import QTableWidget, QTableWidgetItem, QWidget

from .sovUtils import time_and_log
Expand All @@ -20,12 +22,10 @@ def __init__(self, gui, state, parent=None):
self.gui = gui
self.state = state

self.infoTableWidget.setRowCount(3)
self.pixel_info_row_start = 0

self.infoTableWidget.setSizeAdjustPolicy(QTableWidget.AdjustToContents)
self.infoTableWidget.setColumnCount(2)
self.infoTableWidget.setItem(0, 0, QTableWidgetItem('Pixel Coordinate'))
self.infoTableWidget.setItem(1, 0, QTableWidgetItem(' Image Value'))
self.infoTableWidget.setItem(2, 0, QTableWidgetItem(' Overlay Value'))
self.infoTableWidget.setColumnWidth(1, 200)
self.infoTableWidget.setEditTriggers(QTableWidget.NoEditTriggers)
self.infoTableWidget.setSelectionMode(QTableWidget.NoSelection)
self.infoTableWidget.setShowGrid(True)
Expand All @@ -37,7 +37,46 @@ def __init__(self, gui, state, parent=None):

@time_and_log
def update_image(self):
pass
self.infoTableWidget.clear()
self.infoTableWidget.setRowCount(8)
self.infoTableWidget.setItem(0, 0, QTableWidgetItem('Image Information'))
img = self.state.image[self.state.current_image_num]

self.infoTableWidget.setItem(1, 0, QTableWidgetItem(' Image Size'))
info_str = ', '.join(
[f'{x:0.1f}' for x in img.GetLargestPossibleRegion().GetSize()]
)
self.infoTableWidget.setItem(1, 1, QTableWidgetItem(info_str))

self.infoTableWidget.setItem(2, 0, QTableWidgetItem(' Image Origin'))
info_str = ', '.join(
[f'{x:0.1f}' for x in img.GetOrigin()]
)
self.infoTableWidget.setItem(2, 1, QTableWidgetItem(info_str))

self.infoTableWidget.setItem(3, 0, QTableWidgetItem(' Image Spacing'))
if img.GetSpacing()[0] > 1:
info_str = ', '.join(
[f'{x:0.1f}' for x in img.GetSpacing()]
)
else:
info_str = ', '.join(
[f'{x:0.4f}' for x in img.GetSpacing()]
)
self.infoTableWidget.setItem(3, 1, QTableWidgetItem(info_str))

self.infoTableWidget.setItem(4, 0, QTableWidgetItem(' Image Direction'))
info_str = ', '.join(
[f'{x:0.1f}' for x in np.array(img.GetDirection()).flatten()]
)
self.infoTableWidget.setItem(4, 1, QTableWidgetItem(info_str))

self.pixel_info_row_start = 5
self.infoTableWidget.setItem(5, 0, QTableWidgetItem('Pixel Coordinate'))
self.infoTableWidget.setItem(6, 0, QTableWidgetItem(' Image Value'))
self.infoTableWidget.setItem(7, 0, QTableWidgetItem(' Overlay Value'))

self.infoTableWidget.resizeColumnsToContents()

@time_and_log
def update_pixel(self):
Expand All @@ -54,7 +93,7 @@ def update_pixel(self):
pos_str = ', '.join(
[f'{x:0.1f}' for x in self.state.current_pixel_position]
)
self.infoTableWidget.setItem(0, 1, QTableWidgetItem(pos_str))
self.infoTableWidget.setItem(self.pixel_info_row_start, 1, QTableWidgetItem(pos_str))
if (
self.state.image[self.state.current_image_num]
.GetLargestPossibleRegion()
Expand All @@ -68,13 +107,15 @@ def update_pixel(self):
- self.state.image_min[self.state.current_image_num]
) < 1:
self.infoTableWidget.setItem(
1, 1, QTableWidgetItem(f'{img_val:0.4f}')
self.pixel_info_row_start+1, 1, QTableWidgetItem(f'{img_val:0.4f}')
)
else:
self.infoTableWidget.setItem(
1, 1, QTableWidgetItem(f'{img_val:0.1f}')
self.pixel_info_row_start+1, 1, QTableWidgetItem(f'{img_val:0.1f}')
)
self.infoTableWidget.setItem(2, 1, QTableWidgetItem('0'))
self.infoTableWidget.setItem(self.pixel_info_row_start+2, 1, QTableWidgetItem('0'))
else:
self.infoTableWidget.setItem(1, 1, QTableWidgetItem('Outside'))
self.infoTableWidget.setItem(2, 1, QTableWidgetItem('Outside'))
self.infoTableWidget.setItem(self.pixel_info_row_start+1, 1, QTableWidgetItem('Outside'))
self.infoTableWidget.setItem(self.pixel_info_row_start+2, 1, QTableWidgetItem('Outside'))

self.infoTableWidget.resizeColumnsToContents()
11 changes: 8 additions & 3 deletions src/minder3d/lib/sovObjectPanelWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ def update_highlight_selected(self, value):
"""
self.state.highlight_selected = value
for selected_id in self.state.selected_ids:
if selected_id == -1:
continue
self.gui.log(f'update_highlight_selected: Id={selected_id}')
so = self.state.scene_list[
self.state.scene_list_ids.index(selected_id)
Expand Down Expand Up @@ -245,9 +247,12 @@ def rename_selected_object(self):
def delete_selected_objects(self):
"""Delete the selected objects from the scene.
This function deletes the selected objects from the scene by removing them from the scene list and updating the GUI accordingly.
This function deletes the selected objects from the scene by removing
them from the scene list and updating the GUI accordingly.
"""
for so_id in self.state.selected_ids:
if so_id == -1:
continue
scene_idx = self.state.scene_list_ids.index(so_id)
so = self.state.scene_list[scene_idx]
so_parent = so.GetParent()
Expand All @@ -266,7 +271,7 @@ def delete_selected_objects(self):

self.update_gui = True

self.update_scene()
self.gui.update_scene()

@time_and_log
def propogate_properties_to_all(self):
Expand All @@ -285,7 +290,7 @@ def propogate_properties_to_all(self):
for idx in range(len(self.state.scene_list)):
self.state.scene_list_properties[idx]['ColorBy'] = color_by
self.state.scene_list[idx].GetProperty().SetColor(color)
self.redraw_object(self.state.scene_list[idx])
self.gui.redraw_object(self.state.scene_list[idx])

@time_and_log
def propogate_properties_to_similar(self):
Expand Down
3 changes: 2 additions & 1 deletion src/minder3d/lib/sovOtsuLogic.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ def run(self, inputImage, numberOfThresholds):
filter = itk.OtsuMultipleThresholdsImageFilter.New(Input=inputImage)
filter.SetNumberOfThresholds(numberOfThresholds)
filter.Update()
return filter.GetOutput().astype(np.uint8)
img = filter.GetOutput().astype(np.uint8)
return img
48 changes: 48 additions & 0 deletions src/minder3d/lib/sovUtils.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,52 @@ def add_objects_in_mask_image_to_scene(mask_image, scene):
scene.AddChild(mask_so)


@time_and_log
def compress_scene_for_saving(scene):
"""Compresses a scene for saving.
It compresses the input scene by removing unnecessary objects and compressing the scene.
Args:
scene: The scene to be compressed.
Returns:
new_scene: A compressed copy of the scene.
"""
# HERE HERE HERE
mask_objects = get_children_as_list(scene, "ImageMask")
print("Num mask objects = ", len(mask_objects))
mask_image_pointers = list(np.unique([id(mask.GetImage()) for mask in mask_objects]))
print("Num mask object pointers = ", len(mask_image_pointers))
print("Mask image pointers = ", mask_image_pointers)
for mask_object in mask_objects:
imgPntr = id(mask_object.GetImage())
print("Mask object image pointer = ", imgPntr)
if imgPntr not in mask_image_pointers:
print("Removing mask object with image pointer = ", imgPntr)
#mask_object.GetParent().RemoveChild(mask_object)
else:
print("Keeping mask object with image pointer = ", imgPntr)
#mask_image_pointers.remove(id)
return scene

def uncompress_scene_after_loading(scene):
"""Uncompresses a scene after loading.
It uncompresses the input scene by adding necessary objects and uncompressing the scene.
Args:
scene: The scene to be uncompressed.
Returns:
scene: The uncompressed scene.
"""
mask_objects = get_children_as_list(scene, 'ImageMask')
print("Num mask objects = ", len(mask_objects))
for mask_object in mask_objects:
parent = mask_object.GetParent()
parent.RemoveChild(mask_object)
add_objects_in_mask_image_to_scene(mask_object.GetImage(), parent)
return scene

@time_and_log
def get_children_as_list(
grp: itk.GroupSpatialObject, child_type: str = ''
Expand Down Expand Up @@ -290,6 +336,7 @@ def read_group(filename: str, dims: int = 3) -> itk.GroupSpatialObject:
try:
groupFileReader = itk.SpatialObjectReader[dims].New()
groupFileReader.SetFileName(filename)
groupFileReader.SetMetaIOVersion(1)
groupFileReader.Update()
except RuntimeError:
return None
Expand All @@ -314,4 +361,5 @@ def write_group(group: itk.GroupSpatialObject, filename: str):
groupFileWriter = GroupFileWriterType.New()
groupFileWriter.SetFileName(filename)
groupFileWriter.SetInput(group)
groupFileWriter.SetMetaIOVersion(1)
groupFileWriter.Update()
16 changes: 12 additions & 4 deletions src/minder3d/lib/sovView2DPanelWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,18 @@ def create_new_image(self):
self.update_view_plane_coronal(redraw=False)
elif view_plane == 1:
self.update_view_plane_sagittal(redraw=False)
elif view_plane == 2:
else: # if view_plane == 2:
self.update_view_plane_axial(redraw=False)

# Sagittal axis is viewed in the negative direction
flip[self.state.view2D_csa_axis_order[-1][1]] = not flip[
self.state.view2D_csa_axis_order[-1][1]
]
#flip[self.state.view2D_csa_axis_order[-1][1]] = not flip[
#self.state.view2D_csa_axis_order[-1][1]
#]
# If coronal axis is data axis 2, then it is viewed in the negative direction
if self.state.view2D_csa_axis_order[-1][0] == 2:
flip[self.state.view2D_csa_axis_order[-1][0]] = not flip[
self.state.view2D_csa_axis_order[-1][0]
]
self.state.view2D_flip.append(flip)

@time_and_log
Expand Down Expand Up @@ -457,6 +462,9 @@ def update_reset(self):

@time_and_log
def redraw_object(self, so):
if self.state.current_image_num < 0:
return

if (
self.state.highlight_selected
and so.GetId() in self.state.selected_ids
Expand Down
8 changes: 7 additions & 1 deletion src/minder3d/lib/sovView2DPanelWidget.ui
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,16 @@
</property>
<property name="minimumSize">
<size>
<width>35</width>
<width>30</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>30</width>
<height>16777215</height>
</size>
</property>
<property name="font">
<font>
<pointsize>7</pointsize>
Expand Down
3 changes: 0 additions & 3 deletions src/minder3d/lib/sovView2DRenderWindowInteractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ def __init__(self, gui, state, parent=None):
7: 'Crop',
}

self.state.view2D_image_axis_order = [0, 1, 2]
self.state.view2D_csa_axis_order = [2, 0, 1]

self.current_mouse_mode = 0

self.mouse_pressed = False
Expand Down
Loading

0 comments on commit f258a6b

Please sign in to comment.