from coperniFUS import *
from coperniFUS.modules.module_base import Module
from PIL import Image
import functools
[docs]
class RefImageAsPlane(Module):
_DEFAULT_PARAMS = {
'file_path': 'None',
'ignore_anatomical_landmarks_calibration': False,
'plane': 'X',
'alpha': .6,
'px_size': 1e-5, # [m]
'origin_px_xloc': 0, # [px]
'origin_px_yloc': 0, # [px]
}
""" Default configuration parameters used when a parameter value is not yet cached """
def __init__(self, parent_viewer, **kwargs) -> None:
super().__init__(parent_viewer, 'ref_image_as_plane_params', **kwargs)
self.ref_image_glitem = None
self._ref_image_tmat = None
self.ref_image_name = None
self.ref_image = None
# --- Module specific public attributes ---
[docs]
def import_image(self, image_path):
""" Import a reference image to the viewer by providing the image_path.
Loaded image can be removed by calling the delete_rendered_object method """
if image_path == '':
raise ValueError('Invalid file path')
else:
if self._load_img(image_path):
self.ref_image_name = pathlib.Path(image_path).stem
self.parent_viewer.cache.set_attr(
['ref_image_as_plane_params', 'last_used_img_name'],
self.ref_image_name
)
self.set_user_param('file_path', image_path)
@property
def ref_image_tmat(self):
""" Holds the reference image affine transformation matrix """
if self._ref_image_tmat is None:
px_size = self.get_user_param('px_size')
self._ref_image_tmat = af_tr.scale_mat(px_size)
self._ref_image_tmat = self._ref_image_tmat @ af_tr.translat_mat('x', -self.get_user_param('origin_px_xloc') * px_size)
self._ref_image_tmat = self._ref_image_tmat @ af_tr.translat_mat('y', -self.get_user_param('origin_px_yloc') * px_size)
if self.get_user_param('plane') == 'X':
self._ref_image_tmat = self._ref_image_tmat @ af_tr.rot_mat('y', 90)
if self.get_user_param('plane') == '-X':
self._ref_image_tmat = self._ref_image_tmat @ af_tr.rot_mat('y', 90)
self._ref_image_tmat = self._ref_image_tmat @ af_tr.rot_mat('z', 180)
if self.get_user_param('plane') == 'Y':
self._ref_image_tmat = self._ref_image_tmat @ af_tr.rot_mat('y', 90)
self._ref_image_tmat = self._ref_image_tmat @ af_tr.rot_mat('z', 90)
if self.get_user_param('plane') == '-Y':
self._ref_image_tmat = self._ref_image_tmat @ af_tr.rot_mat('y', 90)
self._ref_image_tmat = self._ref_image_tmat @ af_tr.rot_mat('z', 90)
self._ref_image_tmat = self._ref_image_tmat @ af_tr.rot_mat('z', 180)
if self.get_user_param('plane') == '-Z':
self._ref_image_tmat = self._ref_image_tmat @ af_tr.rot_mat('x', 180)
# Apply anatomical landmark calibration transformation if enabled
if not self.get_user_param('ignore_anatomical_landmarks_calibration'):
anatomically_calibrated_img_tmat = self._ref_image_tmat @ self.parent_viewer.anat_calib.landmarks_calib_tmat
else:
anatomically_calibrated_img_tmat = self._ref_image_tmat
return anatomically_calibrated_img_tmat
@ref_image_tmat.setter
def ref_image_tmat(self, value):
if value is not None:
if value.shape != (4, 4):
raise ValueError('Transformation matrix should be of shape (4, 4)')
self._ref_image_tmat = value
# --- Required module attributes ---
[docs]
def init_dock(self):
""" Called on GUI setup to add a module dock """
# Setting up dock layout
self.dock = pyqtw.QDockWidget('Reference Image As Plane', self.parent_viewer)
self.parent_viewer.addDockWidget(pyqtc.Qt.DockWidgetArea.LeftDockWidgetArea, self.dock)
self.dock_widget = pyqtw.QWidget(self.dock)
self.dock.setWidget(self.dock_widget)
self.dock_layout = pyqtw.QGridLayout()
self.dock_widget.setLayout(self.dock_layout)
# Import button
self.select_ref_img_btn = pyqtw.QPushButton('Select Image')
self.select_ref_img_btn.clicked.connect(self._on_import_image_btn_press)
self.dock_layout.addWidget(self.select_ref_img_btn, 0, 0, 1, 3) # Y, X, w, h
# Pixel size setter
unit = 'm'
self.px_size_editor = pyqtw.QLineEdit(si_format(
self.get_user_param('px_size'), format_str='{value} {prefix}' + unit))
self.px_size_editor.editingFinished.connect(functools.partial(self._parse_editor, self.px_size_editor, 'px_size', unit))
self.dock_layout.addWidget(self.px_size_editor, 1, 0, 1, 1) # Y, X, w, h
self.px_size_editor.setToolTip('Size of image pixels in meters')
# Origin setter
unit = 'px'
self.origin_px_xloc_editor = pyqtw.QLineEdit(si_format(
self.get_user_param('origin_px_xloc'), format_str='{value} {prefix}' + unit))
self.origin_px_xloc_editor.editingFinished.connect(
functools.partial(self._parse_editor, self.origin_px_xloc_editor, 'origin_px_xloc', unit))
self.dock_layout.addWidget(self.origin_px_xloc_editor, 1, 1, 1, 1) # Y, X, w, h
self.origin_px_xloc_editor.setToolTip('Origin X coordinate in pixels')
self.origin_px_yloc_editor = pyqtw.QLineEdit(si_format(
self.get_user_param('origin_px_yloc'), format_str='{value} {prefix}' + unit))
self.origin_px_yloc_editor.editingFinished.connect(
functools.partial(self._parse_editor, self.origin_px_yloc_editor, 'origin_px_yloc', unit))
self.dock_layout.addWidget(self.origin_px_yloc_editor, 1, 2, 1, 1) # Y, X, w, h
self.origin_px_xloc_editor.setToolTip('Origin Y coordinate in pixels')
# Select plane button
self.ref_img_plane_selector_btns = {
'X': pyqtw.QPushButton('X'),
'Y': pyqtw.QPushButton('Y'),
'Z': pyqtw.QPushButton('Z'),
}
for ii, (normal_axis, btn) in enumerate(self.ref_img_plane_selector_btns.items()):
btn.clicked.connect(functools.partial(self._set_img_plane, normal_axis))
self.dock_layout.addWidget(btn, 2, ii, 1, 1) # Y, X, w, h
btn.setToolTip(f'Plane normal axis')
self._enable_disable_editors(False)
# Resize to most comptact height
self.dock.adjustSize()
self.dock.setMinimumHeight(self.dock.sizeHint().height())
self.dock.setMaximumHeight(self.dock.sizeHint().height())
[docs]
def add_rendered_object(self):
""" Called when populating the viewer with the module rendered objects """
try: # Load previous ref image if valid path
self.ref_image_name = self.parent_viewer.cache.get_attr(
['ref_image_as_plane_params', 'last_used_img_name'])
if self.ref_image_name is not None:
last_used_ref_img_path = self.get_user_param('file_path')
self._load_img(last_used_ref_img_path)
except:
pass
[docs]
def update_rendered_object(self):
""" Called on render view updates """
if self.ref_image_glitem is not None:
self.ref_image_glitem.resetTransform()
self.ref_image_glitem.applyTransform(pyqtg.QMatrix4x4(self.ref_image_tmat.T.ravel()), local=False)
[docs]
def delete_rendered_object(self):
""" Called on deletion of the module rendered objects """
if self.ref_image_glitem is not None:
self.parent_viewer.gl_view.removeItem(self.ref_image_glitem)
self.parent_viewer.cache.set_attr(['ref_image_as_plane_params', 'last_used_img_name'], None)
self.select_ref_img_btn.setText('Select Image')
self.select_ref_img_btn.clicked.disconnect()
self.select_ref_img_btn.clicked.connect(self._on_import_image_btn_press)
# --- Atlas specific cache wrapper ---
[docs]
def get_user_param(self, param_name, default_value=None):
""" Get module configuration parameter stored in cache (or default values if non existant) """
if self.ref_image_name is not None:
param_value = super().get_user_param(
param_name,
additional_identifiers=[self.ref_image_name],
default_value=default_value)
else:
param_value = self._DEFAULT_PARAMS[param_name]
return param_value
[docs]
def set_user_param(self, param_name, param_value):
""" Set module configuration parameter to cache """
if self.ref_image_name is not None:
super().set_user_param(
param_name,
additional_identifiers=[self.ref_image_name],
param_value=param_value)
# --- Module specific attributes ---
def _parse_editor(self, src_editor, param_name, unit):
edited_text = src_editor.text().replace(' ', '') # remove spaces
edited_text_nounit = edited_text[:-len(unit)]
edited_value = si_parse(edited_text_nounit.replace('u', 'ยต'))
self.set_user_param(param_name, edited_value)
self._update_img_transform()
def _update_editors(self):
self.px_size_editor.setText(si_format(
self.get_user_param('px_size'), format_str='{value} {prefix}' + 'm'))
self.origin_px_xloc_editor.setText(si_format(
self.get_user_param('origin_px_xloc'), format_str='{value} {prefix}' + 'px'))
self.origin_px_yloc_editor.setText(si_format(
self.get_user_param('origin_px_yloc'), format_str='{value} {prefix}' + 'px'))
# Plane buttons
normal_axis = self.get_user_param('plane')
if '-' in normal_axis:
self._update_plane_button_ui(normal_axis.replace('-', ''), reverse=True)
else:
self._update_plane_button_ui(normal_axis)
def _enable_disable_editors(self, enable=True):
for ii, (normal_axis, btn) in enumerate(self.ref_img_plane_selector_btns.items()):
btn.setEnabled(enable)
self.px_size_editor.setEnabled(enable)
self.origin_px_xloc_editor.setEnabled(enable)
self.origin_px_yloc_editor.setEnabled(enable)
def _on_import_image_btn_press(self):
import_path = pyqtw.QFileDialog.getOpenFileName(parent=self.parent_viewer, caption=self.parent_viewer.tr("Select an image"), filter=self.parent_viewer.tr('Image (*.png)'))
try:
self.import_image(import_path[0])
except Exception as e:
self.parent_viewer.show_error_popup('Reference image import', error_description=str(e))
def _load_img(self, img_path):
success = False
if pathlib.Path(img_path).exists():
self.ref_image = Image.open(img_path)
self.select_ref_img_btn.setText('Remove Image')
self.select_ref_img_btn.clicked.disconnect()
self.select_ref_img_btn.clicked.connect(self.delete_rendered_object)
self._show_image()
success = True
else:
raise ValueError('Reference image file not found')
return success
def _show_image(self):
ref_image_asarray = np.array(self.ref_image)
if ref_image_asarray.shape[2] == 3: # Turn RGB array to RGBA
ref_image_asarray_rgba = np.ones((*ref_image_asarray.shape[:2], 4))
ref_image_asarray_rgba[:, :, :3] = ref_image_asarray
ref_image_asarray = ref_image_asarray_rgba
del ref_image_asarray_rgba
elif ref_image_asarray.shape[2] != 4:
raise ValueError('Unsupported png channels numbers')
alpha = self.get_user_param('alpha')
ref_image_asarray[:, :, 3] = int(alpha * 255) # Transparent img
self.ref_image_glitem = gl.GLImageItem(ref_image_asarray)
self.parent_viewer.gl_view.addItem(self.ref_image_glitem, name=f'Ref. image {self.ref_image_name}')
self.ref_image_glitem.setDepthValue(-1) # GL images -> render tree background
self._enable_disable_editors(True)
self._update_img_transform()
self._update_editors()
def _update_img_transform(self):
self.ref_image_tmat = None # Reset
self.update_rendered_object()
def _update_plane_button_ui(self, normal_axis, reverse=False):
self.ref_img_plane_selector_btns['X'].setDown(False)
self.ref_img_plane_selector_btns['Y'].setDown(False)
self.ref_img_plane_selector_btns['Z'].setDown(False)
self.ref_img_plane_selector_btns[normal_axis].setDown(True)
btn_text = normal_axis if not reverse else f'-{normal_axis}'
self.ref_img_plane_selector_btns[normal_axis].setText(btn_text)
def _set_img_plane(self, btn_normal_axis):
# Reverse axis if button is pushed twice
if self.get_user_param('plane') == btn_normal_axis:
normal_axis = f'-{btn_normal_axis}'
reverse = True
else:
normal_axis = btn_normal_axis
reverse = False
self._update_plane_button_ui(btn_normal_axis, reverse)
self.set_user_param('plane', normal_axis)
self._update_img_transform()