From 7a20007cbc40f365b0ad1958877265023e54d2c9 Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Mon, 10 Apr 2023 15:20:09 +0530 Subject: [PATCH 01/16] type annotations added --- docs/devel/example_closure.py | 28 +++---- docs/devel/example_pubsub.py | 27 +++---- docs/devel/example_singleton.py | 68 ++++++++--------- docs/devel/example_singleton_pubsub.py | 53 +++++++------ invesalius/data/bases.py | 100 +++++++++++++------------ 5 files changed, 140 insertions(+), 136 deletions(-) diff --git a/docs/devel/example_closure.py b/docs/devel/example_closure.py index 1d1af5636..b9b26a625 100644 --- a/docs/devel/example_closure.py +++ b/docs/devel/example_closure.py @@ -1,21 +1,23 @@ # Python closure example -def OuterCount(start): - counter = [start] # counter is 1-element array - print "passei por fora" - def InnerCount(): +from typing import List, Callable + +def OuterCount(start: int) -> Callable[[], int]: + counter: List[int] = [start] # counter is 1-element array + print("passei por fora") + def InnerCount() -> int: counter[0] = counter[0] + 1 - print "passei por dentro" + print("passei por dentro") return counter[0] return InnerCount -print "Init counter at 5" -count = OuterCount(5) -print "\nRun counter 3 times" -print count() -print count() -print count() +print("Init counter at 5") +count: Callable[[], int] = OuterCount(5) +print("\nRun counter 3 times") +print(count()) +print(count()) +print(count()) -print "**********" +print("**********") count = OuterCount(0) -print count() \ No newline at end of file +print(count()) diff --git a/docs/devel/example_pubsub.py b/docs/devel/example_pubsub.py index aa79c8326..28c854be2 100644 --- a/docs/devel/example_pubsub.py +++ b/docs/devel/example_pubsub.py @@ -4,6 +4,7 @@ # http://wiki.wxpython.org/ModelViewController # http://wiki.wxpython.org/PubSub from invesalius.pubsub import pub as Publisher +from typing import Dict, Any # The maintainer of Pubsub module is Oliver Schoenborn. # Since the end of 2006 Pubsub is now maintained separately on SourceForge at: @@ -11,7 +12,7 @@ class Student: - def __init__(self, name): + def __init__(self, name: str): self.name = name self.mood = ":|" self.__bind_events() @@ -20,39 +21,39 @@ def __bind_events(self): Publisher.subscribe(self.ReceiveProject, "Set Student Project") Publisher.subscribe(self.ReceiveGrade, "Set Student Grade") - def ReceiveProject(self, pubsub_evt): - projects_dict = pubsub_evt.data + def ReceiveProject(self, pubsub_evt: Any): + projects_dict: Dict[str, str] = pubsub_evt.data self.project = projects_dict[self.name] - print "%s: I've received the project %s" % (self.name, self.project) + print(f"{self.name}: I've received the project {self.project}") - def ReceiveGrade(self, pubsub_evt): - grades_dict = pubsub_evt.data + def ReceiveGrade(self, pubsub_evt: Any): + grades_dict: Dict[str, float] = pubsub_evt.data self.grade = grades_dict[self.name] if self.grade > 6: self.mood = ":)" else: self.mood = ":(" - print "%s: I've received the grade %d %s" % (self.name, self.grade, self.mood) + print(f"{self.name}: I've received the grade {self.grade} {self.mood}") class Teacher: - def __init__(self, name, course): + def __init__(self, name: str, course: Any): self.name = name self.course = course def SendMessage(self): - print "%s: Telling students the projects" % (self.name) + print(f"{self.name}: Telling students the projects") Publisher.sendMessage("Set Student Project", self.course.projects_dict) - print "\n%s: Telling students the grades" % (self.name) + print(f"\n{self.name}: Telling students the grades") Publisher.sendMessage("Set Student Grade", self.course.grades_dict) class Course: - def __init__(self, subject): + def __init__(self, subject: str): self.subject = subject - self.grades_dict = {} - self.projects_dict = {} + self.grades_dict: Dict[str, float] = {} + self.projects_dict: Dict[str, str] = {} # Create students: diff --git a/docs/devel/example_singleton.py b/docs/devel/example_singleton.py index 4567dd78b..cd3d4c59c 100644 --- a/docs/devel/example_singleton.py +++ b/docs/devel/example_singleton.py @@ -7,13 +7,13 @@ class Singleton(type): # This is a Gary Robinson implementation: # http://www.garyrobinson.net/2004/03/python_singleto.html - def __init__(cls,name,bases,dic): - super(Singleton,cls).__init__(name,bases,dic) - cls.instance=None - - def __call__(cls,*args,**kw): + def __init__(cls, name: str, bases: tuple, dic: dict) -> None: + super().__init__(name, bases, dic) + cls.instance = None + + def __call__(cls, *args: tuple, **kw: dict) -> object: if cls.instance is None: - cls.instance=super(Singleton,cls).__call__(*args,**kw) + cls.instance = super().__call__(*args, **kw) return cls.instance class Bone(object): @@ -21,34 +21,34 @@ class Bone(object): # Singleton design pattern for implementing it __metaclass__= Singleton - def __init__(self): - self.size = 100 - - def RemovePart(self, part_size): - self.size -= part_size # self.size = self.size - part_size - -class Dog(): - def __init__(self, name): - self.name = name - self.bone = Bone() - - def EatBonePart(self, part_size): - self.bone.RemovePart(part_size) - -print "Initial state:" -d1 = Dog("Nina") -d2 = Dog("Tang") -print "Bone size of %s: %d"%(d1.name, d1.bone.size) -print "Bone size of %s: %d\n"%(d2.name, d2.bone.size) - -print "Only Nina eats:" -d1.EatBonePart(5) -print "Bone size of %s: %d"%(d1.name, d1.bone.size) -print "Bone size of %s: %d\n"%(d2.name, d2.bone.size) + def __init__(self) -> None: + self.size: int = 100 -print "Tang eats after Nina:" -d2.EatBonePart(20) -print "Bone size of %s: %d"%(d1.name, d1.bone.size) -print "Bone size of %s: %d"%(d2.name, d2.bone.size) + def RemovePart(self, part_size: int) -> None: + self.size -= part_size + + +class Dog: + def __init__(self, name: str) -> None: + self.name: str = name + self.bone: Bone = Bone() + def EatBonePart(self, part_size: int) -> None: + self.bone.RemovePart(part_size) + +print("Initial state:") +d1: Dog = Dog("Nina") +d2: Dog = Dog("Tang") +print(f"Bone size of {d1.name}: {d1.bone.size}") +print(f"Bone size of {d2.name}: {d2.bone.size}\n") + +print("Only Nina eats:") +d1.EatBonePart(5) +print(f"Bone size of {d1.name}: {d1.bone.size}") +print(f"Bone size of {d2.name}: {d2.bone.size}\n") + +print("Tang eats after Nina:") +d2.EatBonePart(20) +print(f"Bone size of {d1.name}: {d1.bone.size}") +print(f"Bone size of {d2.name}: {d2.bone.size}") diff --git a/docs/devel/example_singleton_pubsub.py b/docs/devel/example_singleton_pubsub.py index d03d59ea2..8ee398524 100644 --- a/docs/devel/example_singleton_pubsub.py +++ b/docs/devel/example_singleton_pubsub.py @@ -6,13 +6,13 @@ class Singleton(type): # This is a Gary Robinson implementation: # http://www.garyrobinson.net/2004/03/python_singleto.html - def __init__(cls,name,bases,dic): - super(Singleton,cls).__init__(name,bases,dic) - cls.instance=None - - def __call__(cls,*args,**kw): + def __init__(cls, name: str, bases: tuple, dic: dict): + super(Singleton, cls).__init__(name, bases, dic) + cls.instance = None + + def __call__(cls, *args, **kw): if cls.instance is None: - cls.instance=super(Singleton,cls).__call__(*args,**kw) + cls.instance = super(Singleton, cls).__call__(*args, **kw) return cls.instance class Pizza(object): @@ -21,42 +21,41 @@ class Pizza(object): __metaclass__= Singleton def __init__(self): - self.npieces = 8 + self.npieces: int = 8 self.__bind_events() def __bind_events(self): - ps.Publisher().subscribe(self.RemovePiece, - 'Eat piece of pizza') + Publisher.subscribe(self.RemovePiece, 'Eat piece of pizza') - def RemovePiece(self, pubsub_evt): - person = pubsub_evt.data + def RemovePiece(self, pubsub_evt: Publisher): + person: Person = pubsub_evt.data if self.npieces: self.npieces -= 1 - print "%s ate pizza!"%(person.name) + print(f"{person.name} ate pizza!") else: - print "%s is hungry!"%(person.name) + print(f"{person.name} is hungry!") + class Person(): - def __init__(self, name): - self.name = name - self.pizza = Pizza() + def __init__(self, name: str): + self.name: str = name + self.pizza: Pizza = Pizza() def EatPieceOfPizza(self): - ps.Publisher.sendMessage('Eat piece of pizza',(self)) - + Publisher.sendMessage('Eat piece of pizza', (self,)) + -print "Initial state:" -p1 = Person("Paulo ") -p2 = Person("Thiago") -p3 = Person("Andre ") -people = [p1, p2, p3] +print("Initial state:") +p1: Person = Person("Paulo ") +p2: Person = Person("Thiago") +p3: Person = Person("Andre ") +people: list = [p1, p2, p3] -print "Everyone eats 2 pieces:" +print("Everyone eats 2 pieces:") for i in range(2): for person in people: person.EatPieceOfPizza() -print "Everyone tries to eat another piece:" +print("Everyone tries to eat another piece:") for person in people: - person.EatPieceOfPizza() - + person.EatPieceOfPizza() \ No newline at end of file diff --git a/invesalius/data/bases.py b/invesalius/data/bases.py index 1f332b0eb..a22f24f27 100644 --- a/invesalius/data/bases.py +++ b/invesalius/data/bases.py @@ -3,7 +3,7 @@ import invesalius.data.transformations as tr import invesalius.data.coregistration as dcr -def angle_calculation(ap_axis, coil_axis): +def angle_calculation(ap_axis: np.ndarray, coil_axis: np.ndarray) -> float: """ Calculate angle between two given axis (in degrees) @@ -20,7 +20,7 @@ def angle_calculation(ap_axis, coil_axis): return float(angle) -def base_creation_old(fiducials): +def base_creation_old(fiducials: np.ndarray) -> tuple[np.matrix, np.ndarray, np.matrix]: """ Calculate the origin and matrix for coordinate system transformation. q: origin of coordinate system @@ -34,8 +34,8 @@ def base_creation_old(fiducials): p2 = fiducials[1, :] p3 = fiducials[2, :] - sub1 = p2 - p1 - sub2 = p3 - p1 + sub1: ndarray[Any, dtype] = p2 - p1 + sub2: ndarray[Any, dtype] = p3 - p1 lamb = (sub1[0]*sub2[0]+sub1[1]*sub2[1]+sub1[2]*sub2[2])/np.dot(sub1, sub1) q = p1 + lamb*sub1 @@ -45,7 +45,7 @@ def base_creation_old(fiducials): if not g1.any(): g1 = p2 - q - g3 = np.cross(g2, g1) + g3: ndarray[Any, dtype] = np.cross(g2, g1) g1 = g1/np.sqrt(np.dot(g1, g1)) g2 = g2/np.sqrt(np.dot(g2, g2)) @@ -60,7 +60,11 @@ def base_creation_old(fiducials): return m, q, m_inv -def base_creation(fiducials): + + +import numpy as np + +def base_creation(fiducials: np.ndarray) -> tuple[np.ndarray, np.ndarray]: """ Calculate the origin and matrix for coordinate system transformation. @@ -71,62 +75,57 @@ def base_creation(fiducials): :return: matrix and origin for base transformation """ - p1 = fiducials[0, :] - p2 = fiducials[1, :] - p3 = fiducials[2, :] + p1: np.ndarray = fiducials[0, :] + p2: np.ndarray = fiducials[1, :] + p3: np.ndarray = fiducials[2, :] - sub1 = p2 - p1 - sub2 = p3 - p1 - lamb = np.dot(sub1, sub2)/np.dot(sub1, sub1) + sub1: np.ndarray = p2 - p1 + sub2: np.ndarray = p3 - p1 + lamb: float = np.dot(sub1, sub2) / np.dot(sub1, sub1) - q = p1 + lamb*sub1 - g1 = p3 - q - g2 = p1 - q + q: np.ndarray = p1 + lamb * sub1 + g1: np.ndarray = p3 - q + g2: np.ndarray = p1 - q if not g1.any(): g1 = p2 - q - g3 = np.cross(g1, g2) + g3: np.ndarray = np.cross(g1, g2) - g1 = g1/np.sqrt(np.dot(g1, g1)) - g2 = g2/np.sqrt(np.dot(g2, g2)) - g3 = g3/np.sqrt(np.dot(g3, g3)) + g1 = g1 / np.sqrt(np.dot(g1, g1)) + g2 = g2 / np.sqrt(np.dot(g2, g2)) + g3 = g3 / np.sqrt(np.dot(g3, g3)) - m = np.zeros([3, 3]) - m[:, 0] = g1/np.sqrt(np.dot(g1, g1)) - m[:, 1] = g2/np.sqrt(np.dot(g2, g2)) - m[:, 2] = g3/np.sqrt(np.dot(g3, g3)) + m: np.ndarray = np.zeros([3, 3]) + m[:, 0] = g1 / np.sqrt(np.dot(g1, g1)) + m[:, 1] = g2 / np.sqrt(np.dot(g2, g2)) + m[:, 2] = g3 / np.sqrt(np.dot(g3, g3)) return m, q -def calculate_fre(fiducials_raw, fiducials, ref_mode_id, m_change, m_icp=None): +def calculate_fre(fiducials_raw: np.ndarray, fiducials: np.ndarray, ref_mode_id: int, m_change: np.ndarray, m_icp: list[int, np.ndarray] = None) -> float: """ Calculate the Fiducial Registration Error for neuronavigation. :param fiducials_raw: array of 6 rows (tracker probe and reference) and 3 columns (x, y, z) with coordinates - :type fiducials_raw: numpy.ndarray :param fiducials: array of 6 rows (image and tracker fiducials) and 3 columns (x, y, z) with coordinates - :type fiducials: numpy.ndarray :param ref_mode_id: Reference mode ID - :type ref_mode_id: int :param m_change: 3x3 array representing change of basis from head in tracking system to vtk head system - :type m_change: numpy.ndarray :param m_icp: list with icp flag and 3x3 affine array - :type m_icp: list[int, numpy.ndarray] :return: float number of fiducial registration error """ if m_icp is not None: - icp = [True, m_icp] + icp: list[bool, np.ndarray] = [True, m_icp] else: icp = [False, None] - dist = np.zeros([3, 1]) + dist: np.ndarray = np.zeros([3, 1]) for i in range(0, 6, 2): p_m, _ = dcr.corregistrate_dynamic((m_change, 0), fiducials_raw[i:i+2], ref_mode_id, icp) dist[int(i/2)] = np.sqrt(np.sum(np.power((p_m[:3] - fiducials[int(i/2), :]), 2))) - return float(np.sqrt(np.sum(dist ** 2) / 3)) + return float(np.sqrt(np.sum(dist ** 2)/ 3)) # The function flip_x_m is deprecated and was replaced by a simple minus multiplication of the Y coordinate as follows: @@ -156,35 +155,37 @@ def calculate_fre(fiducials_raw, fiducials, ref_mode_id, m_change, m_icp=None): # # return point_rot -def transform_icp(m_img, m_icp): +import numpy as np +import transformations as tr + +def transform_icp(m_img: np.ndarray, m_icp: np.ndarray) -> np.ndarray: coord_img = [m_img[0, -1], -m_img[1, -1], m_img[2, -1], 1] m_img[0, -1], m_img[1, -1], m_img[2, -1], _ = m_icp @ coord_img m_img[0, -1], m_img[1, -1], m_img[2, -1] = m_img[0, -1], -m_img[1, -1], m_img[2, -1] return m_img -def inverse_transform_icp(m_img, m_icp): +def inverse_transform_icp(m_img: np.ndarray, m_icp: np.ndarray) -> np.ndarray: coord_img = [m_img[0, -1], -m_img[1, -1], m_img[2, -1], 1] m_img[0, -1], m_img[1, -1], m_img[2, -1], _ = np.linalg.inv(m_icp) @ coord_img m_img[0, -1], m_img[1, -1], m_img[2, -1] = m_img[0, -1], -m_img[1, -1], m_img[2, -1] return m_img -def object_registration(fiducials, orients, coord_raw, m_change): +def object_registration(fiducials: np.ndarray, orients: np.ndarray, coord_raw: np.ndarray, m_change: np.ndarray) -> None: """ - :param fiducials: 3x3 array of fiducials translations :param orients: 3x3 array of fiducials orientations in degrees :param coord_raw: nx6 array of coordinates from tracking device where n = 1 is the reference attached to the head :param m_change: 3x3 array representing change of basis from head in tracking system to vtk head system - :return: + :return: None """ coords = np.hstack((fiducials, orients)) - fids_dyn = np.zeros([4, 6]) - fids_img = np.zeros([4, 6]) - fids_raw = np.zeros([3, 3]) + fids_dyn: ndarray[Any, dtype[floating[_64Bit]]] = np.zeros([4, 6]) + fids_img: ndarray[Any, dtype[floating[_64Bit]]] = np.zeros([4, 6]) + fids_raw: ndarray[Any, dtype[floating[_64Bit]]] = np.zeros([3, 3]) # compute fiducials of object with reference to the fixed probe in source frame for ic in range(0, 3): @@ -196,20 +197,21 @@ def object_registration(fiducials, orients, coord_raw, m_change): # the lines below, and then again in dco.coordinates_to_transformation_matrix. # a, b, g = np.radians(coords[3, 3:]) - r_s0_raw = tr.euler_matrix(a, b, g, axes='rzyx') + r_s0_raw: ndarray[Any, dtype[floating[_64Bit]]] = tr.euler_matrix(a, b, g, axes='rzyx') - s0_raw = dco.coordinates_to_transformation_matrix( + s0_raw: ndarray[Any, dtype[floating[_64Bit]]] | Any = dco.coordinates_to_transformation_matrix( position=coords[3, :3], orientation=coords[3, 3:], axes='rzyx', ) + # compute change of basis for object fiducials in source frame base_obj_raw, q_obj_raw = base_creation(fids_raw[:3, :3]) - r_obj_raw = np.identity(4) + r_obj_raw: ndarray[Any, dtype[floating[_64Bit]]] = np.identity(4) r_obj_raw[:3, :3] = base_obj_raw[:3, :3] - t_obj_raw = tr.translation_matrix(q_obj_raw) - m_obj_raw = tr.concatenate_matrices(t_obj_raw, r_obj_raw) + t_obj_raw: ndarray[Any, dtype[floating[_64Bit]]] = tr.translation_matrix(q_obj_raw) + m_obj_raw: ndarray[Any, dtype[floating[_64Bit]]] | Any = tr.concatenate_matrices(t_obj_raw, r_obj_raw) for ic in range(0, 4): if coord_raw.any(): @@ -221,12 +223,12 @@ def object_registration(fiducials, orients, coord_raw, m_change): fids_dyn[ic, 2] = -fids_dyn[ic, 2] # compute object fiducials in vtk head frame - M_p = dco.coordinates_to_transformation_matrix( + M_p: ndarray[Any, dtype[floating[_64Bit]]] | Any = dco.coordinates_to_transformation_matrix( position=fids_dyn[ic, :3], orientation=fids_dyn[ic, 3:], axes='rzyx', ) - M_img = m_change @ M_p + M_img: ndarray[Any, dtype[floating]] | Any = m_change @ M_p angles_img = np.degrees(np.asarray(tr.euler_from_matrix(M_img, 'rzyx'))) coord_img = list(M_img[:3, -1]) @@ -236,11 +238,11 @@ def object_registration(fiducials, orients, coord_raw, m_change): # compute object base change in vtk head frame base_obj_img, _ = base_creation(fids_img[:3, :3]) - r_obj_img = np.identity(4) + r_obj_img: ndarray[Any, dtype[floating[_64Bit]]] = np.identity(4) r_obj_img[:3, :3] = base_obj_img[:3, :3] # compute initial alignment of probe fixed in the object in reference (or static) frame - s0_dyn = dco.coordinates_to_transformation_matrix( + s0_dyn: ndarray[Any, dtype[floating[_64Bit]]] | Any = dco.coordinates_to_transformation_matrix( position=fids_dyn[3, :3], orientation=fids_dyn[3, 3:], axes='rzyx', From 303025734299763c6d462440248835bff378332b Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Fri, 14 Apr 2023 19:57:30 +0530 Subject: [PATCH 02/16] type annotations added --- invesalius/data/brainmesh_handler.py | 289 +++++++++++++-------------- invesalius/data/converters.py | 36 +++- invesalius/data/coordinates.py | 123 +++++++----- invesalius/data/coregistration.py | 78 +++++--- invesalius/data/cursor_actors.py | 168 ++++++++-------- invesalius/data/e_field.py | 60 +++--- invesalius/data/editor.py | 167 +++++++++------- invesalius/data/geometry.py | 246 ++++++++++++----------- invesalius/data/imagedata_utils.py | 137 +++++++------ 9 files changed, 705 insertions(+), 599 deletions(-) diff --git a/invesalius/data/brainmesh_handler.py b/invesalius/data/brainmesh_handler.py index 3cc4c33ad..09f75e6a4 100644 --- a/invesalius/data/brainmesh_handler.py +++ b/invesalius/data/brainmesh_handler.py @@ -1,8 +1,6 @@ import pyacvd -# import os import pyvista import numpy as np -# import Trekker from vtkmodules.vtkCommonCore import vtkFloatArray from vtkmodules.vtkCommonDataModel import ( vtkCellLocator, @@ -43,18 +41,18 @@ import invesalius.data.vtk_utils as vtk_utils class Brain: - def __init__(self, n_peels, window_width, window_level, affine, inv_proj): + def __init__(self, n_peels: int, window_width: float, window_level: float, affine: np.ndarray, inv_proj: np.ndarray): # Create arrays to access the peel data and peel Actors - self.peel = [] - self.peelActors = [] - self.window_width = window_width - self.window_level = window_level - self.numberOfPeels = n_peels - self.affine = affine - self.inv_proj = inv_proj - - def from_mask(self, mask): - mask= np.array(mask.matrix[1:, 1:, 1:]) + self.peel: list = [] + self.peelActors: list = [] + self.window_width: float = window_width + self.window_level: float = window_level + self.numberOfPeels: int = n_peels + self.affine: np.ndarray = affine + self.inv_proj: np.ndarray = inv_proj + + def from_mask(self, mask: np.ndarray) -> None: + mask = np.array(mask.matrix[1:, 1:, 1:]) slic = sl.Slice() image = slic.matrix @@ -78,57 +76,57 @@ def from_mask(self, mask): mask = flip.GetOutput() # Image - self.refImage = image + self.refImage: vtkDataObject = image self._do_surface_creation(mask) - def from_mask_file(self, mask_path): - slic = sl.Slice() - image = slic.matrix - image = np.flip(image, axis=1) - image = to_vtk(image, spacing=slic.spacing) - - # Read the mask - mask_reader = vtkNIFTIImageReader() - mask_reader.SetFileName(mask_path) - mask_reader.Update() - - mask = mask_reader.GetOutput() - - mask_sFormMatrix = mask_reader.GetSFormMatrix() - - # Image - self.refImage = image - - self._do_surface_creation(mask, mask_sFormMatrix) - - - def _do_surface_creation(self, mask, mask_sFormMatrix=None): + def from_mask_file(self, mask_path: str) -> None: + slic = sl.Slice() + image: np.ndarray = slic.matrix + image = np.flip(image, axis=1) + image = to_vtk(image, spacing=slic.spacing) + + # Read the mask + mask_reader: vtkNIFTIImageReader = vtkNIFTIImageReader() + mask_reader.SetFileName(mask_path) + mask_reader.Update() + + mask: vtkImageData = mask_reader.GetOutput() + + mask_sFormMatrix: vtkMatrix4x4 = mask_reader.GetSFormMatrix() + + # Image + self.refImage: vtkImageData = image + + self._do_surface_creation(mask, mask_sFormMatrix) + + + def _do_surface_creation(self, mask: vtkImageData, mask_sFormMatrix: Optional[vtkMatrix4x4] = None) -> None: if mask_sFormMatrix is None: mask_sFormMatrix = vtkMatrix4x4() - value = np.mean(mask.GetScalarRange()) + value: float = np.mean(mask.GetScalarRange()) # Use the mask to create isosurface - mc = vtkContourFilter() + mc: vtkContourFilter = vtkContourFilter() mc.SetInputData(mask) mc.SetValue(0, value) mc.ComputeNormalsOn() mc.Update() # Mask isosurface - refSurface = mc.GetOutput() + refSurface: vtkPolyData = mc.GetOutput() # Create a uniformly meshed surface - tmpPeel = downsample(refSurface) + tmpPeel: vtkPolyData = downsample(refSurface) # Standard space coordinates # Apply coordinate transform to the meshed mask - mask_ijk2xyz = vtkTransform() + mask_ijk2xyz: vtkTransform = vtkTransform() mask_ijk2xyz.SetMatrix(mask_sFormMatrix) - mask_ijk2xyz_filter = vtkTransformPolyDataFilter() + mask_ijk2xyz_filter: vtkTransformPolyDataFilter = vtkTransformPolyDataFilter() mask_ijk2xyz_filter.SetInputData(tmpPeel) mask_ijk2xyz_filter.SetTransform(mask_ijk2xyz) mask_ijk2xyz_filter.Update() @@ -146,87 +144,87 @@ def _do_surface_creation(self, mask, mask_sFormMatrix=None): tmpPeel = fixMesh(tmpPeel) tmpPeel = cleanMesh(tmpPeel) - refImageSpace2_xyz_transform = vtkTransform() + refImageSpace2_xyz_transform: vtkTransform = vtkTransform() refImageSpace2_xyz_transform.SetMatrix(vtk_utils.numpy_to_vtkMatrix4x4(np.linalg.inv(self.affine))) - self.refImageSpace2_xyz = vtkTransformPolyDataFilter() + self.refImageSpace2_xyz: vtkTransformPolyDataFilter = vtkTransformPolyDataFilter() self.refImageSpace2_xyz.SetTransform(refImageSpace2_xyz_transform) - xyz2_refImageSpace_transform = vtkTransform() + xyz2_refImageSpace_transform: vtkTransform = vtkTransform() xyz2_refImageSpace_transform.SetMatrix(vtk_utils.numpy_to_vtkMatrix4x4(self.affine)) - self.xyz2_refImageSpace = vtkTransformPolyDataFilter() + self.xyz2_refImageSpace: vtkTransformPolyDataFilter = vtkTransformPolyDataFilter() self.xyz2_refImageSpace.SetTransform(xyz2_refImageSpace_transform) - currentPeel = tmpPeel - self.currentPeelNo = 0 + currentPeel: vtkPolyData = tmpPeel + self.currentPeelNo: int = 0 currentPeel= self.MapImageOnCurrentPeel(currentPeel) - newPeel = vtkPolyData() + newPeel: vtkPolyData = vtkPolyData() newPeel.DeepCopy(currentPeel) newPeel.DeepCopy(currentPeel) - self.peel_normals = vtkFloatArray() - self.peel_centers = vtkFloatArray() + self.peel_normals: vtkFloatArray = vtkFloatArray() + self.peel_centers: vtkFloatArray = vtkFloatArray() self.peel.append(newPeel) - self.currentPeelActor = vtkActor() + self.currentPeelActor: vtkActor = vtkActor() if not np.all(np.equal(self.affine, np.eye(4))): - affine_vtk = self.CreateTransformedVTKAffine() + affine_vtk: vtkMatrix4x4 = self.CreateTransformedVTKAffine() self.currentPeelActor.SetUserMatrix(affine_vtk) self.GetCurrentPeelActor(currentPeel) self.peelActors.append(self.currentPeelActor) # locator will later find the triangle on the peel surface where the coil's normal intersect - self.locator = vtkCellLocator() + self.locator: vtkCellLocator = vtkCellLocator() self.PeelDown(currentPeel) + - def CreateTransformedVTKAffine(self): - affine_transformed = self.affine.copy() - matrix_shape = tuple(self.inv_proj.matrix_shape) + def CreateTransformedVTKAffine(self) -> vtkMatrix4x4: + affine_transformed: numpy.ndarray = self.affine.copy() + matrix_shape: Tuple[int, int] = tuple(self.inv_proj.matrix_shape) affine_transformed[1, -1] -= matrix_shape[1] return vtk_utils.numpy_to_vtkMatrix4x4(affine_transformed) - def get_actor(self, n): + def get_actor(self, n: int) -> Any: return self.GetPeelActor(n) - def SliceDown(self, currentPeel): + def SliceDown(self, currentPeel: vtkPolyData) -> vtkPolyData: # Warp using the normals - warp = vtkWarpVector() + warp: vtkWarpVector = vtkWarpVector() warp.SetInputData(fixMesh(downsample(currentPeel))) # fixMesh here updates normals needed for warping warp.SetInputArrayToProcess(0, 0, 0, vtkDataObject().FIELD_ASSOCIATION_POINTS, vtkDataSetAttributes().NORMALS) warp.SetScaleFactor(-1) warp.Update() - out = vtkPolyData() - out = upsample(warp.GetPolyDataOutput()) + out: vtkPolyData = upsample(warp.GetPolyDataOutput()) out = smooth(out) out = fixMesh(out) out = cleanMesh(out) currentPeel = out return currentPeel - # def sliceUp(self): - # # Warp using the normals - # warp = vtkWarpVector() + + # def sliceUp(self) -> vtkPolyData: + # warp: vtkWarpVector = vtkWarpVector() # # warp.SetInputData(fixMesh(downsample(currentPeel))) # fixMesh here updates normals needed for warping # warp.SetInputArrayToProcess(0, 0, 0, vtkDataObject().FIELD_ASSOCIATION_POINTS, # vtkDataSetAttributes().NORMALS) # warp.SetScaleFactor(1) # warp.Update() - # - # out = vtkPolyData() - # out = upsample(warp.GetPolyDataOutput()) + + # out: vtkPolyData = upsample(warp.GetPolyDataOutput()) # out = smooth(out) # out = fixMesh(out) # out = cleanMesh(out) - # - # currentPeel = out - def MapImageOnCurrentPeel(self, currentPeel): + # currentPeel: vtkPolyData = out + # return currentPeel + + def MapImageOnCurrentPeel(self, currentPeel: vtkPolyData) -> vtkPolyData: self.xyz2_refImageSpace.SetInputData(currentPeel) self.xyz2_refImageSpace.Update() - probe = vtkProbeFilter() + probe: vtkProbeFilter = vtkProbeFilter() probe.SetInputData(self.xyz2_refImageSpace.GetOutput()) probe.SetSourceData(self.refImage) probe.Update() @@ -237,12 +235,12 @@ def MapImageOnCurrentPeel(self, currentPeel): currentPeel = self.refImageSpace2_xyz.GetOutput() return currentPeel - def PeelDown(self, currentPeel): + def PeelDown(self, currentPeel: vtkPolyData) -> None: for i in range(0, self.numberOfPeels): currentPeel = self.SliceDown(currentPeel) currentPeel = self.MapImageOnCurrentPeel(currentPeel) - newPeel = vtkPolyData() + newPeel: vtkPolyData = vtkPolyData() newPeel.DeepCopy(currentPeel) self.peel.append(newPeel) @@ -253,29 +251,29 @@ def PeelDown(self, currentPeel): self.currentPeelNo += 1 - def TransformPeelPosition(self, p): - peel_transform = vtkTransform() + def TransformPeelPosition(self, p: int) -> vtkPolyData: + peel_transform: vtkTransform = vtkTransform() if not np.all(np.equal(self.affine, np.eye(4))): - affine_vtk = self.CreateTransformedVTKAffine() + affine_vtk: vtkMatrix4x4 = self.CreateTransformedVTKAffine() peel_transform.SetMatrix(affine_vtk) - refpeelspace = vtkTransformPolyDataFilter() + refpeelspace: vtkTransformPolyDataFilter = vtkTransformPolyDataFilter() refpeelspace.SetInputData(self.peel[p]) refpeelspace.SetTransform(peel_transform) refpeelspace.Update() - currentPeel = refpeelspace.GetOutput() + currentPeel: vtkPolyData = refpeelspace.GetOutput() return currentPeel - def GetPeelActor(self, p): - lut = vtkWindowLevelLookupTable() + def GetPeelActor(self, p: int) -> vtkActor: + lut: vtkWindowLevelLookupTable = vtkWindowLevelLookupTable() lut.SetWindow(self.window_width) lut.SetLevel(self.window_level) lut.Build() - init = self.window_level - self.window_width / 2 - end = self.window_level + self.window_width / 2 + init: float = self.window_level - self.window_width / 2 + end: float = self.window_level + self.window_width / 2 # Set mapper auto - mapper = vtkPolyDataMapper() + mapper: vtkPolyDataMapper = vtkPolyDataMapper() mapper.SetInputData(self.peel[p]) mapper.SetScalarRange(init, end) mapper.SetLookupTable(lut) @@ -284,26 +282,26 @@ def GetPeelActor(self, p): # Set actor self.currentPeelActor.SetMapper(mapper) - currentPeel = self.TransformPeelPosition(p) + currentPeel: vtkPolyData = self.TransformPeelPosition(p) self.locator.SetDataSet(currentPeel) self.locator.BuildLocator() - self.peel_centers = GetCenters(currentPeel) - self.peel_normals = GetNormals(currentPeel) + self.peel_centers: np.ndarray = GetCenters(currentPeel) + self.peel_normals: np.ndarray = GetNormals(currentPeel) return self.currentPeelActor - def GetCurrentPeelActor(self, currentPeel): - lut = vtkWindowLevelLookupTable() + def GetCurrentPeelActor(self, currentPeel: vtkPolyData) -> vtkActor: + lut: vtkWindowLevelLookupTable = vtkWindowLevelLookupTable() lut.SetWindow(self.window_width) lut.SetLevel(self.window_level) lut.Build() - init = self.window_level - self.window_width / 2 - end = self.window_level + self.window_width / 2 + init: float = self.window_level - self.window_width / 2 + end: float = self.window_level + self.window_width / 2 # Set mapper auto - mapper = vtkPolyDataMapper() + mapper: vtkPolyDataMapper = vtkPolyDataMapper() mapper.SetInputData(currentPeel) mapper.SetScalarRange(init, end) mapper.SetLookupTable(lut) @@ -318,67 +316,68 @@ def GetCurrentPeelActor(self, currentPeel): return self.currentPeelActor class E_field_brain: - def __init__(self, e_field_mesh): + def __init__(self, e_field_mesh: vtkPolyData) -> None: self.GetEfieldActor(e_field_mesh) - def GetEfieldActor(self, mesh): - self.e_field_mesh_normals = vtkFloatArray() - self.e_field_mesh_centers = vtkFloatArray() + def GetEfieldActor(self, mesh: vtkPolyData) -> vtkActor: + self.e_field_mesh_normals: vtkFloatArray = vtkFloatArray() + self.e_field_mesh_centers: vtkFloatArray = vtkFloatArray() - self.locator_efield_Cell = vtkCellLocator() + self.locator_efield_Cell: vtkCellLocator = vtkCellLocator() self.locator_efield_Cell.SetDataSet(mesh) self.locator_efield_Cell.BuildLocator() - self.locator_efield = vtkPointLocator() + self.locator_efield: vtkPointLocator = vtkPointLocator() self.locator_efield.SetDataSet(mesh) self.locator_efield.BuildLocator() self.e_field_mesh_normals = GetNormals(mesh) self.e_field_mesh_centers = GetCenters(mesh) - self.e_field_mesh = mesh - - self.efield_mapper = vtkPolyDataMapper() - self.lut = CreateLUTTableForEfield(0, 0.001) - -def GetCenters(mesh): - # Compute centers of triangles - centerComputer = vtkCellCenters() # This computes centers of the triangles on the peel - centerComputer.SetInputData(mesh) - centerComputer.Update() - - # This stores the centers for easy access - centers = centerComputer.GetOutput() - return centers - -def GetNormals(mesh): - # Compute normals of triangles - normalComputer = vtkPolyDataNormals() # This computes normals of the triangles on the peel - normalComputer.SetInputData(mesh) - normalComputer.ComputePointNormalsOff() - normalComputer.ComputeCellNormalsOn() - normalComputer.Update() - # This converts to the normals to an array for easy access - normals = normalComputer.GetOutput().GetCellData().GetNormals() - return normals -def CreateLUTTableForEfield(min, max): - lut = vtkLookupTable() - lut.SetTableRange(min, max) - colorSeries = vtkColorSeries() - seriesEnum = colorSeries.BREWER_SEQUENTIAL_YELLOW_ORANGE_BROWN_9 - colorSeries.SetColorScheme(seriesEnum) - colorSeries.BuildLookupTable(lut, colorSeries.ORDINAL) - return lut - -def cleanMesh(inp): - cleaned = vtkCleanPolyData() + self.e_field_mesh: vtkPolyData = mesh + + self.efield_mapper: vtkPolyDataMapper = vtkPolyDataMapper() + self.lut: vtkLookupTable = CreateLUTTableForEfield(0, 0.001) + +def GetCenters(mesh: vtkPolyData) -> vtkPolyData: + # Compute centers of triangles + centerComputer: vtkCellCenters = vtkCellCenters() # This computes centers of the triangles on the peel + centerComputer.SetInputData(mesh) + centerComputer.Update() + + # This stores the centers for easy access + centers: vtkPolyData = centerComputer.GetOutput() + return centers + +def GetNormals(mesh: vtkPolyData) -> vtkFloatArray: + # Compute normals of triangles + normalComputer: vtkPolyDataNormals = vtkPolyDataNormals() # This computes normals of the triangles on the peel + normalComputer.SetInputData(mesh) + normalComputer.ComputePointNormalsOff() + normalComputer.ComputeCellNormalsOn() + normalComputer.Update() + # This converts to the normals to an array for easy access + normals: vtkFloatArray = normalComputer.GetOutput().GetCellData().GetNormals() + return normals + +def CreateLUTTableForEfield(min_val: float, max_val: float) -> vtkLookupTable: + lut: vtkLookupTable = vtkLookupTable() + lut.SetTableRange(min_val, max_val) + colorSeries: vtkColorSeries = vtkColorSeries() + seriesEnum: int = colorSeries.BREWER_SEQUENTIAL_YELLOW_ORANGE_BROWN_9 + colorSeries.SetColorScheme(seriesEnum) + colorSeries.BuildLookupTable(lut, colorSeries.ORDINAL) + return lut + +def cleanMesh(inp: vtkPolyData) -> vtkPolyData: + cleaned: vtkCleanPolyData = vtkCleanPolyData() cleaned.SetInputData(inp) cleaned.Update() return cleaned.GetOutput() -def fixMesh(inp): - normals = vtkPolyDataNormals() +def fixMesh(inp: vtkPolyData) -> vtkPolyData: + normals: vtkPolyDataNormals = vtkPolyDataNormals() normals.SetInputData(inp) normals.SetFeatureAngle(160) normals.SplittingOn() @@ -389,12 +388,12 @@ def fixMesh(inp): return normals.GetOutput() -def upsample(inp): - triangles = vtkTriangleFilter() +def upsample(inp: vtkPolyData) -> vtkPolyData: + triangles: vtkTriangleFilter = vtkTriangleFilter() triangles.SetInputData(inp) triangles.Update() - subdivisionFilter = vtkLinearSubdivisionFilter() + subdivisionFilter: vtkLinearSubdivisionFilter = vtkLinearSubdivisionFilter() subdivisionFilter.SetInputData(triangles.GetOutput()) subdivisionFilter.SetNumberOfSubdivisions(2) subdivisionFilter.Update() @@ -402,8 +401,8 @@ def upsample(inp): return subdivisionFilter.GetOutput() -def smooth(inp): - smoother = vtkWindowedSincPolyDataFilter() +def smooth(inp: vtkPolyData) -> vtkPolyData: + smoother: vtkWindowedSincPolyDataFilter = vtkWindowedSincPolyDataFilter() smoother.SetInputData(inp) smoother.SetNumberOfIterations(20) smoother.BoundarySmoothingOn() @@ -417,7 +416,7 @@ def smooth(inp): return smoother.GetOutput() -def downsample(inp): +def downsample(inp: vtkPolyData) -> vtkPolyData: # surface = vtkSurface() # surface.CreateFromPolyData(inp) # @@ -430,14 +429,14 @@ def downsample(inp): # # clusterNumber = surfaceArea / 20 - mesh = pyvista.PolyData(inp) + mesh: pyvista.PolyData = pyvista.PolyData(inp) # Create clustering object - clus = pyacvd.Clustering(mesh) + clus: pyacvd.Clustering = pyacvd.Clustering(mesh) # mesh is not dense enough for uniform remeshing # clus.subdivide(3) clus.cluster(3000) - Remesh = clus.create_mesh() + Remesh: pyvista.PolyData= clus.create_mesh() # print(Remesh) diff --git a/invesalius/data/converters.py b/invesalius/data/converters.py index e83ef99fc..ad9823cd2 100644 --- a/invesalius/data/converters.py +++ b/invesalius/data/converters.py @@ -19,19 +19,21 @@ import gdcm import numpy as np +from typing import Any, List, Union + from vtkmodules.util import numpy_support from vtkmodules.vtkCommonDataModel import vtkImageData def to_vtk( - n_array, - spacing=(1.0, 1.0, 1.0), - slice_number=0, - orientation="AXIAL", - origin=(0, 0, 0), - padding=(0, 0, 0), -): + n_array: np.ndarray, + spacing: tuple[float, float, float] = (1.0, 1.0, 1.0), + slice_number: int = 0, + orientation: str = "AXIAL", + origin: tuple[int, int, int] = (0, 0, 0), + padding: tuple[int, int, int] = (0, 0, 0), +) -> vtkImageData: if orientation == "SAGITTAL": orientation = "SAGITAL" @@ -94,7 +96,12 @@ def to_vtk( return image_copy -def to_vtk_mask(n_array, spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0)): + +def to_vtk_mask( + n_array: np.ndarray, + spacing: tuple[float, float, float] = (1.0, 1.0, 1.0), + origin: tuple[float, float, float] = (0.0, 0.0, 0.0) +) -> vtkImageData: dz, dy, dx = n_array.shape ox, oy, oz = origin sx, sy, sz = spacing @@ -125,7 +132,11 @@ def to_vtk_mask(n_array, spacing=(1.0, 1.0, 1.0), origin=(0.0, 0.0, 0.0)): return image -def np_rgba_to_vtk(n_array, spacing=(1.0, 1.0, 1.0)): + +def np_rgba_to_vtk( + n_array: np.ndarray, + spacing: tuple[float, float, float] = (1.0, 1.0, 1.0) +) -> vtkImageData: dy, dx, dc = n_array.shape v_image = numpy_support.numpy_to_vtk(n_array.reshape(dy * dx, dc)) @@ -147,8 +158,12 @@ def np_rgba_to_vtk(n_array, spacing=(1.0, 1.0, 1.0)): return image + # Based on http://gdcm.sourceforge.net/html/ConvertNumpy_8py-example.html -def gdcm_to_numpy(image, apply_intercep_scale=True): +def gdcm_to_numpy( + image: gdcm.Image, + apply_intercep_scale: bool = True +) -> Union[np.ndarray, np.ndarray]: map_gdcm_np = { gdcm.PixelFormat.SINGLEBIT: np.uint8, gdcm.PixelFormat.UINT8: np.uint8, @@ -192,3 +207,4 @@ def gdcm_to_numpy(image, apply_intercep_scale=True): return output else: return np_array + diff --git a/invesalius/data/coordinates.py b/invesalius/data/coordinates.py index 14a43f0a5..a31eff94f 100644 --- a/invesalius/data/coordinates.py +++ b/invesalius/data/coordinates.py @@ -21,6 +21,7 @@ import numpy as np import threading import wx +from typing import Tuple, List , Optional import invesalius.data.transformations as tr import invesalius.constants as const @@ -30,20 +31,20 @@ from invesalius.pubsub import pub as Publisher class TrackerCoordinates(): - def __init__(self): - self.coord = None - self.markers_flag = [False, False, False] - self.previous_markers_flag = self.markers_flag - self.nav_status = False + def __init__(self) -> None: + self.coord: Optional[np.ndarray] = None + self.markers_flag: List[bool] = [False, False, False] + self.previous_markers_flag: List[bool] = self.markers_flag + self.nav_status: bool = False self.__bind_events() - def __bind_events(self): + def __bind_events(self) -> None: Publisher.subscribe(self.OnUpdateNavigationStatus, 'Navigation status') - def OnUpdateNavigationStatus(self, nav_status, vis_status): + def OnUpdateNavigationStatus(self, nav_status: bool, vis_status: bool) -> None: self.nav_status = nav_status - def SetCoordinates(self, coord, markers_flag): + def SetCoordinates(self, coord: np.ndarray, markers_flag: List[bool]) -> None: self.coord = coord self.markers_flag = markers_flag if not self.nav_status: @@ -54,7 +55,7 @@ def SetCoordinates(self, coord, markers_flag): wx.CallAfter(Publisher.sendMessage, 'Render volume viewer') self.previous_markers_flag = self.markers_flag - def GetCoordinates(self): + def GetCoordinates(self) -> Tuple[Optional[np.ndarray], List[bool]]: if self.nav_status: wx.CallAfter(Publisher.sendMessage, 'Update tracker coordinates', coord=self.coord.tolist(), markers_flag=self.markers_flag) @@ -65,7 +66,7 @@ def GetCoordinates(self): return self.coord, self.markers_flag -def GetCoordinatesForThread(tracker_connection, tracker_id, ref_mode): +def GetCoordinatesForThread(tracker_connection: TrackerConnection, tracker_id: Optional[str], ref_mode: str) -> Tuple[Optional[np.ndarray], Optional[List[bool]]]: """ Read coordinates from spatial tracking devices using @@ -75,7 +76,8 @@ def GetCoordinatesForThread(tracker_connection, tracker_id, ref_mode): :return: array of six coordinates (x, y, z, alpha, beta, gamma) """ - coord = None + coord: Optional[np.ndarray] = None + markers_flag: Optional[List[bool]] = None if tracker_id: getcoord = {const.MTC: ClaronCoord, const.FASTRAK: PolhemusCoord, @@ -94,7 +96,8 @@ def GetCoordinatesForThread(tracker_connection, tracker_id, ref_mode): return coord, markers_flag -def PolarisP4Coord(tracker_connection, tracker_id, ref_mode): + +def PolarisP4Coord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: trck = tracker_connection.GetConnection() trck.Run() @@ -137,7 +140,9 @@ def PolarisP4Coord(tracker_connection, tracker_id, ref_mode): return coord, [trck.probeID, trck.refID, trck.objID] -def OptitrackCoord(tracker_connection, tracker_id, ref_mode): + + +def OptitrackCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: """ Obtains coordinates and angles of tracking rigid bodies (Measurement Probe, Coil, Head). Converts orientations from quaternion @@ -178,7 +183,7 @@ def OptitrackCoord(tracker_connection, tracker_id, ref_mode): return coord, [trck.probeID, trck.HeadID, trck.coilID] -def PolarisCoord(tracker_connection, tracker_id, ref_mode): +def PolarisCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: trck = tracker_connection.GetConnection() trck.Run() @@ -202,13 +207,13 @@ def PolarisCoord(tracker_connection, tracker_id, ref_mode): return coord, [trck.probeID, trck.refID, trck.objID] -def CameraCoord(tracker_connection, tracker_id, ref_mode): +def CameraCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: trck = tracker_connection.GetConnection() coord, probeID, refID, coilID = trck.Run() return coord, [probeID, refID, coilID] -def ClaronCoord(tracker_connection, tracker_id, ref_mode): +def ClaronCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: trck = tracker_connection.GetConnection() trck.Run() @@ -231,10 +236,11 @@ def ClaronCoord(tracker_connection, tracker_id, ref_mode): return coord, [trck.probeID, trck.refID, trck.coilID] -def PolhemusCoord(tracker_connection, tracker_id, ref_mode): - lib_mode = tracker_connection.GetLibMode() - coord = None +def PolhemusCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[any, List[bool]]: + lib_mode: str = tracker_connection.GetLibMode() + + coord: any = None if lib_mode == 'serial': coord = PolhemusSerialCoord(tracker_connection, tracker_id, ref_mode) @@ -248,26 +254,28 @@ def PolhemusCoord(tracker_connection, tracker_id, ref_mode): return coord, [True, True, True] -def PolhemusWrapperCoord(tracker_connection, tracker_id, ref_mode): + + +def PolhemusWrapperCoord(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: str) -> np.ndarray: trck = tracker_connection.GetConnection() trck.Run() - scale = 10.0 * np.array([1., 1., 1.]) + scale: np.ndarray = 10.0 * np.array([1., 1., 1.]) - coord1 = np.array([float(trck.PositionTooltipX1)*scale[0], float(trck.PositionTooltipY1)*scale[1], + coord1: np.ndarray = np.array([float(trck.PositionTooltipX1)*scale[0], float(trck.PositionTooltipY1)*scale[1], float(trck.PositionTooltipZ1)*scale[2], float(trck.AngleX1), float(trck.AngleY1), float(trck.AngleZ1)]) - coord2 = np.array([float(trck.PositionTooltipX2)*scale[0], float(trck.PositionTooltipY2)*scale[1], + coord2: np.ndarray = np.array([float(trck.PositionTooltipX2)*scale[0], float(trck.PositionTooltipY2)*scale[1], float(trck.PositionTooltipZ2)*scale[2], float(trck.AngleX2), float(trck.AngleY2), float(trck.AngleZ2)]) - coord = np.vstack([coord1, coord2]) + coord: np.ndarray = np.vstack([coord1, coord2]) if tracker_id == 2: - coord3 = np.array([float(trck.PositionTooltipX3) * scale[0], float(trck.PositionTooltipY3) * scale[1], + coord3: np.ndarray = np.array([float(trck.PositionTooltipX3) * scale[0], float(trck.PositionTooltipY3) * scale[1], float(trck.PositionTooltipZ3) * scale[2], float(trck.AngleX3), float(trck.AngleY3), float(trck.AngleZ3)]) - coord4 = np.array([float(trck.PositionTooltipX4) * scale[0], float(trck.PositionTooltipY4) * scale[1], + coord4: np.ndarray = np.array([float(trck.PositionTooltipX4) * scale[0], float(trck.PositionTooltipY4) * scale[1], float(trck.PositionTooltipZ4) * scale[2], float(trck.AngleX4), float(trck.AngleY4), float(trck.AngleZ4)]) coord = np.vstack([coord, coord3, coord4]) @@ -278,7 +286,8 @@ def PolhemusWrapperCoord(tracker_connection, tracker_id, ref_mode): return coord -def PolhemusUSBCoord(tracker_connection, tracker_id, ref_mode): + +def PolhemusUSBCoord(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: bool) -> Optional[Tuple[float, float, float, float, float, float]]: trck = tracker_connection.GetConnection() endpoint = trck[0][(0, 0)][0] @@ -286,10 +295,10 @@ def PolhemusUSBCoord(tracker_connection, tracker_id, ref_mode): # TODO: Check if it's working properly. trck.write(0x02, "P") if tracker_id == 2: - scale = 10. * np.array([1., 1.0, -1.0]) + scale: np.ndarray = 10. * np.array([1., 1.0, -1.0]) else: - scale = 25.4 * np.array([1., 1.0, -1.0]) - coord = None + scale: np.ndarray = 25.4 * np.array([1., 1.0, -1.0]) + coord: Optional[Tuple[float, float, float, float, float, float]] = None if ref_mode: @@ -317,7 +326,8 @@ def PolhemusUSBCoord(tracker_connection, tracker_id, ref_mode): return coord -def PolhemusSerialCoord(tracker_connection, tracker_id, ref_mode): + +def PolhemusSerialCoord(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: bool) -> Optional[np.ndarray]: trck = tracker_connection.GetConnection() # mudanca para fastrak - ref 1 tem somente x, y, z @@ -325,11 +335,12 @@ def PolhemusSerialCoord(tracker_connection, tracker_id, ref_mode): # this method is not optimized to work with all trackers, only with ISOTRAK # serial connection is obsolete, remove in future trck.write(str.encode("P")) - scale = 10. * np.array([1., 1.0, 1.0]) + scale: np.ndarray = 10. * np.array([1., 1.0, 1.0]) lines = trck.readlines() if lines is None: print("The Polhemus is not connected!") + return None else: data = lines[0] data = data.replace(str.encode('-'), str.encode(' -')) @@ -351,14 +362,15 @@ def PolhemusSerialCoord(tracker_connection, tracker_id, ref_mode): return coord -def RobotCoord(tracker_connection, tracker_id, ref_mode): +def RobotCoord(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: bool) -> Tuple[np.ndarray, bool]: tracker_id = tracker_connection.GetTrackerId() coord_tracker, markers_flag = GetCoordinatesForThread(tracker_connection, tracker_id, ref_mode) return np.vstack([coord_tracker[0], coord_tracker[1], coord_tracker[2]]), markers_flag -def DebugCoordRandom(tracker_connection, tracker_id, ref_mode): + +def DebugCoordRandom(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: bool) -> Tuple[np.ndarray, List[int]]: """ Method to simulate a tracking device for debug and error check. Generate a random x, y, z, alfa, beta and gama coordinates in interval [1, 200[ @@ -412,7 +424,8 @@ def DebugCoordRandom(tracker_connection, tracker_id, ref_mode): return np.vstack([coord1, coord2, coord3, coord4]), [int(uniform(0, 5)), int(uniform(0, 5)), int(uniform(0, 5))] -def coordinates_to_transformation_matrix(position, orientation, axes='sxyz'): + +def coordinates_to_transformation_matrix(position: List[float], orientation: List[float], axes: str = 'sxyz') -> np.ndarray: """ Transform vectors consisting of position and orientation (in Euler angles) in 3d-space into a 4x4 transformation matrix that combines the rotation and translation. @@ -423,15 +436,15 @@ def coordinates_to_transformation_matrix(position, orientation, axes='sxyz'): """ a, b, g = np.radians(orientation) - r_ref = tr.euler_matrix(a, b, g, axes=axes) - t_ref = tr.translation_matrix(position) + r_ref = euler_matrix(a, b, g, axes=axes) + t_ref = translation_matrix(position) - m_img = tr.concatenate_matrices(t_ref, r_ref) + m_img = np.dot(t_ref, r_ref) return m_img -def transformation_matrix_to_coordinates(matrix, axes='sxyz'): +def transformation_matrix_to_coordinates(matrix: np.ndarray, axes: str = 'sxyz') -> Tuple[List[float], List[float]]: """ Given a matrix that combines the rotation and translation, return the position and the orientation determined by the matrix. The orientation is given as three Euler angles. @@ -445,10 +458,12 @@ def transformation_matrix_to_coordinates(matrix, axes='sxyz'): translation = tr.translation_from_matrix(matrix) - return translation, angles_as_deg + return translation.tolist(), angles_as_deg.tolist() + + -def dynamic_reference(probe, reference): +def dynamic_reference(probe: List[float], reference: List[float]) -> Tuple[float, float, float, float, float, float]: """ Apply dynamic reference correction to probe coordinates. Uses the alpha, beta and gama rotation angles of reference to rotate the probe coordinate and returns the x, y, z @@ -469,20 +484,19 @@ def dynamic_reference(probe, reference): # a: rotation of plane (X, Y) around Z axis (azimuth) # b: rotation of plane (X', Z) around Y' axis (elevation) # a: rotation of plane (Y', Z') around X'' axis (roll) - m_rot = np.mat([[cos(a) * cos(b), sin(b) * sin(g) * cos(a) - cos(g) * sin(a), - cos(a) * sin(b) * cos(g) + sin(a) * sin(g)], - [cos(b) * sin(a), sin(b) * sin(g) * sin(a) + cos(g) * cos(a), - cos(g) * sin(b) * sin(a) - sin(g) * cos(a)], - [-sin(b), sin(g) * cos(b), cos(b) * cos(g)]]) - - # coord_rot = m_rot.T * vet - coord_rot = vet*m_rot + m_rot = np.mat([[np.cos(a) * np.cos(b), np.sin(b) * np.sin(g) * np.cos(a) - np.cos(g) * np.sin(a), + np.cos(a) * np.sin(b) * np.cos(g) + np.sin(a) * np.sin(g)], + [np.cos(b) * np.sin(a), np.sin(b) * np.sin(g) * np.sin(a) + np.cos(g) * np.cos(a), + np.cos(g) * np.sin(b) * np.sin(a) - np.sin(g) * np.cos(a)], + [-np.sin(b), np.sin(g) * np.cos(b), np.cos(b) * np.cos(g)]]) + + coord_rot = vet * m_rot coord_rot = np.squeeze(np.asarray(coord_rot)) return coord_rot[0], coord_rot[1], -coord_rot[2], probe[3], probe[4], probe[5] -def dynamic_reference_m(probe, reference): +def dynamic_reference_m(probe: List[float], reference: List[float]) -> List[float]: """ Apply dynamic reference correction to probe coordinates. Uses the alpha, beta and gama rotation angles of reference to rotate the probe coordinate and returns the x, y, z @@ -509,7 +523,8 @@ def dynamic_reference_m(probe, reference): return coord_rot -def dynamic_reference_m2(probe, reference): + +def dynamic_reference_m2(probe: List[float], reference: List[float]) -> Tuple[float, float, float, float, float, float]: """ Apply dynamic reference correction to probe coordinates. Uses the alpha, beta and gama rotation angles of reference to rotate the probe coordinate and returns the x, y, z @@ -543,7 +558,8 @@ def dynamic_reference_m2(probe, reference): return coord_rot[0], coord_rot[1], coord_rot[2], np.degrees(al), np.degrees(be), np.degrees(ga) -def str2float(data): + +def str2float(data: str) -> List[float]: """ Converts string detected wth Polhemus device to float array of coordinates. This method applies a correction for the minus sign in string that raises error while splitting the string into coordinates. @@ -563,7 +579,7 @@ def str2float(data): return data -def offset_coordinate(p_old, norm_vec, offset): +def offset_coordinate(p_old: Tuple[float, float, float], norm_vec: Tuple[float, float, float], offset: float) -> Tuple[float, float, float]: """ Translate the coordinates of a point along a vector :param p_old: (x, y, z) array with current point coordinates @@ -574,6 +590,7 @@ def offset_coordinate(p_old, norm_vec, offset): p_offset = p_old - offset * norm_vec return p_offset + class ReceiveCoordinates(threading.Thread): def __init__(self, tracker_connection, tracker_id, TrackerCoordinates, event): threading.Thread.__init__(self, name='ReceiveCoordinates') diff --git a/invesalius/data/coregistration.py b/invesalius/data/coregistration.py index fae00abaf..0534b731d 100644 --- a/invesalius/data/coregistration.py +++ b/invesalius/data/coregistration.py @@ -21,16 +21,20 @@ import queue import threading from time import sleep +from typing import Tuple , List , Optional , Union , Dict , Any , Callable , TypeVar , Generic , Type , cast , overload , Literal , Protocol , runtime_chec + import invesalius.constants as const import invesalius.data.transformations as tr import invesalius.data.bases as bases import invesalius.data.coordinates as dco +import invesalius.data.e_field as e_field + # TODO: Replace the use of degrees by radians in every part of the navigation pipeline -def object_marker_to_center(coord_raw, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw): +def object_marker_to_center(coord_raw: np.ndarray, obj_ref_mode: int, t_obj_raw: np.ndarray, s0_raw: np.ndarray, r_s0_raw: np.ndarray) -> np.ndarray: """Translate and rotate the raw coordinate given by the tracking device to the reference system created during the object registration. @@ -64,7 +68,8 @@ def object_marker_to_center(coord_raw, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw return m_probe -def object_to_reference(coord_raw, m_probe): + +def object_to_reference(coord_raw: np.ndarray, m_probe: np.ndarray) -> np.ndarray: """Compute affine transformation matrix to the reference basis :param coord_raw: Coordinates returned by the tracking device @@ -83,7 +88,7 @@ def object_to_reference(coord_raw, m_probe): return m_dyn -def tracker_to_image(m_change, m_probe_ref, r_obj_img, m_obj_raw, s0_dyn): +def tracker_to_image(m_change: np.ndarray, m_probe_ref: np.ndarray, r_obj_img: np.ndarray, m_obj_raw: np.ndarray, s0_dyn: np.ndarray) -> np.ndarray: """Compute affine transformation matrix to the reference basis :param m_change: Corregistration transformation obtained from fiducials @@ -106,7 +111,8 @@ def tracker_to_image(m_change, m_probe_ref, r_obj_img, m_obj_raw, s0_dyn): return m_img -def image_to_tracker(m_change, coord_raw, target, icp, obj_data): + +def image_to_tracker(m_change: np.ndarray, coord_raw: np.ndarray, target: np.ndarray, icp: np.ndarray, obj_data: List[np.ndarray]) -> np.ndarray: """Compute the transformation matrix to the tracker coordinate system. The transformation matrix is splitted in two steps, one for rotation and one for translation @@ -168,8 +174,7 @@ def image_to_tracker(m_change, coord_raw, target, icp, obj_data): return m_target_in_tracker -def corregistrate_object_dynamic(inp, coord_raw, ref_mode_id, icp): - +def corregistrate_object_dynamic(inp: Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]) -> Tuple[Tuple[float, float, float, float, float, float], np.ndarray]: m_change, obj_ref_mode, t_obj_raw, s0_raw, r_s0_raw, s0_dyn, m_obj_raw, r_obj_img = inp # transform raw marker coordinate to object center @@ -198,7 +203,9 @@ def corregistrate_object_dynamic(inp, coord_raw, ref_mode_id, icp): return coord, m_img -def compute_marker_transformation(coord_raw, obj_ref_mode): + + +def compute_marker_transformation(coord_raw: np.ndarray, obj_ref_mode: int) -> np.ndarray: m_probe = dco.coordinates_to_transformation_matrix( position=coord_raw[obj_ref_mode, :3], orientation=coord_raw[obj_ref_mode, 3:], @@ -207,7 +214,7 @@ def compute_marker_transformation(coord_raw, obj_ref_mode): return m_probe -def corregistrate_dynamic(inp, coord_raw, ref_mode_id, icp): +def corregistrate_dynamic(inp: Tuple[np.ndarray, int], coord_raw: np.ndarray, ref_mode_id: bool, icp: np.ndarray) -> Tuple[Tuple[float, float, float, float, float, float], np.ndarray]: m_change, obj_ref_mode = inp @@ -238,14 +245,16 @@ def corregistrate_dynamic(inp, coord_raw, ref_mode_id, icp): return coord, m_img -def apply_icp(m_img, icp): + +def apply_icp(m_img: np.ndarray, icp: Tuple[bool, np.ndarray]) -> np.ndarray: use_icp, m_icp = icp if use_icp: m_img = bases.transform_icp(m_img, m_icp) return m_img -def ComputeRelativeDistanceToTarget(target_coord=None, img_coord=None, m_target=None, m_img=None): + +def ComputeRelativeDistanceToTarget(target_coord: Optional[np.ndarray] = None, img_coord: Optional[np.ndarray] = None, m_target: Optional[np.ndarray] = None, m_img: Optional[np.ndarray] = None) -> Tuple[float, float, float, float, float, float]: if m_target is None: m_target = dco.coordinates_to_transformation_matrix( position=target_coord[:3], @@ -267,11 +276,14 @@ def ComputeRelativeDistanceToTarget(target_coord=None, img_coord=None, m_target= distance = [m_relative_target[0, -1], m_relative_target[1, -1], m_relative_target[2, -1], \ np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2])] - return distance + return tuple(distance) + + + class CoordinateCorregistrate(threading.Thread): - def __init__(self, ref_mode_id, tracker, coreg_data, view_tracts, queues, event, sle, tracker_id, target, icp,e_field_loaded): + def __init__(self, ref_mode_id: int, tracker: Any, coreg_data: Any, view_tracts: Any, queues: List[Any], event: threading.Event, sle: Any, tracker_id: int, target: Optional[List[float]], icp: Tuple[bool, np.ndarray], e_field_loaded: bool): threading.Thread.__init__(self, name='CoordCoregObject') self.ref_mode_id = ref_mode_id self.tracker = tracker @@ -285,8 +297,8 @@ def __init__(self, ref_mode_id, tracker, coreg_data, view_tracts, queues, event, self.sle = sle self.icp_queue = queues[2] self.object_at_target_queue = queues[3] - self.use_icp = icp.use_icp - self.m_icp = icp.m_icp + self.use_icp = icp[0] + self.m_icp = icp[1] self.last_coord = None self.tracker_id = tracker_id self.target = target @@ -300,24 +312,24 @@ def __init__(self, ref_mode_id, tracker, coreg_data, view_tracts, queues, event, # do this transformation only once, and doing it in the correct place. # self.target[1] = -self.target[1] - - def run(self): - coreg_data = self.coreg_data - view_obj = 1 - + + def run(self) -> None: + coreg_data: Any = self.coreg_data + view_obj: int = 1 + # print('CoordCoreg: event {}'.format(self.event.is_set())) while not self.event.is_set(): try: if not self.icp_queue.empty(): self.use_icp, self.m_icp = self.icp_queue.get_nowait() - + if not self.object_at_target_queue.empty(): self.target_flag = self.object_at_target_queue.get_nowait() - + # print(f"Set the coordinate") coord_raw, markers_flag = self.tracker.TrackerCoordinates.GetCoordinates() coord, m_img = corregistrate_object_dynamic(coreg_data, coord_raw, self.ref_mode_id, [self.use_icp, self.m_icp]) - + # XXX: This is not the best place to do the logic related to approaching the target when the # debug tracker is in use. However, the trackers (including the debug trackers) operate in # the tracker space where it is hard to make the tracker approach the target in the image space. @@ -327,25 +339,25 @@ def run(self): # those abstractions do not currently exist and doing them would need a larger refactoring. # if self.tracker_id == const.DEBUGTRACKAPPROACH and self.target is not None: - + if self.last_coord is None: self.last_coord = np.array(coord) else: coord = self.last_coord + (self.target - self.last_coord) * 0.05 self.last_coord = coord - + angles = [np.radians(coord[3]), np.radians(coord[4]), np.radians(coord[5])] translate = coord[0:3] m_img = tr.compose_matrix(angles=angles, translate=translate) - + m_img_flip = m_img.copy() m_img_flip[1, -1] = -m_img_flip[1, -1] # self.pipeline.set_message(m_img_flip) - + self.coord_queue.put_nowait([coord, markers_flag, m_img, view_obj]) # print('CoordCoreg: put {}'.format(count)) # count += 1 - + if self.view_tracts: self.coord_tracts_queue.put_nowait(m_img_flip) if self.e_field_loaded: @@ -359,7 +371,7 @@ def run(self): class CoordinateCorregistrateNoObject(threading.Thread): - def __init__(self, ref_mode_id, tracker, coreg_data, view_tracts, queues, event, sle, icp, e_field_loaded): + def __init__(self, ref_mode_id: int, tracker: Any, coreg_data: Any, view_tracts: bool, queues: List[Any], event: threading.Event, sle: float, icp: Any, e_field_loaded: bool) -> None: threading.Thread.__init__(self, name='CoordCoregNoObject') self.ref_mode_id = ref_mode_id self.tracker = tracker @@ -375,9 +387,9 @@ def __init__(self, ref_mode_id, tracker, coreg_data, view_tracts, queues, event, self.efield_queue = queues[3] self.e_field_loaded = e_field_loaded - def run(self): - coreg_data = self.coreg_data - view_obj = 0 + def run(self) -> None: + coreg_data: Any = self.coreg_data + view_obj: int = 0 # print('CoordCoreg: event {}'.format(self.event.is_set())) while not self.event.is_set(): @@ -388,7 +400,8 @@ def run(self): self.use_icp, self.m_icp = self.icp_queue.get_nowait() # print(f"Set the coordinate") #print(self.icp, self.m_icp) - coord_raw, markers_flag = self.tracker.TrackerCoordinates.GetCoordinates() + coord_raw: np.ndarray = self.tracker.TrackerCoordinates.GetCoordinates() + markers_flag: bool = False # assuming this is a boolean coord, m_img = corregistrate_dynamic(coreg_data, coord_raw, self.ref_mode_id, [self.use_icp, self.m_icp]) # print("Coord: ", coord) m_img_flip = m_img.copy() @@ -408,6 +421,7 @@ def run(self): sleep(self.sle) + # class CoregistrationStatic(threading.Thread): # """ # Thread to update the coordinates with the fiducial points diff --git a/invesalius/data/cursor_actors.py b/invesalius/data/cursor_actors.py index 91c020cd6..910f7065b 100644 --- a/invesalius/data/cursor_actors.py +++ b/invesalius/data/cursor_actors.py @@ -19,7 +19,8 @@ import math -import numpy +import numpy +from typing import Any, Tuple, Union , Optional, List from vtkmodules.util import numpy_support from vtkmodules.vtkCommonCore import vtkLookupTable, vtkVersion @@ -39,7 +40,7 @@ 'CORONAL': 1, 'SAGITAL': 0} -def to_vtk(n_array, spacing, slice_number, orientation): +def to_vtk(n_array: numpy.ndarray, spacing: Tuple[float, float, float], slice_number: int, orientation: str) -> vtkImageData: """ It transforms a numpy array into a vtkImageData. """ @@ -79,15 +80,16 @@ def to_vtk(n_array, spacing, slice_number, orientation): return image_copy + class CursorBase(object): - def __init__(self): - self.colour = (0.0, 0.0, 1.0) - self.opacity = 1 - self.size = 15.0 - self.unit = 'mm' - self.orientation = "AXIAL" - self.spacing = (1, 1, 1) - self.position = (0, 0, 0) + def __init__(self) -> None: + self.colour: Tuple[float, float, float] = (0.0, 0.0, 1.0) + self.opacity: int = 1 + self.size: float = 15.0 + self.unit: str = 'mm' + self.orientation: str = "AXIAL" + self.spacing: Tuple[float, float, float] = (1, 1, 1) + self.position: Tuple[int, int, int] = (0, 0, 0) if vtkVersion().GetVTKVersion() > '5.8.0': self.mapper = vtkImageSliceMapper() cursor_property = vtkImageProperty() @@ -101,26 +103,27 @@ def __init__(self): self._build_actor() self._calculate_area_pixels() - def SetSize(self, diameter): + def SetSize(self, diameter: float) -> None: self.radius = diameter/2.0 self._build_actor() self._calculate_area_pixels() - def SetUnit(self, unit): + def SetUnit(self, unit: str) -> None: self.unit = unit self._build_actor() self._calculate_area_pixels() - def SetColour(self, colour): + def SetColour(self, colour: Tuple[float, float, float]) -> None: self.colour = colour self._build_actor() - def SetOrientation(self, orientation): + def SetOrientation(self, orientation: str) -> None: self.orientation = orientation self._build_actor() self._calculate_area_pixels() - def SetPosition(self, position): + + def SetPosition(self, position: Tuple[int, int, int]) -> None: # Overriding SetPosition method because in rectangles with odd # dimensions there is no half position. self.position = position @@ -129,95 +132,98 @@ def SetPosition(self, position): tx = self.actor.GetXRange()[1] - self.actor.GetXRange()[0] ty = self.actor.GetYRange()[1] - self.actor.GetYRange()[0] tz = self.actor.GetZRange()[1] - self.actor.GetZRange()[0] - + if self.orientation == 'AXIAL': if self.points.shape[0] % 2: y = py - ty / 2.0 else: y = py - ty / 2.0 + self.spacing[1] / 2.0 - + if self.points.shape[1] % 2: x = px - tx / 2.0 else: x = px - tx / 2.0 + self.spacing[0] / 2.0 z = pz - + if self.mapper: x += sx / 2.0 y += sy / 2.0 - + elif self.orientation == 'CORONAL': if self.points.shape[0] % 2: z = pz - tz / 2.0 else: z = pz - tz / 2.0 + self.spacing[2] / 2.0 - + if self.points.shape[1] % 2: x = px - tx / 2.0 else: x = px - tx / 2.0 + self.spacing[0] / 2.0 y = py - + if self.mapper: x += sx / 2.0 z += sz / 2.0 - + elif self.orientation == 'SAGITAL': # height shape is odd if self.points.shape[1] % 2: y = py - ty / 2.0 else: y = py - ty / 2.0 + self.spacing[1] / 2.0 - + if self.points.shape[0] % 2: z = pz - tz / 2.0 else: z = pz - tz / 2.0 + self.spacing[2] / 2.0 x = px - + if self.mapper: y += sy / 2.0 z += sz / 2.0 - + else: if self.points.shape[0] % 2: y = py - ty / 2.0 else: y = py - ty / 2.0 + self.spacing[1] / 2.0 - + if self.points.shape[1] % 2: x = px - tx / 2.0 else: x = px - tx / 2.0 + self.spacing[0] / 2.0 z = pz - + if self.mapper: x += sx / 2.0 y += sy / 2.0 - + self.actor.SetPosition(x, y, z) + - def SetSpacing(self, spacing): + + + def SetSpacing(self, spacing: Tuple[float, float, float]) -> None: self.spacing = spacing self._build_actor() self._calculate_area_pixels() - def Show(self, value=1): + def Show(self, value: int = 1) -> None: if value: self.actor.VisibilityOn() else: self.actor.VisibilityOff() - def GetPixels(self): + def GetPixels(self) -> List[List[int]]: return self.points - def _build_actor(self): + def _build_actor(self) -> None: pass - def _calculate_area_pixels(self): + def _calculate_area_pixels(self) -> None: pass - def _set_colour(self, imagedata, colour): + def _set_colour(self, imagedata: vtkImageData, colour: Tuple[float, float, float, float]) -> vtkImageData: scalar_range = int(imagedata.GetScalarRange()[1]) r, g, b = colour[:3] @@ -244,18 +250,22 @@ def _set_colour(self, imagedata, colour): return img_colours_mask.GetOutput() + class CursorCircle(CursorBase): - # TODO: Think and try to change this class to an actor + # TODO: Think and try to change this class to an actor # CursorCircleActor(vtkActor) def __init__(self): self.radius = 15.0 + + def __init__(self) -> None: + self.radius: float = 15.0 super(CursorCircle, self).__init__() - def _build_actor(self): + def _build_actor(self) -> None: """ Function to plot the circle """ - r = self.radius + r: float = self.radius if self.unit == "µm": r /= 1000.0 if self.unit == "px": @@ -263,12 +273,12 @@ def _build_actor(self): else: sx, sy, sz = self.spacing if self.orientation == 'AXIAL': - xi = math.floor(-r/sx) - xf = math.ceil(r/sx) + 1 - yi = math.floor(-r/sy) - yf = math.ceil(r/sy) + 1 - zi = 0 - zf = 1 + xi: int = math.floor(-r/sx) + xf: int = math.ceil(r/sx) + 1 + yi: int = math.floor(-r/sy) + yf: int = math.ceil(r/sy) + 1 + zi: int = 0 + zf: int = 1 elif self.orientation == 'CORONAL': xi = math.floor(-r/sx) xf = math.ceil(r/sx) + 1 @@ -286,65 +296,64 @@ def _build_actor(self): z,y,x = numpy.ogrid[zi:zf,yi:yf, xi:xf] - circle_m = (z*sz)**2 + (y*sy)**2 + (x*sx)**2 <= r**2 - circle_i = to_vtk(circle_m.astype('uint8'), + circle_m: numpy.ndarray = (z*sz)**2 + (y*sy)**2 + (x*sx)**2 <= r**2 + circle_i: vtkImageData = to_vtk(circle_m.astype('uint8'), self.spacing, 0, self.orientation) - circle_ci = self._set_colour(circle_i, self.colour) + circle_ci: vtkImageData = self._set_colour(circle_i, self.colour) if self.mapper is None: + self.actor: vtkImageActor = vtkImageActor() self.actor.SetInputData(circle_ci) self.actor.InterpolateOff() self.actor.PickableOff() self.actor.SetDisplayExtent(circle_ci.GetExtent()) else: + self.mapper: vtkImageSliceMapper = vtkImageSliceMapper() self.mapper.SetInputData(circle_ci) self.mapper.BorderOn() self.mapper.SetOrientation(ORIENTATION[self.orientation]) - def _calculate_area_pixels(self): + def _calculate_area_pixels(self) -> None: """ Return the cursor's pixels. """ - r = self.radius + r: float = self.radius if self.unit == "µm": r /= 1000.0 if self.unit == "px": sx, sy, sz = 1.0, 1.0, 1.0 else: if self.orientation == 'AXIAL': - sx = self.spacing[0] - sy = self.spacing[1] + sx, sy, sz = self.spacing[0], self.spacing[1], self.spacing[2] elif self.orientation == 'CORONAL': - sx = self.spacing[0] - sy = self.spacing[2] + sx, sy, sz = self.spacing[0], self.spacing[2], self.spacing[1] elif self.orientation == 'SAGITAL': - sx = self.spacing[1] - sy = self.spacing[2] - - xi = math.floor(-r/sx) - xf = math.ceil(r/sx) + 1 - yi = math.floor(-r/sy) - yf = math.ceil(r/sy) + 1 - + sx, sy, sz = self.spacing[1], self.spacing[2], self.spacing[0] + + xi: int = math.floor(-r/sx) + xf: int = math.ceil(r/sx) + 1 + yi: int = math.floor(-r/sy) + yf: int = math.ceil(r/sy) + 1 + y,x = numpy.ogrid[yi:yf, xi:xf] - - index = (y*sy)**2 + (x*sx)**2 <= r**2 - self.points = index + + index: numpy.ndarray = (y*sy)**2 + (x*sx)**2 <= r**2 + self.points: numpy.ndarray = index + class CursorRectangle(CursorBase): - def __init__(self): - self.radius = 15.0 + def __init__(self) -> None: + self.radius: float = 15.0 super(CursorRectangle, self).__init__() - - def _build_actor(self): + def _build_actor(self) -> None: """ Function to plot the Retangle """ print("Building rectangle cursor", self.orientation) - r = self.radius + r: float = self.radius if self.unit == "µm": r /= 1000.0 if self.unit == "px": @@ -352,9 +361,9 @@ def _build_actor(self): else: sx, sy, sz = self.spacing if self.orientation == 'AXIAL': - x = int(math.floor(2*r/sx)) - y = int(math.floor(2*r/sy)) - z = 1 + x: int = int(math.floor(2*r/sx)) + y: int = int(math.floor(2*r/sy)) + z: int = 1 elif self.orientation == 'CORONAL': x = int(math.floor(r/sx)) y = 1 @@ -364,9 +373,9 @@ def _build_actor(self): y = int(math.floor(r/sy)) z = int(math.floor(r/sz)) - rectangle_m = numpy.ones((z, y, x), dtype='uint8') - rectangle_i = to_vtk(rectangle_m, self.spacing, 0, self.orientation) - rectangle_ci = self._set_colour(rectangle_i, self.colour) + rectangle_m: numpy.ndarray = numpy.ones((z, y, x), dtype='uint8') + rectangle_i: vtkImageData = to_vtk(rectangle_m, self.spacing, 0, self.orientation) + rectangle_ci:vtkImageData = self._set_colour(rectangle_i, self.colour) if self.mapper is None: self.actor.SetInputData(rectangle_ci) @@ -378,8 +387,8 @@ def _build_actor(self): self.mapper.BorderOn() self.mapper.SetOrientation(ORIENTATION[self.orientation]) - def _calculate_area_pixels(self): - r = self.radius + def _calculate_area_pixels(self) -> None: + r: float = self.radius if self.unit == "µm": r /= 1000.0 if self.unit == "px": @@ -387,8 +396,8 @@ def _calculate_area_pixels(self): else: sx, sy, sz = self.spacing if self.orientation == 'AXIAL': - x = int(math.floor(2*r/sx)) - y = int(math.floor(2*r/sy)) + x: int = int(math.floor(2*r/sx)) + y: int = int(math.floor(2*r/sy)) elif self.orientation == 'CORONAL': x = int(math.floor(r/sx)) y = int(math.floor(r/sz)) @@ -396,4 +405,5 @@ def _calculate_area_pixels(self): x = int(math.floor(r/sy)) y = int(math.floor(r/sz)) - self.points = numpy.ones((y, x), dtype='bool') + self.points: numpy.ndarray = numpy.ones((y, x), dtype='bool') + diff --git a/invesalius/data/e_field.py b/invesalius/data/e_field.py index aab3f4518..6202fcee1 100644 --- a/invesalius/data/e_field.py +++ b/invesalius/data/e_field.py @@ -1,12 +1,12 @@ import queue import threading import time +from typing import Any, List, Union import numpy as np -from vtkmodules.vtkCommonCore import ( - vtkIdList) +from vtkmodules.vtkCommonCore import vtkIdList -def Get_coil_position(m_img): +def Get_coil_position(m_img: np.ndarray) -> list: # coil position cp : the center point at the bottom of the coil casing, # corresponds to the origin of the coil template. # coil normal cn: outer normal of the coil, i.e. away from the head @@ -15,44 +15,45 @@ def Get_coil_position(m_img): # % rotation matrix for the coil coordinates # T = [ct1;ct2;cn]; - m_img_flip = m_img.copy() + m_img_flip: np.ndarray = m_img.copy() m_img_flip[1, -1] = -m_img_flip[1, -1] - cp = m_img_flip[:-1, -1] # coil center - cp = cp * 0.001 # convert to meters - cp = cp.tolist() + cp: list = m_img_flip[:-1, -1].tolist() # coil center + cp = [x * 0.001 for x in cp] # convert to meters - ct1 = m_img_flip[:3, 1] # is from posterior to anterior direction of the coil - ct2 = m_img_flip[:3, 0] # is from left to right direction of the coil - coil_dir = m_img_flip[:-1, 0] - coil_face = m_img_flip[:-1, 1] - cn = np.cross(coil_dir, coil_face) - T_rot = np.append(ct1, ct2, axis=0) + ct1: np.ndarray = m_img_flip[:3, 1] # is from posterior to anterior direction of the coil + ct2: np.ndarray = m_img_flip[:3, 0] # is from left to right direction of the coil + coil_dir: np.ndarray = m_img_flip[:-1, 0] + coil_face: np.ndarray = m_img_flip[:-1, 1] + cn: np.ndarray = np.cross(coil_dir, coil_face) + T_rot: np.ndarray = np.append(ct1, ct2, axis=0) T_rot = np.append(T_rot, cn, axis=0) * 0.001 # append and convert to meters T_rot = T_rot.tolist() # to list - return [T_rot,cp] + return [T_rot, cp] + + class Visualize_E_field_Thread(threading.Thread): - def __init__(self, queues, event, sle, neuronavigation_api, debug_efield_enorm): + def __init__(self, queues: List[queue.Queue], event: threading.Event, sle: float, neuronavigation_api: Any, debug_efield_enorm: Union[np.ndarray, None]) -> None: threading.Thread.__init__(self, name='Visualize_E_field_Thread') #self.inp = inp #list of inputs - self.efield_queue = queues[0] - self.e_field_norms_queue = queues[1] - self.e_field_IDs_queue = queues[2] + self.efield_queue: queue.Queue = queues[0] + self.e_field_norms_queue: queue.Queue = queues[1] + self.e_field_IDs_queue: queue.Queue = queues[2] #self.tracts_queue = queues[1] # self.visualization_queue = visualization_queue - self.event = event - self.sle = sle - self.neuronavigation_api = neuronavigation_api - self.ID_list = vtkIdList() - self.coord_old = [] + self.event: threading.Event = event + self.sle: float = sle + self.neuronavigation_api: Any = neuronavigation_api + self.ID_list: vtkIdList = vtkIdList() + self.coord_old: List[float] = [] if isinstance(debug_efield_enorm, np.ndarray): - self.enorm_debug = debug_efield_enorm - self.debug = True + self.enorm_debug: np.ndarray = debug_efield_enorm + self.debug: bool = True else: - self.debug = False + self.debug: bool = False - def run(self): + def run(self) -> None: while not self.event.is_set(): if not self.e_field_IDs_queue.empty(): @@ -73,9 +74,9 @@ def run(self): if np.all(self.coord_old != coord): [T_rot, cp] = Get_coil_position(m_img) if self.debug: - enorm = self.enorm_debug + enorm: np.ndarray = self.enorm_debug else: - enorm = self.neuronavigation_api.update_efield(position=cp, orientation=coord[3:], T_rot=T_rot) + enorm: np.ndarray = self.neuronavigation_api.update_efield(position=cp, orientation=coord[3:], T_rot=T_rot) try: self.e_field_norms_queue.put_nowait((enorm)) except queue.Full: @@ -84,3 +85,4 @@ def run(self): self.coord_old = coord time.sleep(self.sle) + diff --git a/invesalius/data/editor.py b/invesalius/data/editor.py index f62612880..51b698c9e 100644 --- a/invesalius/data/editor.py +++ b/invesalius/data/editor.py @@ -18,16 +18,16 @@ #-------------------------------------------------------------------------- from invesalius.pubsub import pub as Publisher +from typing import Tuple from vtkmodules.vtkCommonCore import vtkLookupTable from vtkmodules.vtkImagingCore import vtkImageBlend, vtkImageMapToColors from vtkmodules.vtkInteractionStyle import vtkInteractorStyleImage from vtkmodules.vtkRenderingCore import vtkImageActor, vtkCellPicker, vtkImageMapper - -AXIAL = 2 -CORONAL = 1 -SAGITAL = 0 +AXIAL: int = 2 +CORONAL: int = 1 +SAGITAL: int = 0 class Editor: """ @@ -40,77 +40,78 @@ class Editor: editor.Render() """ - def __init__(self): + def __init__(self) -> None: - self.interactor = None - self.image_original = None - self.image_threshold = None - self.render = None + self.interactor: vtkRenderWindowInteractor = None + self.image_original: vtkImageData = None + self.image_threshold: vtkImageData = None + self.render: vtkRenderer = None - self.lut = vtkLookupTable() - self.lut_original = vtkLookupTable() - self.image_color = vtkImageMapToColors() - self.blend = blend = vtkImageBlend() - self.map = map = vtkImageMapper() + self.lut: vtkLookupTable = vtkLookupTable() + self.lut_original: vtkLookupTable = vtkLookupTable() + self.image_color: vtkImageMapToColors = vtkImageMapToColors() + self.blend: vtkImageBlend = vtkImageBlend() + self.map: vtkImageMapper = vtkImageMapper() - self.actor = actor = vtkImageActor() - self.actor2 = actor2 = vtkImageActor() - self.actor3 = actor3 = vtkImageActor() + self.actor: vtkImageActor = vtkImageActor() + self.actor2: vtkImageActor = vtkImageActor() + self.actor3: vtkImageActor = vtkImageActor() - self.image_color_o = vtkImageMapToColors() + self.image_color_o: vtkImageMapToColors = vtkImageMapToColors() - self.operation_type = 0 - self.w = None + self.operation_type: int = 0 + self.w: Tuple[int, int] = None - self.slice = 0 - self.clicked = 0 - self.orientation = AXIAL + self.slice: int = 0 + self.clicked: int = 0 + self.orientation: int = AXIAL - self.w = (200, 1200) + self.w: Tuple[int, int] = (200, 1200) #self.plane_widget_x = vtkImagePlaneWidget() #self.actor.PickableOff() - - def SetInteractor(self, interactor): - - self.interactor = interactor - self.render = interactor.GetRenderWindow().GetRenderers().GetFirstRenderer() - istyle = vtkInteractorStyleImage() - istyle.SetInteractor(interactor) - istyle.AutoAdjustCameraClippingRangeOn() - interactor.SetInteractorStyle(istyle) - - istyle.AddObserver("LeftButtonPressEvent", self.Click) - istyle.AddObserver("LeftButtonReleaseEvent", self.Release) - istyle.AddObserver("MouseMoveEvent",self.Moved) - - pick = self.pick = vtkCellPicker() - - def SetActor(self, actor): + + def SetInteractor(self, interactor: vtkRenderWindowInteractor) -> None: + + self.interactor = interactor + self.render = interactor.GetRenderWindow().GetRenderers().GetFirstRenderer() + + istyle: vtkInteractorStyleImage = vtkInteractorStyleImage() + istyle.SetInteractor(interactor) + istyle.AutoAdjustCameraClippingRangeOn() + interactor.SetInteractorStyle(istyle) + + istyle.AddObserver("LeftButtonPressEvent", self.Click) + istyle.AddObserver("LeftButtonReleaseEvent", self.Release) + istyle.AddObserver("MouseMoveEvent",self.Moved) + + pick = self.pick = vtkCellPicker() + + def SetActor(self, actor: vtkImageActor) -> None: self.actor = actor - def SetImage(self, image): + def SetImage(self, image: vtkImageData) -> None: self.image = image - def SetCursor(self, cursor): + def SetCursor(self, cursor: Cursor) -> None: self.cursor = cursor self.cursor.CursorCircle(self.render) - def SetOrientation(self, orientation): + def SetOrientation(self, orientation: int) -> None: self.orientation = orientation - def Click(self, obj, evt): + def Click(self, obj: Any, evt: Any) -> None: self.clicked = 1 - def Release(self, obj, evt): + def Release(self, obj: Any, evt: Any) -> None: self.clicked = 0 - def Moved(self, obj, evt): - pos = self.interactor.GetEventPosition() - wx = pos[0] #- last[0] - wy = pos[1] #- last[1] + def Moved(self, obj: Any, evt: Any) -> None: + pos: Tuple[int, int] = self.interactor.GetEventPosition() + wx: int = pos[0] #- last[0] + wy: int = pos[1] #- last[1] self.pick.Pick(wx, wy, 0, self.render) x,y,z = self.pick.GetPickPosition() @@ -129,14 +130,15 @@ def Moved(self, obj, evt): obj.OnMouseMove() self.interactor.Render() + - def From3dToImagePixel(self, mPos, pPos): + def From3dToImagePixel(self, mPos: Tuple[int, int], pPos: Tuple[float, float, float]) -> Tuple[float, float, float]: """ mPos - The mouse position in the screen position. pPos - the pick position in the 3d world """ x, y, z = pPos - bounds = self.actor.GetBounds() + bounds: Tuple[float, float, float, float, float, float] = self.actor.GetBounds() #c = vtkCoordinate() #c.SetCoordinateSystemToWorld() @@ -146,17 +148,23 @@ def From3dToImagePixel(self, mPos, pPos): #c.SetValue(bounds[1::2]) #xf, yf = c.GetComputedViewportValue(self.render) - xi, xf, yi, yf, zi, zf = bounds + xi: float = bounds[0] + xf: float = bounds[1] + yi: float = bounds[2] + yf: float = bounds[3] + zi: float = bounds[4] + zf: float = bounds[5] + #c.SetValue(x, y, z) #wx, wy = c.GetComputedViewportValue(self.render) - wx = x - xi - wy = y - yi - wz = z - zi + wx: float = x - xi + wy: float = y - yi + wz: float = z - zi - dx = xf - xi - dy = yf - yi - dz = zf - zi + dx: float = xf - xi + dy: float = yf - yi + dz: float = zf - zi try: wx = (wx * self.image.GetDimensions()[0]) / dx @@ -173,53 +181,63 @@ def From3dToImagePixel(self, mPos, pPos): return wx, wy, wz - def SetInput(self, image_original, image_threshold): + + def __init__(self): + self.image_original: vtkImageData = None + self.image_threshold: vtkImageData = None + self.slice: int = 0 + self.map = None + self.interactor = None + self.image: vtkImageData = None + self.w: Tuple[float, float] = (0.0, 0.0) + + def SetInput(self, image_original: vtkImageData, image_threshold: vtkImageData) -> None: self.image_original = image_original self.image_threshold = image_threshold - - def SetSlice(self, a): + + def SetSlice(self, a: int) -> None: self.slice = a - def ChangeShowSlice(self, value): + def ChangeShowSlice(self, value: int) -> None: self.map.SetZSlice(value) self.interactor.Render() - def ErasePixel(self, x, y, z): + def ErasePixel(self, x: int, y: int, z: int) -> None: """ Deletes pixel, it is necessary to pass x, y and z. """ self.image.SetScalarComponentFromDouble(x, y, z, 0, 0) self.image.Update() - def FillPixel(self, x, y, z, colour = 3200): + def FillPixel(self, x: int, y: int, z: int, colour: int = 3200) -> None: """ Fill pixel, it is necessary to pass x, y and z """ self.image.SetScalarComponentFromDouble(x, y, z, 0, colour) - def PixelThresholdLevel(self, x, y, z): + def PixelThresholdLevel(self, x: int, y: int, z: int) -> None: """ Erase or Fill with Threshold level """ - pixel_colour = self.image.GetScalarComponentAsDouble(x, y, z, 0) + pixel_colour: float = self.image.GetScalarComponentAsDouble(x, y, z, 0) - thres_i = self.w[0] - thres_f = self.w[1] + thres_i: float = self.w[0] + thres_f: float = self.w[1] if (pixel_colour >= thres_i) and (pixel_colour <= thres_f): if (pixel_colour <= 0): self.FillPixel(x, y, z, 1) else: - self.FillPixel(x, y, z, pixel_colour) + self.FillPixel(x, y, z, int(pixel_colour)) else: self.ErasePixel(x, y, z) - - def DoOperation(self, xc, yc, zc): + + def DoOperation(self, xc: int, yc: int, zc: int) -> None: """ This method scans the circle line by line. Extracted equation. @@ -252,10 +270,9 @@ def DoOperation(self, xc, yc, zc): except: pass #if extent[0] <= k+xc <= extent[1] \ - #and extent[2] <= yi+yc <=extent[3]] + #and extent[2] <= yi+yc <=extent[3]] - def SetOperationType(self, operation_type = 0, - w = (100,1200)): + def SetOperationType(self, operation_type: int = 0, w: Tuple[float, float] = (100,1200)) -> None: """ Set Operation Type 0 -> Remove diff --git a/invesalius/data/geometry.py b/invesalius/data/geometry.py index 45a7403ea..866b163e9 100644 --- a/invesalius/data/geometry.py +++ b/invesalius/data/geometry.py @@ -19,6 +19,7 @@ # -------------------------------------------------------------------------- import math +from typing import List, Tuple, Union, Optional, Dict, Any import numpy as np from vtkmodules.vtkRenderingCore import vtkCoordinate @@ -36,45 +37,45 @@ class Box(metaclass=utils.Singleton): """ def __init__(self): - self.xi = None - self.xf = None + self.xi = None # type: float + self.xf = None # type: float - self.yi = None - self.yf = None + self.yi = None # type: float + self.yf = None # type: float - self.zi = None - self.zf = None + self.zi = None # type: float + self.zf = None # type: float - self.size_x = None - self.size_y = None - self.size_z = None + self.size_x = None # type: float + self.size_y = None # type: float + self.size_z = None # type: float - self.sagital = {} - self.coronal = {} - self.axial = {} + self.sagital = {} # type: dict + self.coronal = {} # type: dict + self.axial = {} # type: dict - self.xs = None - self.ys = None - self.zs = None + self.xs = None # type: float + self.ys = None # type: float + self.zs = None # type: float - self.first_run = True + self.first_run = True # type: bool - def SetX(self, i, f): + def SetX(self, i: float, f: float) -> None: self.xi = i self.xf = f self.size_x = f - def SetY(self, i, f): + def SetY(self, i: float, f: float) -> None: self.yi = i self.yf = f self.size_y = f - def SetZ(self, i, f): + def SetZ(self, i: float, f: float) -> None: self.zi = i self.zf = f self.size_z = f - def SetSpacing(self, x, y, z): + def SetSpacing(self, x: float, y: float, z: float) -> None: self.xs = x self.ys = y self.zs = z @@ -95,79 +96,81 @@ def SetSpacing(self, x, y, z): if self.first_run: self.first_run = False - def MakeMatrix(self): + + def MakeMatrix(self) -> None: """ Update values in a matrix to each orientation. """ - + self.sagital[const.SAGITAL_LEFT] = [ [self.xi, self.yi - (self.ys / 2), self.zi], [self.xi, self.yi - (self.ys / 2), self.zf], ] - + self.sagital[const.SAGITAL_RIGHT] = [ [self.xi, self.yf + (self.ys / 2), self.zi], [self.xi, self.yf + (self.ys / 2), self.zf], ] - + self.sagital[const.SAGITAL_BOTTOM] = [ [self.xi, self.yi, self.zi - (self.zs / 2)], [self.xi, self.yf, self.zi - (self.zs / 2)], ] - + self.sagital[const.SAGITAL_UPPER] = [ [self.xi, self.yi, self.zf + (self.zs / 2)], [self.xi, self.yf, self.zf + (self.zs / 2)], ] - + self.coronal[const.CORONAL_BOTTOM] = [ [self.xi, self.yi, self.zi - (self.zs / 2)], [self.xf, self.yf, self.zi - (self.zs / 2)], ] - + self.coronal[const.CORONAL_UPPER] = [ [self.xi, self.yi, self.zf + (self.zs / 2)], [self.xf, self.yf, self.zf + (self.zs / 2)], ] - + self.coronal[const.CORONAL_LEFT] = [ [self.xi - (self.xs / 2), self.yi, self.zi], [self.xi - (self.xs / 2), self.yf, self.zf], ] - + self.coronal[const.CORONAL_RIGHT] = [ [self.xf + (self.xs / 2), self.yi, self.zi], [self.xf + (self.xs / 2), self.yf, self.zf], ] - + self.axial[const.AXIAL_BOTTOM] = [ [self.xi, self.yi - (self.ys / 2), self.zi], [self.xf, self.yi - (self.ys / 2), self.zf], ] - + self.axial[const.AXIAL_UPPER] = [ [self.xi, self.yf + (self.ys / 2), self.zi], [self.xf, self.yf + (self.ys / 2), self.zf], ] - + self.axial[const.AXIAL_LEFT] = [ [self.xi - (self.xs / 2), self.yi, self.zi], [self.xi - (self.xs / 2), self.yf, self.zf], ] - + self.axial[const.AXIAL_RIGHT] = [ [self.xf + (self.xs / 2), self.yi, self.zi], [self.xf + (self.xs / 2), self.yf, self.zf], ] + + Publisher.sendMessage("Update crop limits into gui", limits=self.GetLimits()) # type: ignore + - Publisher.sendMessage("Update crop limits into gui", limits=self.GetLimits()) - - def GetLimits(self): + def GetLimits(self) -> List[int]: """ Return the bounding box limits (initial and final) in x, y and z. """ - - limits = [ + + limits: List[int] = [ int(self.xi / self.xs), int(self.xf / self.xs), int(self.yi / self.ys), @@ -175,105 +178,110 @@ def GetLimits(self): int(self.zi / self.zs), int(self.zf / self.zs), ] - + return limits + + - def UpdatePositionBySideBox(self, pc, axis, position): + def UpdatePositionBySideBox(self, pc: List[float], axis: str, position: str) -> None: """ Checks the coordinates are in any side of box and update it. Is necessary to move limits of box. """ - + if axis == "AXIAL": if position == const.AXIAL_UPPER: if pc[1] > self.yi and pc[1] > 0 and pc[1] <= self.size_y: self.yf = pc[1] - + if position == const.AXIAL_BOTTOM: if pc[1] < self.yf and pc[1] >= 0: self.yi = pc[1] - + if position == const.AXIAL_LEFT: if pc[0] < self.xf and pc[0] >= 0: self.xi = pc[0] - + if position == const.AXIAL_RIGHT: if pc[0] > self.xi and pc[0] <= self.size_x: self.xf = pc[0] - + if axis == "SAGITAL": if position == const.SAGITAL_UPPER: if pc[2] > self.zi and pc[2] > 0 and pc[2] <= self.size_z: self.zf = pc[2] - + if position == const.SAGITAL_BOTTOM: if pc[2] < self.zf and pc[2] >= 0: self.zi = pc[2] - + if position == const.SAGITAL_LEFT: if pc[1] < self.yf and pc[1] >= 0: self.yi = pc[1] - + if position == const.SAGITAL_RIGHT: if pc[1] > self.yi and pc[1] <= self.size_y: self.yf = pc[1] - + if axis == "CORONAL": if position == const.CORONAL_UPPER: if pc[2] > self.zi and pc[2] > 0 and pc[2] <= self.size_z: self.zf = pc[2] - + if position == const.CORONAL_BOTTOM: if pc[2] < self.zf and pc[2] >= 0: self.zi = pc[2] - + if position == const.CORONAL_LEFT: if pc[0] < self.xf and pc[0] >= 0: self.xi = pc[0] - + if position == const.CORONAL_RIGHT: if pc[0] > self.yi and pc[0] <= self.size_y: self.xf = pc[0] - + self.MakeMatrix() - - def UpdatePositionByInsideBox(self, pc, axis): + + + def UpdatePositionByInsideBox(self, pc: List[float], axis: str) -> None: """ Checks the coordinates are inside the box and update it. Is necessary to move box in pan event. """ - + if axis == "AXIAL": - + if self.yf + pc[1] <= self.size_y and self.yi + pc[1] >= 0: self.yf = self.yf + pc[1] self.yi = self.yi + pc[1] - + if self.xf + pc[0] <= self.size_x and self.xi + pc[0] >= 0: self.xf = self.xf + pc[0] self.xi = self.xi + pc[0] - + if axis == "SAGITAL": - + if self.yf + pc[1] <= self.size_y and self.yi + pc[1] >= 0: self.yf = self.yf + pc[1] self.yi = self.yi + pc[1] - + if self.zf + pc[2] <= self.size_z and self.zi + pc[2] >= 0: self.zf = self.zf + pc[2] self.zi = self.zi + pc[2] - + if axis == "CORONAL": - + if self.xf + pc[0] <= self.size_x and self.xi + pc[0] >= 0: self.xf = self.xf + pc[0] self.xi = self.xi + pc[0] - + if self.zf + pc[2] <= self.size_z and self.zi + pc[2] >= 0: self.zf = self.zf + pc[2] self.zi = self.zi + pc[2] - + self.MakeMatrix() + + class DrawCrop2DRetangle: @@ -283,31 +291,36 @@ class DrawCrop2DRetangle: anatomical orientation (axial, sagital or coronal). """ - def __init__(self): - self.viewer = None - self.points_in_display = {} - self.box = None - self.mouse_pressed = False - self.canvas = None - self.status_move = None - self.crop_pan = None - self.last_x = 0 - self.last_y = 0 - self.last_z = 0 - self.layer = 0 + def __init__(self) -> None: + self.viewer: Optional[Viewer] = None + self.points_in_display: Dict[str, List[float]] = {} + self.box: Optional[List[int]] = None + self.mouse_pressed: bool = False + self.canvas: Optional[Canvas] = None + self.status_move: Optional[str] = None + self.crop_pan: Optional[List[float]] = None + self.last_x: int = 0 + self.last_y: int = 0 + self.last_z: int = 0 + self.layer: int = 0 + + - def MouseMove(self, x, y): + + def MouseMove(self, x: int, y: int) -> None: self.MouseInLine(x, y) x_pos_sl_, y_pos_sl_ = self.viewer.get_slice_pixel_coord_by_screen_pos(x, y) - slice_spacing = self.viewer.slice_.spacing + slice_spacing: Tuple[float, float, float] = self.viewer.slice_.spacing xs, ys, zs = slice_spacing x_pos_sl = x_pos_sl_ * xs y_pos_sl = y_pos_sl_ * ys - x, y, z = self.viewer.get_voxel_coord_by_screen_pos(x, y) + x: int + y: int + z: int = self.viewer.get_voxel_coord_by_screen_pos(x, y) if self.viewer.orientation == "AXIAL": @@ -363,9 +376,9 @@ def MouseMove(self, x, y): (x * xs, y * ys, z * zs), self.viewer.orientation, self.status_move ) - nv_x = x - self.last_x - nv_y = y - self.last_y - nv_z = z - self.last_z + nv_x: int = x - self.last_x + nv_y: int = y - self.last_y + nv_z: int = z - self.last_z if self.mouse_pressed and self.crop_pan: self.box.UpdatePositionByInsideBox( @@ -377,28 +390,29 @@ def MouseMove(self, x, y): self.last_z = z Publisher.sendMessage("Redraw canvas") - - def ReleaseLeft(self): + + + def ReleaseLeft(self) -> None: self.status_move = None - def LeftPressed(self, x, y): + def LeftPressed(self, x: int, y: int) -> None: self.mouse_pressed = True - def MouseInLine(self, x, y): + def MouseInLine(self, x: int, y: int) -> None: x_pos_sl_, y_pos_sl_ = self.viewer.get_slice_pixel_coord_by_screen_pos(x, y) - slice_spacing = self.viewer.slice_.spacing + slice_spacing: Tuple[float, float, float] = self.viewer.slice_.spacing xs, ys, zs = slice_spacing if self.viewer.orientation == "AXIAL": - x_pos_sl = x_pos_sl_ * xs - y_pos_sl = y_pos_sl_ * ys + x_pos_sl: float = x_pos_sl_ * xs + y_pos_sl: float = y_pos_sl_ * ys for k, p in self.box.axial.items(): - p0 = p[0] - p1 = p[1] + p0: Tuple[float, float] = p[0] + p1: Tuple[float, float] = p[1] - dist = self.distance_from_point_line( + dist: float = self.distance_from_point_line( (p0[0], p0[1]), (p1[0], p1[1]), (x_pos_sl, y_pos_sl) ) @@ -420,16 +434,17 @@ def MouseInLine(self, x, y): if not (self.mouse_pressed) and k != self.status_move: self.status_move = None + if self.viewer.orientation == "CORONAL": - x_pos_sl = x_pos_sl_ * xs - y_pos_sl = y_pos_sl_ * zs + x_pos_sl: float = x_pos_sl_ * xs + y_pos_sl: float = x_pos_sl_ * zs for k, p in self.box.coronal.items(): - p0 = p[0] - p1 = p[1] + p0: Tuple[float, float, float] = p[0] + p1: Tuple[float, float, float] = p[1] - dist = self.distance_from_point_line( + dist: float = self.distance_from_point_line( (p0[0], p0[2]), (p1[0], p1[2]), (x_pos_sl, y_pos_sl) ) if dist <= 2: @@ -452,14 +467,14 @@ def MouseInLine(self, x, y): self.status_move = None if self.viewer.orientation == "SAGITAL": - x_pos_sl = x_pos_sl_ * ys - y_pos_sl = y_pos_sl_ * zs + x_pos_sl: float = x_pos_sl_ * ys + y_pos_sl: float = x_pos_sl_ * zs for k, p in self.box.sagital.items(): - p0 = p[0] - p1 = p[1] + p0: Tuple[float, float, float] = p[0] + p1: Tuple[float, float, float] = p[1] - dist = self.distance_from_point_line( + dist: float = self.distance_from_point_line( (p0[1], p0[2]), (p1[1], p1[2]), (x_pos_sl, y_pos_sl) ) @@ -482,7 +497,8 @@ def MouseInLine(self, x, y): if not (self.mouse_pressed) and k != self.status_move: self.status_move = None - def draw_to_canvas(self, gc, canvas): + + def draw_to_canvas(self, gc: wx.GraphicsContext, canvas: Any) -> None: """ Draws to an wx.GraphicsContext. @@ -493,7 +509,7 @@ def draw_to_canvas(self, gc, canvas): self.canvas = canvas self.UpdateValues(canvas) - def point_into_box(self, p1, p2, pc, axis): + def point_into_box(self, p1: Tuple[float, float, float], p2: Tuple[float, float, float], pc: Tuple[float, float, float], axis: str) -> bool: if axis == "AXIAL": if ( @@ -527,8 +543,10 @@ def point_into_box(self, p1, p2, pc, axis): return True else: return False - - def point_between_line(self, p1, p2, pc, axis): + + + + def point_between_line(self, p1: Tuple[float, float, float], p2: Tuple[float, float, float], pc: Tuple[float, float, float], axis: str) -> bool: """ Checks whether a point is in the line limits """ @@ -555,7 +573,7 @@ def point_between_line(self, p1, p2, pc, axis): else: return False - def distance_from_point_line(self, p1, p2, pc): + def distance_from_point_line(self, p1: Tuple[float, float, float], p2: Tuple[float, float, float], pc: Tuple[float, float, float]) -> float: """ Calculate the distance from point pc to a line formed by p1 and p2. """ @@ -576,15 +594,14 @@ def distance_from_point_line(self, p1, p2, pc): distance = math.sin(theta) * len_A return distance - def Coord3DtoDisplay(self, x, y, z, canvas): - + def Coord3DtoDisplay(self, x: float, y: float, z: float, canvas: Any) -> Tuple[float, float]: coord = vtkCoordinate() coord.SetValue(x, y, z) cx, cy = coord.GetComputedDisplayValue(canvas.evt_renderer) return (cx, cy) - def MakeBox(self): + def MakeBox(self) -> None: slice_size = self.viewer.slice_.matrix.shape zf, yf, xf = slice_size[0] - 1, slice_size[1] - 1, slice_size[2] - 1 @@ -600,8 +617,8 @@ def MakeBox(self): box.SetZ(0, zf) box.SetSpacing(xs, ys, zs) box.MakeMatrix() - - def UpdateValues(self, canvas): + + def UpdateValues(self, canvas: Any) -> None: box = self.box slice_number = self.viewer.slice_data.number @@ -653,6 +670,7 @@ def UpdateValues(self, canvas): (s_cxi, s_cyi), (s_cxf, s_cyf), colour=(255, 255, 255, 255) ) - def SetViewer(self, viewer): + def SetViewer(self, viewer: Any) -> None: self.viewer = viewer self.MakeBox() + \ No newline at end of file diff --git a/invesalius/data/imagedata_utils.py b/invesalius/data/imagedata_utils.py index ff780bc63..c4e2e9931 100644 --- a/invesalius/data/imagedata_utils.py +++ b/invesalius/data/imagedata_utils.py @@ -20,6 +20,7 @@ import math import sys import tempfile +from typing import Tuple, Union,Optional,List import gdcm import imageio @@ -44,11 +45,11 @@ if sys.platform == "win32": try: import win32api - _has_win32api = True + _has_win32api: bool = True except ImportError: - _has_win32api = False + _has_win32api: bool = False else: - _has_win32api = False + _has_win32api: bool = False # TODO: Test cases which are originally in sagittal/coronal orientation # and have gantry @@ -75,9 +76,8 @@ def ResampleImage3D(imagedata, value): return resample.GetOutput() -def ResampleImage2D( - imagedata, px=None, py=None, resolution_percentage=None, update_progress=None -): + +def ResampleImage2D(imagedata: vtkImageResample, px: float = None, py: float = None, resolution_percentage: float = None, update_progress: callable = None) -> vtkImageResample: """ Resample vtkImageData matrix. """ @@ -117,7 +117,7 @@ def ResampleImage2D( return resample.GetOutput() -def resize_slice(im_array, resolution_percentage): +def resize_slice(im_array: numpy.ndarray, resolution_percentage: float) -> numpy.ndarray: """ Uses ndimage.zoom to resize a slice. @@ -129,7 +129,8 @@ def resize_slice(im_array, resolution_percentage): return out -def resize_image_array(image, resolution_percentage, as_mmap=False): + +def resize_image_array(image: np.ndarray, resolution_percentage: float, as_mmap: bool = False) -> np.ndarray: out = zoom(image, resolution_percentage, image.dtype, order=2) if as_mmap: fname = tempfile.mktemp(suffix="_resized") @@ -139,32 +140,32 @@ def resize_image_array(image, resolution_percentage, as_mmap=False): return out -def read_dcm_slice_as_np2(filename, resolution_percentage=1.0): +def read_dcm_slice_as_np2(filename: str, resolution_percentage: float = 1.0) -> np.ndarray: reader = gdcm.ImageReader() reader.SetFileName(filename) reader.Read() image = reader.GetImage() - output = converters.gdcm_to_numpy(image) + output = numpy_support.vtk_to_numpy(image.GetPointData().GetScalars()).reshape(image.GetDimensions()[::-1]) if resolution_percentage < 1.0: output = zoom(output, resolution_percentage) return output -def FixGantryTilt(matrix, spacing, tilt): +def FixGantryTilt(matrix: np.ndarray, spacing: tuple, tilt: float) -> None: """ Fix gantry tilt given a vtkImageData and the tilt value. Return new vtkImageData. """ - angle = numpy.radians(tilt) + angle = np.radians(tilt) spacing = spacing[0], spacing[1], spacing[2] - gntan = math.tan(angle) + gntan = np.tan(angle) for n, slice_ in enumerate(matrix): offset = gntan * n * spacing[2] matrix[n] = shift(slice_, (-offset / spacing[1], 0), cval=matrix.min()) -def BuildEditedImage(imagedata, points): +def BuildEditedImage(imagedata, points) -> vtkImageAppend: """ Editing the original image in accordance with the edit points in the editor, it is necessary to generate the @@ -218,10 +219,10 @@ def BuildEditedImage(imagedata, points): app.SetInput(1, gauss.GetOutput()) app.Update() - return app.GetOutput() + return app -def Export(imagedata, filename, bin=False): +def Export(imagedata, filename: str, bin: bool = False) -> None: writer = vtkXMLImageDataWriter() writer.SetFileName(filename) if bin: @@ -232,7 +233,7 @@ def Export(imagedata, filename, bin=False): # writer.Write() -def Import(filename): +def Import(filename: str) -> vtkXMLImageDataReader: reader = vtkXMLImageDataReader() reader.SetFileName(filename) # TODO: Check if the code bellow is necessary @@ -242,7 +243,7 @@ def Import(filename): return reader.GetOutput() -def View(imagedata): +def View(imagedata) -> None: viewer = vtkImageViewer() viewer.SetInput(imagedata) viewer.SetColorWindow(200) @@ -254,7 +255,7 @@ def View(imagedata): time.sleep(10) -def ExtractVOI(imagedata, xi, xf, yi, yf, zi, zf): +def ExtractVOI(imagedata, xi: int, xf: int, yi: int, yf: int, zi: int, zf: int) -> vtkExtractVOI: """ Cropping the vtkImagedata according with values. @@ -267,7 +268,9 @@ def ExtractVOI(imagedata, xi, xf, yi, yf, zi, zf): return voi.GetOutput() -def create_dicom_thumbnails(image, window=None, level=None): + + +def create_dicom_thumbnails(image: object, window: Optional[float] = None, level: Optional[float] = None) -> Union[str, List[str]]: pf = image.GetPixelFormat() np_image = converters.gdcm_to_numpy(image, pf.GetSamplesPerPixel() == 1) if window is None or level is None: @@ -299,16 +302,17 @@ def create_dicom_thumbnails(image, window=None, level=None): return thumbnail_path -def array2memmap(arr, filename=None): +def array2memmap(arr: np.ndarray, filename: Optional[str] = None) -> np.memmap: if filename is None: filename = tempfile.mktemp(prefix="inv3_", suffix=".dat") - matrix = numpy.memmap(filename, mode="w+", dtype=arr.dtype, shape=arr.shape) + matrix = np.memmap(filename, mode="w+", dtype=arr.dtype, shape=arr.shape) matrix[:] = arr[:] matrix.flush() return matrix -def bitmap2memmap(files, slice_size, orientation, spacing, resolution_percentage): + +def bitmap2memmap(files: List[str], slice_size: Tuple[int, int], orientation: str, spacing: Tuple[float, float], resolution_percentage: float) -> Tuple[np.memmap, str]: """ From a list of dicom files it creates memmap file in the temp folder and returns it and its related filename. @@ -351,7 +355,7 @@ def bitmap2memmap(files, slice_size, orientation, spacing, resolution_percentage ) if resolution_percentage == 1.0: - matrix = numpy.memmap(temp_file, mode="w+", dtype="int16", shape=shape) + matrix = np.memmap(temp_file, mode="w+", dtype="int16", shape=shape) cont = 0 max_scalar = None @@ -359,6 +363,8 @@ def bitmap2memmap(files, slice_size, orientation, spacing, resolution_percentage xy_shape = None first_resample_entry = False + + for n, f in enumerate(files): image_as_array = bitmap_reader.ReadBitmap(f) @@ -386,7 +392,7 @@ def bitmap2memmap(files, slice_size, orientation, spacing, resolution_percentage if not (first_resample_entry): shape = shape[0], yx_shape[0], yx_shape[1] - matrix = numpy.memmap(temp_file, mode="w+", dtype="int16", shape=shape) + matrix = np.memmap(temp_file, mode="w+", dtype="int16", shape=shape) first_resample_entry = True image = image_resized @@ -428,32 +434,33 @@ def bitmap2memmap(files, slice_size, orientation, spacing, resolution_percentage return matrix, scalar_range, temp_file -def dcm2memmap(files, slice_size, orientation, resolution_percentage): + +def dcm2memmap(files: List[str], slice_size: Tuple[int, int], orientation: str, resolution_percentage: float) -> Tuple[np.memmap, Tuple[int, int], str]: """ From a list of dicom files it creates memmap file in the temp folder and returns it and its related filename. """ if len(files) > 1: - message = _("Generating multiplanar visualization...") + message: str = _("Generating multiplanar visualization...") update_progress = vtk_utils.ShowProgress( len(files) - 1, dialog_type="ProgressDialog" ) - first_slice = read_dcm_slice_as_np2(files[0], resolution_percentage) - slice_size = first_slice.shape[::-1] + first_slice: np.ndarray = read_dcm_slice_as_np2(files[0], resolution_percentage) + slice_size: Tuple[int, int] = first_slice.shape[::-1] - temp_file = tempfile.mktemp() + temp_file: str = tempfile.mktemp() if orientation == "SAGITTAL": - shape = slice_size[0], slice_size[1], len(files) + shape: Tuple[int, int, int] = slice_size[0], slice_size[1], len(files) elif orientation == "CORONAL": - shape = slice_size[1], len(files), slice_size[0] + shape: Tuple[int, int, int] = slice_size[1], len(files), slice_size[0] else: - shape = len(files), slice_size[1], slice_size[0] + shape: Tuple[int, int, int] = len(files), slice_size[1], slice_size[0] - matrix = numpy.memmap(temp_file, mode="w+", dtype="int16", shape=shape) + matrix: np.memmap = np.memmap(temp_file, mode="w+", dtype="int16", shape=shape) for n, f in enumerate(files): - im_array = read_dcm_slice_as_np2(f, resolution_percentage)[::-1] + im_array: np.ndarray = read_dcm_slice_as_np2(f, resolution_percentage)[::-1] if orientation == "CORONAL": matrix[:, shape[1] - n - 1, :] = im_array @@ -468,12 +475,13 @@ def dcm2memmap(files, slice_size, orientation, resolution_percentage): update_progress(n, message) matrix.flush() - scalar_range = matrix.min(), matrix.max() + scalar_range: Tuple[int, int] = matrix.min(), matrix.max() return matrix, scalar_range, temp_file -def dcmmf2memmap(dcm_file, orientation): + +def dcmmf2memmap(dcm_file: str, orientation: str) -> Tuple[np.memmap, Tuple[int, int], Tuple[float, float, float], str]: reader = gdcm.ImageReader() reader.SetFileName(dcm_file) reader.Read() @@ -484,60 +492,61 @@ def dcmmf2memmap(dcm_file, orientation): np_image = converters.gdcm_to_numpy(image, pf.GetSamplesPerPixel() == 1) if samples_per_pixel == 3: np_image = image_normalize(rgb2gray(np_image), 0, 255) - temp_file = tempfile.mktemp() - matrix = numpy.memmap(temp_file, mode="w+", dtype="int16", shape=np_image.shape) + temp_file: str = tempfile.mktemp() + matrix: np.memmap = np.memmap(temp_file, mode="w+", dtype="int16", shape=np_image.shape) z, y, x = np_image.shape if orientation == "CORONAL": - spacing = xs, zs, ys + spacing: Tuple[float, float, float] = xs, zs, ys matrix.shape = y, z, x for n in range(z): matrix[:, n, :] = np_image[n][::-1] elif orientation == "SAGITTAL": - spacing = zs, ys, xs + spacing: Tuple[float, float, float] = zs, ys, xs matrix.shape = y, x, z for n in range(z): matrix[:, :, n] = np_image[n][::-1] else: - spacing = xs, ys, zs + spacing: Tuple[float, float, float] = xs, ys, zs matrix[:] = np_image[:, ::-1, :] matrix.flush() - scalar_range = matrix.min(), matrix.max() + scalar_range: Tuple[int, int] = matrix.min(), matrix.max() return matrix, scalar_range, spacing, temp_file -def img2memmap(group): +def img2memmap(group) -> Tuple[np.memmap, Tuple[int, int], str]: """ From a nibabel image data creates a memmap file in the temp folder and returns it and its related filename. """ - temp_file = tempfile.mktemp() + temp_file: str = tempfile.mktemp() - data = group.get_fdata() + data: np.ndarray = group.get_fdata() # if scalar range is larger than uint16 maximum number, the image needs # to be rescalaed so that no negative values are created when converting to int16 # maximum of 10000 was selected arbitrarily by testing with one MRI example # alternatively could test if "data.dtype == 'float64'" but maybe it is too specific - if numpy.ptp(data) > (2**16/2-1): + if np.ptp(data) > (2**16/2-1): data = image_normalize(data, min_=0, max_=10000, output_dtype=np.int16) # Convert RAS+ to default InVesalius orientation ZYX - data = numpy.swapaxes(data, 0, 2) - data = numpy.fliplr(data) + data = np.swapaxes(data, 0, 2) + data = np.fliplr(data) - matrix = numpy.memmap(temp_file, mode="w+", dtype=np.int16, shape=data.shape) + matrix: np.memmap = np.memmap(temp_file, mode="w+", dtype=np.int16, shape=data.shape) matrix[:] = data[:] matrix.flush() - scalar_range = numpy.amin(matrix), numpy.amax(matrix) + scalar_range: Tuple[int, int] = np.amin(matrix), np.amax(matrix) return matrix, scalar_range, temp_file -def get_LUT_value_255(data, window, level): + +def get_LUT_value_255(data: np.ndarray, window: float, level: float) -> np.ndarray: shape = data.shape data_ = data.ravel() data = np.piecewise( @@ -552,7 +561,7 @@ def get_LUT_value_255(data, window, level): return data -def get_LUT_value(data, window, level): +def get_LUT_value(data: np.ndarray, window: float, level: float) -> np.ndarray: shape = data.shape data_ = data.ravel() data = np.piecewise(data_, @@ -563,7 +572,7 @@ def get_LUT_value(data, window, level): return data -def image_normalize(image, min_=0.0, max_=1.0, output_dtype=np.int16): +def image_normalize(image: np.ndarray, min_: float = 0.0, max_: float = 1.0, output_dtype: type = np.int16) -> np.ndarray: output = np.empty(shape=image.shape, dtype=output_dtype) imin, imax = image.min(), image.max() output[:] = (image - imin) * ((max_ - min_) / (imax - imin)) + min_ @@ -575,7 +584,8 @@ def image_normalize(image, min_=0.0, max_=1.0, output_dtype=np.int16): # - the voxel coordinate system. # - InVesalius's internal coordinate system, # -def convert_world_to_voxel(xyz, affine): + +def convert_world_to_voxel(xyz: np.ndarray, affine: np.ndarray) -> np.ndarray: """ Convert a coordinate from the world space ((x, y, z); scanner space; millimeters) to the voxel space ((i, j, k)). This is achieved by multiplying a coordinate by the inverse @@ -595,7 +605,7 @@ def convert_world_to_voxel(xyz, affine): return ijk -def convert_invesalius_to_voxel(position): +def convert_invesalius_to_voxel(position: np.ndarray) -> np.ndarray: """ Convert position from InVesalius space to the voxel space. @@ -612,7 +622,8 @@ def convert_invesalius_to_voxel(position): return np.array((position[0], slice.spacing[1]*(slice.matrix.shape[1] - 1) - position[1], position[2])) -def convert_invesalius_to_world(position, orientation): + +def convert_invesalius_to_world(position: np.ndarray, orientation: np.ndarray) -> tuple: """ Convert position and orientation from InVesalius space to the world space. @@ -632,8 +643,8 @@ def convert_invesalius_to_world(position, orientation): slice = sl.Slice() if slice.affine is None: - position_world = (None, None, None) - orientation_world = (None, None, None) + position_world: tuple[float, float, float] = (None, None, None) + orientation_world: tuple[float, float, float] = (None, None, None) return position_world, orientation_world @@ -654,7 +665,9 @@ def convert_invesalius_to_world(position, orientation): return position_world, orientation_world -def create_grid(xy_range, z_range, z_offset, spacing): + + +def create_grid(xy_range: tuple[int, int], z_range: tuple[int, int], z_offset: int, spacing: int) -> np.ndarray: x = np.arange(xy_range[0], xy_range[1] + 1, spacing) y = np.arange(xy_range[0], xy_range[1] + 1, spacing) z = z_offset + np.arange(z_range[0], z_range[1] + 1, spacing) @@ -671,7 +684,7 @@ def create_grid(xy_range, z_range, z_offset, spacing): return coord_list_w -def create_spherical_grid(radius=10, subdivision=1): +def create_spherical_grid(radius: int = 10, subdivision: int = 1) -> np.ndarray: x = np.linspace(-radius, radius, int(2 * radius / subdivision) + 1) xv, yv, zv = np.meshgrid(x, x, x) coord_grid = np.array([xv, yv, zv]) @@ -685,7 +698,7 @@ def create_spherical_grid(radius=10, subdivision=1): return sph_sort -def random_sample_sphere(radius=3, size=100): +def random_sample_sphere(radius: int = 3, size: int = 100) -> np.ndarray: uvw = np.random.normal(0, 1, (size, 3)) norm = np.linalg.norm(uvw, axis=1, keepdims=True) # Change/remove **(1./3) to make samples more concentrated around the center From 33ce4cc396575e2a456a35576972e2af925b371c Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Sat, 15 Apr 2023 22:43:01 +0530 Subject: [PATCH 03/16] added type annotations --- invesalius/data/bases.py | 19 +- invesalius/data/converters.py | 14 +- invesalius/data/coordinates.py | 28 +- invesalius/data/imagedata_utils.py | 2 +- invesalius/data/mask.py | 137 ++++---- invesalius/data/measures.py | 517 +++++++++++++++-------------- 6 files changed, 383 insertions(+), 334 deletions(-) diff --git a/invesalius/data/bases.py b/invesalius/data/bases.py index a22f24f27..954011ee3 100644 --- a/invesalius/data/bases.py +++ b/invesalius/data/bases.py @@ -2,6 +2,7 @@ import invesalius.data.coordinates as dco import invesalius.data.transformations as tr import invesalius.data.coregistration as dcr +from typing import Tuple, Any, Union, List, Optional, overload def angle_calculation(ap_axis: np.ndarray, coil_axis: np.ndarray) -> float: """ @@ -20,7 +21,7 @@ def angle_calculation(ap_axis: np.ndarray, coil_axis: np.ndarray) -> float: return float(angle) -def base_creation_old(fiducials: np.ndarray) -> tuple[np.matrix, np.ndarray, np.matrix]: +def base_creation_old(fiducials: np.ndarray) -> Tuple[np.matrix, np.ndarray, np.matrix]: """ Calculate the origin and matrix for coordinate system transformation. q: origin of coordinate system @@ -34,8 +35,8 @@ def base_creation_old(fiducials: np.ndarray) -> tuple[np.matrix, np.ndarray, np. p2 = fiducials[1, :] p3 = fiducials[2, :] - sub1: ndarray[Any, dtype] = p2 - p1 - sub2: ndarray[Any, dtype] = p3 - p1 + sub1: np.ndarray[Any, dtype] = p2 - p1 + sub2: np.ndarray[Any, dtype] = p3 - p1 lamb = (sub1[0]*sub2[0]+sub1[1]*sub2[1]+sub1[2]*sub2[2])/np.dot(sub1, sub1) q = p1 + lamb*sub1 @@ -45,7 +46,7 @@ def base_creation_old(fiducials: np.ndarray) -> tuple[np.matrix, np.ndarray, np. if not g1.any(): g1 = p2 - q - g3: ndarray[Any, dtype] = np.cross(g2, g1) + g3: np.ndarray[Any, dtype] = np.cross(g2, g1) g1 = g1/np.sqrt(np.dot(g1, g1)) g2 = g2/np.sqrt(np.dot(g2, g2)) @@ -64,7 +65,7 @@ def base_creation_old(fiducials: np.ndarray) -> tuple[np.matrix, np.ndarray, np. import numpy as np -def base_creation(fiducials: np.ndarray) -> tuple[np.ndarray, np.ndarray]: +def base_creation(fiducials: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: """ Calculate the origin and matrix for coordinate system transformation. @@ -104,7 +105,7 @@ def base_creation(fiducials: np.ndarray) -> tuple[np.ndarray, np.ndarray]: return m, q -def calculate_fre(fiducials_raw: np.ndarray, fiducials: np.ndarray, ref_mode_id: int, m_change: np.ndarray, m_icp: list[int, np.ndarray] = None) -> float: +def calculate_fre(fiducials_raw: np.ndarray, fiducials: np.ndarray, ref_mode_id: int, m_change: np.ndarray, m_icp: List[int, np.ndarray] = None) -> float: """ Calculate the Fiducial Registration Error for neuronavigation. @@ -183,9 +184,9 @@ def object_registration(fiducials: np.ndarray, orients: np.ndarray, coord_raw: n coords = np.hstack((fiducials, orients)) - fids_dyn: ndarray[Any, dtype[floating[_64Bit]]] = np.zeros([4, 6]) - fids_img: ndarray[Any, dtype[floating[_64Bit]]] = np.zeros([4, 6]) - fids_raw: ndarray[Any, dtype[floating[_64Bit]]] = np.zeros([3, 3]) + fids_img: np.ndarray[Any, dtype[floating[_64Bit]]] = np.zeros([4, 6]) + fids_dyn: np.ndarray[Any, dtype[floating[_64Bit]]] = np.zeros([4, 6]) + fids_raw: np.ndarray[Any, dtype[floating[_64Bit]]] = np.zeros([3, 3]) # compute fiducials of object with reference to the fixed probe in source frame for ic in range(0, 3): diff --git a/invesalius/data/converters.py b/invesalius/data/converters.py index ad9823cd2..97381c70d 100644 --- a/invesalius/data/converters.py +++ b/invesalius/data/converters.py @@ -19,7 +19,7 @@ import gdcm import numpy as np -from typing import Any, List, Union +from typing import Any, List, Union, Tuple, Optional, overload from vtkmodules.util import numpy_support @@ -28,11 +28,11 @@ def to_vtk( n_array: np.ndarray, - spacing: tuple[float, float, float] = (1.0, 1.0, 1.0), + spacing: Tuple[float, float, float] = (1.0, 1.0, 1.0), slice_number: int = 0, orientation: str = "AXIAL", - origin: tuple[int, int, int] = (0, 0, 0), - padding: tuple[int, int, int] = (0, 0, 0), + origin: Tuple[int, int, int] = (0, 0, 0), + padding: Tuple[int, int, int] = (0, 0, 0), ) -> vtkImageData: if orientation == "SAGITTAL": orientation = "SAGITAL" @@ -99,8 +99,8 @@ def to_vtk( def to_vtk_mask( n_array: np.ndarray, - spacing: tuple[float, float, float] = (1.0, 1.0, 1.0), - origin: tuple[float, float, float] = (0.0, 0.0, 0.0) + spacing: Tuple[float, float, float] = (1.0, 1.0, 1.0), + origin: Tuple[float, float, float] = (0.0, 0.0, 0.0) ) -> vtkImageData: dz, dy, dx = n_array.shape ox, oy, oz = origin @@ -135,7 +135,7 @@ def to_vtk_mask( def np_rgba_to_vtk( n_array: np.ndarray, - spacing: tuple[float, float, float] = (1.0, 1.0, 1.0) + spacing: Tuple[float, float, float] = (1.0, 1.0, 1.0) ) -> vtkImageData: dy, dx, dc = n_array.shape v_image = numpy_support.numpy_to_vtk(n_array.reshape(dy * dx, dc)) diff --git a/invesalius/data/coordinates.py b/invesalius/data/coordinates.py index a31eff94f..3fc025bfb 100644 --- a/invesalius/data/coordinates.py +++ b/invesalius/data/coordinates.py @@ -66,7 +66,7 @@ def GetCoordinates(self) -> Tuple[Optional[np.ndarray], List[bool]]: return self.coord, self.markers_flag -def GetCoordinatesForThread(tracker_connection: TrackerConnection, tracker_id: Optional[str], ref_mode: str) -> Tuple[Optional[np.ndarray], Optional[List[bool]]]: +def GetCoordinatesForThread(tracker_connection, tracker_id: Optional[str], ref_mode: str) -> Tuple[Optional[np.ndarray], Optional[List[bool]]]: """ Read coordinates from spatial tracking devices using @@ -97,7 +97,7 @@ def GetCoordinatesForThread(tracker_connection: TrackerConnection, tracker_id: O return coord, markers_flag -def PolarisP4Coord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: +def PolarisP4Coord(tracker_connection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: trck = tracker_connection.GetConnection() trck.Run() @@ -142,7 +142,7 @@ def PolarisP4Coord(tracker_connection: TrackerConnection, tracker_id: str, ref_m -def OptitrackCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: +def OptitrackCoord(tracker_connection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: """ Obtains coordinates and angles of tracking rigid bodies (Measurement Probe, Coil, Head). Converts orientations from quaternion @@ -183,7 +183,7 @@ def OptitrackCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_m return coord, [trck.probeID, trck.HeadID, trck.coilID] -def PolarisCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: +def PolarisCoord(tracker_connection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: trck = tracker_connection.GetConnection() trck.Run() @@ -207,13 +207,13 @@ def PolarisCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mod return coord, [trck.probeID, trck.refID, trck.objID] -def CameraCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: +def CameraCoord(tracker_connection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: trck = tracker_connection.GetConnection() coord, probeID, refID, coilID = trck.Run() return coord, [probeID, refID, coilID] -def ClaronCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: +def ClaronCoord(tracker_connection, tracker_id: str, ref_mode: str) -> Tuple[np.ndarray, List[str]]: trck = tracker_connection.GetConnection() trck.Run() @@ -237,7 +237,7 @@ def ClaronCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode -def PolhemusCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mode: str) -> Tuple[any, List[bool]]: +def PolhemusCoord(tracker_connection, tracker_id: str, ref_mode: str) -> Tuple[any, List[bool]]: lib_mode: str = tracker_connection.GetLibMode() coord: any = None @@ -256,7 +256,7 @@ def PolhemusCoord(tracker_connection: TrackerConnection, tracker_id: str, ref_mo -def PolhemusWrapperCoord(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: str) -> np.ndarray: +def PolhemusWrapperCoord(tracker_connection, tracker_id: int, ref_mode: str) -> np.ndarray: trck = tracker_connection.GetConnection() trck.Run() @@ -287,7 +287,7 @@ def PolhemusWrapperCoord(tracker_connection: TrackerConnection, tracker_id: int, -def PolhemusUSBCoord(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: bool) -> Optional[Tuple[float, float, float, float, float, float]]: +def PolhemusUSBCoord(tracker_connection, tracker_id: int, ref_mode: bool) -> Optional[Tuple[float, float, float, float, float, float]]: trck = tracker_connection.GetConnection() endpoint = trck[0][(0, 0)][0] @@ -327,7 +327,7 @@ def PolhemusUSBCoord(tracker_connection: TrackerConnection, tracker_id: int, ref -def PolhemusSerialCoord(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: bool) -> Optional[np.ndarray]: +def PolhemusSerialCoord(tracker_connection, tracker_id: int, ref_mode: bool) -> Optional[np.ndarray]: trck = tracker_connection.GetConnection() # mudanca para fastrak - ref 1 tem somente x, y, z @@ -362,7 +362,7 @@ def PolhemusSerialCoord(tracker_connection: TrackerConnection, tracker_id: int, return coord -def RobotCoord(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: bool) -> Tuple[np.ndarray, bool]: +def RobotCoord(tracker_connection, tracker_id: int, ref_mode: bool) -> Tuple[np.ndarray, bool]: tracker_id = tracker_connection.GetTrackerId() coord_tracker, markers_flag = GetCoordinatesForThread(tracker_connection, tracker_id, ref_mode) @@ -370,7 +370,7 @@ def RobotCoord(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: return np.vstack([coord_tracker[0], coord_tracker[1], coord_tracker[2]]), markers_flag -def DebugCoordRandom(tracker_connection: TrackerConnection, tracker_id: int, ref_mode: bool) -> Tuple[np.ndarray, List[int]]: +def DebugCoordRandom(tracker_connection, tracker_id: int, ref_mode: bool) -> Tuple[np.ndarray, List[int]]: """ Method to simulate a tracking device for debug and error check. Generate a random x, y, z, alfa, beta and gama coordinates in interval [1, 200[ @@ -436,8 +436,8 @@ def coordinates_to_transformation_matrix(position: List[float], orientation: Lis """ a, b, g = np.radians(orientation) - r_ref = euler_matrix(a, b, g, axes=axes) - t_ref = translation_matrix(position) + r_ref =a, b, g, axes=axes + t_ref = (position) m_img = np.dot(t_ref, r_ref) diff --git a/invesalius/data/imagedata_utils.py b/invesalius/data/imagedata_utils.py index c4e2e9931..8d1831f4a 100644 --- a/invesalius/data/imagedata_utils.py +++ b/invesalius/data/imagedata_utils.py @@ -667,7 +667,7 @@ def convert_invesalius_to_world(position: np.ndarray, orientation: np.ndarray) - -def create_grid(xy_range: tuple[int, int], z_range: tuple[int, int], z_offset: int, spacing: int) -> np.ndarray: +def create_grid(xy_range: Tuple[int, int], z_range: Tuple[int, int], z_offset: int, spacing: int) -> np.ndarray: x = np.arange(xy_range[0], xy_range[1] + 1, spacing) y = np.arange(xy_range[0], xy_range[1] + 1, spacing) z = z_offset + np.arange(z_range[0], z_range[1] + 1, spacing) diff --git a/invesalius/data/mask.py b/invesalius/data/mask.py index 1fda81dcc..60b2f2618 100644 --- a/invesalius/data/mask.py +++ b/invesalius/data/mask.py @@ -24,6 +24,7 @@ import tempfile import time import weakref +from typing import Any, List, Union, Tuple, Optional, overload import invesalius.constants as const import invesalius.data.converters as converters @@ -37,7 +38,7 @@ class EditionHistoryNode(object): - def __init__(self, index, orientation, array, clean=False): + def __init__(self, index: int, orientation: str, array: np.ndarray, clean: bool = False) -> None: self.index = index self.orientation = orientation self.filename = tempfile.mktemp(suffix='.npy') @@ -45,11 +46,11 @@ def __init__(self, index, orientation, array, clean=False): self._save_array(array) - def _save_array(self, array): + def _save_array(self, array: np.ndarray) -> None: np.save(self.filename, array) print("Saving history", self.index, self.orientation, self.filename, self.clean) - def commit_history(self, mvolume): + def commit_history(self, mvolume: np.ndarray) -> None: array = np.load(self.filename) if self.orientation == 'AXIAL': mvolume[self.index+1,1:,1:] = array @@ -73,16 +74,18 @@ def __del__(self): os.remove(self.filename) + + class EditionHistory(object): - def __init__(self, size=50): - self.history = [] - self.index = -1 - self.size = size * 2 + def __init__(self, size: int = 50) -> None: + self.history: List[EditionHistoryNode] = [] + self.index: int = -1 + self.size: int = size * 2 Publisher.sendMessage("Enable undo", value=False) Publisher.sendMessage("Enable redo", value=False) - def new_node(self, index, orientation, array, p_array, clean): + def new_node(self, index: int, orientation: str, array: np.ndarray, p_array: np.ndarray, clean: bool) -> None: # Saving the previous state, used to undo/redo correctly. p_node = EditionHistoryNode(index, orientation, p_array, clean) self.add(p_node) @@ -90,7 +93,7 @@ def new_node(self, index, orientation, array, p_array, clean): node = EditionHistoryNode(index, orientation, array, clean) self.add(node) - def add(self, node): + def add(self, node: EditionHistoryNode) -> None: if self.index == self.size: self.history.pop(0) self.index -= 1 @@ -104,7 +107,7 @@ def add(self, node): Publisher.sendMessage("Enable undo", value=True) Publisher.sendMessage("Enable redo", value=False) - def undo(self, mvolume, actual_slices=None): + def undo(self, mvolume: np.ndarray, actual_slices: Optional[Dict[str, int]] = None) -> None: h = self.history if self.index > 0: #if self.index > 0 and h[self.index].clean: @@ -131,7 +134,10 @@ def undo(self, mvolume, actual_slices=None): Publisher.sendMessage("Enable undo", value=False) print("AT", self.index, len(self.history), self.history[self.index].filename) - def redo(self, mvolume, actual_slices=None): + + + + def redo(self, mvolume: np.ndarray, actual_slices: Optional[Dict[str, int]] = None) -> None: h = self.history if self.index < len(h) - 1: #if self.index < len(h) - 1 and h[self.index].clean: @@ -159,11 +165,11 @@ def redo(self, mvolume, actual_slices=None): Publisher.sendMessage("Enable redo", value=False) print("AT", self.index, len(h), h[self.index].filename) - def _reload_slice(self, index): + def _reload_slice(self, index: int) -> None: Publisher.sendMessage(('Set scroll position', self.history[index].orientation), - index=self.history[index].index) + index=self.history[index].index) - def _config_undo_redo(self, visible): + def _config_undo_redo(self, visible: bool) -> None: v_undo = False v_redo = False @@ -178,57 +184,59 @@ def _config_undo_redo(self, visible): Publisher.sendMessage("Enable undo", value=v_undo) Publisher.sendMessage("Enable redo", value=v_redo) - def clear_history(self): + def clear_history(self) -> None: self.history = [] self.index = -1 Publisher.sendMessage("Enable undo", value=False) Publisher.sendMessage("Enable redo", value=False) - + class Mask(): - general_index = -1 - def __init__(self): + general_index: int = -1 + + def __init__(self) -> None: Mask.general_index += 1 - self.index = Mask.general_index - self.matrix = None - self.spacing = (1.0, 1.0, 1.0) - self.imagedata = None - self.colour = random.choice(const.MASK_COLOUR) - self.opacity = const.MASK_OPACITY - self.threshold_range = const.THRESHOLD_RANGE - self.name = const.MASK_NAME_PATTERN %(Mask.general_index+1) - self.edition_threshold_range = [const.THRESHOLD_OUTVALUE, const.THRESHOLD_INVALUE] - self.is_shown = 1 - self.edited_points = {} - self.was_edited = False - self.volume = None - self.auto_update_mask = True - self.modified_time = 0 + self.index: int = Mask.general_index + self.matrix: Optional[np.ndarray] = None + self.spacing: Tuple[float, float, float] = (1.0, 1.0, 1.0) + self.imagedata: Optional[np.ndarray] = None + self.colour: str = random.choice(const.MASK_COLOUR) + self.opacity: float = const.MASK_OPACITY + self.threshold_range: Tuple[int, int] = const.THRESHOLD_RANGE + self.name: str = const.MASK_NAME_PATTERN % (Mask.general_index + 1) + self.edition_threshold_range: List[int] = [const.THRESHOLD_OUTVALUE, const.THRESHOLD_INVALUE] + self.is_shown: int = 1 + self.edited_points: Dict[Tuple[int, int, int], bool] = {} + self.was_edited: bool = False + self.volume: Optional[Volume] = None + self.auto_update_mask: bool = True + self.modified_time: int = 0 self.__bind_events() - self._modified_callbacks = [] + self._modified_callbacks: List[Callable[[], None]] = [] - self.history = EditionHistory() + self.history: EditionHistory = EditionHistory() - def __bind_events(self): + def __bind_events(self) -> None: Publisher.subscribe(self.OnFlipVolume, 'Flip volume') Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') - def as_vtkimagedata(self): + + def as_vtkimagedata(self) -> vtk.vtkImageData: print("Converting to VTK") vimg = converters.to_vtk_mask(self.matrix, self.spacing) print("Converted") return vimg - def set_colour(self, colour): + def set_colour(self, colour: str) -> None: self.colour = colour if self.volume is not None: self.volume.set_colour(colour) Publisher.sendMessage("Render volume viewer") - def save_history(self, index, orientation, array, p_array, clean=False): + def save_history(self, index: int, orientation: Tuple[int, int, int], array: np.ndarray, p_array: np.ndarray, clean: bool = False) -> None: self.history.new_node(index, orientation, array, p_array, clean) - def undo_history(self, actual_slices): + def undo_history(self, actual_slices: List[np.ndarray]) -> None: self.history.undo(self.matrix, actual_slices) self.modified() @@ -236,7 +244,7 @@ def undo_history(self, actual_slices): session = ses.Session() session.ChangeProject() - def redo_history(self, actual_slices): + def redo_history(self, actual_slices: List[np.ndarray]) -> None: self.history.redo(self.matrix, actual_slices) self.modified() @@ -244,7 +252,7 @@ def redo_history(self, actual_slices): session = ses.Session() session.ChangeProject() - def on_show(self): + def on_show(self) -> None: self.history._config_undo_redo(self.is_shown) session = ses.Session() @@ -252,14 +260,14 @@ def on_show(self): Publisher.sendMessage('Show mask preview', index=self.index, flag=bool(self.is_shown)) Publisher.sendMessage("Render volume viewer") - def create_3d_preview(self): + def create_3d_preview(self) -> None: if self.volume is None: if self.imagedata is None: self.imagedata = self.as_vtkimagedata() self.volume = VolumeMask(self) self.volume.create_volume() - def _update_imagedata(self, update_volume_viewer=True): + def _update_imagedata(self, update_volume_viewer: bool = True) -> None: if self.imagedata is not None: dz, dy, dx = self.matrix.shape # np_image = numpy_support.vtk_to_numpy(self.imagedata.GetPointData().GetScalars()) @@ -273,7 +281,8 @@ def _update_imagedata(self, update_volume_viewer=True): if update_volume_viewer: Publisher.sendMessage("Render volume viewer") - def SavePlist(self, dir_temp, filelist): + + def SavePlist(self, dir_temp: str, filelist: Dict[str, str]) -> str: mask = {} filename = u'mask_%d' % self.index mask_filename = u'%s.dat' % filename @@ -302,8 +311,9 @@ def SavePlist(self, dir_temp, filelist): filelist[temp_plist] = plist_filename return plist_filename + - def OpenPList(self, filename): + def OpenPList(self, filename: str) -> None: with open(filename, 'r+b') as f: mask = plistlib.load(f, fmt=plistlib.FMT_XML) @@ -322,7 +332,7 @@ def OpenPList(self, filename): path = os.path.join(dirpath, mask_file) self._open_mask(path, tuple(shape)) - def OnFlipVolume(self, axis): + def OnFlipVolume(self, axis: int) -> None: submatrix = self.matrix[1:, 1:, 1:] if axis == 0: submatrix[:] = submatrix[::-1] @@ -335,7 +345,7 @@ def OnFlipVolume(self, axis): self.matrix[0, 0, 1::] = self.matrix[0, 0, :0:-1] self.modified() - def OnSwapVolumeAxes(self, axes): + def OnSwapVolumeAxes(self, axes: Tuple[int, int]) -> None: axis0, axis1 = axes self.matrix = self.matrix.swapaxes(axis0, axis1) if self.volume: @@ -343,22 +353,24 @@ def OnSwapVolumeAxes(self, axes): self.volume.change_imagedata() self.modified() - def _save_mask(self, filename): + def _save_mask(self, filename: str) -> None: shutil.copyfile(self.temp_file, filename) - def _open_mask(self, filename, shape, dtype='uint8'): + def _open_mask(self, filename: str, shape: Tuple[int, int, int], dtype: str = 'uint8') -> None: print(">>", filename, shape) self.temp_file = filename self.matrix = np.memmap(filename, shape=shape, dtype=dtype, mode="r+") - def _set_class_index(self, index): - Mask.general_index = index + @classmethod + def _set_class_index(cls, index: int) -> None: + cls.general_index = index - def add_modified_callback(self, callback): + + def add_modified_callback(self, callback) -> None: ref = weakref.WeakMethod(callback) self._modified_callbacks.append(ref) - def remove_modified_callback(self, callback): + def remove_modified_callback(self, callback) -> bool: callbacks = [] removed = False for cb in self._modified_callbacks: @@ -370,7 +382,7 @@ def remove_modified_callback(self, callback): self._modified_callbacks = callbacks return removed - def create_mask(self, shape): + def create_mask(self, shape: Tuple[int, int, int]) -> None: """ Creates a new mask object. This method do not append this new mask into the project. @@ -381,7 +393,7 @@ def create_mask(self, shape): shape = shape[0] + 1, shape[1] + 1, shape[2] + 1 self.matrix = np.memmap(self.temp_file, mode='w+', dtype='uint8', shape=shape) - def modified(self, all_volume=False): + def modified(self, all_volume: bool = False) -> None: if all_volume: self.matrix[0] = 1 self.matrix[:, 0, :] = 1 @@ -400,11 +412,11 @@ def modified(self, all_volume=False): callbacks.append(callback) self._modified_callbacks = callbacks - def clean(self): + def clean(self) -> None: self.matrix[1:, 1:, 1:] = 0 self.modified(all_volume=True) - def cleanup(self): + def cleanup(self) -> None: if self.is_shown: self.history._config_undo_redo(False) if self.volume: @@ -413,8 +425,9 @@ def cleanup(self): self.imagedata = None self.volume = None del self.matrix + - def copy(self, copy_name): + def copy(self, copy_name: str) -> 'Mask': """ creates and return a copy from the mask instance. @@ -436,10 +449,10 @@ def copy(self, copy_name): return new_mask - def clear_history(self): + def clear_history(self) -> None: self.history.clear_history() - def fill_holes_auto(self, target, conn, orientation, index, size): + def fill_holes_auto(self, target: str, conn: int, orientation: str, index: int, size: int) -> None: CON2D = {4: 1, 8: 2} CON3D = {6: 1, 18: 2, 26: 3} @@ -482,7 +495,7 @@ def fill_holes_auto(self, target, conn, orientation, index, size): if ret: self.save_history(index, orientation, matrix.copy(), cp_mask) - def __del__(self): + def __del__(self) -> None: # On Linux self.matrix is already removed so it gives an error try: del self.matrix diff --git a/invesalius/data/measures.py b/invesalius/data/measures.py index f548f2495..e7ab9711a 100644 --- a/invesalius/data/measures.py +++ b/invesalius/data/measures.py @@ -1,8 +1,10 @@ # -*- coding: UTF-8 -*- +from aifc import _File import math import sys - +from typing import Any, Dict, List, Optional, Tuple, Union, int +import wx from invesalius.pubsub import pub as Publisher import numpy as np @@ -31,55 +33,60 @@ from invesalius import math_utils from invesalius.gui.widgets.canvas_renderer import TextBox, Ellipse, Polygon, CanvasHandlerBase -TYPE = {const.LINEAR: _(u"Linear"), +TYPE: Dict[int, Any] = {const.LINEAR: _(u"Linear"), const.ANGULAR: _(u"Angular"), const.DENSITY_ELLIPSE: _(u"Density Ellipse"), const.DENSITY_POLYGON: _(u"Density Polygon"), } -LOCATION = {const.SURFACE: _(u"3D"), + +from typing import Any, Dict + +LOCATION: Dict[int, Any] = {const.SURFACE: _(u"3D"), const.AXIAL: _(u"Axial"), const.CORONAL: _(u"Coronal"), const.SAGITAL: _(u"Sagittal") } -map_locations_id = { +map_locations_id: Dict[str, int] = { "3D": const.SURFACE, "AXIAL": const.AXIAL, "CORONAL": const.CORONAL, "SAGITAL": const.SAGITAL, } -map_id_locations = {const.SURFACE: "3D", +map_id_locations: Dict[int, str] = {const.SURFACE: "3D", const.AXIAL: "AXIAL", const.CORONAL: "CORONAL", const.SAGITAL: "SAGITAL", } if sys.platform == 'win32': - MEASURE_LINE_COLOUR = (255, 0, 0, 255) - MEASURE_TEXT_COLOUR = (0, 0, 0) - MEASURE_TEXTBOX_COLOUR = (255, 255, 165, 255) + MEASURE_LINE_COLOUR: Tuple[int, int, int, int] = (255, 0, 0, 255) + MEASURE_TEXT_COLOUR: Tuple[int, int, int] = (0, 0, 0) + MEASURE_TEXTBOX_COLOUR: Tuple[int, int, int, int] = (255, 255, 165, 255) else: - MEASURE_LINE_COLOUR = (255, 0, 0, 128) - MEASURE_TEXT_COLOUR = (0, 0, 0) - MEASURE_TEXTBOX_COLOUR = (255, 255, 165, 255) + MEASURE_LINE_COLOUR: Tuple[int, int, int, int] = (255, 0, 0, 128) + MEASURE_TEXT_COLOUR: Tuple[int, int, int] = (0, 0, 0) + MEASURE_TEXTBOX_COLOUR: Tuple[int, int, int, int] = (255, 255, 165, 255) + + +DEBUG_DENSITY: bool = False -DEBUG_DENSITY = False class MeasureData(metaclass=utils.Singleton): """ Responsible to keep measures data. """ - def __init__(self): - self.measures = {const.SURFACE: {}, - const.AXIAL: {}, - const.CORONAL: {}, - const.SAGITAL: {}} - self._list_measures = [] - - def append(self, m): + def __init__(self) -> None: + self.measures: Dict[int, Dict[int, List[Any]]] = {const.SURFACE: {}, + const.AXIAL: {}, + const.CORONAL: {}, + const.SAGITAL: {}} + self._list_measures: List[Any] = [] + + def append(self, m: Any) -> None: try: self.measures[m[0].location][m[0].slice_number].append(m) except KeyError: @@ -87,17 +94,17 @@ def append(self, m): self._list_measures.append(m) - def clean(self): + def clean(self) -> None: self.measures = {const.SURFACE: {}, const.AXIAL: {}, const.CORONAL: {}, const.SAGITAL: {}} self._list_measures = [] - def get(self, location, slice_number): + def get(self, location: str, slice_number: int) -> List[Any]: return self.measures[map_locations_id[location]].get(slice_number, []) - def pop(self, idx=None): + def pop(self, idx: int = None) -> Any: if idx is None: m = self._list_measures.pop() else: @@ -105,31 +112,32 @@ def pop(self, idx=None): self.measures[m[0].location][m[0].slice_number].remove(m) return m - def remove(self, m): + def remove(self, m: Any) -> None: self._list_measures.remove(m) self.measures[m[0].location][m[0].slice_number].remove(m) - def __contains__(self, m): + def __contains__(self, m: Any) -> bool: return m in self._list_measures - def __getitem__(self, idx): + def __getitem__(self, idx: int) -> Any: return self._list_measures[idx] - def __len__(self): + def __len__(self) -> int: return len(self._list_measures) + class MeasurementManager(object): """ A class to manage the use (Addition, remotion and visibility) from measures. """ - def __init__(self): - self.current = None - self.measures = MeasureData() + def __init__(self) -> None: + self.current: Any = None + self.measures: MeasureData = MeasureData() self._bind_events() - def _bind_events(self): + def _bind_events(self) -> None: Publisher.subscribe(self._add_point, "Add measurement point") Publisher.subscribe(self._change_name, "Change measurement name") Publisher.subscribe(self._remove_measurements, "Remove measurements") @@ -141,7 +149,7 @@ def _bind_events(self): Publisher.subscribe(self._add_density_measure, "Add density measurement") Publisher.subscribe(self.OnCloseProject, 'Close project data') - def _load_measurements(self, measurement_dict, spacing=(1.0, 1.0, 1.0)): + def _load_measurements(self, measurement_dict: Dict[int, Any], spacing: Tuple[float, float, float] = (1.0, 1.0, 1.0)) -> None: for i in measurement_dict: m = measurement_dict[i] @@ -204,21 +212,23 @@ def _load_measurements(self, measurement_dict, spacing=(1.0, 1.0, 1.0)): else: Publisher.sendMessage('Redraw canvas') - def _add_point(self, position, type, location, slice_number=0, radius=const.PROP_MEASURE): - to_remove = False + + + def _add_point(self, position: Tuple[float, float, float], type: str, location: str, slice_number: int = 0, radius: float = const.PROP_MEASURE) -> None: + to_remove: bool = False if self.current is None: - to_create = True + to_create: bool = True elif self.current[0].location != location: - to_create = True - to_remove = True + to_create: bool = True + to_remove: bool = True elif self.current[0].slice_number != slice_number: - to_create = True - to_remove = True + to_create: bool = True + to_remove: bool = True else: - to_create = False + to_create: bool = False if to_create: - m = Measurement() + m: Measurement = Measurement() m.index = len(self.measures) m.location = location m.slice_number = slice_number @@ -232,7 +242,7 @@ def _add_point(self, position, type, location, slice_number=0, radius=const.PROP # actors = self.current[1].GetActors() # slice_number = self.current[0].slice_number # Publisher.sendMessage(('Remove actors ' + str(self.current[0].location)), - # (actors, slice_number)) + # (actors, slice_number)) self.measures.pop()[1].Remove() if self.current[0].location == const.SURFACE: Publisher.sendMessage('Render volume viewer') @@ -250,6 +260,7 @@ def _add_point(self, position, type, location, slice_number=0, radius=const.PROP x, y, z = position actors = mr.AddPoint(x, y, z) m.points.append(position) + if m.location == const.SURFACE: Publisher.sendMessage("Add actors " + str(location), actors=actors) @@ -258,26 +269,26 @@ def _add_point(self, position, type, location, slice_number=0, radius=const.PROP self.measures.append(self.current) if mr.IsComplete(): - index = prj.Project().AddMeasurement(m) + index: int = prj.Project().AddMeasurement(m) #m.index = index # already done in proj - name = m.name - colour = m.colour - m.value = mr.GetValue() - type_ = TYPE[type] - location = LOCATION[location] + name: str = m.name + colour: Tuple[int, int, int] = m.colour + m.value: float = mr.GetValue() + type_: str = TYPE[type] + location: str = LOCATION[location] if type == const.LINEAR: - value = u"%.3f mm"% m.value + value: str = u"%.3f mm"% m.value else: - value = u"%.3f°"% m.value + value: str = u"%.3f°"% m.value - msg = 'Update measurement info in GUI', + msg: str = 'Update measurement info in GUI', Publisher.sendMessage(msg, index=index, name=name, colour=colour, location=location, type_=type_, value=value) self.current = None - def _change_measure_point_pos(self, index, npoint, pos): + def _change_measure_point_pos(self, index: int, npoint: int, pos: Tuple[float, float, float]) -> None: m, mr = self.measures[index] x, y, z = pos if npoint == 0: @@ -292,26 +303,28 @@ def _change_measure_point_pos(self, index, npoint, pos): m.value = mr.GetValue() - name = m.name - colour = m.colour - m.value = mr.GetValue() - type_ = TYPE[m.type] - location = LOCATION[m.location] + name: str = m.name + colour: Tuple[int, int, int] = m.colour + m.value: float = mr.GetValue() + type_: str = TYPE[m.type] + location: str = LOCATION[m.location] if m.type == const.LINEAR: - value = u"%.3f mm"% m.value + value: str = u"%.3f mm"% m.value else: - value = u"%.3f°"% m.value + value: str = u"%.3f°"% m.value Publisher.sendMessage('Update measurement info in GUI', index=index, name=name, colour=colour, location=location, type_=type_, value=value) - def _change_name(self, index, name): - self.measures[index][0].name = name - - def _remove_measurements(self, indexes): + + + def _change_name(self, index: int, name: str) -> None: + self.measures[index][0].name = name + + def _remove_measurements(self, indexes: List[int]) -> None: for index in indexes: m, mr = self.measures.pop(index) try: @@ -322,14 +335,14 @@ def _remove_measurements(self, indexes): prj.Project().RemoveMeasurement(index) if m.location == const.SURFACE: Publisher.sendMessage('Remove actors ' + str(m.location), - actors=mr.GetActors()) + actors=mr.GetActors()) Publisher.sendMessage('Redraw canvas') Publisher.sendMessage('Render volume viewer') session = ses.Session() session.ChangeProject() - def _set_visibility(self, index, visibility): + def _set_visibility(self, index: int, visibility: bool) -> None: m, mr = self.measures[index] m.visible = visibility mr.SetVisibility(visibility) @@ -338,20 +351,21 @@ def _set_visibility(self, index, visibility): else: Publisher.sendMessage('Redraw canvas') - def _rm_incomplete_measurements(self): + def _rm_incomplete_measurements(self) -> None: if self.current is None: return - m, mr = self.current + m: Measurement = self.current[0] + mr: Union[LinearMeasure, AngularMeasure] = self.current[1] if not mr.IsComplete(): - idx = self.measures._list_measures.index((m, mr)) + idx: int = self.measures._list_measures.index((m, mr)) self.measures.remove((m, mr)) Publisher.sendMessage("Remove GUI measurement", measure_index=idx) actors = mr.GetActors() slice_number = self.current[0].slice_number if m.location == const.SURFACE: Publisher.sendMessage(('Remove actors ' + str(self.current[0].location)), - actors=actors) + actors=actors) if self.current[0].location == const.SURFACE: Publisher.sendMessage('Render volume viewer') else: @@ -360,59 +374,63 @@ def _rm_incomplete_measurements(self): # if self.measures: # self.measures.pop() self.current = None + - def _add_density_measure(self, density_measure): - m = DensityMeasurement() - m.index = len(self.measures) - m.location = density_measure.location - m.slice_number = density_measure.slice_number - m.colour = density_measure.colour - m.value = density_measure._mean - m.area = density_measure._area - m.mean = density_measure._mean - m.min = density_measure._min - m.max = density_measure._max - m.std = density_measure._std - if density_measure.format == 'ellipse': - m.points = [density_measure.center, density_measure.point1, density_measure.point2] - m.type = const.DENSITY_ELLIPSE - elif density_measure.format == 'polygon': - m.points = density_measure.points - m.type = const.DENSITY_POLYGON - density_measure.index = m.index - - density_measure.set_measurement(m) - - self.measures.append((m, density_measure)) - - index = prj.Project().AddMeasurement(m) + + def _add_density_measure(self, density_measure: Union[EllipseDensityMeasure, PolygonDensityMeasure]) -> None: + m = DensityMeasurement() + m.index = len(self.measures) + m.location = density_measure.location + m.slice_number = density_measure.slice_number + m.colour = density_measure.colour + m.value = density_measure._mean + m.area = density_measure._area + m.mean = density_measure._mean + m.min = density_measure._min + m.max = density_measure._max + m.std = density_measure._std + if density_measure.format == 'ellipse': + m.points = [density_measure.center, density_measure.point1, density_measure.point2] + m.type = const.DENSITY_ELLIPSE + elif density_measure.format == 'polygon': + m.points = density_measure.points + m.type = const.DENSITY_POLYGON + density_measure.index = m.index + + density_measure.set_measurement(m) + + self.measures.append((m, density_measure)) + + index = prj.Project().AddMeasurement(m) + + msg: str = 'Update measurement info in GUI', + Publisher.sendMessage(msg, + index=m.index, name=m.name, colour=m.colour, + location=density_measure.orientation, + type_='Density', + value='%.3f' % m.value) + + def OnCloseProject(self) -> None: + self.measures.clean() - msg = 'Update measurement info in GUI', - Publisher.sendMessage(msg, - index=m.index, name=m.name, colour=m.colour, - location=density_measure.orientation, - type_='Density', - value='%.3f' % m.value) - def OnCloseProject(self): - self.measures.clean() class Measurement(): - general_index = -1 - def __init__(self): + general_index: int = -1 + def __init__(self) -> None: Measurement.general_index += 1 - self.index = Measurement.general_index - self.name = const.MEASURE_NAME_PATTERN %(self.index+1) - self.colour = next(const.MEASURE_COLOUR) - self.value = 0 - self.location = const.SURFACE # AXIAL, CORONAL, SAGITTAL - self.type = const.LINEAR # ANGULAR - self.slice_number = 0 - self.points = [] - self.visible = True - - def Load(self, info): + self.index: int = Measurement.general_index + self.name: str = const.MEASURE_NAME_PATTERN %(self.index+1) + self.colour: str = next(const.MEASURE_COLOUR) + self.value: float = 0 + self.location: str = const.SURFACE # AXIAL, CORONAL, SAGITTAL + self.type: str = const.LINEAR # ANGULAR + self.slice_number: int = 0 + self.points: List = [] + self.visible: bool = True + + def Load(self, info: Dict[str, any]) -> None: self.index = info["index"] self.name = info["name"] self.colour = info["colour"] @@ -423,8 +441,8 @@ def Load(self, info): self.points = info["points"] self.visible = info["visible"] - def get_as_dict(self): - d = { + def get_as_dict(self) -> Dict[str, any]: + d: Dict[str, any] = { 'index': self.index, 'name': self.name, 'colour': self.colour, @@ -438,25 +456,26 @@ def get_as_dict(self): return d + class DensityMeasurement(): - general_index = -1 - def __init__(self): + general_index: int = -1 + def __init__(self) -> None: DensityMeasurement.general_index += 1 - self.index = DensityMeasurement.general_index - self.name = const.MEASURE_NAME_PATTERN %(self.index+1) - self.colour = next(const.MEASURE_COLOUR) - self.area = 0 - self.min = 0 - self.max = 0 - self.mean = 0 - self.std = 0 - self.location = const.AXIAL - self.type = const.DENSITY_ELLIPSE - self.slice_number = 0 - self.points = [] - self.visible = True - - def Load(self, info): + self.index: int = DensityMeasurement.general_index + self.name: str = const.MEASURE_NAME_PATTERN %(self.index+1) + self.colour: str = next(const.MEASURE_COLOUR) + self.area: float = 0 + self.min: float = 0 + self.max: float = 0 + self.mean: float = 0 + self.std: float = 0 + self.location: str = const.AXIAL + self.type: str = const.DENSITY_ELLIPSE + self.slice_number: int = 0 + self.points: List = [] + self.visible: bool = True + + def Load(self, info: Dict[str, any]) -> None: self.index = info["index"] self.name = info["name"] self.colour = info["colour"] @@ -472,8 +491,8 @@ def Load(self, info): self.mean = info["mean"] self.std = info["std"] - def get_as_dict(self): - d = { + def get_as_dict(self) -> Dict[str, any]: + d: Dict[str, any] = { 'index': self.index, 'name': self.name, 'colour': self.colour, @@ -492,64 +511,69 @@ def get_as_dict(self): return d -class CirclePointRepresentation(object): + +from typing import Tuple +import vtk + +class CirclePointRepresentation: """ This class represents a circle that indicate a point in the surface """ - def __init__(self, colour=(1, 0, 0), radius=1.0): + def __init__(self, colour: Tuple[float, float, float] = (1, 0, 0), radius: float = 1.0) -> None: """ colour: the colour of the representation radius: the radius of circle representation """ - self.colour = colour - self.radius = radius + self.colour: Tuple[float, float, float] = colour + self.radius: float = radius - def GetRepresentation(self, x, y, z): + def GetRepresentation(self, x: float, y: float, z: float) -> vtkActor: """ Return a actor that represents the point in the given x, y, z point """ - sphere = vtkSphereSource() + sphere: vtkSphereSource = vtkSphereSource() sphere.SetCenter(x, y, z) sphere.SetRadius(self.radius) # c = vtkCoordinate() # c.SetCoordinateSystemToWorld() - m = vtkPolyDataMapper() + m: vtkPolyDataMapper = vtkPolyDataMapper() m.SetInputConnection(sphere.GetOutputPort()) # m.SetTransformCoordinate(c) - a = vtkActor() + a: vtkActor = vtkActor() a.SetMapper(m) a.GetProperty().SetColor(self.colour) return a -class CrossPointRepresentation(object): + +class CrossPointRepresentation: """ This class represents a cross that indicate a point in the surface """ - def __init__(self, camera, colour=(1, 0, 0), size=1.0): + def __init__(self, camera, colour: Tuple[float, float, float] = (1, 0, 0), size: float = 1.0) -> None: """ colour: the colour of the representation size: the size of the representation camera: the active camera, to get the orientation to draw the cross """ self.camera = camera - self.colour = colour - self.size = size + self.colour: Tuple[float, float, float] = colour + self.size: float = size - def GetRepresentation(self, x, y, z): - pc = self.camera.GetPosition() # camera position - pf = self.camera.GetFocalPoint() # focal position - pp = (x, y, z) # point where the user clicked + def GetRepresentation(self, x: float, y: float, z: float) -> vtkActor2D: + pc: Tuple[float, float, float] = self.camera.GetPosition() # camera position + pf: Tuple[float, float, float] = self.camera.GetFocalPoint() # focal position + pp: Tuple[float, float, float] = (x, y, z) # point where the user clicked # Vector from camera position to user clicked point - vcp = [j-i for i,j in zip(pc, pp)] + vcp: Tuple[float, float, float] = tuple([j-i for i,j in zip(pc, pp)]) # Vector from camera position to camera focal point - vcf = [j-i for i,j in zip(pc, pf)] + vcf: Tuple[float, float, float] = tuple([j-i for i,j in zip(pc, pf)]) # the vector where the perpendicular vector will be given - n = [0,0,0] + n: List[float] = [0,0,0] # The cross, or vectorial product, give a vector perpendicular to vcp # and vcf, in this case this vector will be in horizontal, this vector # will be stored in the variable "n" @@ -557,61 +581,63 @@ def GetRepresentation(self, x, y, z): # then normalize n to only indicate the direction of this vector vtkMath.Normalize(n) # then - p1 = [i*self.size + j for i,j in zip(n, pp)] - p2 = [i*-self.size + j for i,j in zip(n, pp)] + p1: Tuple[float, float, float] = tuple([i*self.size + j for i,j in zip(n, pp)]) + p2: Tuple[float, float, float] = tuple([i*-self.size + j for i,j in zip(n, pp)]) - sh = vtkLineSource() + sh: vtkLineSource = vtkLineSource() sh.SetPoint1(p1) sh.SetPoint2(p2) - n = [0,0,0] - vcn = [j-i for i,j in zip(p1, pc)] + n: List[float] = [0,0,0] + vcn: Tuple[float, float, float] = tuple([j-i for i,j in zip(p1, pc)]) vtkMath.Cross(vcp, vcn, n) vtkMath.Normalize(n) - p3 = [i*self.size + j for i,j in zip(n, pp)] - p4 = [i*-self.size +j for i,j in zip(n, pp)] + p3: Tuple[float, float, float] = tuple([i*self.size + j for i,j in zip(n, pp)]) + p4: Tuple[float, float, float] = tuple([i*-self.size +j for i,j in zip(n, pp)]) - sv = vtkLineSource() + sv: vtkLineSource = vtkLineSource() sv.SetPoint1(p3) sv.SetPoint2(p4) - cruz = vtkAppendPolyData() + cruz: vtkAppendPolyData = vtkAppendPolyData() cruz.AddInputData(sv.GetOutput()) cruz.AddInputData(sh.GetOutput()) - c = vtkCoordinate() + c: vtkCoordinate = vtkCoordinate() c.SetCoordinateSystemToWorld() - m = vtkPolyDataMapper2D() + m: vtkPolyDataMapper2D = vtkPolyDataMapper2D() m.SetInputConnection(cruz.GetOutputPort()) m.SetTransformCoordinate(c) - a = vtkActor2D() + a: vtkActor2D = vtkActor2D() a.SetMapper(m) a.GetProperty().SetColor(self.colour) return a -class LinearMeasure(object): - def __init__(self, colour=(1, 0, 0), representation=None): - self.colour = colour - self.points = [] - self.point_actor1 = None - self.point_actor2 = None - self.line_actor = None - self.text_actor = None - self.renderer = None - self.layer = 0 + + +class LinearMeasure: + def __init__(self, colour: Tuple[float, float, float] = (1, 0, 0), representation: Optional[CirclePointRepresentation] = None) -> None: + self.colour: Tuple[float, float, float] = colour + self.points: List[Tuple[float, float, float]] = [] + self.point_actor1: Optional[vtkActor2D] = None + self.point_actor2: Optional[vtkActor2D] = None + self.line_actor: Optional[vtkActor2D] = None + self.text_actor: Optional[vtkActor2D] = None + self.renderer: Optional[vtkRenderer] = None + self.layer: int = 0 if not representation: representation = CirclePointRepresentation(colour) - self.representation = representation + self.representation: CirclePointRepresentation = representation - def IsComplete(self): + def IsComplete(self) -> bool: """ Is this measure complete? """ return not self.point_actor2 is None - def AddPoint(self, x, y, z): + def AddPoint(self, x: float, y: float, z: float) -> Tuple[Optional[vtkActor2D], ...]: if not self.point_actor1: self.SetPoint1(x, y, z) return (self.point_actor1, ) @@ -619,7 +645,7 @@ def AddPoint(self, x, y, z): self.SetPoint2(x, y, z) return (self.point_actor2, self.line_actor, self.text_actor) - def SetPoint1(self, x, y, z): + def SetPoint1(self, x: float, y: float, z: float) -> None: if len(self.points) == 0: self.points.append((x, y, z)) self.point_actor1 = self.representation.GetRepresentation(x, y, z) @@ -634,7 +660,8 @@ def SetPoint1(self, x, y, z): self.Remove() self.point_actor1 = self.representation.GetRepresentation(*self.points[0]) - def SetPoint2(self, x, y, z): + + def SetPoint2(self, x: float, y: float, z: float) -> None: if len(self.points) == 1: self.points.append((x, y, z)) self.point_actor2 = self.representation.GetRepresentation(*self.points[1]) @@ -646,11 +673,11 @@ def SetPoint2(self, x, y, z): self.point_actor2 = self.representation.GetRepresentation(*self.points[1]) self.CreateMeasure() - def CreateMeasure(self): + def CreateMeasure(self) -> None: self._draw_line() self._draw_text() - def _draw_line(self): + def _draw_line(self) -> None: line = vtkLineSource() line.SetPoint1(self.points[0]) line.SetPoint2(self.points[1]) @@ -667,7 +694,7 @@ def _draw_line(self): a.GetProperty().SetColor(self.colour) self.line_actor = a - def _draw_text(self): + def _draw_text(self) -> None: p1, p2 = self.points text = ' %.3f mm ' % \ math.sqrt(vtkMath.Distance2BetweenPoints(p1, p2)) @@ -689,7 +716,7 @@ def _draw_text(self): a.GetProperty().SetOpacity(0.75) self.text_actor = a - def draw_to_canvas(self, gc, canvas): + def draw_to_canvas(self, gc: wx.GraphicsContext, canvas: wx.Window) -> None: """ Draws to an wx.GraphicsContext. @@ -713,17 +740,18 @@ def draw_to_canvas(self, gc, canvas): txt = u"%.3f mm" % self.GetValue() canvas.draw_text_box(txt, ((points[0][0]+points[1][0])/2.0, (points[0][1]+points[1][1])/2.0), txt_colour=MEASURE_TEXT_COLOUR, bg_colour=MEASURE_TEXTBOX_COLOUR) - def GetNumberOfPoints(self): + def GetNumberOfPoints(self) -> int: return len(self.points) - def GetValue(self): + def GetValue(self) -> float: if self.IsComplete(): p1, p2 = self.points return math.sqrt(vtkMath.Distance2BetweenPoints(p1, p2)) else: return 0.0 - def SetRenderer(self, renderer): + + def SetRenderer(self, renderer: vtkRenderer) -> None: if self.point_actor1: self.renderer.RemoveActor(self.point_actor1) renderer.AddActor(self.point_actor1) @@ -742,13 +770,13 @@ def SetRenderer(self, renderer): self.renderer = renderer - def SetVisibility(self, v): + def SetVisibility(self, v: bool) -> None: self.point_actor1.SetVisibility(v) self.point_actor2.SetVisibility(v) self.line_actor.SetVisibility(v) self.text_actor.SetVisibility(v) - def GetActors(self): + def GetActors(self) -> List[vtkActor]: """ Get the actors already created in this measure. """ @@ -763,33 +791,35 @@ def GetActors(self): actors.append(self.text_actor) return actors - def Remove(self): + def Remove(self) -> None: actors = self.GetActors() Publisher.sendMessage("Remove actors " + str(const.SURFACE), actors=actors) - def __del__(self): + def __del__(self) -> None: self.Remove() -class AngularMeasure(object): - def __init__(self, colour=(1, 0, 0), representation=None): - self.colour = colour - self.points = [] - self.number_of_points = 0 - self.point_actor1 = None - self.point_actor2 = None - self.point_actor3 = None - self.line_actor = None - self.text_actor = None - self.layer = 0 + + +class AngularMeasure: + def __init__(self, colour: Tuple[float, float, float] = (1, 0, 0), representation: Union[None, CirclePointRepresentation] = None) -> None: + self.colour: Tuple[float, float, float] = colour + self.points: List[Tuple[float, float, float]] = [] + self.number_of_points: int = 0 + self.point_actor1: Union[None, vtkActor] = None + self.point_actor2: Union[None, vtkActor] = None + self.point_actor3: Union[None, vtkActor] = None + self.line_actor: Union[None, vtkActor] = None + self.text_actor: Union[None, vtkActor] = None + self.layer: int = 0 if not representation: representation = CirclePointRepresentation(colour) - self.representation = representation + self.representation: CirclePointRepresentation = representation - def IsComplete(self): + def IsComplete(self) -> bool: return not self.point_actor3 is None - def AddPoint(self, x, y, z): + def AddPoint(self, x: float, y: float, z: float) -> Tuple[Union[None, vtkActor], ...]: if not self.point_actor1: self.SetPoint1(x, y, z) return (self.point_actor1,) @@ -800,7 +830,7 @@ def AddPoint(self, x, y, z): self.SetPoint3(x, y, z) return (self.point_actor3, self.line_actor, self.text_actor) - def SetPoint1(self, x, y, z): + def SetPoint1(self, x: float, y: float, z: float) -> None: if self.number_of_points == 0: self.points.append((x, y, z)) self.number_of_points = 1 @@ -818,7 +848,7 @@ def SetPoint1(self, x, y, z): self.point_actor1 = self.representation.GetRepresentation(*self.points[0]) self.point_actor2 = self.representation.GetRepresentation(*self.points[1]) - def SetPoint2(self, x, y, z): + def SetPoint2(self, x: float, y: float, z: float) -> None: if self.number_of_points == 1: self.number_of_points = 2 self.points.append((x, y, z)) @@ -836,7 +866,8 @@ def SetPoint2(self, x, y, z): self.point_actor1 = self.representation.GetRepresentation(*self.points[0]) self.point_actor2 = self.representation.GetRepresentation(*self.points[1]) - def SetPoint3(self, x, y, z): + + def SetPoint3(self, x: float, y: float, z: float) -> None: if self.number_of_points == 2: self.number_of_points = 3 self.points.append((x, y, z)) @@ -855,11 +886,11 @@ def SetPoint3(self, x, y, z): self.point_actor1 = self.representation.GetRepresentation(*self.points[0]) self.point_actor2 = self.representation.GetRepresentation(*self.points[1]) - def CreateMeasure(self): + def CreateMeasure(self) -> None: self._draw_line() self._draw_text() - def _draw_line(self): + def _draw_line(self) -> None: line1 = vtkLineSource() line1.SetPoint1(self.points[0]) line1.SetPoint2(self.points[1]) @@ -887,12 +918,9 @@ def _draw_line(self): a.GetProperty().SetColor(self.colour) self.line_actor = a - def DrawArc(self): - - d1 = math.sqrt(vtkMath.Distance2BetweenPoints(self.points[0], - self.points[1])) - d2 = math.sqrt(vtkMath.Distance2BetweenPoints(self.points[2], - self.points[1])) + def DrawArc(self) -> vtkArcSource: + d1: float = math.sqrt(vtkMath.Distance2BetweenPoints(self.points[0], self.points[1])) + d2: float = math.sqrt(vtkMath.Distance2BetweenPoints(self.points[2], self.points[1])) if d1 < d2: d = d1 @@ -916,10 +944,9 @@ def DrawArc(self): arc.SetResolution(20) return arc - def _draw_text(self): - text = u' %.3f ' % \ - self.CalculateAngle() - x,y,z= self.points[1] + def _draw_text(self) -> None: + text: str = u' %.3f ' % self.CalculateAngle() + x, y, z = self.points[1] textsource = vtkTextSource() textsource.SetText(text) textsource.SetBackgroundColor((250/255.0, 247/255.0, 218/255.0)) @@ -932,10 +959,10 @@ def _draw_text(self): a.SetMapper(m) a.DragableOn() a.GetPositionCoordinate().SetCoordinateSystemToWorld() - a.GetPositionCoordinate().SetValue(x,y,z) + a.GetPositionCoordinate().SetValue(x, y, z) self.text_actor = a - def draw_to_canvas(self, gc, canvas): + def draw_to_canvas(self, gc: wx.GraphicsContext, canvas: wx.Canvas) -> None: """ Draws to an wx.GraphicsContext. @@ -944,21 +971,29 @@ def draw_to_canvas(self, gc, canvas): canvas: the canvas it's being drawn. """ - coord = vtkCoordinate() - points = [] + coord: vtkCoordinate = vtkCoordinate() + points: List[Tuple[float, float]] = [] for p in self.points: coord.SetValue(p) + cx: float + cy: float cx, cy = coord.GetComputedDoubleDisplayValue(canvas.evt_renderer) # canvas.draw_circle((cx, cy), 2.5) points.append((cx, cy)) if len(points) > 1: for (p0, p1) in zip(points[:-1:], points[1::]): + r: float + g: float + b: float r, g, b = self.colour canvas.draw_line(p0, p1, colour=(r*255, g*255, b*255, 255)) if len(points) == 3: - txt = u"%.3f° / %.3f°" % (self.GetValue(), 360.0 - self.GetValue()) + txt: str = u"%.3f° / %.3f°" % (self.GetValue(), 360.0 - self.GetValue()) + r: float + g: float + b: float r, g, b = self.colour canvas.draw_arc(points[1], points[0], points[2], line_colour=(int(r*255), int(g*255), int(b*255), 255)) @@ -966,27 +1001,27 @@ def draw_to_canvas(self, gc, canvas): txt_colour=MEASURE_TEXT_COLOUR, bg_colour=MEASURE_TEXTBOX_COLOUR) - def GetNumberOfPoints(self): + def GetNumberOfPoints(self) -> int: return self.number_of_points - def GetValue(self): + def GetValue(self) -> float: if self.IsComplete(): return self.CalculateAngle() else: return 0.0 - def SetVisibility(self, v): + def SetVisibility(self, v: bool) -> None: self.point_actor1.SetVisibility(v) self.point_actor2.SetVisibility(v) self.point_actor3.SetVisibility(v) self.line_actor.SetVisibility(v) self.text_actor.SetVisibility(v) - def GetActors(self): + def GetActors(self) -> List[vtkActor]: """ Get the actors already created in this measure. """ - actors = [] + actors: List[vtkActor] = [] if self.point_actor1: actors.append(self.point_actor1) if self.point_actor2: @@ -999,7 +1034,7 @@ def GetActors(self): actors.append(self.text_actor) return actors - def CalculateAngle(self): + def CalculateAngle(self) -> float: """ Calculate the angle between 2 vectors in 3D space. It is based on law of cosines for vector. @@ -1007,21 +1042,21 @@ def CalculateAngle(self): product between the magnitude from that vectors. Then the angle is inverse cosine. """ - v1 = [j-i for i,j in zip(self.points[0], self.points[1])] - v2 = [j-i for i,j in zip(self.points[2], self.points[1])] + v1: list = [j-i for i,j in zip(self.points[0], self.points[1])] + v2: list = [j-i for i,j in zip(self.points[2], self.points[1])] try: - cos = vtkMath.Dot(v1, v2)/(vtkMath.Norm(v1)*vtkMath.Norm(v2)) + cos: float = vtkMath.Dot(v1, v2)/(vtkMath.Norm(v1)*vtkMath.Norm(v2)) except ZeroDivisionError: return 0.0 - angle = math.degrees(math.acos(cos)) + angle: float = math.degrees(math.acos(cos)) return angle def Remove(self): - actors = self.GetActors() + actors: list = self.GetActors() Publisher.sendMessage("Remove actors " + str(const.SURFACE), actors=actors) - def SetRenderer(self, renderer): + def SetRenderer(self, renderer: object): if self.point_actor1: self.renderer.RemoveActor(self.point_actor1) renderer.AddActor(self.point_actor1) @@ -1042,7 +1077,7 @@ def SetRenderer(self, renderer): self.renderer.RemoveActor(self.text_actor) renderer.AddActor(self.text_actor) - self.renderer = renderer + self.renderer: object = renderer def __del__(self): self.Remove() @@ -1116,7 +1151,7 @@ def set_density_values(self, _min, _max, _mean, _std, _area): self._std = _std self._area = _area - text = _('Area: %.3f\n' + text = _File('Area: %.3f\n' 'Min: %.3f\n' 'Max: %.3f\n' 'Mean: %.3f\n' From d35fd467c6b3a4a9a6e5c5588952c3054013d9d2 Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Sun, 16 Apr 2023 01:21:27 +0530 Subject: [PATCH 04/16] added type annotations --- invesalius/data/bases.py | 2 +- invesalius/data/brainmesh_handler.py | 7 +- invesalius/data/coordinates.py | 32 +- invesalius/data/coregistration.py | 36 +- invesalius/data/cursor_actors.py | 16 +- invesalius/data/e_field.py | 5 + invesalius/data/editor.py | 12 +- invesalius/data/geometry.py | 12 +- invesalius/data/imagedata_utils.py | 30 +- invesalius/data/mask.py | 36 +- invesalius/data/measures.py | 417 +++++++++++----------- invesalius/data/orientation.py | 95 ++--- invesalius/data/polydata_utils.py | 69 ++-- invesalius/data/record_coords.py | 43 +-- invesalius/data/serial_port_connection.py | 31 +- 15 files changed, 434 insertions(+), 409 deletions(-) diff --git a/invesalius/data/bases.py b/invesalius/data/bases.py index 954011ee3..610f5c9af 100644 --- a/invesalius/data/bases.py +++ b/invesalius/data/bases.py @@ -56,7 +56,7 @@ def base_creation_old(fiducials: np.ndarray) -> Tuple[np.matrix, np.ndarray, np. [g2[0], g2[1], g2[2]], [g3[0], g3[1], g3[2]]]) - m_inv = m.I + m_inv: matrix = m.I return m, q, m_inv diff --git a/invesalius/data/brainmesh_handler.py b/invesalius/data/brainmesh_handler.py index 09f75e6a4..4b4f8e98b 100644 --- a/invesalius/data/brainmesh_handler.py +++ b/invesalius/data/brainmesh_handler.py @@ -1,6 +1,7 @@ import pyacvd import pyvista import numpy as np +from typing import Tuple, List, Dict, Any, Union, Optional, overload from vtkmodules.vtkCommonCore import vtkFloatArray from vtkmodules.vtkCommonDataModel import ( vtkCellLocator, @@ -36,6 +37,10 @@ vtkColorSeries, vtkNamedColors ) +#import for vtkImageData +from vtkmodules.vtkCommonDataModel import ( + vtkImageData, +) import invesalius.data.slice_ as sl from invesalius.data.converters import to_vtk import invesalius.data.vtk_utils as vtk_utils @@ -178,7 +183,7 @@ def _do_surface_creation(self, mask: vtkImageData, mask_sFormMatrix: Optional[vt def CreateTransformedVTKAffine(self) -> vtkMatrix4x4: - affine_transformed: numpy.ndarray = self.affine.copy() + affine_transformed: np.ndarray = self.affine.copy() matrix_shape: Tuple[int, int] = tuple(self.inv_proj.matrix_shape) affine_transformed[1, -1] -= matrix_shape[1] diff --git a/invesalius/data/coordinates.py b/invesalius/data/coordinates.py index 3fc025bfb..d8ea8b781 100644 --- a/invesalius/data/coordinates.py +++ b/invesalius/data/coordinates.py @@ -21,7 +21,7 @@ import numpy as np import threading import wx -from typing import Tuple, List , Optional +from typing import Tuple, List , Optional, Any, Union, Dict, Callable import invesalius.data.transformations as tr import invesalius.constants as const @@ -112,8 +112,8 @@ def PolarisP4Coord(tracker_connection, tracker_id: str, ref_mode: str) -> Tuple[ if probe[:7] == "MISSING": coord1 = np.hstack(([0, 0, 0], [0, 0, 0])) else: - q = [int(probe[i:i + 6]) * 0.0001 for i in range(0, 24, 6)] - t = [int(probe[i:i + 7]) * 0.01 for i in range(24, 45, 7)] + q: list[float] = [int(probe[i:i + 6]) * 0.0001 for i in range(0, 24, 6)] + t: list[float] = [int(probe[i:i + 7]) * 0.01 for i in range(24, 45, 7)] angles_probe = np.degrees(tr.euler_from_quaternion(q, axes='rzyx')) trans_probe = np.array(t).astype(float) coord1 = np.hstack((trans_probe, angles_probe)) @@ -303,12 +303,12 @@ def PolhemusUSBCoord(tracker_connection, tracker_id: int, ref_mode: bool) -> Opt if ref_mode: data = trck.read(endpoint.bEndpointAddress, 2 * endpoint.wMaxPacketSize) - data = str2float(data.tostring()) + data: List[float] = str2float(data.tostring()) # six coordinates of first and second sensor: x, y, z and alfa, beta and gama # jump one element for reference to avoid the sensor ID returned by Polhemus - probe = data[0], data[1], data[2], data[3], data[4], data[5], data[6] - reference = data[7], data[8], data[9], data[10], data[11], data[12], data[13] + probe: tuple[float, float, float, float, float, float, float] = data[0], data[1], data[2], data[3], data[4], data[5], data[6] + reference: tuple[float, float, float, float, float, float, float] = data[7], data[8], data[9], data[10], data[11], data[12], data[13] if probe.all() and reference.all(): coord = dynamic_reference(probe, reference) @@ -345,14 +345,14 @@ def PolhemusSerialCoord(tracker_connection, tracker_id: int, ref_mode: bool) -> data = lines[0] data = data.replace(str.encode('-'), str.encode(' -')) data = [s for s in data.split()] - data = [float(s) for s in data[1:len(data)]] + data: list[float] = [float(s) for s in data[1:len(data)]] probe = np.array([data[0] * scale[0], data[1] * scale[1], data[2] * scale[2], data[3], data[4], data[5]]) if ref_mode: data2 = lines[1] data2 = data2.replace(str.encode('-'), str.encode(' -')) data2 = [s for s in data2.split()] - data2 = [float(s) for s in data2[1:len(data2)]] + data2: list[float] = [float(s) for s in data2[1:len(data2)]] reference = np.array( [data2[0] * scale[0], data2[1] * scale[1], data2[2] * scale[2], data2[3], data2[4], data2[5]]) else: @@ -395,8 +395,8 @@ def DebugCoordRandom(tracker_connection, tracker_id: int, ref_mode: bool) -> Tup # # else: - dx = [-30, 30] - dt = [-180, 180] + dx: list[int] = [-30, 30] + dt: list[int] = [-180, 180] coord1 = np.array([uniform(*dx), uniform(*dx), uniform(*dx), uniform(*dt), uniform(*dt), uniform(*dt)]) @@ -437,7 +437,7 @@ def coordinates_to_transformation_matrix(position: List[float], orientation: Lis a, b, g = np.radians(orientation) r_ref =a, b, g, axes=axes - t_ref = (position) + t_ref: List[float] = (position) m_img = np.dot(t_ref, r_ref) @@ -453,7 +453,7 @@ def transformation_matrix_to_coordinates(matrix: np.ndarray, axes: str = 'sxyz') :param axes: The order in which the rotations are done for the axes. See transformations.py for details. Defaults to 'sxyz'. :return: The position (a vector of length 3) and Euler angles for the orientation in degrees (a vector of length 3). """ - angles = tr.euler_from_matrix(matrix, axes=axes) + angles: tuple[float, float, float] = tr.euler_from_matrix(matrix, axes=axes) angles_as_deg = np.degrees(angles) translation = tr.translation_from_matrix(matrix) @@ -477,14 +477,14 @@ def dynamic_reference(probe: List[float], reference: List[float]) -> Tuple[float """ a, b, g = np.radians(reference[3:6]) - vet = np.asmatrix(probe[0:3] - reference[0:3]) + vet: matrix = np.asmatrix(probe[0:3] - reference[0:3]) # vet = np.mat(vet.reshape(3, 1)) # Attitude matrix given by Patriot manual # a: rotation of plane (X, Y) around Z axis (azimuth) # b: rotation of plane (X', Z) around Y' axis (elevation) # a: rotation of plane (Y', Z') around X'' axis (roll) - m_rot = np.mat([[np.cos(a) * np.cos(b), np.sin(b) * np.sin(g) * np.cos(a) - np.cos(g) * np.sin(a), + m_rot: matrix = np.mat([[np.cos(a) * np.cos(b), np.sin(b) * np.sin(g) * np.cos(a) - np.cos(g) * np.sin(a), np.cos(a) * np.sin(b) * np.cos(g) + np.sin(a) * np.sin(g)], [np.cos(b) * np.sin(a), np.sin(b) * np.sin(g) * np.sin(a) + np.cos(g) * np.cos(a), np.cos(g) * np.sin(b) * np.sin(a) - np.sin(g) * np.cos(a)], @@ -592,7 +592,7 @@ def offset_coordinate(p_old: Tuple[float, float, float], norm_vec: Tuple[float, class ReceiveCoordinates(threading.Thread): - def __init__(self, tracker_connection, tracker_id, TrackerCoordinates, event): + def __init__(self, tracker_connection, tracker_id, TrackerCoordinates, event) -> None: threading.Thread.__init__(self, name='ReceiveCoordinates') self.tracker_connection = tracker_connection @@ -600,7 +600,7 @@ def __init__(self, tracker_connection, tracker_id, TrackerCoordinates, event): self.event = event self.TrackerCoordinates = TrackerCoordinates - def run(self): + def run(self) -> None: while not self.event.is_set(): coord_raw, markers_flag = GetCoordinatesForThread(self.tracker_connection, self.tracker_id, const.DEFAULT_REF_MODE) self.TrackerCoordinates.SetCoordinates(coord_raw, markers_flag) diff --git a/invesalius/data/coregistration.py b/invesalius/data/coregistration.py index 0534b731d..0bcd73c18 100644 --- a/invesalius/data/coregistration.py +++ b/invesalius/data/coregistration.py @@ -194,10 +194,10 @@ def corregistrate_object_dynamic(inp: Tuple[np.ndarray, np.ndarray, np.ndarray, m_img = apply_icp(m_img, icp) # compute rotation angles - angles = tr.euler_from_matrix(m_img, axes='sxyz') + angles: tuple[float, float, float] = tr.euler_from_matrix(m_img, axes='sxyz') # create output coordinate list - coord = m_img[0, -1], m_img[1, -1], m_img[2, -1], \ + coord: tuple = m_img[0, -1], m_img[1, -1], m_img[2, -1], \ np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2]) return coord, m_img @@ -236,10 +236,10 @@ def corregistrate_dynamic(inp: Tuple[np.ndarray, int], coord_raw: np.ndarray, re m_img = apply_icp(m_img, icp) # compute rotation angles - angles = tr.euler_from_matrix(m_img, axes='sxyz') + angles: tuple[float, float, float] = tr.euler_from_matrix(m_img, axes='sxyz') # create output coordinate list - coord = m_img[0, -1], m_img[1, -1], m_img[2, -1],\ + coord: tuple = m_img[0, -1], m_img[1, -1], m_img[2, -1],\ np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2]) return coord, m_img @@ -270,10 +270,10 @@ def ComputeRelativeDistanceToTarget(target_coord: Optional[np.ndarray] = None, i m_relative_target = np.linalg.inv(m_target) @ m_img # compute rotation angles - angles = tr.euler_from_matrix(m_relative_target, axes='sxyz') + angles: tuple[float, float, float] = tr.euler_from_matrix(m_relative_target, axes='sxyz') # create output coordinate list - distance = [m_relative_target[0, -1], m_relative_target[1, -1], m_relative_target[2, -1], \ + distance: list = [m_relative_target[0, -1], m_relative_target[1, -1], m_relative_target[2, -1], \ np.degrees(angles[0]), np.degrees(angles[1]), np.degrees(angles[2])] return tuple(distance) @@ -285,23 +285,23 @@ def ComputeRelativeDistanceToTarget(target_coord: Optional[np.ndarray] = None, i class CoordinateCorregistrate(threading.Thread): def __init__(self, ref_mode_id: int, tracker: Any, coreg_data: Any, view_tracts: Any, queues: List[Any], event: threading.Event, sle: Any, tracker_id: int, target: Optional[List[float]], icp: Tuple[bool, np.ndarray], e_field_loaded: bool): threading.Thread.__init__(self, name='CoordCoregObject') - self.ref_mode_id = ref_mode_id + self.ref_mode_id: int = ref_mode_id self.tracker = tracker self.coreg_data = coreg_data self.coord_queue = queues[0] self.view_tracts = view_tracts self.coord_tracts_queue = queues[1] self.efield_queue = queues[4] - self.e_field_loaded = e_field_loaded - self.event = event + self.e_field_loaded: bool = e_field_loaded + self.event: Event = event self.sle = sle self.icp_queue = queues[2] self.object_at_target_queue = queues[3] - self.use_icp = icp[0] + self.use_icp: bool = icp[0] self.m_icp = icp[1] self.last_coord = None - self.tracker_id = tracker_id - self.target = target + self.tracker_id: int = tracker_id + self.target:Union[List[float], None] = target self.target_flag = False if self.target is not None: @@ -346,7 +346,7 @@ def run(self) -> None: coord = self.last_coord + (self.target - self.last_coord) * 0.05 self.last_coord = coord - angles = [np.radians(coord[3]), np.radians(coord[4]), np.radians(coord[5])] + angles: list = [np.radians(coord[3]), np.radians(coord[4]), np.radians(coord[5])] translate = coord[0:3] m_img = tr.compose_matrix(angles=angles, translate=translate) @@ -373,19 +373,19 @@ def run(self) -> None: class CoordinateCorregistrateNoObject(threading.Thread): def __init__(self, ref_mode_id: int, tracker: Any, coreg_data: Any, view_tracts: bool, queues: List[Any], event: threading.Event, sle: float, icp: Any, e_field_loaded: bool) -> None: threading.Thread.__init__(self, name='CoordCoregNoObject') - self.ref_mode_id = ref_mode_id + self.ref_mode_id: int = ref_mode_id self.tracker = tracker self.coreg_data = coreg_data self.coord_queue = queues[0] - self.view_tracts = view_tracts + self.view_tracts: bool = view_tracts self.coord_tracts_queue = queues[1] - self.event = event - self.sle = sle + self.event: Event = event + self.sle: float = sle self.icp_queue = queues[2] self.use_icp = icp.use_icp self.m_icp = icp.m_icp self.efield_queue = queues[3] - self.e_field_loaded = e_field_loaded + self.e_field_loaded: bool = e_field_loaded def run(self) -> None: coreg_data: Any = self.coreg_data diff --git a/invesalius/data/cursor_actors.py b/invesalius/data/cursor_actors.py index 910f7065b..cfebc96eb 100644 --- a/invesalius/data/cursor_actors.py +++ b/invesalius/data/cursor_actors.py @@ -20,7 +20,7 @@ import math import numpy -from typing import Any, Tuple, Union , Optional, List +from typing import Any, Tuple, Union , Optional, List, Dict, Literal from vtkmodules.util import numpy_support from vtkmodules.vtkCommonCore import vtkLookupTable, vtkVersion @@ -36,7 +36,7 @@ import invesalius.constants as const -ORIENTATION = {'AXIAL': 2, +ORIENTATION: Dict[str, int] = {'AXIAL': 2, 'CORONAL': 1, 'SAGITAL': 0} @@ -55,7 +55,7 @@ def to_vtk(n_array: numpy.ndarray, spacing: Tuple[float, float, float], slice_nu v_image = numpy_support.numpy_to_vtk(n_array.flat) if orientation == 'AXIAL': - extent = (0, dx -1, 0, dy -1, slice_number, slice_number + dz - 1) + extent: tuple[Literal[0], int, Literal[0], int, int, int] = (0, dx -1, 0, dy -1, slice_number, slice_number + dz - 1) elif orientation == 'SAGITAL': extent = (slice_number, slice_number + dx - 1, 0, dy - 1, 0, dz - 1) elif orientation == 'CORONAL': @@ -104,7 +104,7 @@ def __init__(self) -> None: self._calculate_area_pixels() def SetSize(self, diameter: float) -> None: - self.radius = diameter/2.0 + self.radius: float = diameter/2.0 self._build_actor() self._calculate_area_pixels() @@ -143,7 +143,7 @@ def SetPosition(self, position: Tuple[int, int, int]) -> None: x = px - tx / 2.0 else: x = px - tx / 2.0 + self.spacing[0] / 2.0 - z = pz + z: int = pz if self.mapper: x += sx / 2.0 @@ -159,7 +159,7 @@ def SetPosition(self, position: Tuple[int, int, int]) -> None: x = px - tx / 2.0 else: x = px - tx / 2.0 + self.spacing[0] / 2.0 - y = py + y: int = py if self.mapper: x += sx / 2.0 @@ -176,7 +176,7 @@ def SetPosition(self, position: Tuple[int, int, int]) -> None: z = pz - tz / 2.0 else: z = pz - tz / 2.0 + self.spacing[2] / 2.0 - x = px + x: int = px if self.mapper: y += sy / 2.0 @@ -254,7 +254,7 @@ def _set_colour(self, imagedata: vtkImageData, colour: Tuple[float, float, float class CursorCircle(CursorBase): # TODO: Think and try to change this class to an actor # CursorCircleActor(vtkActor) - def __init__(self): + def __init__(self) -> None: self.radius = 15.0 def __init__(self) -> None: diff --git a/invesalius/data/e_field.py b/invesalius/data/e_field.py index 6202fcee1..96030cc99 100644 --- a/invesalius/data/e_field.py +++ b/invesalius/data/e_field.py @@ -3,9 +3,14 @@ import time from typing import Any, List, Union + + + + import numpy as np from vtkmodules.vtkCommonCore import vtkIdList + def Get_coil_position(m_img: np.ndarray) -> list: # coil position cp : the center point at the bottom of the coil casing, # corresponds to the origin of the coil template. diff --git a/invesalius/data/editor.py b/invesalius/data/editor.py index 51b698c9e..706aa8b48 100644 --- a/invesalius/data/editor.py +++ b/invesalius/data/editor.py @@ -18,13 +18,19 @@ #-------------------------------------------------------------------------- from invesalius.pubsub import pub as Publisher -from typing import Tuple +from typing import Tuple, Optional, Any from vtkmodules.vtkCommonCore import vtkLookupTable from vtkmodules.vtkImagingCore import vtkImageBlend, vtkImageMapToColors from vtkmodules.vtkInteractionStyle import vtkInteractorStyleImage from vtkmodules.vtkRenderingCore import vtkImageActor, vtkCellPicker, vtkImageMapper +from vtkmodules.vtkCommonDataModel import vtkImageData +from vtkmodules.vtkRenderingCore import vtkRenderer +from vtkmodules.vtkRenderingCore import vtkRenderWindowInteractor + + + AXIAL: int = 2 CORONAL: int = 1 SAGITAL: int = 0 @@ -183,7 +189,7 @@ def From3dToImagePixel(self, mPos: Tuple[int, int], pPos: Tuple[float, float, fl - def __init__(self): + def __init__(self) -> None: self.image_original: vtkImageData = None self.image_threshold: vtkImageData = None self.slice: int = 0 @@ -245,7 +251,7 @@ def DoOperation(self, xc: int, yc: int, zc: int) -> None: """ extent = self.image.GetWholeExtent() cursor = self.cursor - b = [0,0,0,0,0,0] + b: list[int] = [0,0,0,0,0,0] self.actor.GetDisplayBounds(b) xs, ys, zs = self.image.GetSpacing() try: diff --git a/invesalius/data/geometry.py b/invesalius/data/geometry.py index 866b163e9..17c1640f0 100644 --- a/invesalius/data/geometry.py +++ b/invesalius/data/geometry.py @@ -19,7 +19,7 @@ # -------------------------------------------------------------------------- import math -from typing import List, Tuple, Union, Optional, Dict, Any +from typing import List, Tuple, Union, Optional, Dict, Any import numpy as np from vtkmodules.vtkRenderingCore import vtkCoordinate @@ -36,7 +36,7 @@ class Box(metaclass=utils.Singleton): coordinates (min and max) of box used in crop-mask. """ - def __init__(self): + def __init__(self) -> None: self.xi = None # type: float self.xf = None # type: float @@ -585,13 +585,13 @@ def distance_from_point_line(self, p1: Tuple[float, float, float], p2: Tuple[flo A = np.array(pc) - np.array(p1) B = np.array(p2) - np.array(p1) # Calculate the size from those vectors - len_A = np.linalg.norm(A) - len_B = np.linalg.norm(B) + len_A: float = np.linalg.norm(A) + len_B: float = np.linalg.norm(B) # calculate the angle theta (in radians) between those vector - theta = math.acos(np.dot(A, B) / (len_A * len_B)) + theta: float = math.acos(np.dot(A, B) / (len_A * len_B)) # Using the sin from theta, calculate the adjacent leg, which is the # distance from the point to the line - distance = math.sin(theta) * len_A + distance: float = math.sin(theta) * len_A return distance def Coord3DtoDisplay(self, x: float, y: float, z: float, canvas: Any) -> Tuple[float, float]: diff --git a/invesalius/data/imagedata_utils.py b/invesalius/data/imagedata_utils.py index 8d1831f4a..ae07daa71 100644 --- a/invesalius/data/imagedata_utils.py +++ b/invesalius/data/imagedata_utils.py @@ -87,8 +87,8 @@ def ResampleImage2D(imagedata: vtkImageResample, px: float = None, py: float = N dimensions = imagedata.GetDimensions() if resolution_percentage: - factor_x = resolution_percentage - factor_y = resolution_percentage + factor_x: float = resolution_percentage + factor_y: float = resolution_percentage else: if abs(extent[1] - extent[3]) < abs(extent[3] - extent[5]): f = extent[1] @@ -133,7 +133,7 @@ def resize_slice(im_array: numpy.ndarray, resolution_percentage: float) -> numpy def resize_image_array(image: np.ndarray, resolution_percentage: float, as_mmap: bool = False) -> np.ndarray: out = zoom(image, resolution_percentage, image.dtype, order=2) if as_mmap: - fname = tempfile.mktemp(suffix="_resized") + fname: str = tempfile.mktemp(suffix="_resized") out_mmap = np.memmap(fname, shape=out.shape, dtype=out.dtype, mode="w+") out_mmap[:] = out return out_mmap @@ -290,7 +290,7 @@ def create_dicom_thumbnails(image: object, window: Optional[float] = None, level thumbnail_paths.append(thumbnail_path) return thumbnail_paths else: - thumbnail_path = tempfile.mktemp(prefix="thumb_", suffix=".png") + thumbnail_path: str = tempfile.mktemp(prefix="thumb_", suffix=".png") if pf.GetSamplesPerPixel() == 1: thumb_image = zoom(np_image, 0.25) thumb_image = np.array( @@ -323,13 +323,13 @@ def bitmap2memmap(files: List[str], slice_size: Tuple[int, int], orientation: st len(files) - 1, dialog_type="ProgressDialog" ) - temp_file = tempfile.mktemp() + temp_file: str = tempfile.mktemp() if orientation == "SAGITTAL": if resolution_percentage == 1.0: - shape = slice_size[1], slice_size[0], len(files) + shape: tuple[int, int, int] = slice_size[1], slice_size[0], len(files) else: - shape = ( + shape: tuple[int, int, int] = ( math.ceil(slice_size[1] * resolution_percentage), math.ceil(slice_size[0] * resolution_percentage), len(files), @@ -337,18 +337,18 @@ def bitmap2memmap(files: List[str], slice_size: Tuple[int, int], orientation: st elif orientation == "CORONAL": if resolution_percentage == 1.0: - shape = slice_size[1], len(files), slice_size[0] + shape: tuple[int, int, int] = slice_size[1], len(files), slice_size[0] else: - shape = ( + shape: tuple[int, int, int] = ( math.ceil(slice_size[1] * resolution_percentage), len(files), math.ceil(slice_size[0] * resolution_percentage), ) else: if resolution_percentage == 1.0: - shape = len(files), slice_size[1], slice_size[0] + shape: tuple[int, int, int] = len(files), slice_size[1], slice_size[0] else: - shape = ( + shape: tuple[int, int, int] = ( len(files), math.ceil(slice_size[1] * resolution_percentage), math.ceil(slice_size[0] * resolution_percentage), @@ -547,7 +547,7 @@ def img2memmap(group) -> Tuple[np.memmap, Tuple[int, int], str]: def get_LUT_value_255(data: np.ndarray, window: float, level: float) -> np.ndarray: - shape = data.shape + shape: Tuple[int, ...] = data.shape data_ = data.ravel() data = np.piecewise( data_, @@ -562,7 +562,7 @@ def get_LUT_value_255(data: np.ndarray, window: float, level: float) -> np.ndarr def get_LUT_value(data: np.ndarray, window: float, level: float) -> np.ndarray: - shape = data.shape + shape: Tuple[int, ...] = data.shape data_ = data.ravel() data = np.piecewise(data_, [data_ <= (level - 0.5 - (window-1)/2), @@ -674,7 +674,7 @@ def create_grid(xy_range: Tuple[int, int], z_range: Tuple[int, int], z_offset: i xv, yv, zv = np.meshgrid(x, y, -z) coord_grid = np.array([xv, yv, zv]) # create grid of points - grid_number = x.shape[0] * y.shape[0] * z.shape[0] + grid_number: int = x.shape[0] * y.shape[0] * z.shape[0] coord_grid = coord_grid.reshape([3, grid_number]).T # sort grid from distance to the origin/coil center coord_list = coord_grid[np.argsort(np.linalg.norm(coord_grid, axis=1)), :] @@ -689,7 +689,7 @@ def create_spherical_grid(radius: int = 10, subdivision: int = 1) -> np.ndarray: xv, yv, zv = np.meshgrid(x, x, x) coord_grid = np.array([xv, yv, zv]) # create grid of points - grid_number = x.shape[0] ** 3 + grid_number: int = x.shape[0] ** 3 coord_grid = coord_grid.reshape([3, grid_number]).T sph_grid = coord_grid[np.linalg.norm(coord_grid, axis=1) < radius, :] diff --git a/invesalius/data/mask.py b/invesalius/data/mask.py index 60b2f2618..aa99fdaf8 100644 --- a/invesalius/data/mask.py +++ b/invesalius/data/mask.py @@ -24,7 +24,7 @@ import tempfile import time import weakref -from typing import Any, List, Union, Tuple, Optional, overload +from typing import Any, List, Union, Tuple, Optional, overload, Dict, Callable, TypeVar, Generic, Type, cast, TYPE_CHECKING import invesalius.constants as const import invesalius.data.converters as converters @@ -39,10 +39,10 @@ class EditionHistoryNode(object): def __init__(self, index: int, orientation: str, array: np.ndarray, clean: bool = False) -> None: - self.index = index - self.orientation = orientation - self.filename = tempfile.mktemp(suffix='.npy') - self.clean = clean + self.index: int = index + self.orientation: str = orientation + self.filename: str = tempfile.mktemp(suffix='.npy') + self.clean: bool = clean self._save_array(array) @@ -69,7 +69,7 @@ def commit_history(self, mvolume: np.ndarray) -> None: print("applying to", self.orientation, "at slice", self.index) - def __del__(self): + def __del__(self) -> None: print("Removing", self.filename) os.remove(self.filename) @@ -108,7 +108,7 @@ def add(self, node: EditionHistoryNode) -> None: Publisher.sendMessage("Enable redo", value=False) def undo(self, mvolume: np.ndarray, actual_slices: Optional[Dict[str, int]] = None) -> None: - h = self.history + h: List[EditionHistoryNode] = self.history if self.index > 0: #if self.index > 0 and h[self.index].clean: ##self.index -= 1 @@ -138,7 +138,7 @@ def undo(self, mvolume: np.ndarray, actual_slices: Optional[Dict[str, int]] = No def redo(self, mvolume: np.ndarray, actual_slices: Optional[Dict[str, int]] = None) -> None: - h = self.history + h: List[EditionHistoryNode] = self.history if self.index < len(h) - 1: #if self.index < len(h) - 1 and h[self.index].clean: ##self.index += 1 @@ -284,9 +284,9 @@ def _update_imagedata(self, update_volume_viewer: bool = True) -> None: def SavePlist(self, dir_temp: str, filelist: Dict[str, str]) -> str: mask = {} - filename = u'mask_%d' % self.index - mask_filename = u'%s.dat' % filename - mask_filepath = os.path.join(dir_temp, mask_filename) + filename: str = u'mask_%d' % self.index + mask_filename: str = u'%s.dat' % filename + mask_filepath: str = os.path.join(dir_temp, mask_filename) filelist[self.temp_file] = mask_filename #self._save_mask(mask_filepath) @@ -301,10 +301,10 @@ def SavePlist(self, dir_temp: str, filelist: Dict[str, str]) -> str: mask['mask_shape'] = self.matrix.shape mask['edited'] = self.was_edited - plist_filename = filename + u'.plist' + plist_filename: str = filename + u'.plist' #plist_filepath = os.path.join(dir_temp, plist_filename) - temp_plist = tempfile.mktemp() + temp_plist: str = tempfile.mktemp() with open(temp_plist, 'w+b') as f: plistlib.dump(mask, f) @@ -328,8 +328,8 @@ def OpenPList(self, filename: str) -> None: shape = mask['mask_shape'] self.was_edited = mask.get('edited', False) - dirpath = os.path.abspath(os.path.split(filename)[0]) - path = os.path.join(dirpath, mask_file) + dirpath: str = os.path.abspath(os.path.split(filename)[0]) + path: str = os.path.join(dirpath, mask_file) self._open_mask(path, tuple(shape)) def OnFlipVolume(self, axis: int) -> None: @@ -358,7 +358,7 @@ def _save_mask(self, filename: str) -> None: def _open_mask(self, filename: str, shape: Tuple[int, int, int], dtype: str = 'uint8') -> None: print(">>", filename, shape) - self.temp_file = filename + self.temp_file: str = filename self.matrix = np.memmap(filename, shape=shape, dtype=dtype, mode="r+") @classmethod @@ -453,8 +453,8 @@ def clear_history(self) -> None: self.history.clear_history() def fill_holes_auto(self, target: str, conn: int, orientation: str, index: int, size: int) -> None: - CON2D = {4: 1, 8: 2} - CON3D = {6: 1, 18: 2, 26: 3} + CON2D: dict[int, int] = {4: 1, 8: 2} + CON3D: dict[int, int] = {6: 1, 18: 2, 26: 3} if target == '3D': cp_mask = self.matrix.copy() diff --git a/invesalius/data/measures.py b/invesalius/data/measures.py index e7ab9711a..ffb8d255f 100644 --- a/invesalius/data/measures.py +++ b/invesalius/data/measures.py @@ -3,7 +3,7 @@ from aifc import _File import math import sys -from typing import Any, Dict, List, Optional, Tuple, Union, int +from typing import Any, Dict, List, Optional, Tuple, Union, int, float, str, bytes, bool import wx from invesalius.pubsub import pub as Publisher @@ -177,10 +177,10 @@ def _load_measurements(self, measurement_dict: Dict[int, Any], spacing: Tuple[fl else: if m.location == const.AXIAL: - radius = min(spacing[1], spacing[2]) * const.PROP_MEASURE + radius: float = min(spacing[1], spacing[2]) * const.PROP_MEASURE elif m.location == const.CORONAL: - radius = min(spacing[0], spacing[1]) * const.PROP_MEASURE + radius: float = min(spacing[0], spacing[1]) * const.PROP_MEASURE elif m.location == const.SAGITAL: radius = min(spacing[1], spacing[2]) * const.PROP_MEASURE @@ -401,7 +401,7 @@ def _add_density_measure(self, density_measure: Union[EllipseDensityMeasure, Pol self.measures.append((m, density_measure)) - index = prj.Project().AddMeasurement(m) + index: int = prj.Project().AddMeasurement(m) msg: str = 'Update measurement info in GUI', Publisher.sendMessage(msg, @@ -696,7 +696,7 @@ def _draw_line(self) -> None: def _draw_text(self) -> None: p1, p2 = self.points - text = ' %.3f mm ' % \ + text: str = ' %.3f mm ' % \ math.sqrt(vtkMath.Distance2BetweenPoints(p1, p2)) x,y,z=[(i+j)/2 for i,j in zip(p1, p2)] textsource = vtkTextSource() @@ -737,7 +737,7 @@ def draw_to_canvas(self, gc: wx.GraphicsContext, canvas: wx.Window) -> None: r, g, b = self.colour canvas.draw_line(p0, p1, colour=(r*255, g*255, b*255, 255)) - txt = u"%.3f mm" % self.GetValue() + txt: str = u"%.3f mm" % self.GetValue() canvas.draw_text_box(txt, ((points[0][0]+points[1][0])/2.0, (points[0][1]+points[1][1])/2.0), txt_colour=MEASURE_TEXT_COLOUR, bg_colour=MEASURE_TEXTBOX_COLOUR) def GetNumberOfPoints(self) -> int: @@ -923,19 +923,19 @@ def DrawArc(self) -> vtkArcSource: d2: float = math.sqrt(vtkMath.Distance2BetweenPoints(self.points[2], self.points[1])) if d1 < d2: - d = d1 - p1 = self.points[0] + d: float = d1 + p1: Tuple[float, float, float] = self.points[0] a,b,c = [j-i for i,j in zip(self.points[1], self.points[2])] else: d = d2 p1 = self.points[2] a,b,c = [j-i for i,j in zip(self.points[1], self.points[0])] - t = (d / math.sqrt(a**2 + b**2 + c**2)) - x = self.points[1][0] + a*t - y = self.points[1][1] + b*t - z = self.points[1][2] + c*t - p2 = (x, y, z) + t: float = (d / math.sqrt(a**2 + b**2 + c**2)) + x: float = self.points[1][0] + a*t + y: float = self.points[1][1] + b*t + z: float = self.points[1][2] + c*t + p2: tuple[float, float, float] = (x, y, z) arc = vtkArcSource() arc.SetPoint1(p1) @@ -1052,11 +1052,11 @@ def CalculateAngle(self) -> float: angle: float = math.degrees(math.acos(cos)) return angle - def Remove(self): + def Remove(self) -> None: actors: list = self.GetActors() Publisher.sendMessage("Remove actors " + str(const.SURFACE), actors=actors) - def SetRenderer(self, renderer: object): + def SetRenderer(self, renderer: object) -> None: if self.point_actor1: self.renderer.RemoveActor(self.point_actor1) renderer.AddActor(self.point_actor1) @@ -1079,48 +1079,48 @@ def SetRenderer(self, renderer: object): self.renderer: object = renderer - def __del__(self): + def __del__(self) -> None: self.Remove() class CircleDensityMeasure(CanvasHandlerBase): - def __init__(self, orientation, slice_number, colour=(255, 0, 0, 255), interactive=True): + def __init__(self, orientation: str, slice_number: int, colour: Tuple[int, int, int, int] = (255, 0, 0, 255), interactive: bool = True) -> None: super(CircleDensityMeasure, self).__init__(None) self.parent = None self.children = [] self.layer = 0 - self.colour = colour - self.center = (0.0, 0.0, 0.0) - self.point1 = (0.0, 0.0, 0.0) - self.point2 = (0.0, 0.0, 0.0) + self.colour: Tuple[int, int, int, int] = colour + self.center: Tuple[float, float, float] = (0.0, 0.0, 0.0) + self.point1: Tuple[float, float, float] = (0.0, 0.0, 0.0) + self.point2: Tuple[float, float, float] = (0.0, 0.0, 0.0) - self.orientation = orientation - self.slice_number = slice_number + self.orientation: str = orientation + self.slice_number: int = slice_number - self.format = 'ellipse' + self.format: str = 'ellipse' - self.location = map_locations_id[self.orientation] - self.index = 0 + self.location: int = map_locations_id[self.orientation] + self.index: int = 0 - self._area = 0 - self._min = 0 - self._max = 0 - self._mean = 0 - self._std = 0 + self._area: float = 0 + self._min: float = 0 + self._max: float = 0 + self._mean: float = 0 + self._std: float = 0 - self._measurement = None + self._measurement: Optional[MeasurementItem] = None - self.ellipse = Ellipse(self, self.center, self.point1, self.point2, + self.ellipse: Ellipse = Ellipse(self, self.center, self.point1, self.point2, fill=False, line_colour=self.colour) self.ellipse.layer = 1 self.add_child(self.ellipse) - self.text_box = None + self.text_box: Optional[TextBox] = None - self._need_calc = True - self.interactive = interactive + self._need_calc: bool = True + self.interactive: bool = interactive - def set_center(self, pos): + def set_center(self, pos: Tuple[float, float, float]) -> None: self.center = pos self._need_calc = True self.ellipse.center = self.center @@ -1128,7 +1128,7 @@ def set_center(self, pos): if self._measurement: self._measurement.points = [self.center, self.point1, self.point2] - def set_point1(self, pos): + def set_point1(self, pos: Tuple[float, float, float]) -> None: self.point1 = pos self._need_calc = True self.ellipse.set_point1(self.point1) @@ -1136,26 +1136,27 @@ def set_point1(self, pos): if self._measurement: self._measurement.points = [self.center, self.point1, self.point2] - def set_point2(self, pos): - self.point2 = pos - self._need_calc = True - self.ellipse.set_point2(self.point2) - - if self._measurement: - self._measurement.points = [self.center, self.point1, self.point2] - def set_density_values(self, _min, _max, _mean, _std, _area): + def set_point2(self, pos: Tuple[float, float, float]) -> None: + self.point2 = pos + self._need_calc = True + self.ellipse.set_point2(self.point2) + + if self._measurement: + self._measurement.points = [self.center, self.point1, self.point2] + + def set_density_values(self, _min: float, _max: float, _mean: float, _std: float, _area: float) -> None: self._min = _min self._max = _max self._mean = _mean self._std = _std self._area = _area - text = _File('Area: %.3f\n' - 'Min: %.3f\n' - 'Max: %.3f\n' - 'Mean: %.3f\n' - 'Std: %.3f' % (self._area, self._min, self._max, self._mean, self._std)) + text: str = _File('Area: %.3f\n' + 'Min: %.3f\n' + 'Max: %.3f\n' + 'Mean: %.3f\n' + 'Std: %.3f' % (self._area, self._min, self._max, self._mean, self._std)) if self.text_box is None: self.text_box = TextBox(self, text, self.point1, MEASURE_TEXT_COLOUR, MEASURE_TEXTBOX_COLOUR) @@ -1168,31 +1169,31 @@ def set_density_values(self, _min, _max, _mean, _std, _area): self._measurement.value = self._mean self._update_gui_info() - def _update_gui_info(self): - msg = 'Update measurement info in GUI', + def _update_gui_info(self) -> None: + msg: str = 'Update measurement info in GUI', print(msg) if self._measurement: m = self._measurement Publisher.sendMessage(msg, - index=m.index, name=m.name, colour=m.colour, - location= self.orientation, - type_=_('Density Ellipse'), - value='%.3f' % m.value) + index=m.index, name=m.name, colour=m.colour, + location= self.orientation, + type_=_('Density Ellipse'), + value='%.3f' % m.value) - def set_measurement(self, dm): + def set_measurement(self, dm: Optional[MeasurementItem]) -> None: self._measurement = dm - def SetVisibility(self, value): + def SetVisibility(self, value: bool) -> None: self.visible = value self.ellipse.visible = value - def _3d_to_2d(self, renderer, pos): + def _3d_to_2d(self, renderer: vtkRenderer, pos: Tuple[float, float, float]) -> Tuple[float, float]: coord = vtkCoordinate() coord.SetValue(pos) cx, cy = coord.GetComputedDoubleDisplayValue(renderer) return cx, cy - def is_over(self, x, y): + def is_over(self, x: float, y: float) -> None: return None # if self.interactive: # if self.ellipse.is_over(x, y): @@ -1200,12 +1201,13 @@ def is_over(self, x, y): # elif self.text_box.is_over(x, y): # return self.text_box.is_over(x, y) # return None + - def set_interactive(self, value): - self.interactive = bool(value) - self.ellipse.interactive = self.interactive - - def draw_to_canvas(self, gc, canvas): + def set_interactive(self, value: bool) -> None: + self.interactive = bool(value) + self.ellipse.interactive = self.interactive + + def draw_to_canvas(self, gc: wx.GraphicsContext, canvas: Any) -> None: """ Draws to an wx.GraphicsContext. @@ -1227,14 +1229,14 @@ def draw_to_canvas(self, gc, canvas): # self.text_box.draw_to_canvas(gc, canvas) # # self.handle_tl.draw_to_canvas(gc, canvas) - def calc_area(self): + def calc_area(self) -> float: if self.orientation == 'AXIAL': - a = abs(self.point1[0] - self.center[0]) - b = abs(self.point2[1] - self.center[1]) + a: float = abs(self.point1[0] - self.center[0]) + b: float = abs(self.point2[1] - self.center[1]) elif self.orientation == 'CORONAL': - a = abs(self.point1[0] - self.center[0]) - b = abs(self.point2[2] - self.center[2]) + a: float = abs(self.point1[0] - self.center[0]) + b: float = abs(self.point2[2] - self.center[2]) elif self.orientation == 'SAGITAL': a = abs(self.point1[1] - self.center[1]) @@ -1242,51 +1244,52 @@ def calc_area(self): return math_utils.calc_ellipse_area(a, b) - def calc_density(self): + + def calc_density(self) -> None: from invesalius.data.slice_ import Slice - slc = Slice() - n = self.slice_number - orientation = self.orientation - img_slice = slc.get_image_slice(orientation, n) + slc: Slice = Slice() + n: int = self.slice_number + orientation: str = self.orientation + img_slice: np.ndarray = slc.get_image_slice(orientation, n) dy, dx = img_slice.shape - spacing = slc.spacing + spacing: Tuple[float, float, float] = slc.spacing if orientation == 'AXIAL': sx, sy = spacing[0], spacing[1] cx, cy = self.center[0], self.center[1] - a = abs(self.point1[0] - self.center[0]) - b = abs(self.point2[1] - self.center[1]) + a: float = abs(self.point1[0] - self.center[0]) + b: float = abs(self.point2[1] - self.center[1]) - n = slc.buffer_slices["AXIAL"].index + 1 - m = slc.current_mask.matrix[n, 1:, 1:] + n: int = slc.buffer_slices["AXIAL"].index + 1 + m: np.ndarray = slc.current_mask.matrix[n, 1:, 1:] elif orientation == 'CORONAL': sx, sy = spacing[0], spacing[2] cx, cy = self.center[0], self.center[2] - a = abs(self.point1[0] - self.center[0]) - b = abs(self.point2[2] - self.center[2]) + a: float = abs(self.point1[0] - self.center[0]) + b: float = abs(self.point2[2] - self.center[2]) - n = slc.buffer_slices["CORONAL"].index + 1 - m = slc.current_mask.matrix[1:, n, 1:] + n: int = slc.buffer_slices["CORONAL"].index + 1 + m: np.ndarray = slc.current_mask.matrix[1:, n, 1:] elif orientation == 'SAGITAL': sx, sy = spacing[1], spacing[2] cx, cy = self.center[1], self.center[2] - a = abs(self.point1[1] - self.center[1]) - b = abs(self.point2[2] - self.center[2]) + a: float = abs(self.point1[1] - self.center[1]) + b: float = abs(self.point2[2] - self.center[2]) - n = slc.buffer_slices["SAGITAL"].index + 1 - m = slc.current_mask.matrix[1:, 1:, n] + n: int = slc.buffer_slices["SAGITAL"].index + 1 + m: np.ndarray = slc.current_mask.matrix[1:, 1:, n] # a = np.linalg.norm(np.array(self.point1) - np.array(self.center)) # b = np.linalg.norm(np.array(self.point2) - np.array(self.center)) mask_y, mask_x = np.ogrid[0:dy*sy:sy, 0:dx*sx:sx] # mask = ((mask_x - cx)**2 + (mask_y - cy)**2) <= (radius ** 2) - mask = (((mask_x-cx)**2 / a**2) + ((mask_y-cy)**2 / b**2)) <= 1.0 + mask: np.ndarray = (((mask_x-cx)**2 / a**2) + ((mask_y-cy)**2 / b**2)) <= 1.0 # try: # test_img = np.zeros_like(img_slice) @@ -1302,20 +1305,20 @@ def calc_density(self): except IndexError: pass - values = img_slice[mask] + values: np.ndarray = img_slice[mask] try: - _min = values.min() - _max = values.max() - _mean = values.mean() - _std = values.std() + _min: float = values.min() + _max: float = values.max() + _mean: float = values.mean() + _std: float = values.std() except ValueError: _min = 0 _max = 0 _mean = 0 _std = 0 - _area = self.calc_area() + _area: float = self.calc_area() if self._measurement: self._measurement.points = [self.center, self.point1, self.point2] @@ -1327,48 +1330,48 @@ def calc_density(self): self._measurement.area = float(_area) self.set_density_values(_min, _max, _mean, _std, _area) - - def IsComplete(self): + + def IsComplete(self) -> bool: return True - def on_mouse_move(self, evt): - old_center = self.center + def on_mouse_move(self, evt: Event) -> None: + old_center: Tuple[float, float, float] = self.center self.center = self.ellipse.center self.set_point1(self.ellipse.point1) self.set_point2(self.ellipse.point2) - diff = tuple((i-j for i,j in zip(self.center, old_center))) - self.text_box.position = tuple((i+j for i,j in zip(self.text_box.position, diff))) + diff: Tuple[int] = tuple((i-j for i,j in zip(self.center, old_center))) + self.text_box.position: Tuple[int] = tuple((i+j for i,j in zip(self.text_box.position, diff))) if self._measurement: - self._measurement.points = [self.center, self.point1, self.point2] - self._measurement.value = self._mean - self._measurement.mean = self._mean - self._measurement.min = self._min - self._measurement.max = self._max - self._measurement.std = self._std - - session = ses.Session() + self._measurement.points: List[Tuple[int]] = [self.center, self.point1, self.point2] + self._measurement.value: float = self._mean + self._measurement.mean: float = self._mean + self._measurement.min: float = self._min + self._measurement.max: float = self._max + self._measurement.std: float = self._std + + session: Session = ses.Session() session.ChangeProject() - def on_select(self, evt): - self.layer = 50 + def on_select(self, evt: Event) -> None: + self.layer: int = 50 - def on_deselect(self, evt): - self.layer = 0 + def on_deselect(self, evt: Event) -> None: + self.layer: int = 0 class PolygonDensityMeasure(CanvasHandlerBase): - def __init__(self, orientation, slice_number, colour=(255, 0, 0, 255), interactive=True): + def __init__(self, orientation: str, slice_number: int, colour: Tuple[int, int, int, int] = (255, 0, 0, 255), interactive: bool = True) -> None: super(PolygonDensityMeasure, self).__init__(None) self.parent = None self.children = [] self.layer = 0 self.colour = colour - self.points = [] + self.points: List[Tuple[float, float]] = [] - self.orientation = orientation + self.orientation: str = orientation self.slice_number = slice_number self.complete = False @@ -1378,13 +1381,13 @@ def __init__(self, orientation, slice_number, colour=(255, 0, 0, 255), interacti self.location = map_locations_id[self.orientation] self.index = 0 - self._area = 0 - self._min = 0 - self._max = 0 - self._mean = 0 - self._std = 0 + self._area: float = 0 + self._min: float = 0 + self._max: float = 0 + self._mean: float = 0 + self._std: float = 0 - self._dist_tbox = (0, 0, 0) + self._dist_tbox: Tuple[float, float, float] = (0, 0, 0) self._measurement = None @@ -1395,10 +1398,10 @@ def __init__(self, orientation, slice_number, colour=(255, 0, 0, 255), interacti self.text_box = None self._need_calc = False - self.interactive = interactive + self.interactive: bool = interactive - def on_mouse_move(self, evt): + def on_mouse_move(self, evt) -> None: self.points = self.polygon.points self._need_calc = self.complete @@ -1406,18 +1409,18 @@ def on_mouse_move(self, evt): self._measurement.points = self.points if self.text_box: - bounds = self.get_bounds() - p = [bounds[3], bounds[4], bounds[5]] + bounds: Tuple[float, float, float, float, float, float] = self.get_bounds() + p: list[float] = [bounds[3], bounds[4], bounds[5]] if evt.root_event_obj is self.text_box: self._dist_tbox = [i-j for i,j in zip(self.text_box.position, p)] else: self.text_box.position = [i+j for i,j in zip(self._dist_tbox, p)] print("text box position", self.text_box.position) - session = ses.Session() + session: ses.Session = ses.Session() session.ChangeProject() - def draw_to_canvas(self, gc, canvas): + def draw_to_canvas(self, gc: Any, canvas: Any) -> None: if self._need_calc: self.calc_density(canvas) # if self.visible: @@ -1430,61 +1433,60 @@ def draw_to_canvas(self, gc, canvas): # self.text_box.draw_to_canvas(gc, canvas) # self._dist_tbox = [j-i for i,j in zip(p, self.text_box.position)] - def insert_point(self, point): + def insert_point(self, point: Tuple[float, float]) -> None: print("insert points", len(self.points)) self.polygon.append_point(point) self.points.append(point) - def complete_polygon(self): + def complete_polygon(self) -> None: # if len(self.points) >= 3: self.polygon.closed = True self._need_calc = True self.complete = True - bounds = self.get_bounds() - p = [bounds[3], bounds[4], bounds[5]] + bounds: Tuple[float, float, float, float, float, float] = self.get_bounds() + p: list[float] = [bounds[3], bounds[4], bounds[5]] if self.text_box is None: p[0] += 5 self.text_box = TextBox(self, '', p, MEASURE_TEXT_COLOUR, MEASURE_TEXTBOX_COLOUR) self.text_box.layer = 2 self.add_child(self.text_box) - def calc_density(self, canvas): - from invesalius.data.slice_ import Slice - slc = Slice() - n = self.slice_number - orientation = self.orientation - img_slice = slc.get_image_slice(orientation, n) - dy, dx = img_slice.shape - spacing = slc.spacing + def calc_density(self, canvas: Any) -> None: + from invesalius.data.slice_ import Slice + slc: Slice = Slice() + n: int = self.slice_number + orientation: str = self.orientation + img_slice: np.ndarray = slc.get_image_slice(orientation, n) + dy: int; dx: int = img_slice.shape + spacing: Tuple[float, float, float] = slc.spacing if orientation == 'AXIAL': - sx, sy = spacing[0], spacing[1] - n = slc.buffer_slices["AXIAL"].index + 1 - m = slc.current_mask.matrix[n, 1:, 1:] - plg_points = [(x/sx, y/sy) for (x, y, z) in self.points] + sx: float; sy: float = spacing[0], spacing[1] + n: int = slc.buffer_slices["AXIAL"].index + 1 + m: np.ndarray = slc.current_mask.matrix[n, 1:, 1:] + plg_points: List[Tuple[float, float]] = [(x/sx, y/sy) for (x, y, z) in self.points] elif orientation == 'CORONAL': - sx, sy = spacing[0], spacing[2] - n = slc.buffer_slices["CORONAL"].index + 1 - m = slc.current_mask.matrix[1:, n, 1:] - plg_points = [(x/sx, z/sy) for (x, y, z) in self.points] + sx: float; sy: float = spacing[0], spacing[2] + n: int = slc.buffer_slices["CORONAL"].index + 1 + m: np.ndarray = slc.current_mask.matrix[1:, n, 1:] + plg_points: List[Tuple[float, float]] = [(x/sx, z/sy) for (x, y, z) in self.points] elif orientation == 'SAGITAL': - sx, sy = spacing[1], spacing[2] - n = slc.buffer_slices["SAGITAL"].index + 1 - m = slc.current_mask.matrix[1:, 1:, n] - - plg_points = [(y/sx, z/sy) for (x, y, z) in self.points] - - plg_tmp = Polygon(None, plg_points, fill=True, - line_colour=(0, 0, 0, 0), - fill_colour=(255, 255, 255, 255), width=1, - interactive=False, is_3d=False) - h, w = img_slice.shape - arr = canvas.draw_element_to_array([plg_tmp, ], size=(w, h), flip=False) - mask = arr[:, :, 0] >= 128 + sx: float; sy: float = spacing[1], spacing[2] + n: int = slc.buffer_slices["SAGITAL"].index + 1 + m: np.ndarray = slc.current_mask.matrix[1:, 1:, n] + plg_points: List[Tuple[float, float]] = [(y/sx, z/sy) for (x, y, z) in self.points] + + plg_tmp: Polygon = Polygon(None, plg_points, fill=True, + line_colour=(0, 0, 0, 0), + fill_colour=(255, 255, 255, 255), width=1, + interactive=False, is_3d=False) + h: int; w: int = img_slice.shape + arr: np.ndarray = canvas.draw_element_to_array([plg_tmp, ], size=(w, h), flip=False) + mask: np.ndarray = arr[:, :, 0] >= 128 print("mask sum", mask.sum()) @@ -1498,20 +1500,20 @@ def calc_density(self, canvas): except IndexError: pass - values = img_slice[mask] + values: np.ndarray = img_slice[mask] try: - _min = values.min() - _max = values.max() - _mean = values.mean() - _std = values.std() + _min: float = values.min() + _max: float = values.max() + _mean: float = values.mean() + _std: float = values.std() except ValueError: - _min = 0 - _max = 0 - _mean = 0 - _std = 0 + _min: float = 0 + _max: float = 0 + _mean: float = 0 + _std: float = 0 - _area = self.calc_area() + _area: float = self.calc_area() if self._measurement: self._measurement.points = self.points @@ -1525,51 +1527,51 @@ def calc_density(self, canvas): self.set_density_values(_min, _max, _mean, _std, _area) self.calc_area() - self._need_calc = False - - def calc_area(self): + self._need_calc: bool = False + + def calc_area(self) -> float: if self.orientation == 'AXIAL': - points = [(x, y) for (x, y, z) in self.points] + points: List[Tuple[float, float]] = [(x, y) for (x, y, z) in self.points] elif self.orientation == 'CORONAL': - points = [(x, z) for (x, y, z) in self.points] + points: List[Tuple[float, float]] = [(x, z) for (x, y, z) in self.points] elif self.orientation == 'SAGITAL': - points = [(y, z) for (x, y, z) in self.points] - area = math_utils.calc_polygon_area(points) + points: List[Tuple[float, float]] = [(y, z) for (x, y, z) in self.points] + area: float = math_utils.calc_polygon_area(points) print('Points', points) print('xv = %s;' % [i[0] for i in points]) print('yv = %s;' % [i[1] for i in points]) print('Area', area) return area - def get_bounds(self): - min_x = min(self.points, key=lambda x: x[0])[0] - max_x = max(self.points, key=lambda x: x[0])[0] + def get_bounds(self) -> Tuple[float, float, float, float, float, float]: + min_x: float = min(self.points, key=lambda x: x[0])[0] + max_x: float = max(self.points, key=lambda x: x[0])[0] - min_y = min(self.points, key=lambda x: x[1])[1] - max_y = max(self.points, key=lambda x: x[1])[1] + min_y: float = min(self.points, key=lambda x: x[1])[1] + max_y: float = max(self.points, key=lambda x: x[1])[1] - min_z = min(self.points, key=lambda x: x[2])[2] - max_z = max(self.points, key=lambda x: x[2])[2] + min_z: float = min(self.points, key=lambda x: x[2])[2] + max_z: float = max(self.points, key=lambda x: x[2])[2] print(self.points) return (min_x, min_y, min_z, max_x, max_y, max_z) - def IsComplete(self): + def IsComplete(self) -> bool: return self.complete - def set_measurement(self, dm): + def set_measurement(self, dm: float) -> None: self._measurement = dm - def SetVisibility(self, value): + def SetVisibility(self, value: bool) -> None: self.visible = value self.polygon.visible = value - def set_interactive(self, value): + def set_interactive(self, value: bool) -> None: self.interactive = bool(value) self.polygon.interactive = self.interactive - def is_over(self, x, y): + def is_over(self, x: float, y: float) -> bool: None # if self.interactive: # if self.polygon.is_over(x, y): @@ -1579,21 +1581,21 @@ def is_over(self, x, y): # return self.text_box.is_over(x, y) # return None - def set_density_values(self, _min, _max, _mean, _std, _area): + def set_density_values(self, _min: float, _max: float, _mean: float, _std: float, _area: float) -> None: self._min = _min self._max = _max self._mean = _mean self._std = _std self._area = _area - text = _('Area: %.3f\n' - 'Min: %.3f\n' - 'Max: %.3f\n' - 'Mean: %.3f\n' - 'Std: %.3f' % (self._area, self._min, self._max, self._mean, self._std)) + text = ('Area: %.3f\n' + 'Min: %.3f\n' + 'Max: %.3f\n' + 'Mean: %.3f\n' + 'Std: %.3f' % (self._area, self._min, self._max, self._mean, self._std)) - bounds = self.get_bounds() - p = [bounds[3], bounds[4], bounds[5]] + bounds: Tuple[float, float, float, float, float, float] = self.get_bounds() + p: list[float] = [bounds[3], bounds[4], bounds[5]] dx = self.text_box.position[0] - p[0] dy = self.text_box.position[1] - p[1] @@ -1606,14 +1608,15 @@ def set_density_values(self, _min, _max, _mean, _std, _area): self._measurement.value = self._mean self._update_gui_info() - def _update_gui_info(self): - msg = 'Update measurement info in GUI', + def _update_gui_info(self) -> None: + msg: str = 'Update measurement info in GUI', print(msg) if self._measurement: - m = self._measurement + m: float = self._measurement Publisher.sendMessage(msg, - index=m.index, name=m.name, - colour=m.colour, - location=self.orientation, - type_=_('Density Polygon'), - value='%.3f' % m.value) + index=m.index, name=m.name, + colour=m.colour, + location=self.orientation, + type_=('Density Polygon'), + value='%.3f' % m.value) + \ No newline at end of file diff --git a/invesalius/data/orientation.py b/invesalius/data/orientation.py index 3bcb35f73..eaa96bc88 100644 --- a/invesalius/data/orientation.py +++ b/invesalius/data/orientation.py @@ -16,22 +16,24 @@ # PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais # detalhes. #-------------------------------------------------------------------------- -AXIAL = 2 -CORONAL = 1 -SAGITAL = 0 +AXIAL: int = 2 +CORONAL: int = 1 +SAGITAL: int = 0 + +from typing import Any, Optional, Tuple class Orientation(object): - def __init__(self, interactor, actor): - self.interactor = interactor - self.actor = actor - self.image = actor.GetInput() - self.ren = interactor.GetRenderWindow().GetRenderers().GetFirstRenderer() - self.slice = 0 - - def SetOrientation(self, orientation): - cam = self.ren.GetActiveCamera() - self.orientation = orientation - extent = self.image.GetWholeExtent() + def __init__(self, interactor: object, actor: object) -> None: + self.interactor: object = interactor + self.actor: object = actor + self.image: object = actor.GetInput() + self.ren: object = interactor.GetRenderWindow().GetRenderers().GetFirstRenderer() + self.slice: int = 0 + + def SetOrientation(self, orientation: int) -> None: + cam: object = self.ren.GetActiveCamera() + self.orientation: int = orientation + extent: tuple = self.image.GetWholeExtent() if orientation == AXIAL: cam.SetFocalPoint(0, 0, 0) @@ -39,8 +41,8 @@ def SetOrientation(self, orientation): cam.ComputeViewPlaneNormal() cam.SetViewUp(0, 1, 0) - xs = extent[1] - extent[0] + 1 - ys = extent[3] - extent[2] + 1 + xs: int = extent[1] - extent[0] + 1 + ys: int = extent[3] - extent[2] + 1 elif orientation == CORONAL: cam.SetFocalPoint(0, 0, 0) @@ -48,8 +50,8 @@ def SetOrientation(self, orientation): cam.ComputeViewPlaneNormal() cam.SetViewUp(0, 0, 1) - xs = extent[1] - extent[0] + 1 - ys = extent[5] - extent[4] + 1 + xs: int = extent[1] - extent[0] + 1 + ys: int = extent[5] - extent[4] + 1 elif orientation == SAGITAL: cam.SetFocalPoint(0, 0, 0) @@ -57,14 +59,14 @@ def SetOrientation(self, orientation): cam.ComputeViewPlaneNormal() cam.SetViewUp(0, 0, 1) - xs = extent[3] - extent[2] + 1 - ys = extent[5] - extent[4] + 1 + xs: int = extent[3] - extent[2] + 1 + ys: int = extent[5] - extent[4] + 1 if xs < 150: - scale = 75 + scale: int = 75 else: - scale = (xs - 1)/2.0 + scale: int = (xs - 1)/2.0 cam.OrthogonalizeViewUp() @@ -79,33 +81,33 @@ def SetOrientation(self, orientation): self.ren.Render() - def UpdateDisplayExtent(self): - extent = self.image.GetWholeExtent() + def UpdateDisplayExtent(self) -> None: + extent: list = self.image.GetWholeExtent() if self.orientation == AXIAL: - xs = extent[1] - extent[0] + 1 - ys = extent[3] - extent[2] + 1 + xs: int = extent[1] - extent[0] + 1 + ys: int = extent[3] - extent[2] + 1 - actor = self.actor + actor: object = self.actor actor.SetInput(self.image) actor.SetDisplayExtent(extent[0], extent[1], - extent[2], extent[3], - self.slice, self.slice) + extent[2], extent[3], + self.slice, self.slice) elif self.orientation == CORONAL: - xs = extent[1] - extent[0] + 1 - ys = extent[5] - extent[4] + 1 + xs: int = extent[1] - extent[0] + 1 + ys: int = extent[5] - extent[4] + 1 - actor = self.actor + actor: object = self.actor actor.SetInput(self.image) actor.SetDisplayExtent(extent[0], extent[1], self.slice, self.slice, extent[4], extent[5]) elif self.orientation == SAGITAL: - xs = extent[3] - extent[2] + 1 - ys = extent[5] - extent[4] + 1 + xs: int = extent[3] - extent[2] + 1 + ys: int = extent[5] - extent[4] + 1 - actor = self.actor + actor: object = self.actor actor.SetInput(self.image) actor.SetDisplayExtent(self.slice, self.slice, extent[2], extent[3], @@ -115,23 +117,24 @@ def UpdateDisplayExtent(self): self.ren.ResetCameraClippingRange() - cam = self.ren.GetActiveCamera() + cam: object = self.ren.GetActiveCamera() - bounds = self.actor.GetBounds() - spos = bounds[self.orientation*2] - cpos = cam.GetPosition()[self.orientation] - range = abs(spos - cpos) - spacing = self.actor.GetInput().GetSpacing() - avg_spacing = sum(spacing)/3.0 + bounds: list = self.actor.GetBounds() + spos: float = bounds[self.orientation*2] + cpos: float = cam.GetPosition()[self.orientation] + range: float = abs(spos - cpos) + spacing: list = self.actor.GetInput().GetSpacing() + avg_spacing: float = sum(spacing)/3.0 cam.SetClippingRange(range - avg_spacing * 3.0, range +\ - avg_spacing * 3.0) - + avg_spacing * 3.0) + self.ren.Render() - def SetSlice(self, slice): + def SetSlice(self, slice: int) -> None: self.slice = slice self.UpdateDisplayExtent() - def GetMaxSlice(self): + def GetMaxSlice(self) -> int: return self.actor.GetSliceNumberMax() + \ No newline at end of file diff --git a/invesalius/data/polydata_utils.py b/invesalius/data/polydata_utils.py index fc2cc368c..14df58fd5 100644 --- a/invesalius/data/polydata_utils.py +++ b/invesalius/data/polydata_utils.py @@ -20,6 +20,7 @@ import sys import wx +from typing import Callable, List, Optional, Tuple, Union from vtkmodules.vtkCommonDataModel import vtkPolyData from vtkmodules.vtkFiltersCore import ( @@ -44,23 +45,23 @@ if sys.platform == 'win32': try: import win32api - _has_win32api = True + _has_win32api: bool = True except ImportError: - _has_win32api = False + _has_win32api: bool = False else: - _has_win32api = False + _has_win32api: bool = False # Update progress value in GUI -UpdateProgress = vu.ShowProgress() +UpdateProgress: callable = vu.ShowProgress() -def ApplyDecimationFilter(polydata, reduction_factor): +def ApplyDecimationFilter(polydata: vtkPolyData, reduction_factor: float) -> vtkPolyData: """ Reduce number of triangles of the given vtkPolyData, based on reduction_factor. """ # Important: vtkQuadricDecimation presented better results than # vtkDecimatePro - decimation = vtkQuadricDecimation() + decimation: vtkQuadricDecimation = vtkQuadricDecimation() decimation.SetInputData(polydata) decimation.SetTargetReduction(reduction_factor) decimation.GetOutput().ReleaseDataFlagOn() @@ -68,11 +69,11 @@ def ApplyDecimationFilter(polydata, reduction_factor): UpdateProgress(decimation, "Reducing number of triangles...")) return decimation.GetOutput() -def ApplySmoothFilter(polydata, iterations, relaxation_factor): +def ApplySmoothFilter(polydata: vtkPolyData, iterations: int, relaxation_factor: float) -> vtkPolyData: """ Smooth given vtkPolyData surface, based on iteration and relaxation_factor. """ - smoother = vtkSmoothPolyDataFilter() + smoother: vtkSmoothPolyDataFilter = vtkSmoothPolyDataFilter() smoother.SetInputData(polydata) smoother.SetNumberOfIterations(iterations) smoother.SetFeatureAngle(80) @@ -87,53 +88,53 @@ def ApplySmoothFilter(polydata, iterations, relaxation_factor): -def FillSurfaceHole(polydata): +def FillSurfaceHole(polydata: vtkPolyData) -> vtkPolyData: """ Fill holes in the given polydata. """ # Filter used to detect and fill holes. Only fill print("Filling polydata") - filled_polydata = vtkFillHolesFilter() + filled_polydata: vtkFillHolesFilter = vtkFillHolesFilter() filled_polydata.SetInputData(polydata) filled_polydata.SetHoleSize(500) return filled_polydata.GetOutput() -def CalculateSurfaceVolume(polydata): +def CalculateSurfaceVolume(polydata: vtkPolyData) -> float: """ Calculate the volume from the given polydata """ # Filter used to calculate volume and area from a polydata - measured_polydata = vtkMassProperties() + measured_polydata: vtkMassProperties = vtkMassProperties() measured_polydata.SetInputData(polydata) return measured_polydata.GetVolume() -def CalculateSurfaceArea(polydata): +def CalculateSurfaceArea(polydata: vtkPolyData) -> float: """ Calculate the volume from the given polydata """ # Filter used to calculate volume and area from a polydata - measured_polydata = vtkMassProperties() + measured_polydata: vtkMassProperties = vtkMassProperties() measured_polydata.SetInputData(polydata) return measured_polydata.GetSurfaceArea() -def Merge(polydata_list): - append = vtkAppendPolyData() +def Merge(polydata_list:List[vtkPolyData]) -> vtkPolyData: + append: vtkAppendPolyData = vtkAppendPolyData() for polydata in polydata_list: - triangle = vtkTriangleFilter() + triangle: vtkTriangleFilter = vtkTriangleFilter() triangle.SetInputData(polydata) triangle.Update() append.AddInputData(triangle.GetOutput()) append.Update() - clean = vtkCleanPolyData() + clean: vtkCleanPolyData = vtkCleanPolyData() clean.SetInputData(append.GetOutput()) clean.Update() return append.GetOutput() -def Export(polydata, filename, bin=False): - writer = vtkXMLPolyDataWriter() +def Export(polydata: vtkPolyData, filename: str, bin: bool=False) -> None: + writer: vtkXMLPolyDataWriter = vtkXMLPolyDataWriter() if _has_win32api: touch(filename) filename = win32api.GetShortPathName(filename) @@ -145,8 +146,8 @@ def Export(polydata, filename, bin=False): writer.SetInputData(polydata) writer.Write() -def Import(filename): - reader = vtkXMLPolyDataReader() +def Import(filename: str) -> vtkPolyData: + reader: vtkXMLPolyDataReader = vtkXMLPolyDataReader() try: reader.SetFileName(filename.encode(wx.GetDefaultPyEncoding())) except AttributeError: @@ -154,29 +155,29 @@ def Import(filename): reader.Update() return reader.GetOutput() -def LoadPolydata(path): +def LoadPolydata(path: str) -> vtkPolyData: if path.lower().endswith('.stl'): - reader = vtkSTLReader() + reader: vtkSTLReader = vtkSTLReader() elif path.lower().endswith('.ply'): - reader = vtkPLYReader() + reader: vtkPLYReader = vtkPLYReader() elif path.lower().endswith('.obj'): - reader = vtkOBJReader() + reader: vtkOBJReader = vtkOBJReader() elif path.lower().endswith('.vtp'): - reader = vtkXMLPolyDataReader() + reader: vtkXMLPolyDataReader = vtkXMLPolyDataReader() else: assert False, "Not a valid extension." reader.SetFileName(path) reader.Update() - polydata = reader.GetOutput() + polydata: vtkPolyData = reader.GetOutput() return polydata -def JoinSeedsParts(polydata, point_id_list): +def JoinSeedsParts(polydata: vtkPolyData, point_id_list: list) -> vtkPolyData: """ The function require vtkPolyData and point id from vtkPolyData. @@ -188,7 +189,7 @@ def JoinSeedsParts(polydata, point_id_list): pos = 1 for seed in point_id_list: conn.AddSeed(seed) - UpdateProgress(pos, _("Analysing selected regions...")) + UpdateProgress(pos, ("Analysing selected regions...")) pos += 1 conn.AddObserver("ProgressEvent", lambda obj, evt: @@ -199,7 +200,7 @@ def JoinSeedsParts(polydata, point_id_list): result.DeepCopy(conn.GetOutput()) return result -def SelectLargestPart(polydata): +def SelectLargestPart(polydata: vtkPolyData) -> vtkPolyData: """ """ UpdateProgress = vu.ShowProgress(1) @@ -214,7 +215,7 @@ def SelectLargestPart(polydata): result.DeepCopy(conn.GetOutput()) return result -def SplitDisconectedParts(polydata): +def SplitDisconectedParts(polydata: vtkPolyData) -> list: """ """ conn = vtkPolyDataConnectivityFilter() @@ -227,7 +228,7 @@ def SplitDisconectedParts(polydata): conn.SetExtractionModeToSpecifiedRegions() conn.Update() - polydata_collection = [] + polydata_collection: list = [] # Update progress value in GUI progress = nregions -1 @@ -244,6 +245,6 @@ def SplitDisconectedParts(polydata): polydata_collection.append(p) if progress: - UpdateProgress(region, _("Splitting disconnected regions...")) + UpdateProgress(region, ("Splitting disconnected regions...")) return polydata_collection diff --git a/invesalius/data/record_coords.py b/invesalius/data/record_coords.py index 47ea442fc..861e52121 100644 --- a/invesalius/data/record_coords.py +++ b/invesalius/data/record_coords.py @@ -19,7 +19,7 @@ import threading import time - +from typing import Any, Optional, Tuple import wx from numpy import array, savetxt, hstack,vstack, asarray import invesalius.gui.dialogs as dlg @@ -31,41 +31,42 @@ class Record(threading.Thread): Thread created to save obj coords with software during neuronavigation """ - def __init__(self, nav_id, timestamp): + def __init__(self, nav_id: int, timestamp: float) -> None: threading.Thread.__init__(self) - self.nav_id = nav_id - self.coord = None - self.timestamp = timestamp - self.coord_list = array([]) + self.nav_id: int = nav_id + self.coord: array = None + self.timestamp: float = timestamp + self.coord_list: array = array([]) self.__bind_events() - self._pause_ = False + self._pause_: bool = False self.start() - def __bind_events(self): + def __bind_events(self) -> None: # Publisher.subscribe(self.UpdateCurrentCoords, 'Co-registered points') Publisher.subscribe(self.UpdateCurrentCoords, 'Set cross focal point') - def UpdateCurrentCoords(self, position): - self.coord = asarray(position) + def UpdateCurrentCoords(self, position: array) -> None: + self.coord: array = asarray(position) - def stop(self): - self._pause_ = True + def stop(self) -> None: + self._pause_: bool = True #save coords dialog - filename = dlg.ShowLoadSaveDialog(message=_(u"Save coords as..."), - wildcard=_("Coordinates files (*.csv)|*.csv"), - style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, - default_filename="coords.csv", save_ext="csv") + filename: str = dlg.ShowLoadSaveDialog(message=_(u"Save coords as..."), + wildcard=("Coordinates files (*.csv)|*.csv"), + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + default_filename="coords.csv", save_ext="csv") if filename: savetxt(filename, self.coord_list, delimiter=',', fmt='%.4f', header="time, x, y, z, a, b, g", comments="") - def run(self): - initial_time = time.time() + def run(self) -> None: + initial_time: float = time.time() while self.nav_id: - relative_time = asarray(time.time() - initial_time) + relative_time: float = asarray(time.time() - initial_time) time.sleep(self.timestamp) if self.coord_list.size == 0: - self.coord_list = hstack((relative_time, self.coord)) + self.coord_list: array = hstack((relative_time, self.coord)) else: - self.coord_list = vstack((self.coord_list, hstack((relative_time, self.coord)))) + self.coord_list: array = vstack((self.coord_list, hstack((relative_time, self.coord)))) if self._pause_: return + \ No newline at end of file diff --git a/invesalius/data/serial_port_connection.py b/invesalius/data/serial_port_connection.py index 187d889e6..0107404fb 100644 --- a/invesalius/data/serial_port_connection.py +++ b/invesalius/data/serial_port_connection.py @@ -21,28 +21,28 @@ import threading import time - +from typing import Any, Optional, Tuple from invesalius import constants from invesalius.pubsub import pub as Publisher class SerialPortConnection(threading.Thread): - def __init__(self, com_port, baud_rate, serial_port_queue, event, sleep_nav): + def __init__(self, com_port: Optional[str], baud_rate: int, serial_port_queue: queue.Queue, event: threading.Event, sleep_nav: float) -> None: """ Thread created to communicate using the serial port to interact with software during neuronavigation. """ threading.Thread.__init__(self, name='Serial port') - self.connection = None - self.stylusplh = False + self.connection: Optional[Any] = None + self.stylusplh: bool = False - self.com_port = com_port - self.baud_rate = baud_rate - self.serial_port_queue = serial_port_queue - self.event = event - self.sleep_nav = sleep_nav + self.com_port: Optional[str] = com_port + self.baud_rate: int = baud_rate + self.serial_port_queue: queue.Queue = serial_port_queue + self.event: threading.Event = event + self.sleep_nav: float = sleep_nav - def Connect(self): + def Connect(self) -> None: if self.com_port is None: print("Serial port init error: COM port is unset.") return @@ -55,25 +55,25 @@ def Connect(self): except: print("Serial port init error: Connecting to port {} failed.".format(self.com_port)) - def Disconnect(self): + def Disconnect(self) -> None: if self.connection: self.connection.close() print("Connection to port {} closed.".format(self.com_port)) Publisher.sendMessage('Serial port connection', state=False) - def SendPulse(self): + def SendPulse(self) -> None: try: self.connection.send_break(constants.PULSE_DURATION_IN_MILLISECONDS / 1000) Publisher.sendMessage('Serial port pulse triggered') except: print("Error: Serial port could not be written into.") - def run(self): + def run(self) -> None: while not self.event.is_set(): - trigger_on = False + trigger_on: bool = False try: - lines = self.connection.readlines() + lines: bytes = self.connection.readlines() if lines: trigger_on = True except: @@ -103,3 +103,4 @@ def run(self): time.sleep(0.3) else: self.Disconnect() + \ No newline at end of file From 7a896b0e521ea5ff11e68d0abc64871d0a7ded44 Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Sun, 16 Apr 2023 02:07:45 +0530 Subject: [PATCH 05/16] added type info --- invesalius/data/converters.py | 44 +++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/invesalius/data/converters.py b/invesalius/data/converters.py index 97381c70d..774d0be0e 100644 --- a/invesalius/data/converters.py +++ b/invesalius/data/converters.py @@ -19,12 +19,14 @@ import gdcm import numpy as np -from typing import Any, List, Union, Tuple, Optional, overload +from typing import Any, List, Union, Tuple, Optional, overload, Literal from vtkmodules.util import numpy_support from vtkmodules.vtkCommonDataModel import vtkImageData +from vtk import vtkPoints, vtkCellArray, vtkPolyData, vtkTriangle + def to_vtk( n_array: np.ndarray, @@ -48,7 +50,7 @@ def to_vtk( v_image = numpy_support.numpy_to_vtk(n_array.flat) if orientation == "AXIAL": - extent = ( + extent: tuple[int, int, int, int, int, int] = ( 0 - px, dx - 1 - px, 0 - py, @@ -111,7 +113,7 @@ def to_vtk_mask( oz -= sz v_image = numpy_support.numpy_to_vtk(n_array.flat) - extent = (0, dx - 1, 0, dy - 1, 0, dz - 1) + extent: tuple[Literal[0], int, Literal[0], int, Literal[0], int] = (0, dx - 1, 0, dy - 1, 0, dz - 1) # Generating the vtkImageData image = vtkImageData() @@ -140,7 +142,7 @@ def np_rgba_to_vtk( dy, dx, dc = n_array.shape v_image = numpy_support.numpy_to_vtk(n_array.reshape(dy * dx, dc)) - extent = (0, dx - 1, 0, dy - 1, 0, 0) + extent: tuple[Literal[0], int, Literal[0], int, Literal[0], Literal[0]] = (0, dx - 1, 0, dy - 1, 0, 0) # Generating the vtkImageData image = vtkImageData() @@ -208,3 +210,37 @@ def gdcm_to_numpy( else: return np_array + + +def convert_custom_bin_to_vtk(filename: str) -> vtkPolyData: + numbers: np.ndarray = np.fromfile(filename, count=3, dtype=np.int32) + points: np.ndarray = np.fromfile(filename, dtype=np.float32) + elements: np.ndarray = np.fromfile(filename, dtype=np.int32) + + points1: np.ndarray = points[3:(numbers[1]) * 3 + 3]*1000 + elements1: np.ndarray = elements[numbers[1] * 3 + 3:] + + points2: np.ndarray = points1.reshape(numbers[1], 3) + elements2: np.ndarray = elements1.reshape(numbers[2], 3) + + points: vtkPoints = vtkPoints() + triangles: vtkCellArray = vtkCellArray() + polydata: vtkPolyData = vtkPolyData() + + for i in range(len(points2)): + points.InsertNextPoint(points2[i]) + for i in range(len(elements2)): + triangle: vtkTriangle = vtkTriangle() + triangle.GetPointIds().SetId(0, elements2[i, 0]) + triangle.GetPointIds().SetId(1, elements2[i, 1]) + triangle.GetPointIds().SetId(2, elements2[i, 2]) + + triangles.InsertNextCell(triangle) + + polydata.SetPoints(points) + polydata.SetPolys(triangles) + + return polydata + + + From c6a06b0ff38067bb462f025f06e8c033d57032f9 Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Mon, 17 Apr 2023 00:07:35 +0530 Subject: [PATCH 06/16] added type info --- invesalius/data/converters.py | 2 +- invesalius/data/slice_.py | 783 +++++++------- invesalius/data/slice_data.py | 60 +- invesalius/data/styles.py | 1558 +++++++++++++++------------- invesalius/data/styles_3d.py | 242 +++-- invesalius/data/surface_process.py | 170 +-- 6 files changed, 1478 insertions(+), 1337 deletions(-) diff --git a/invesalius/data/converters.py b/invesalius/data/converters.py index fc01e1291..e48375b83 100644 --- a/invesalius/data/converters.py +++ b/invesalius/data/converters.py @@ -29,7 +29,7 @@ from vtk import vtkPoints, vtkCellArray, vtkPolyData, vtkTriangle -======= + from vtkmodules.vtkCommonCore import ( vtkPoints, ) diff --git a/invesalius/data/slice_.py b/invesalius/data/slice_.py index c7914fcc6..fe49e28c4 100644 --- a/invesalius/data/slice_.py +++ b/invesalius/data/slice_.py @@ -18,9 +18,13 @@ # -------------------------------------------------------------------------- import os import tempfile +from typing import Dict, List, Tuple, Union, Optional, Any + + import numpy as np from scipy import ndimage + from vtkmodules.vtkCommonCore import vtkLookupTable from vtkmodules.vtkImagingColor import vtkImageMapToWindowLevelColors from vtkmodules.vtkImagingCore import ( @@ -33,6 +37,8 @@ vtkColorTransferFunction, vtkWindowLevelLookupTable, ) +#import for vtkImageData +from vtkmodules.vtkCommonDataModel import vtkImageData from invesalius.pubsub import pub as Publisher @@ -47,9 +53,10 @@ from invesalius.project import Project from invesalius_cy import mips, transforms -OTHER = 0 -PLIST = 1 -WIDGET = 2 + +OTHER: int = 0 +PLIST: int = 1 +WIDGET: int = 2 class SliceBuffer(object): @@ -58,26 +65,26 @@ class SliceBuffer(object): from actual slices from each orientation. """ - def __init__(self): - self.index = -1 - self.image = None - self.mask = None - self.vtk_image = None - self.vtk_mask = None + def __init__(self) -> None: + self.index: int = -1 + self.image: np.ndarray = None + self.mask: Mask = None + self.vtk_image: vtkImageMapToColors = None + self.vtk_mask: vtkImageMapToColors = None - def discard_vtk_mask(self): + def discard_vtk_mask(self) -> None: self.vtk_mask = None - def discard_vtk_image(self): + def discard_vtk_image(self) -> None: self.vtk_image = None - def discard_mask(self): + def discard_mask(self) -> None: self.mask = None - def discard_image(self): + def discard_image(self) -> None: self.image = None - def discard_buffer(self): + def discard_buffer(self) -> None: self.index = -1 self.image = None self.mask = None @@ -85,94 +92,97 @@ def discard_buffer(self): self.vtk_mask = None + # Only one slice will be initialized per time (despite several viewers # show it from distinct perspectives). # Therefore, we use Singleton design pattern for implementing it. + + class Slice(metaclass=utils.Singleton): - def __init__(self): - self.current_mask = None + def __init__(self) -> None: + self.current_mask: Mask = None self.blend_filter = None - self.histogram = None - self._matrix = None - self._affine = np.identity(4) - self._n_tracts = 0 + self.histogram: np.ndarray = None + self._matrix: np.ndarray = None + self._affine: AffineTransform = np.identity(4) + self._n_tracts: int = 0 self._tracker = None - self.aux_matrices = {} - self.aux_matrices_colours = {} - self.state = const.STATE_DEFAULT + self.aux_matrices: Dict[str, np.ndarray] = {} + self.aux_matrices_colours: Dict[str, Tuple[float, float, float]] = {} + self.state: str = const.STATE_DEFAULT - self.to_show_aux = "" + self.to_show_aux: str = "" - self._type_projection = const.PROJECTION_NORMAL - self.n_border = const.PROJECTION_BORDER_SIZE + self._type_projection: str = const.PROJECTION_NORMAL + self.n_border: int = const.PROJECTION_BORDER_SIZE - self.interp_method = 2 + self.interp_method: int = 2 - self._spacing = (1.0, 1.0, 1.0) - self.center = [0, 0, 0] + self._spacing: Tuple[float, float, float] = (1.0, 1.0, 1.0) + self.center: List[float] = [0, 0, 0] - self.q_orientation = np.array((1, 0, 0, 0)) + self.q_orientation: np.ndarray = np.array((1, 0, 0, 0)) - self.number_of_colours = 256 - self.saturation_range = (0, 0) - self.hue_range = (0, 0) - self.value_range = (0, 1) + self.number_of_colours: int = 256 + self.saturation_range: Tuple[float, float] = (0, 0) + self.hue_range: Tuple[float, float] = (0, 0) + self.value_range: Tuple[float, float] = (0, 1) - self.buffer_slices = { + self.buffer_slices: Dict[str, SliceBuffer] = { "AXIAL": SliceBuffer(), "CORONAL": SliceBuffer(), "SAGITAL": SliceBuffer(), } - self.num_gradient = 0 - self.interaction_style = st.StyleStateManager() + self.num_gradient: int = 0 + self.interaction_style: st.StyleStateManager = st.StyleStateManager() - self.values = None - self.nodes = None + self.values: np.ndarray = None + self.nodes: np.ndarray = None - self.from_ = OTHER + self.from_: int = OTHER self.__bind_events() - self.opacity = 0.8 + self.opacity: float = 0.8 @property - def matrix(self): + def matrix(self) -> np.ndarray: return self._matrix @matrix.setter - def matrix(self, value): + def matrix(self, value: np.ndarray) -> None: self._matrix = value i, e = value.min(), value.max() - r = int(e) - int(i) + r: int = int(e) - int(i) self.histogram = np.histogram(self._matrix, r, (i, e))[0] self.center = [ (s * d / 2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing) ] @property - def spacing(self): + def spacing(self) -> Tuple[float, float, float]: return self._spacing @spacing.setter - def spacing(self, value): + def spacing(self, value: Tuple[float, float, float]) -> None: self._spacing = value self.center = [ (s * d / 2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing) ] @property - def affine(self): + def affine(self) -> AffineTransform: return self._affine @affine.setter - def affine(self, value): + def affine(self, value: AffineTransform) -> None: self._affine = value @property - def n_tracts(self): + def n_tracts(self) -> int: return self._n_tracts @n_tracts.setter - def n_tracts(self, value): + def n_tracts(self, value: int) -> None: self._n_tracts = value @property @@ -180,10 +190,11 @@ def tracker(self): return self._tracker @tracker.setter - def tracker(self, value): + def tracker(self, value) -> None: self._tracker = value - def __bind_events(self): + + def __bind_events(self) -> None: # General slice control Publisher.subscribe(self.CreateSurfaceFromIndex, "Create surface from index") # Mask control @@ -256,8 +267,8 @@ def __bind_events(self): Publisher.subscribe(self._set_interpolation_method, "Set interpolation method") - def GetMaxSliceNumber(self, orientation): - shape = self.matrix.shape + def GetMaxSliceNumber(self, orientation: str) -> int: + shape: Tuple[int, ...] = self.matrix.shape # Because matrix indexing starts with 0 so the last slice is the shape # minu 1. @@ -268,12 +279,12 @@ def GetMaxSliceNumber(self, orientation): elif orientation == "SAGITAL": return shape[2] - 1 - def discard_all_buffers(self): + def discard_all_buffers(self) -> None: for buffer_ in self.buffer_slices.values(): buffer_.discard_vtk_mask() buffer_.discard_mask() - def OnRemoveMasks(self, mask_indexes): + def OnRemoveMasks(self, mask_indexes: list) -> None: proj = Project() for item in mask_indexes: # if the deleted mask is the current mask, cleans the current mask @@ -289,9 +300,9 @@ def OnRemoveMasks(self, mask_indexes): Publisher.sendMessage("Reload actual slice") proj.RemoveMask(item) - def OnDuplicateMasks(self, mask_indexes): + def OnDuplicateMasks(self, mask_indexes: list) -> None: proj = Project() - mask_dict = proj.mask_dict + mask_dict: TwoWaysDictionary = proj.mask_dict for index in mask_indexes: original_mask = mask_dict[index] # compute copy name @@ -302,13 +313,13 @@ def OnDuplicateMasks(self, mask_indexes): copy_mask = original_mask.copy(new_name) self._add_mask_into_proj(copy_mask) - def OnEnableStyle(self, style): + def OnEnableStyle(self, style: str) -> None: if style in const.SLICE_STYLES: new_state = self.interaction_style.AddState(style) Publisher.sendMessage("Set slice interaction style", style=new_state) self.state = style - def OnDisableStyle(self, style): + def OnDisableStyle(self, style: str) -> None: if style in const.SLICE_STYLES: new_state = self.interaction_style.RemoveState(style) Publisher.sendMessage("Set slice interaction style", style=new_state) @@ -317,99 +328,100 @@ def OnDisableStyle(self, style): Publisher.sendMessage("Set interactor default cursor") self.state = new_state - def OnDisableActualStyle(self): - actual_state = self.interaction_style.GetActualState() + def OnDisableActualStyle(self) -> None: + actual_state: str = self.interaction_style.GetActualState() if actual_state != const.STATE_DEFAULT: - new_state = self.interaction_style.RemoveState(actual_state) + new_state: str = self.interaction_style.RemoveState(actual_state) Publisher.sendMessage("Set slice interaction style", style=new_state) # if (actual_state == const.SLICE_STATE_EDITOR): # Publisher.sendMessage('Set interactor default cursor') - self.state = new_state + self.state: str = new_state - def OnCloseProject(self): + def OnCloseProject(self) -> None: self.CloseProject() - def CloseProject(self): - f = self._matrix.filename + def CloseProject(self) -> None: + f: str = self._matrix.filename self._matrix._mmap.close() - self._matrix = None + self._matrix: np.ndarray = None os.remove(f) - self.current_mask = None + self.current_mask: Mask = None for name in self.aux_matrices: - m = self.aux_matrices[name] + m: np.ndarray = self.aux_matrices[name] try: - f = m.filename + f: str = m.filename except AttributeError: continue m._mmap.close() - m = None + m: np.ndarray = None os.remove(f) - self.aux_matrices = {} + self.aux_matrices: Dict[str, np.ndarray] = {} - self.values = None - self.nodes = None - self.from_ = OTHER - self.state = const.STATE_DEFAULT + self.values: np.ndarray = None + self.nodes: np.ndarray = None + self.from_: int = OTHER + self.state: str = const.STATE_DEFAULT - self.number_of_colours = 256 - self.saturation_range = (0, 0) - self.hue_range = (0, 0) - self.value_range = (0, 1) + self.number_of_colours: int = 256 + self.saturation_range: Tuple[float, float] = (0, 0) + self.hue_range: Tuple[float, float] = (0, 0) + self.value_range: Tuple[float, float] = (0, 1) - self.interaction_style.Reset() + self.interaction_style: st.StyleStateManager = st.StyleStateManager() Publisher.sendMessage("Select first item from slice menu") + - def __set_current_mask_threshold_limits(self, threshold_range): - thresh_min = threshold_range[0] - thresh_max = threshold_range[1] + def __set_current_mask_threshold_limits(self, threshold_range: Tuple[float, float]) -> None: + thresh_min: float = threshold_range[0] + thresh_max: float = threshold_range[1] if self.current_mask: - index = self.current_mask.index + index: int = self.current_mask.index self.SetMaskEditionThreshold(index, (thresh_min, thresh_max)) - def __add_mask(self, mask_name): + def __add_mask(self, mask_name: str) -> None: self.create_new_mask(name=mask_name) self.SetMaskColour(self.current_mask.index, self.current_mask.colour) - def __add_mask_thresh(self, mask_name, thresh, colour): + def __add_mask_thresh(self, mask_name: str, thresh: Tuple[float, float], colour: Tuple[int, int, int]) -> None: self.create_new_mask(name=mask_name, threshold_range=thresh, colour=colour) self.SetMaskColour(self.current_mask.index, self.current_mask.colour) self.SelectCurrentMask(self.current_mask.index) Publisher.sendMessage("Reload actual slice") - def __select_current_mask(self, index): + def __select_current_mask(self, index: int) -> None: self.SelectCurrentMask(index) - def __set_current_mask_edition_threshold(self, threshold_range): + def __set_current_mask_edition_threshold(self, threshold_range: Tuple[float, float]) -> None: if self.current_mask: - index = self.current_mask.index + index: int = self.current_mask.index self.SetMaskEditionThreshold(index, threshold_range) - def __set_current_mask_threshold(self, threshold_range): + def __set_current_mask_threshold(self, threshold_range: Tuple[float, float]) -> None: if self.current_mask is None: return - proj = Project() - index = proj.mask_dict.get_key(self.current_mask) + proj: Project = Project() + index: int = proj.mask_dict.get_key(self.current_mask) self.num_gradient += 1 self.current_mask.matrix[:] = 0 self.current_mask.clear_history() if self.current_mask.auto_update_mask and self.current_mask.volume is not None: - to_reload = True + to_reload: bool = True self.SetMaskThreshold( index, threshold_range, - slice_number = None, - orientation = None + slice_number=None, + orientation=None ) self.discard_all_buffers() Publisher.sendMessage("Reload actual slice") self.current_mask.modified(all_volume=True) return - to_reload = False + to_reload: bool = False if threshold_range != self.current_mask.threshold_range: for orientation in self.buffer_slices: self.buffer_slices[orientation].discard_vtk_mask() @@ -421,33 +433,35 @@ def __set_current_mask_threshold(self, threshold_range): ) # TODO: merge this code with apply_slice_buffer_to_mask - b_mask = self.buffer_slices["AXIAL"].mask + b_mask: Mask = self.buffer_slices["AXIAL"].mask if b_mask is not None: - n = self.buffer_slices["AXIAL"].index + 1 + n: int = self.buffer_slices["AXIAL"].index + 1 self.current_mask.matrix[n, 1:, 1:] = b_mask self.current_mask.matrix[n, 0, 0] = 1 - b_mask = self.buffer_slices["CORONAL"].mask + b_mask: Mask = self.buffer_slices["CORONAL"].mask if b_mask is not None: - n = self.buffer_slices["CORONAL"].index + 1 + n: int = self.buffer_slices["CORONAL"].index + 1 self.current_mask.matrix[1:, n, 1:] = b_mask self.current_mask.matrix[0, n, 0] = 1 - b_mask = self.buffer_slices["SAGITAL"].mask + b_mask: Mask = self.buffer_slices["SAGITAL"].mask if b_mask is not None: - n = self.buffer_slices["SAGITAL"].index + 1 + n: int = self.buffer_slices["SAGITAL"].index + 1 self.current_mask.matrix[1:, 1:, n] = b_mask self.current_mask.matrix[0, 0, n] = 1 if to_reload: Publisher.sendMessage("Reload actual slice") self.current_mask.modified(all_volume=False) + - def __set_current_mask_threshold_actual_slice(self, threshold_range): + + def __set_current_mask_threshold_actual_slice(self, threshold_range: Tuple[float, float]) -> None: if self.current_mask is None: return - proj = Project() - index = proj.mask_dict.get_key(self.current_mask) + proj: Project = Project() + index: int = proj.mask_dict.get_key(self.current_mask) for orientation in self.buffer_slices: self.buffer_slices[orientation].discard_vtk_mask() self.SetMaskThreshold( @@ -459,18 +473,19 @@ def __set_current_mask_threshold_actual_slice(self, threshold_range): self.num_gradient += 1 Publisher.sendMessage("Reload actual slice") + - def __set_current_mask_colour(self, colour): + def __set_current_mask_colour(self, colour: List[int]) -> None: # "if" is necessary because wx events are calling this before any mask # has been created if self.current_mask: - colour_vtk = [c / 255.0 for c in colour] + colour_vtk: list[float] = [c / 255.0 for c in colour] self.SetMaskColour(self.current_mask.index, colour_vtk) - def __set_mask_name(self, index, name): + def __set_mask_name(self, index: int, name: str) -> None: self.SetMaskName(index, name) - def __show_mask(self, index, value): + def __show_mask(self, index: int, value: bool) -> None: # "if" is necessary because wx events are calling this before any mask # has been created if self.current_mask: @@ -482,19 +497,19 @@ def __show_mask(self, index, value): self.SetTypeProjection(const.PROJECTION_NORMAL) Publisher.sendMessage("Reload actual slice") - def __hide_current_mask(self): + def __hide_current_mask(self) -> None: if self.current_mask: - index = self.current_mask.index + index: int = self.current_mask.index value = False Publisher.sendMessage("Show mask", index=index, value=value) - def __show_current_mask(self): + def __show_current_mask(self) -> None: if self.current_mask: - index = self.current_mask.index + index: int = self.current_mask.index value = True Publisher.sendMessage("Show mask", index=index, value=value) - def __clean_current_mask(self): + def __clean_current_mask(self) -> None: if self.current_mask: self.current_mask.clean() for buffer_ in self.buffer_slices.values(): @@ -507,18 +522,14 @@ def __clean_current_mask(self): session = ses.Session() session.ChangeProject() - def __export_slice(self, filename): - import h5py - + def __export_slice(self, filename: str) -> None: f = h5py.File(filename, "w") f["data"] = self.matrix f["spacing"] = self.spacing f.flush() f.close() - def __export_actual_mask(self, filename): - import h5py - + def __export_actual_mask(self, filename: str) -> None: f = h5py.File(filename, "w") self.do_threshold_to_all_slices() f["data"] = self.current_mask.matrix[1:, 1:, 1:] @@ -526,52 +537,52 @@ def __export_actual_mask(self, filename): f.flush() f.close() - def create_temp_mask(self): - temp_file = tempfile.mktemp() - shape = self.matrix.shape + def create_temp_mask(self) -> Tuple[str, np.memmap]: + temp_file: str = tempfile.mktemp() + shape: Tuple[int, ...] = self.matrix.shape matrix = np.memmap(temp_file, mode="w+", dtype="uint8", shape=shape) return temp_file, matrix - def edit_mask_pixel(self, operation, index, position, radius, orientation): - mask = self.buffer_slices[orientation].mask + def edit_mask_pixel(self, operation: str, index: np.ndarray, position: Union[int, Tuple[int, int]], radius: int, orientation: str) -> None: + mask: Mask = self.buffer_slices[orientation].mask image = self.buffer_slices[orientation].image thresh_min, thresh_max = self.current_mask.edition_threshold_range if hasattr(position, "__iter__"): px, py = position if orientation == "AXIAL": - sx = self.spacing[0] - sy = self.spacing[1] + sx: float = self.spacing[0] + sy: float = self.spacing[1] elif orientation == "CORONAL": - sx = self.spacing[0] - sy = self.spacing[2] + sx: float = self.spacing[0] + sy: float = self.spacing[2] elif orientation == "SAGITAL": - sx = self.spacing[2] - sy = self.spacing[1] + sx: float = self.spacing[2] + sy: float = self.spacing[1] else: if orientation == "AXIAL": - sx = self.spacing[0] - sy = self.spacing[1] - py = position / mask.shape[1] - px = position % mask.shape[1] + sx: float = self.spacing[0] + sy: float = self.spacing[1] + py: int = position / mask.shape[1] + px: int = position % mask.shape[1] elif orientation == "CORONAL": - sx = self.spacing[0] - sy = self.spacing[2] - py = position / mask.shape[1] - px = position % mask.shape[1] + sx: float = self.spacing[0] + sy: float = self.spacing[2] + py: int = position / mask.shape[1] + px: int = position % mask.shape[1] elif orientation == "SAGITAL": - sx = self.spacing[2] - sy = self.spacing[1] - py = position / mask.shape[1] - px = position % mask.shape[1] - - cx = index.shape[1] / 2 + 1 - cy = index.shape[0] / 2 + 1 - xi = int(px - index.shape[1] + cx) - xf = int(xi + index.shape[1]) - yi = int(py - index.shape[0] + cy) - yf = int(yi + index.shape[0]) + sx: float = self.spacing[2] + sy: float = self.spacing[1] + py: int = position / mask.shape[1] + px: int = position % mask.shape[1] + + cx: float = index.shape[1] / 2 + 1 + cy: float = index.shape[0] / 2 + 1 + xi: int = int(px - index.shape[1] + cx) + xf: int = int(xi + index.shape[1]) + yi: int = int(py - index.shape[0] + cy) + yf: int = int(yi + index.shape[0]) if yi < 0: index = index[abs(yi) :, :] @@ -623,61 +634,61 @@ def edit_mask_pixel(self, operation, index, position, radius, orientation): session.ChangeProject() def GetSlices( - self, orientation, slice_number, number_slices, inverted=False, border_size=1.0 - ): + self, orientation: str, slice_number: int, number_slices: int, inverted: bool = False, border_size: float = 1.0 + ) -> vtkImageData: if ( self.buffer_slices[orientation].index == slice_number and self._type_projection == const.PROJECTION_NORMAL ): if self.buffer_slices[orientation].vtk_image: - image = self.buffer_slices[orientation].vtk_image + image: vtkImageData = self.buffer_slices[orientation].vtk_image else: - n_image = self.get_image_slice( + n_image: np.ndarray = self.get_image_slice( orientation, slice_number, number_slices, inverted, border_size ) - image = converters.to_vtk( + image: vtkImageData = converters.to_vtk( n_image, self.spacing, slice_number, orientation ) - ww_wl_image = self.do_ww_wl(image) - image = self.do_colour_image(ww_wl_image) + ww_wl_image: vtkImageData = self.do_ww_wl(image) + image: vtkImageData = self.do_colour_image(ww_wl_image) if self.current_mask and self.current_mask.is_shown: if self.buffer_slices[orientation].vtk_mask: # Prints that during navigation causes delay in update # print "Getting from buffer" - mask = self.buffer_slices[orientation].vtk_mask + mask: vtkImageData = self.buffer_slices[orientation].vtk_mask else: # Prints that during navigation causes delay in update # print "Do not getting from buffer" - n_mask = self.get_mask_slice(orientation, slice_number) - mask = converters.to_vtk( + n_mask: np.ndarray = self.get_mask_slice(orientation, slice_number) + mask: vtkImageData = converters.to_vtk( n_mask, self.spacing, slice_number, orientation ) - mask = self.do_colour_mask(mask, self.opacity) + mask: vtkImageData = self.do_colour_mask(mask, self.opacity) self.buffer_slices[orientation].mask = n_mask - final_image = self.do_blend(image, mask) + final_image: vtkImageData = self.do_blend(image, mask) self.buffer_slices[orientation].vtk_mask = mask else: - final_image = image + final_image: vtkImageData = image self.buffer_slices[orientation].vtk_image = image else: - n_image = self.get_image_slice( + n_image: np.ndarray = self.get_image_slice( orientation, slice_number, number_slices, inverted, border_size ) - image = converters.to_vtk(n_image, self.spacing, slice_number, orientation) - ww_wl_image = self.do_ww_wl(image) - image = self.do_colour_image(ww_wl_image) + image: vtkImageData = converters.to_vtk(n_image, self.spacing, slice_number, orientation) + ww_wl_image: vtkImageData = self.do_ww_wl(image) + image: vtkImageData = self.do_colour_image(ww_wl_image) if self.current_mask and self.current_mask.is_shown: - n_mask = self.get_mask_slice(orientation, slice_number) - mask = converters.to_vtk( + n_mask: np.ndarray = self.get_mask_slice(orientation, slice_number) + mask: vtkImageData = converters.to_vtk( n_mask, self.spacing, slice_number, orientation ) - mask = self.do_colour_mask(mask, self.opacity) - final_image = self.do_blend(image, mask) + mask: vtkImageData = self.do_colour_mask(mask, self.opacity) + final_image: vtkImageData = self.do_blend(image, mask) else: - n_mask = None - final_image = image - mask = None + n_mask: np.ndarray = None + final_image: vtkImageData = image + mask: vtkImageData = None self.buffer_slices[orientation].index = slice_number self.buffer_slices[orientation].mask = n_mask @@ -685,9 +696,9 @@ def GetSlices( self.buffer_slices[orientation].vtk_mask = mask if self.to_show_aux == "watershed" and self.current_mask is not None and self.current_mask.is_shown: - m = self.get_aux_slice("watershed", orientation, slice_number) - tmp_vimage = converters.to_vtk(m, self.spacing, slice_number, orientation) - cimage = self.do_custom_colour( + m: np.ndarray = self.get_aux_slice("watershed", orientation, slice_number) + tmp_vimage: vtkImageData = converters.to_vtk(m, self.spacing, slice_number, orientation) + cimage: vtkImageData = self.do_custom_colour( tmp_vimage, { 0: (0.0, 0.0, 0.0, 0.0), @@ -695,58 +706,58 @@ def GetSlices( 2: (1.0, 0.0, 0.0, 1.0), }, ) - final_image = self.do_blend(final_image, cimage) + final_image: vtkImageData = self.do_blend(final_image, cimage) elif self.to_show_aux and self.current_mask: - m = self.get_aux_slice(self.to_show_aux, orientation, slice_number) - tmp_vimage = converters.to_vtk(m, self.spacing, slice_number, orientation) + m: np.ndarray = self.get_aux_slice(self.to_show_aux, orientation, slice_number) + tmp_vimage: vtkImageData = converters.to_vtk(m, self.spacing, slice_number, orientation) try: - colour_table = self.aux_matrices_colours[self.to_show_aux] + colour_table: dict = self.aux_matrices_colours[self.to_show_aux] except KeyError: - colour_table = { + colour_table: dict = { 0: (0.0, 0.0, 0.0, 0.0), 1: (0.0, 0.0, 0.0, 0.0), 254: (1.0, 0.0, 0.0, 1.0), 255: (1.0, 0.0, 0.0, 1.0), } - aux_image = self.do_custom_colour( + aux_image: vtkImageData = self.do_custom_colour( tmp_vimage, colour_table ) - final_image = self.do_blend(final_image, aux_image) + final_image: vtkImageData = self.do_blend(final_image, aux_image) return final_image - + def get_image_slice( self, - orientation, - slice_number, - number_slices=1, - inverted=False, - border_size=1.0, - ): + orientation: str, + slice_number: int, + number_slices: int = 1, + inverted: bool = False, + border_size: float = 1.0, + ) -> np.ndarray: dz, dy, dx = self.matrix.shape if ( self.buffer_slices[orientation].index == slice_number and self.buffer_slices[orientation].image is not None ): - n_image = self.buffer_slices[orientation].image + n_image: np.ndarray = self.buffer_slices[orientation].image else: if self._type_projection == const.PROJECTION_NORMAL: - number_slices = 1 + number_slices: int = 1 if np.any(self.q_orientation[1::]): cx, cy, cz = self.center - T0 = transformations.translation_matrix((-cz, -cy, -cx)) + T0: np.ndarray = transformations.translation_matrix((-cz, -cy, -cx)) # Rx = transformations.rotation_matrix(rx, (0, 0, 1)) # Ry = transformations.rotation_matrix(ry, (0, 1, 0)) # Rz = transformations.rotation_matrix(rz, (1, 0, 0)) # # R = transformations.euler_matrix(rz, ry, rx, 'rzyx') # R = transformations.concatenate_matrices(Rx, Ry, Rz) - R = transformations.quaternion_matrix(self.q_orientation) - T1 = transformations.translation_matrix((cz, cy, cx)) - M = transformations.concatenate_matrices(T1, R.T, T0) + R: np.ndarray = transformations.quaternion_matrix(self.q_orientation) + T1: np.ndarray = transformations.translation_matrix((cz, cy, cx)) + M: np.ndarray = transformations.concatenate_matrices(T1, R.T, T0) if orientation == "AXIAL": - tmp_array = np.array( + tmp_array: np.ndarray = np.array( self.matrix[slice_number : slice_number + number_slices] ) if np.any(self.q_orientation[1::]): @@ -761,19 +772,19 @@ def get_image_slice( tmp_array, ) if self._type_projection == const.PROJECTION_NORMAL: - n_image = tmp_array.reshape(dy, dx) + n_image: np.ndarray = tmp_array.reshape(dy, dx) else: if inverted: - tmp_array = tmp_array[::-1] + tmp_array: np.ndarray = tmp_array[::-1] if self._type_projection == const.PROJECTION_MaxIP: - n_image = np.array(tmp_array).max(0) + n_image: np.ndarray = np.array(tmp_array).max(0) elif self._type_projection == const.PROJECTION_MinIP: - n_image = np.array(tmp_array).min(0) + n_image: np.ndarray = np.array(tmp_array).min(0) elif self._type_projection == const.PROJECTION_MeanIP: - n_image = np.array(tmp_array).mean(0) + n_image: np.ndarray = np.array(tmp_array).mean(0) elif self._type_projection == const.PROJECTION_LMIP: - n_image = np.empty( + n_image: np.ndarray = np.empty( shape=(tmp_array.shape[1], tmp_array.shape[2]), dtype=tmp_array.dtype, ) @@ -781,7 +792,7 @@ def get_image_slice( tmp_array, 0, self.window_level, self.window_level, n_image ) elif self._type_projection == const.PROJECTION_MIDA: - n_image = np.empty( + n_image: np.ndarray = np.empty( shape=(tmp_array.shape[1], tmp_array.shape[2]), dtype=tmp_array.dtype, ) @@ -789,7 +800,7 @@ def get_image_slice( tmp_array, 0, self.window_level, self.window_level, n_image ) elif self._type_projection == const.PROJECTION_CONTOUR_MIP: - n_image = np.empty( + n_image: np.ndarray = np.empty( shape=(tmp_array.shape[1], tmp_array.shape[2]), dtype=tmp_array.dtype, ) @@ -803,7 +814,7 @@ def get_image_slice( n_image, ) elif self._type_projection == const.PROJECTION_CONTOUR_LMIP: - n_image = np.empty( + n_image: np.ndarray = np.empty( shape=(tmp_array.shape[1], tmp_array.shape[2]), dtype=tmp_array.dtype, ) @@ -817,7 +828,7 @@ def get_image_slice( n_image, ) elif self._type_projection == const.PROJECTION_CONTOUR_MIDA: - n_image = np.empty( + n_image: np.ndarray = np.empty( shape=(tmp_array.shape[1], tmp_array.shape[2]), dtype=tmp_array.dtype, ) @@ -831,10 +842,10 @@ def get_image_slice( n_image, ) else: - n_image = np.array(self.matrix[slice_number]) + n_image: np.ndarray = np.array(self.matrix[slice_number]) elif orientation == "CORONAL": - tmp_array = np.array( + tmp_array: np.array = np.array( self.matrix[:, slice_number : slice_number + number_slices, :] ) if np.any(self.q_orientation[1::]): @@ -850,22 +861,22 @@ def get_image_slice( ) if self._type_projection == const.PROJECTION_NORMAL: - n_image = tmp_array.reshape(dz, dx) + n_image: np.array = tmp_array.reshape(dz, dx) else: # if slice_number == 0: # slice_number = 1 # if slice_number - number_slices < 0: # number_slices = slice_number if inverted: - tmp_array = tmp_array[:, ::-1, :] + tmp_array: np.array = tmp_array[:, ::-1, :] if self._type_projection == const.PROJECTION_MaxIP: - n_image = np.array(tmp_array).max(1) + n_image: np.array = np.array(tmp_array).max(1) elif self._type_projection == const.PROJECTION_MinIP: - n_image = np.array(tmp_array).min(1) + n_image: np.array = np.array(tmp_array).min(1) elif self._type_projection == const.PROJECTION_MeanIP: - n_image = np.array(tmp_array).mean(1) + n_image: np.array = np.array(tmp_array).mean(1) elif self._type_projection == const.PROJECTION_LMIP: - n_image = np.empty( + n_image: np.array = np.empty( shape=(tmp_array.shape[0], tmp_array.shape[2]), dtype=tmp_array.dtype, ) @@ -923,9 +934,10 @@ def get_image_slice( n_image, ) else: - n_image = np.array(self.matrix[:, slice_number, :]) + n_image: np.array = np.array(self.matrix[:, slice_number, :]) + elif orientation == "SAGITAL": - tmp_array = np.array( + tmp_array: np.array = np.array( self.matrix[:, :, slice_number : slice_number + number_slices] ) if np.any(self.q_orientation[1::]): @@ -944,13 +956,13 @@ def get_image_slice( n_image = tmp_array.reshape(dz, dy) else: if inverted: - tmp_array = tmp_array[:, :, ::-1] + tmp_array: np.array = tmp_array[:, :, ::-1] if self._type_projection == const.PROJECTION_MaxIP: - n_image = np.array(tmp_array).max(2) + n_image: np.array = np.array(tmp_array).max(2) elif self._type_projection == const.PROJECTION_MinIP: - n_image = np.array(tmp_array).min(2) + n_image: np.array = np.array(tmp_array).min(2) elif self._type_projection == const.PROJECTION_MeanIP: - n_image = np.array(tmp_array).mean(2) + n_image: np.array = np.array(tmp_array).mean(2) elif self._type_projection == const.PROJECTION_LMIP: n_image = np.empty( shape=(tmp_array.shape[0], tmp_array.shape[1]), @@ -1011,12 +1023,12 @@ def get_image_slice( n_image, ) else: - n_image = np.array(self.matrix[:, :, slice_number]) + n_image: np.array = np.array(self.matrix[:, :, slice_number: int]) self.buffer_slices[orientation].image = n_image return n_image - def get_mask_slice(self, orientation, slice_number): + def get_mask_slice(self, orientation: str, slice_number: int) -> np.array: """ It gets the from actual mask the given slice from given orientation """ @@ -1028,7 +1040,7 @@ def get_mask_slice(self, orientation, slice_number): and self.buffer_slices[orientation].mask is not None ): return self.buffer_slices[orientation].mask - n = slice_number + 1 + n: int = slice_number + 1 if orientation == "AXIAL": if self.current_mask.matrix[n, 0, 0] == 0: mask = self.current_mask.matrix[n, 1:, 1:] @@ -1067,7 +1079,7 @@ def get_mask_slice(self, orientation, slice_number): return n_mask - def get_aux_slice(self, name, orientation, n): + def get_aux_slice(self, name: str, orientation: str, n: int) -> np.array: m = self.aux_matrices[name] if orientation == "AXIAL": return np.array(m[n]) @@ -1076,7 +1088,7 @@ def get_aux_slice(self, name, orientation, n): elif orientation == "SAGITAL": return np.array(m[:, :, n]) - def GetNumberOfSlices(self, orientation): + def GetNumberOfSlices(self, orientation: str) -> int: if orientation == "AXIAL": return self.matrix.shape[0] elif orientation == "CORONAL": @@ -1084,7 +1096,7 @@ def GetNumberOfSlices(self, orientation): elif orientation == "SAGITAL": return self.matrix.shape[2] - def SetMaskColour(self, index, colour, update=True): + def SetMaskColour(self, index: int, colour: list, update: bool=True) -> None: "Set a mask colour given its index and colour (RGB 0-1 values)" proj = Project() proj.mask_dict[index].set_colour(colour) @@ -1104,7 +1116,7 @@ def SetMaskColour(self, index, colour, update=True): session = ses.Session() session.ChangeProject() - def SetMaskName(self, index, name): + def SetMaskName(self, index: int, name: str) -> None: "Rename a mask given its index and the new name" proj = Project() proj.mask_dict[index].name = name @@ -1112,14 +1124,14 @@ def SetMaskName(self, index, name): session = ses.Session() session.ChangeProject() - def SetMaskEditionThreshold(self, index, threshold_range): + def SetMaskEditionThreshold(self, index: int, threshold_range: tuple) -> None: "Set threshold bounds to be used while editing slice" proj = Project() proj.mask_dict[index].edition_threshold_range = threshold_range def SetMaskThreshold( - self, index, threshold_range, slice_number=None, orientation=None - ): + self, index: int, threshold_range: tuple, slice_number: int=None, orientation: str=None + ) -> None: """ Set a mask threshold range given its index and tuple of min and max threshold values. @@ -1159,7 +1171,7 @@ def SetMaskThreshold( ) proj.mask_dict[index].threshold_range = threshold_range - def ShowMask(self, index, value): + def ShowMask(self, index: int, value: int) -> None: "Show a mask given its index and 'show' value (0: hide, other: show)" proj = Project() mask = proj.mask_dict[index] @@ -1186,7 +1198,7 @@ def ShowMask(self, index, value): # --------------------------------------------------------------------------- - def SelectCurrentMask(self, index): + def SelectCurrentMask(self, index: int) -> None: "Insert mask data, based on given index, into pipeline." if self.current_mask: self.current_mask.is_shown = False @@ -1221,7 +1233,7 @@ def SelectCurrentMask(self, index): # --------------------------------------------------------------------------- - def CreateSurfaceFromIndex(self, surface_parameters): + def CreateSurfaceFromIndex(self, surface_parameters: dict) -> None: proj = Project() mask = proj.mask_dict[surface_parameters["options"]["index"]] @@ -1233,16 +1245,16 @@ def CreateSurfaceFromIndex(self, surface_parameters): surface_parameters=surface_parameters, ) - def GetOutput(self): + def GetOutput(self) -> object: return self.blend_filter.GetOutput() - def _set_projection_type(self, projection_id): + def _set_projection_type(self, projection_id: int) -> None: self.SetTypeProjection(projection_id) - def _set_interpolation_method(self, interp_method): + def _set_interpolation_method(self, interp_method: int) -> None: self.SetInterpolationMethod(interp_method) - def SetTypeProjection(self, tprojection): + def SetTypeProjection(self, tprojection: int) -> None: if self._type_projection != tprojection: if self._type_projection == const.PROJECTION_NORMAL: Publisher.sendMessage("Hide current mask") @@ -1258,16 +1270,16 @@ def SetTypeProjection(self, tprojection): Publisher.sendMessage("Check projection menu", projection_id=tprojection) - def SetInterpolationMethod(self, interp_method): + def SetInterpolationMethod(self, interp_method: str) -> None: if self.interp_method != interp_method: self.interp_method = interp_method for buffer_ in self.buffer_slices.values(): buffer_.discard_buffer() Publisher.sendMessage("Reload actual slice") - def UpdateWindowLevelBackground(self, window, level): - self.window_width = window - self.window_level = level + def UpdateWindowLevelBackground(self, window: int, level: int) -> None: + self.window_width: int = window + self.window_level: int = level for buffer_ in self.buffer_slices.values(): if self._type_projection in ( @@ -1283,7 +1295,7 @@ def UpdateWindowLevelBackground(self, window, level): Publisher.sendMessage("Reload actual slice") - def UpdateColourTableBackground(self, values): + def UpdateColourTableBackground(self, values: list) -> None: self.from_ = OTHER self.number_of_colours = values[0] self.saturation_range = values[1] @@ -1293,7 +1305,7 @@ def UpdateColourTableBackground(self, values): buffer_.discard_vtk_image() Publisher.sendMessage("Reload actual slice") - def UpdateColourTableBackgroundPlist(self, values): + def UpdateColourTableBackgroundPlist(self, values: list) -> None: self.values = values self.from_ = PLIST for buffer_ in self.buffer_slices.values(): @@ -1301,7 +1313,7 @@ def UpdateColourTableBackgroundPlist(self, values): Publisher.sendMessage("Reload actual slice") - def UpdateColourTableBackgroundWidget(self, nodes): + def UpdateColourTableBackgroundWidget(self, nodes: list) -> None: self.nodes = nodes self.from_ = WIDGET for buffer_ in self.buffer_slices.values(): @@ -1325,7 +1337,7 @@ def UpdateColourTableBackgroundWidget(self, nodes): Publisher.sendMessage("Reload actual slice") - def UpdateSlice3D(self, widget, orientation): + def UpdateSlice3D(self, widget: object, orientation: str) -> None: img = self.buffer_slices[orientation].vtk_image original_orientation = Project().original_orientation cast = vtkImageCast() @@ -1346,14 +1358,14 @@ def UpdateSlice3D(self, widget, orientation): def create_new_mask( self, - name=None, - colour=None, - opacity=None, - threshold_range=None, - edition_threshold_range=None, - add_to_project=True, - show=True, - ): + name: str = None, + colour: tuple = None, + opacity: float = None, + threshold_range: tuple = None, + edition_threshold_range: tuple = None, + add_to_project: bool = True, + show: bool = True, + ) -> Mask: """ Creates a new mask and add it to project. @@ -1393,7 +1405,7 @@ def create_new_mask( return future_mask - def _add_mask_into_proj(self, mask, show=True): + def _add_mask_into_proj(self, mask: Mask, show: bool = True) -> None: """ Insert a new mask into project and retrieve its index. @@ -1402,7 +1414,7 @@ def _add_mask_into_proj(self, mask, show=True): show: indicate if the mask will be shown. """ proj = Project() - index = proj.AddMask(mask) + index: int = proj.AddMask(mask) mask.index = index ## update gui related to mask @@ -1416,25 +1428,25 @@ def _add_mask_into_proj(self, mask, show=True): Publisher.sendMessage("Change mask selected", index=mask.index) Publisher.sendMessage("Update slice viewer") - def do_ww_wl(self, image): + def do_ww_wl(self, image: vtkImageData) -> vtkImageData: if self.from_ == PLIST: - lut = vtkWindowLevelLookupTable() + lut: vtkWindowLevelLookupTable = vtkWindowLevelLookupTable() lut.SetWindow(self.window_width) lut.SetLevel(self.window_level) lut.Build() - i = 0 + i: int = 0 for r, g, b in self.values: lut.SetTableValue(i, r / 255.0, g / 255.0, b / 255.0, 1.0) i += 1 - colorer = vtkImageMapToColors() + colorer: vtkImageMapToColors = vtkImageMapToColors() colorer.SetInputData(image) colorer.SetLookupTable(lut) colorer.SetOutputFormatToRGB() colorer.Update() elif self.from_ == WIDGET: - lut = vtkColorTransferFunction() + lut: vtkColorTransferFunction = vtkColorTransferFunction() for n in self.nodes: r, g, b = n.colour @@ -1442,13 +1454,13 @@ def do_ww_wl(self, image): lut.Build() - colorer = vtkImageMapToColors() + colorer: vtkImageMapToColors = vtkImageMapToColors() colorer.SetLookupTable(lut) colorer.SetInputData(image) colorer.SetOutputFormatToRGB() colorer.Update() else: - colorer = vtkImageMapToWindowLevelColors() + colorer: vtkImageMapToWindowLevelColors = vtkImageMapToWindowLevelColors() colorer.SetInputData(image) colorer.SetWindow(self.window_width) colorer.SetLevel(self.window_level) @@ -1457,19 +1469,19 @@ def do_ww_wl(self, image): return colorer.GetOutput() - def _update_wwwl_widget_nodes(self, ww, wl): + def _update_wwwl_widget_nodes(self, ww: float, wl: float) -> None: if self.from_ == WIDGET: - knodes = sorted(self.nodes) + knodes: List[Node] = sorted(self.nodes) - p1 = knodes[0] - p2 = knodes[-1] - half = (p2.value - p1.value) / 2.0 - middle = p1.value + half + p1: Node = knodes[0] + p2: Node = knodes[-1] + half: float = (p2.value - p1.value) / 2.0 + middle: float = p1.value + half - shiftWL = wl - middle - shiftWW = p1.value + shiftWL - (wl - 0.5 * ww) + shiftWL: float = wl - middle + shiftWW: float = p1.value + shiftWL - (wl - 0.5 * ww) - factor = 1.0 + factor: float = 1.0 for n, node in enumerate(knodes): factor = abs(node.value - middle) / half @@ -1482,7 +1494,7 @@ def _update_wwwl_widget_nodes(self, ww, wl): else: node.value += shiftWW * factor - def do_threshold_to_a_slice(self, slice_matrix, mask, threshold=None): + def do_threshold_to_a_slice(self, slice_matrix: np.ndarray, mask: np.ndarray, threshold: Tuple[float, float] = None) -> np.ndarray: """ Based on the current threshold bounds generates a threshold mask to given slice_matrix. @@ -1492,20 +1504,20 @@ def do_threshold_to_a_slice(self, slice_matrix, mask, threshold=None): else: thresh_min, thresh_max = self.current_mask.threshold_range - m = ((slice_matrix >= thresh_min) & (slice_matrix <= thresh_max)) * 255 + m: np.ndarray = ((slice_matrix >= thresh_min) & (slice_matrix <= thresh_max)) * 255 m[mask == 1] = 1 m[mask == 2] = 2 m[mask == 253] = 253 m[mask == 254] = 254 return m.astype("uint8") - def do_threshold_to_all_slices(self, mask=None): + def do_threshold_to_all_slices(self, mask: Mask = None) -> None: """ Apply threshold to all slices. Params: - mask: the mask where result of the threshold will be stored.If - None, it'll be the current mask. + None, it'll be the current mask. """ if mask is None: mask = self.current_mask @@ -1518,12 +1530,12 @@ def do_threshold_to_all_slices(self, mask=None): mask.matrix.flush() - def do_colour_image(self, imagedata): + def do_colour_image(self, imagedata: vtkImageData) -> vtkImageData: if self.from_ in (PLIST, WIDGET): return imagedata else: # map scalar values into colors - lut_bg = vtkLookupTable() + lut_bg: vtkLookupTable = vtkLookupTable() lut_bg.SetTableRange(imagedata.GetScalarRange()) lut_bg.SetSaturationRange(self.saturation_range) lut_bg.SetHueRange(self.hue_range) @@ -1531,7 +1543,7 @@ def do_colour_image(self, imagedata): lut_bg.Build() # map the input image through a lookup table - img_colours_bg = vtkImageMapToColors() + img_colours_bg: vtkImageMapToColors = vtkImageMapToColors() img_colours_bg.SetOutputFormatToRGB() img_colours_bg.SetLookupTable(lut_bg) img_colours_bg.SetInputData(imagedata) @@ -1544,7 +1556,7 @@ def do_colour_mask(self, imagedata, opacity): r, g, b = self.current_mask.colour[:3] # map scalar values into colors - lut_mask = vtkLookupTable() + lut_mask: vtkLookupTable = vtkLookupTable() lut_mask.SetNumberOfColors(256) lut_mask.SetHueRange(const.THRESHOLD_HUE_RANGE) lut_mask.SetSaturationRange(1, 1) @@ -1562,7 +1574,7 @@ def do_colour_mask(self, imagedata, opacity): # self.lut_mask = lut_mask # map the input image through a lookup table - img_colours_mask = vtkImageMapToColors() + img_colours_mask: vtkImageMapToColors = vtkImageMapToColors() img_colours_mask.SetLookupTable(lut_mask) img_colours_mask.SetOutputFormatToRGBA() img_colours_mask.SetInputData(imagedata) @@ -1571,13 +1583,13 @@ def do_colour_mask(self, imagedata, opacity): return img_colours_mask.GetOutput() - def do_custom_colour(self, imagedata, map_colours): + def do_custom_colour(self, imagedata: vtkImageData, map_colours: dict) -> vtkImageData: # map scalar values into colors - minv = min(map_colours) - maxv = max(map_colours) - ncolours = maxv - minv + 1 + minv: int = min(map_colours) + maxv: int = max(map_colours) + ncolours: int = maxv - minv + 1 - lut_mask = vtkLookupTable() + lut_mask: vtkLookupTable = vtkLookupTable() lut_mask.SetNumberOfColors(ncolours) lut_mask.SetHueRange(const.THRESHOLD_HUE_RANGE) lut_mask.SetSaturationRange(1, 1) @@ -1594,7 +1606,7 @@ def do_custom_colour(self, imagedata, map_colours): # self.lut_mask = lut_mask # map the input image through a lookup table - img_colours_mask = vtkImageMapToColors() + img_colours_mask: vtkImageMapToColors = vtkImageMapToColors() img_colours_mask.SetLookupTable(lut_mask) img_colours_mask.SetOutputFormatToRGBA() img_colours_mask.SetInputData(imagedata) @@ -1603,11 +1615,11 @@ def do_custom_colour(self, imagedata, map_colours): return img_colours_mask.GetOutput() - def do_blend(self, imagedata, mask): + def do_blend(self, imagedata: vtkImageData, mask: vtkImageData) -> vtkImageData: """ blend image with the mask. """ - blend_imagedata = vtkImageBlend() + blend_imagedata: vtkImageBlend = vtkImageBlend() blend_imagedata.SetBlendModeToNormal() # blend_imagedata.SetOpacity(0, 1.0) blend_imagedata.SetOpacity(1, 0.8) @@ -1617,113 +1629,115 @@ def do_blend(self, imagedata, mask): return blend_imagedata.GetOutput() - def _do_boolean_op(self, operation, mask1, mask2): + def _do_boolean_op(self, operation: str, mask1: vtkImageData, mask2: vtkImageData) -> None: self.do_boolean_op(operation, mask1, mask2) - def do_boolean_op(self, op, m1, m2): - name_ops = { + def do_boolean_op(self, op: str, m1: Mask, m2: Mask) -> None: + name_ops: Dict[str, str] = { const.BOOLEAN_UNION: _(u"Union"), const.BOOLEAN_DIFF: _(u"Diff"), const.BOOLEAN_AND: _(u"Intersection"), const.BOOLEAN_XOR: _(u"XOR"), } - - name = u"%s_%s_%s" % (name_ops[op], m1.name, m2.name) - proj = Project() - mask_dict = proj.mask_dict - names_list = [mask_dict[i].name for i in mask_dict.keys()] - new_name = utils.next_copy_name(name, names_list) - - future_mask = Mask() + + name: str = u"%s_%s_%s" % (name_ops[op], m1.name, m2.name) + proj: Project = Project() + mask_dict: Dict[int, Mask] = proj.mask_dict + names_list: List[str] = [mask_dict[i].name for i in mask_dict.keys()] + new_name: str = utils.next_copy_name(name, names_list) + + future_mask: Mask = Mask() future_mask.create_mask(self.matrix.shape) future_mask.spacing = self.spacing future_mask.name = new_name - + future_mask.matrix[:] = 1 - m = future_mask.matrix[1:, 1:, 1:] - + m: np.ndarray = future_mask.matrix[1:, 1:, 1:] + self.do_threshold_to_all_slices(m1) - m1 = m1.matrix[1:, 1:, 1:] - + m1: np.ndarray = m1.matrix[1:, 1:, 1:] + self.do_threshold_to_all_slices(m2) - m2 = m2.matrix[1:, 1:, 1:] - + m2: np.ndarray = m2.matrix[1:, 1:, 1:] + if op == const.BOOLEAN_UNION: m[:] = ((m1 > 2) + (m2 > 2)) * 255 - + elif op == const.BOOLEAN_DIFF: m[:] = ((m1 > 2) ^ ((m1 > 2) & (m2 > 2))) * 255 - + elif op == const.BOOLEAN_AND: m[:] = ((m1 > 2) & (m2 > 2)) * 255 - + elif op == const.BOOLEAN_XOR: m[:] = np.logical_xor((m1 > 2), (m2 > 2)) * 255 - + for o in self.buffer_slices: self.buffer_slices[o].discard_mask() self.buffer_slices[o].discard_vtk_mask() - + future_mask.was_edited = True self._add_mask_into_proj(future_mask) - - def apply_slice_buffer_to_mask(self, orientation): + + def apply_slice_buffer_to_mask(self, orientation: str) -> None: """ Apply the modifications (edition) in mask buffer to mask. """ - b_mask = self.buffer_slices[orientation].mask - index = self.buffer_slices[orientation].index - + b_mask: np.ndarray = self.buffer_slices[orientation].mask + index: int = self.buffer_slices[orientation].index + # TODO: Voltar a usar marcacao na mascara if orientation == "AXIAL": # if self.current_mask.matrix[index+1, 0, 0] != 2: # self.current_mask.save_history(index, orientation, # self.current_mask.matrix[index+1,1:,1:], # clean=True) - p_mask = self.current_mask.matrix[index + 1, 1:, 1:].copy() + p_mask: np.ndarray = self.current_mask.matrix[index + 1, 1:, 1:].copy() self.current_mask.matrix[index + 1, 1:, 1:] = b_mask self.current_mask.matrix[index + 1, 0, 0] = 2 - + elif orientation == "CORONAL": # if self.current_mask.matrix[0, index+1, 0] != 2: # self.current_mask.save_history(index, orientation, # self.current_mask.matrix[1:, index+1, 1:], # clean=True) - p_mask = self.current_mask.matrix[1:, index + 1, 1:].copy() + p_mask: np.ndarray = self.current_mask.matrix[1:, index + 1, 1:].copy() self.current_mask.matrix[1:, index + 1, 1:] = b_mask self.current_mask.matrix[0, index + 1, 0] = 2 - + elif orientation == "SAGITAL": # if self.current_mask.matrix[0, 0, index+1] != 2: # self.current_mask.save_history(index, orientation, # self.current_mask.matrix[1:, 1:, index+1], # clean=True) - p_mask = self.current_mask.matrix[1:, 1:, index + 1].copy() + p_mask: np.ndarray = self.current_mask.matrix[1:, 1:, index + 1].copy() self.current_mask.matrix[1:, 1:, index + 1] = b_mask self.current_mask.matrix[0, 0, index + 1] = 2 - + self.current_mask.save_history(index, orientation, b_mask, p_mask) self.current_mask.was_edited = True - + for o in self.buffer_slices: if o != orientation: self.buffer_slices[o].discard_mask() self.buffer_slices[o].discard_vtk_mask() Publisher.sendMessage("Reload actual slice") - - def apply_reorientation(self): - temp_file = tempfile.mktemp() - mcopy = np.memmap( + + def apply_reorientation(self) -> None: + temp_file: str = tempfile.mktemp() + mcopy: np.memmap = np.memmap( temp_file, shape=self.matrix.shape, dtype=self.matrix.dtype, mode="w+" ) mcopy[:] = self.matrix - - cx, cy, cz = self.center - T0 = transformations.translation_matrix((-cz, -cy, -cx)) - R = transformations.quaternion_matrix(self.q_orientation) - T1 = transformations.translation_matrix((cz, cy, cx)) - M = transformations.concatenate_matrices(T1, R.T, T0) - + + cx: float + cy: float + cz: float = self.center + T0: np.ndarray = transformations.translation_matrix((-cz, -cy, -cx)) + R: np.ndarray = transformations.quaternion_matrix(self.q_orientation) + T1: np.ndarray = transformations.translation_matrix((cz, cy, cx)) + M: np.ndarray = transformations.concatenate_matrices(T1, R.T, T0) + transforms.apply_view_matrix_transform( mcopy, self.spacing, @@ -1734,28 +1748,29 @@ def apply_reorientation(self): mcopy.min(), self.matrix, ) - + del mcopy os.remove(temp_file) - - self.q_orientation = np.array((1, 0, 0, 0)) - self.center = [ + + self.q_orientation: np.ndarray = np.array((1, 0, 0, 0)) + self.center: List[float] = [ (s * d / 2.0) for (d, s) in zip(self.matrix.shape[::-1], self.spacing) ] - + self.__clean_current_mask() if self.current_mask: self.current_mask.matrix[:] = 0 self.current_mask.was_edited = False - + for o in self.buffer_slices: self.buffer_slices[o].discard_buffer() - + Publisher.sendMessage("Reload actual slice") + - def __undo_edition(self): - buffer_slices = self.buffer_slices - actual_slices = { + def __undo_edition(self) -> None: + buffer_slices: Dict[str, Slice] = self.buffer_slices + actual_slices: Dict[str, int] = { "AXIAL": buffer_slices["AXIAL"].index, "CORONAL": buffer_slices["CORONAL"].index, "SAGITAL": buffer_slices["SAGITAL"].index, @@ -1767,9 +1782,9 @@ def __undo_edition(self): self.buffer_slices[o].discard_vtk_mask() Publisher.sendMessage("Reload actual slice") - def __redo_edition(self): - buffer_slices = self.buffer_slices - actual_slices = { + def __redo_edition(self) -> None: + buffer_slices: Dict[str, Slice] = self.buffer_slices + actual_slices: Dict[str, int] = { "AXIAL": buffer_slices["AXIAL"].index, "CORONAL": buffer_slices["CORONAL"].index, "SAGITAL": buffer_slices["SAGITAL"].index, @@ -1781,11 +1796,11 @@ def __redo_edition(self): self.buffer_slices[o].discard_vtk_mask() Publisher.sendMessage("Reload actual slice") - def _open_image_matrix(self, filename, shape, dtype): - self.matrix_filename = filename - self.matrix = np.memmap(filename, shape=shape, dtype=dtype, mode="r+") + def _open_image_matrix(self, filename: str, shape: Tuple[int], dtype: np.dtype) -> None: + self.matrix_filename: str = filename + self.matrix: np.memmap = np.memmap(filename, shape=shape, dtype=dtype, mode="r+") - def OnFlipVolume(self, axis): + def OnFlipVolume(self, axis: int) -> None: if axis == 0: self.matrix[:] = self.matrix[::-1] elif axis == 1: @@ -1796,35 +1811,35 @@ def OnFlipVolume(self, axis): for buffer_ in self.buffer_slices.values(): buffer_.discard_buffer() - def OnSwapVolumeAxes(self, axes): + def OnSwapVolumeAxes(self, axes: Tuple[int]) -> None: axis0, axis1 = axes - self.matrix = self.matrix.swapaxes(axis0, axis1) + self.matrix: np.ndarray = self.matrix.swapaxes(axis0, axis1) if (axis0, axis1) == (2, 1): - self.spacing = self.spacing[1], self.spacing[0], self.spacing[2] + self.spacing: Tuple[float] = self.spacing[1], self.spacing[0], self.spacing[2] elif (axis0, axis1) == (2, 0): - self.spacing = self.spacing[2], self.spacing[1], self.spacing[0] + self.spacing: Tuple[float] = self.spacing[2], self.spacing[1], self.spacing[0] elif (axis0, axis1) == (1, 0): - self.spacing = self.spacing[0], self.spacing[2], self.spacing[1] + self.spacing: Tuple[float] = self.spacing[0], self.spacing[2], self.spacing[1] for buffer_ in self.buffer_slices.values(): buffer_.discard_buffer() - def OnExportMask(self, filename, filetype): - imagedata = self.current_mask.imagedata + def OnExportMask(self, filename: str, filetype: str) -> None: + imagedata: vtkImageData = self.current_mask.imagedata # imagedata = self.imagedata if filetype == const.FILETYPE_IMAGEDATA: iu.Export(imagedata, filename) - def _fill_holes_auto(self, parameters): - target = parameters["target"] - conn = parameters["conn"] - orientation = parameters["orientation"] - size = parameters["size"] + def _fill_holes_auto(self, parameters: Dict[str, Any]) -> None: + target: str = parameters["target"] + conn: int = parameters["conn"] + orientation: str = parameters["orientation"] + size: int = parameters["size"] if target == "2D": - index = self.buffer_slices[orientation].index + index: int = self.buffer_slices[orientation].index else: - index = 0 + index: int = 0 self.do_threshold_to_all_slices() self.current_mask.fill_holes_auto(target, conn, orientation, index, size) @@ -1840,31 +1855,31 @@ def _fill_holes_auto(self, parameters): self.current_mask.modified(target == '3D') Publisher.sendMessage("Reload actual slice") - def calc_image_density(self, mask=None): + def calc_image_density(self, mask: Mask = None) -> Tuple[float]: if mask is None: - mask = self.current_mask + mask: Mask = self.current_mask self.do_threshold_to_all_slices(mask) - values = self.matrix[mask.matrix[1:, 1:, 1:] > 127] + values: np.ndarray = self.matrix[mask.matrix[1:, 1:, 1:] > 127] if len(values): - _min = values.min() - _max = values.max() - _mean = values.mean() - _std = values.std() + _min: float = values.min() + _max: float = values.max() + _mean: float = values.mean() + _std: float = values.std() return _min, _max, _mean, _std else: return 0, 0, 0, 0 - def calc_mask_area(self, mask=None): + def calc_mask_area(self, mask: Mask = None) -> float: if mask is None: - mask = self.current_mask + mask: Mask = self.current_mask self.do_threshold_to_all_slices(mask) - bin_img = mask.matrix[1:, 1:, 1:] > 127 + bin_img: np.ndarray = mask.matrix[1:, 1:, 1:] > 127 sx, sy, sz = self.spacing - kernel = np.zeros((3, 3, 3)) + kernel: np.ndarray = np.zeros((3, 3, 3)) kernel[1, 1, 1] = 2 * sx * sy + 2 * sx * sz + 2 * sy * sz kernel[0, 1, 1] = -(sx * sy) kernel[2, 1, 1] = -(sx * sy) @@ -1876,15 +1891,15 @@ def calc_mask_area(self, mask=None): kernel[1, 1, 2] = -(sy * sz) # area = ndimage.generic_filter(bin_img * 1.0, _conv_area, size=(3, 3, 3), mode='constant', cval=1, extra_arguments=(sx, sy, sz)).sum() - area = transforms.convolve_non_zero(bin_img * 1.0, kernel, 1).sum() + area: float = transforms.convolve_non_zero(bin_img * 1.0, kernel, 1).sum() return area -def _conv_area(x, sx, sy, sz): - x = x.reshape((3, 3, 3)) +def _conv_area(x: np.ndarray, sx: float, sy: float, sz: float) -> float: + x: np.ndarray = x.reshape((3, 3, 3)) if x[1, 1, 1]: - kernel = np.zeros((3, 3, 3)) + kernel: np.ndarray = np.zeros((3, 3, 3)) kernel[1, 1, 1] = 2 * sx * sy + 2 * sx * sz + 2 * sy * sz kernel[0, 1, 1] = -(sx * sy) kernel[2, 1, 1] = -(sx * sy) diff --git a/invesalius/data/slice_data.py b/invesalius/data/slice_data.py index 4e4e47901..49075a2a1 100644 --- a/invesalius/data/slice_data.py +++ b/invesalius/data/slice_data.py @@ -20,47 +20,50 @@ import invesalius.constants as const import invesalius.data.vtk_utils as vu +from typing import Any, Optional, Tuple +BORDER_UP: int = 1 +BORDER_DOWN: int = 2 +BORDER_LEFT: int = 4 +BORDER_RIGHT: int = 8 +BORDER_ALL: int = BORDER_UP | BORDER_DOWN | BORDER_LEFT | BORDER_RIGHT +BORDER_NONE: int = 0 -BORDER_UP = 1 -BORDER_DOWN = 2 -BORDER_LEFT = 4 -BORDER_RIGHT = 8 -BORDER_ALL = BORDER_UP | BORDER_DOWN | BORDER_LEFT | BORDER_RIGHT -BORDER_NONE = 0 -class SliceData(object): - def __init__(self): - self.actor = None - self.cursor = None - self.text = None - self.layer = 99 - self.number = 0 - self.orientation = 'AXIAL' - self.renderer = None - self.canvas_renderer = None - self.overlay_renderer = None + + +class SliceData(object()): + def __init__(self) -> None: + self.actor: Any = None + self.cursor: Any = None + self.text: Any = None + self.layer: int = 99 + self.number: int = 0 + self.orientation: str = 'AXIAL' + self.renderer: Any = None + self.canvas_renderer: Any = None + self.overlay_renderer: Any = None self.__create_text() - def __create_text(self): - colour = const.ORIENTATION_COLOUR[self.orientation] + def __create_text(self) -> None: + colour: Tuple[int, int, int] = const.ORIENTATION_COLOUR[self.orientation] - text = vu.TextZero() + text: Any = vu.TextZero() text.SetColour(colour) text.SetSize(const.TEXT_SIZE_LARGE) text.SetPosition(const.TEXT_POS_LEFT_DOWN_ZERO) text.SetSymbolicSize(wx.FONTSIZE_LARGE) - #text.SetVerticalJustificationToBottom() + # text.SetVerticalJustificationToBottom() text.SetValue(self.number) self.text = text - def SetCursor(self, cursor): + def SetCursor(self, cursor: Any) -> None: if self.cursor: self.overlay_renderer.RemoveActor(self.cursor.actor) self.overlay_renderer.AddActor(cursor.actor) self.cursor = cursor - def SetNumber(self, init, end=None): + def SetNumber(self, init: int, end: Optional[int] = None) -> None: if end is None: self.number = init self.text.SetValue("%d" % self.number) @@ -69,19 +72,20 @@ def SetNumber(self, init, end=None): self.text.SetValue("%d - %d" % (init, end)) self.text.SetPosition(const.TEXT_POS_LEFT_DOWN_ZERO) - def SetOrientation(self, orientation): + def SetOrientation(self, orientation: str) -> None: self.orientation = orientation - def Hide(self): + def Hide(self) -> None: self.overlay_renderer.RemoveActor(self.actor) self.renderer.RemoveActor(self.text.actor) - def Show(self): + def Show(self) -> None: self.renderer.AddActor(self.actor) self.renderer.AddActor(self.text.actor) - def draw_to_canvas(self, gc, canvas): + def draw_to_canvas(self, gc: Any, canvas: Any) -> None: w, h = self.renderer.GetSize() - colour = const.ORIENTATION_COLOUR[self.orientation] + colour: Tuple[int, int, int] = const.ORIENTATION_COLOUR[self.orientation] canvas.draw_rectangle((0, 0), w, h, line_colour=[255*i for i in colour] + [255], line_width=2) self.text.draw_to_canvas(gc, canvas) + \ No newline at end of file diff --git a/invesalius/data/styles.py b/invesalius/data/styles.py index 0b9e1a536..163f46b48 100644 --- a/invesalius/data/styles.py +++ b/invesalius/data/styles.py @@ -22,6 +22,7 @@ import tempfile import time from concurrent import futures +from typing import Any, Dict, List, Optional, Tuple, Union, cast, overload import numpy as np import wx @@ -34,6 +35,8 @@ from skimage.morphology import watershed from vtkmodules.vtkFiltersSources import vtkLineSource +from vtkmodules.vtkImagingCore import vtkImageActor, vtkImageCast, vtkImageReslice +from vtkmodules.vtkImagingGeneral import vtkImageGaussianSmooth from vtkmodules.vtkInteractionStyle import ( vtkInteractorStyleImage, vtkInteractorStyleRubberBandZoom, @@ -44,8 +47,13 @@ vtkCoordinate, vtkPolyDataMapper2D, vtkWorldPointPicker, + ) + + + + from invesalius.pubsub import pub as Publisher import invesalius.constants as const @@ -69,28 +77,29 @@ # from time import sleep # --- -ORIENTATIONS = { +ORIENTATIONS: dict = { "AXIAL": const.AXIAL, "CORONAL": const.CORONAL, "SAGITAL": const.SAGITAL, } -BRUSH_FOREGROUND=1 -BRUSH_BACKGROUND=2 -BRUSH_ERASE=0 +BRUSH_FOREGROUND: int = 1 +BRUSH_BACKGROUND: int = 2 +BRUSH_ERASE: int = 0 -WATERSHED_OPERATIONS = {_("Erase"): BRUSH_ERASE, +WATERSHED_OPERATIONS: dict = {_("Erase"): BRUSH_ERASE, _("Foreground"): BRUSH_FOREGROUND, _("Background"): BRUSH_BACKGROUND,} + class BaseImageInteractorStyle(vtkInteractorStyleImage): - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: self.viewer = viewer - self.right_pressed = False - self.left_pressed = False - self.middle_pressed = False + self.right_pressed: bool = False + self.left_pressed: bool = False + self.middle_pressed: bool = False self.AddObserver("LeftButtonPressEvent", self.OnPressLeftButton) self.AddObserver("LeftButtonReleaseEvent", self.OnReleaseLeftButton) @@ -101,31 +110,31 @@ def __init__(self, viewer): self.AddObserver("MiddleButtonPressEvent", self.OnMiddleButtonPressEvent) self.AddObserver("MiddleButtonReleaseEvent", self.OnMiddleButtonReleaseEvent) - def OnPressLeftButton(self, evt, obj): + def OnPressLeftButton(self, evt: Any, obj: Any) -> None: self.left_pressed = True - def OnReleaseLeftButton(self, evt, obj): + def OnReleaseLeftButton(self, evt: Any, obj: Any) -> None: self.left_pressed = False - def OnPressRightButton(self, evt, obj): + def OnPressRightButton(self, evt: Any, obj: Any) -> None: self.right_pressed = True self.viewer.last_position_mouse_move = \ self.viewer.interactor.GetLastEventPosition() - def OnReleaseRightButton(self, evt, obj): + def OnReleaseRightButton(self, evt: Any, obj: Any) -> None: self.right_pressed = False - def OnMiddleButtonPressEvent(self, evt, obj): + def OnMiddleButtonPressEvent(self, evt: Any, obj: Any) -> None: self.middle_pressed = True - def OnMiddleButtonReleaseEvent(self, evt, obj): + def OnMiddleButtonReleaseEvent(self, evt: Any, obj: Any) -> None: self.middle_pressed = False - def GetMousePosition(self): + def GetMousePosition(self) -> Tuple[int, int]: mx, my = self.viewer.get_vtk_mouse_position() return mx, my - def GetPickPosition(self, mouse_position=None): + def GetPickPosition(self, mouse_position: Optional[Tuple[int, int]] = None) -> Tuple[float, float, float]: if mouse_position is None: mx, my = self.GetMousePosition() else: @@ -138,18 +147,19 @@ def GetPickPosition(self, mouse_position=None): + class DefaultInteractorStyle(BaseImageInteractorStyle): """ Interactor style responsible for Default functionalities: * Zoom moving mouse with right button pressed; * Change the slices with the scroll. """ - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: BaseImageInteractorStyle.__init__(self, viewer) - self.state_code = const.STATE_DEFAULT + self.state_code: int = const.STATE_DEFAULT - self.viewer = viewer + self.viewer: Any = viewer # Zoom using right button self.AddObserver("RightButtonPressEvent",self.OnZoomRightClick) @@ -160,10 +170,10 @@ def __init__(self, viewer): self.AddObserver("MouseWheelBackwardEvent", self.OnScrollBackward) self.AddObserver("EnterEvent",self.OnFocus) - def OnFocus(self, evt, obj): + def OnFocus(self, evt: Any, obj: Any) -> None: self.viewer.SetFocus() - def OnZoomRightMove(self, evt, obj): + def OnZoomRightMove(self, evt: Any, obj: Any) -> None: if (self.right_pressed): evt.Dolly() evt.OnRightButtonDown() @@ -172,16 +182,16 @@ def OnZoomRightMove(self, evt, obj): evt.Pan() evt.OnMiddleButtonDown() - def OnZoomRightClick(self, evt, obj): + def OnZoomRightClick(self, evt: Any, obj: Any) -> None: evt.StartDolly() - def OnZoomRightRelease(self, evt, obj): + def OnZoomRightRelease(self, evt: Any, obj: Any) -> None: print('EndDolly') evt.OnRightButtonUp() # evt.EndDolly() self.right_pressed = False - def OnScrollForward(self, evt, obj): + def OnScrollForward(self, evt: Any, obj: Any) -> None: iren = self.viewer.interactor viewer = self.viewer if iren.GetShiftKey(): @@ -195,7 +205,7 @@ def OnScrollForward(self, evt, obj): else: self.viewer.OnScrollForward() - def OnScrollBackward(self, evt, obj): + def OnScrollBackward(self, evt: Any, obj: Any) -> None: iren = self.viewer.interactor viewer = self.viewer @@ -211,23 +221,24 @@ def OnScrollBackward(self, evt, obj): self.viewer.OnScrollBackward() + class BaseImageEditionInteractorStyle(DefaultInteractorStyle): - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: super().__init__(viewer) - self.viewer = viewer - self.orientation = self.viewer.orientation + self.viewer: Any = viewer + self.orientation: str = self.viewer.orientation - self.picker = vtkWorldPointPicker() - self.matrix = None + self.picker: vtkWorldPointPicker = vtkWorldPointPicker() + self.matrix: Optional[np.ndarray] = None - self.cursor = None - self.brush_size = const.BRUSH_SIZE - self.brush_format = const.DEFAULT_BRUSH_FORMAT - self.brush_colour = const.BRUSH_COLOUR + self.cursor: Optional[Union[ca.CursorRectangle, ca.CursorCircle]] = None + self.brush_size: int = const.BRUSH_SIZE + self.brush_format: str = const.DEFAULT_BRUSH_FORMAT + self.brush_colour: Tuple[int, int, int] = const.BRUSH_COLOUR self._set_cursor() - self.fill_value = 254 + self.fill_value: int = 254 self.AddObserver("EnterEvent", self.OnBIEnterInteractor) self.AddObserver("LeaveEvent", self.OnBILeaveInteractor) @@ -241,7 +252,7 @@ def __init__(self, viewer): self.AddObserver("MouseWheelForwardEvent", self.OnBIScrollForward) self.AddObserver("MouseWheelBackwardEvent", self.OnBIScrollBackward) - def _set_cursor(self): + def _set_cursor(self) -> None: if const.DEFAULT_BRUSH_FORMAT == const.BRUSH_SQUARE: self.cursor = ca.CursorRectangle() elif const.DEFAULT_BRUSH_FORMAT == const.BRUSH_CIRCLE: @@ -259,37 +270,38 @@ def _set_cursor(self): self.cursor.SetSize(self.brush_size) self.viewer.slice_data.SetCursor(self.cursor) - def set_brush_size(self, size): + def set_brush_size(self, size: int) -> None: self.brush_size = size self._set_cursor() - def set_brush_format(self, format): + def set_brush_format(self, format: str) -> None: self.brush_format = format self._set_cursor() - def set_brush_operation(self, operation): + def set_brush_operation(self, operation: Any) -> None: self.brush_operation = operation self._set_cursor() - def set_fill_value(self, fill_value): + def set_fill_value(self, fill_value: int) -> None: self.fill_value = fill_value - def set_matrix(self, matrix): + def set_matrix(self, matrix: Optional[np.ndarray]) -> None: self.matrix = matrix - def OnBIEnterInteractor(self, obj, evt): + def OnBIEnterInteractor(self, obj: Any, evt: Any) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return self.viewer.slice_data.cursor.Show() self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) self.viewer.interactor.Render() - def OnBILeaveInteractor(self, obj, evt): + def OnBILeaveInteractor(self, obj: Any, evt: Any) -> None: self.viewer.slice_data.cursor.Show(0) self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) self.viewer.interactor.Render() - def OnBIBrushClick(self, obj, evt): + + def OnBIBrushClick(self, obj: object, evt: object) -> None: try: self.before_brush_click() except AttributeError: @@ -298,9 +310,9 @@ def OnBIBrushClick(self, obj, evt): if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return - viewer = self.viewer - iren = viewer.interactor - operation = self.config.operation + viewer: object = self.viewer + iren: object = viewer.interactor + operation: object = self.config.operation viewer._set_editor_cursor_visibility(1) @@ -314,8 +326,8 @@ def OnBIBrushClick(self, obj, evt): position = viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz) index = slice_data.number - cursor = slice_data.cursor - radius = cursor.radius + cursor: object = slice_data.cursor + radius: int = cursor.radius slice_data.cursor.SetPosition((wx, wy, wz)) @@ -329,7 +341,7 @@ def OnBIBrushClick(self, obj, evt): viewer.OnScrollBar() - def OnBIBrushMove(self, obj, evt): + def OnBIBrushMove(self, obj: object, evt: object) -> None: try: self.before_brush_move() except AttributeError: @@ -338,8 +350,8 @@ def OnBIBrushMove(self, obj, evt): if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return - viewer = self.viewer - iren = viewer.interactor + viewer: object = self.viewer + iren: object = viewer.interactor viewer._set_editor_cursor_visibility(1) @@ -352,11 +364,11 @@ def OnBIBrushMove(self, obj, evt): slice_data.cursor.SetPosition((wx, wy, wz)) if (self.left_pressed): - cursor = slice_data.cursor - radius = cursor.radius + cursor: object = slice_data.cursor + radius: int = cursor.radius - position = viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz) - index = slice_data.number + position: int = viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz) + index: int = slice_data.number slice_data.cursor.SetPosition((wx, wy, wz)) self.edit_mask_pixel(self.fill_value, index, cursor.GetPixels(), @@ -369,7 +381,7 @@ def OnBIBrushMove(self, obj, evt): else: viewer.interactor.Render() - def OnBIBrushRelease(self, evt, obj): + def OnBIBrushRelease(self, evt: object, obj: object) -> None: try: self.before_brush_release() except AttributeError: @@ -382,137 +394,138 @@ def OnBIBrushRelease(self, evt, obj): self.viewer.discard_mask_cache(all_orientations=True, vtk_cache=True) Publisher.sendMessage('Reload actual slice') - def edit_mask_pixel(self, fill_value, n, index, position, radius, orientation): + def edit_mask_pixel(self, fill_value: int, n: int, index: object, position: int, radius: int, orientation: str) -> None: if orientation == 'AXIAL': - matrix = self.matrix[n, :, :] + matrix: object = self.matrix[n, :, :] elif orientation == 'CORONAL': - matrix = self.matrix[:, n, :] + matrix: object = self.matrix[:, n, :] elif orientation == 'SAGITAL': - matrix = self.matrix[:, :, n] + matrix: object = self.matrix[:, :, n] - spacing = self.viewer.slice_.spacing + spacing: object = self.viewer.slice_.spacing if hasattr(position, '__iter__'): px, py = position if orientation == 'AXIAL': - sx = spacing[0] - sy = spacing[1] + sx: float = spacing[0] + sy: float = spacing[1] elif orientation == 'CORONAL': - sx = spacing[0] - sy = spacing[2] + sx: float = spacing[0] + sy: float = spacing[2] elif orientation == 'SAGITAL': - sx = spacing[2] - sy = spacing[1] + sx: float = spacing[2] + sy: float = spacing[1] else: if orientation == 'AXIAL': - sx = spacing[0] - sy = spacing[1] - py = position / matrix.shape[1] - px = position % matrix.shape[1] + sx: float = spacing[0] + sy: float = spacing[1] + py: int = position / matrix.shape[1] + px: int = position % matrix.shape[1] elif orientation == 'CORONAL': - sx = spacing[0] - sy = spacing[2] - py = position / matrix.shape[1] - px = position % matrix.shape[1] + sx: float = spacing[0] + sy: float = spacing[2] + py: int = position / matrix.shape[1] + px: int = position % matrix.shape[1] elif orientation == 'SAGITAL': - sx = spacing[2] - sy = spacing[1] - py = position / matrix.shape[1] - px = position % matrix.shape[1] - - cx = index.shape[1] / 2 + 1 - cy = index.shape[0] / 2 + 1 - xi = int(px - index.shape[1] + cx) - xf = int(xi + index.shape[1]) - yi = int(py - index.shape[0] + cy) - yf = int(yi + index.shape[0]) + sx: float = spacing[2] + sy: float = spacing[1] + py: int = position / matrix.shape[1] + px: int = position % matrix.shape[1] + + cx: int = index.shape[1] / 2 + 1 + cy: int = index.shape[0] / 2 + 1 + xi: int = int(px - index.shape[1] + cx) + xf: int = int(xi + index.shape[1]) + yi: int = int(py - index.shape[0] + cy) + yf: int = int(yi + index.shape[0]) if yi < 0: - index = index[abs(yi):,:] - yi = 0 + index: object = index[abs(yi):,:] + yi: int = 0 if yf > matrix.shape[0]: - index = index[:index.shape[0]-(yf-matrix.shape[0]), :] - yf = matrix.shape[0] + index: object = index[:index.shape[0]-(yf-matrix.shape[0]), :] + yf: int = matrix.shape[0] if xi < 0: - index = index[:,abs(xi):] - xi = 0 + index: object = index[:,abs(xi):] + xi: int = 0 if xf > matrix.shape[1]: - index = index[:,:index.shape[1]-(xf-matrix.shape[1])] - xf = matrix.shape[1] + index: object = index[:,:index.shape[1]-(xf-matrix.shape[1])] + xf: int = matrix.shape[1] # Verifying if the points is over the image array. if (not 0 <= xi <= matrix.shape[1] and not 0 <= xf <= matrix.shape[1]) or \ (not 0 <= yi <= matrix.shape[0] and not 0 <= yf <= matrix.shape[0]): return - roi_m = matrix[yi:yf,xi:xf] + roi_m: object = matrix[yi:yf,xi:xf] # Checking if roi_i has at least one element. if roi_m.size: roi_m[index] = self.fill_value - def OnBIScrollForward(self, evt, obj): - iren = self.viewer.interactor + def OnBIScrollForward(self, evt: Any, obj: Any) -> None: + iren: vtkRenderWindowInteractor = self.viewer.interactor if iren.GetControlKey(): - size = self.brush_size + 1 + size: int = self.brush_size + 1 if size <= 100: self.set_brush_size(size) else: self.OnScrollForward(obj, evt) - def OnBIScrollBackward(self, evt, obj): - iren = self.viewer.interactor + def OnBIScrollBackward(self, evt: Any, obj: Any) -> None: + iren: vtkRenderWindowInteractor = self.viewer.interactor if iren.GetControlKey(): - size = self.brush_size - 1 + size: int = self.brush_size - 1 if size > 0: self.set_brush_size(size) else: self.OnScrollBackward(obj, evt) + class CrossInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for the Cross. """ - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.SLICE_STATE_CROSS + self.state_code: int = const.SLICE_STATE_CROSS - self.viewer = viewer - self.orientation = viewer.orientation - self.slice_actor = viewer.slice_data.actor - self.slice_data = viewer.slice_data + self.viewer: Any = viewer + self.orientation: str = viewer.orientation + self.slice_actor: vtkActor = viewer.slice_data.actor + self.slice_data: Any = viewer.slice_data - self.picker = vtkWorldPointPicker() + self.picker: vtkWorldPointPicker = vtkWorldPointPicker() self.AddObserver("MouseMoveEvent", self.OnCrossMove) self.AddObserver("LeftButtonPressEvent", self.OnCrossMouseClick) self.AddObserver("LeftButtonReleaseEvent", self.OnReleaseLeftButton) - def SetUp(self): + def SetUp(self) -> None: self.viewer._set_cross_visibility(1) Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: self.viewer._set_cross_visibility(0) Publisher.sendMessage('Remove tracts') Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) - def OnCrossMouseClick(self, obj, evt): - iren = obj.GetInteractor() + def OnCrossMouseClick(self, obj: Any, evt: Any) -> None: + iren: vtkRenderWindowInteractor = obj.GetInteractor() self.ChangeCrossPosition(iren) - def OnCrossMove(self, obj, evt): + def OnCrossMove(self, obj: Any, evt: Any) -> None: # The user moved the mouse with left button pressed if self.left_pressed: - iren = obj.GetInteractor() + iren: vtkRenderWindowInteractor = obj.GetInteractor() self.ChangeCrossPosition(iren) - def ChangeCrossPosition(self, iren): + def ChangeCrossPosition(self, iren) -> None: mouse_x, mouse_y = self.GetMousePosition() x, y, z = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) self.viewer.UpdateSlicesPosition([x, y, z]) @@ -520,27 +533,31 @@ def ChangeCrossPosition(self, iren): Publisher.sendMessage('Set cross focal point', position=[x, y, z, None, None, None]) Publisher.sendMessage('Update slice viewer') - def OnScrollBar(self, *args, **kwargs): - # Update other slice's cross according to the new focal point from - # the actual orientation. - x, y, z = self.viewer.cross.GetFocalPoint() - self.viewer.UpdateSlicesPosition([x, y, z]) - Publisher.sendMessage('Set cross focal point', position=[x, y, z, None, None, None]) - Publisher.sendMessage('Update slice viewer') + + def OnScrollBar(self, *args: Any, **kwargs: Any) -> None: + # Update other slice's cross according to the new focal point from + # the actual orientation. + x: float + y: float + z: float = self.viewer.cross.GetFocalPoint() + self.viewer.UpdateSlicesPosition([x, y, z]) + Publisher.sendMessage('Set cross focal point', position=[x, y, z, None, None, None]) + Publisher.sendMessage('Update slice viewer') + class TractsInteractorStyle(CrossInteractorStyle): """ Interactor style responsible for tracts visualization. """ - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: CrossInteractorStyle.__init__(self, viewer) # self.state_code = const.SLICE_STATE_TRACTS - self.viewer = viewer + self.viewer: Any = viewer # print("Im fucking brilliant!") - self.tracts = None + self.tracts: Optional[Any] = None # data_dir = b'C:\Users\deoliv1\OneDrive\data\dti' # FOD_path = b"sub-P0_dwi_FOD.nii" @@ -566,83 +583,84 @@ def __init__(self, viewer): # Publisher.sendMessage('Toggle toolbar item', # _id=self.state_code, value=False) - def OnTractsMove(self, obj, evt): + def OnTractsMove(self, obj: Any, evt: Any) -> None: # The user moved the mouse with left button pressed if self.left_pressed: # print("OnTractsMove interactor style") # iren = obj.GetInteractor() self.ChangeTracts(True) - def OnTractsMouseClick(self, obj, evt): + def OnTractsMouseClick(self, obj: Any, evt: Any) -> None: # print("Single mouse click") # self.tracts = dtr.compute_and_visualize_tracts(self.tracker, self.seed, self.left_pressed) self.ChangeTracts(True) - def OnTractsReleaseLeftButton(self, obj, evt): + def OnTractsReleaseLeftButton(self, obj: Any, evt: Any) -> None: # time.sleep(3.) self.tracts.stop() # self.ChangeCrossPosition(iren) - def ChangeTracts(self, pressed): - # print("Trying to compute tracts") - self.tracts = dtr.compute_and_visualize_tracts(self.tracker, self.seed, self.affine_vtk, pressed) - # mouse_x, mouse_y = iren.GetEventPosition() - # wx, wy, wz = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) - # px, py = self.viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz) - # coord = self.viewer.calcultate_scroll_position(px, py) - # Publisher.sendMessage('Update cross position', position=(wx, wy, wz)) - # # self.ScrollSlice(coord) - # Publisher.sendMessage('Set ball reference position', position=(wx, wy, wz)) - # Publisher.sendMessage('Co-registered points', arg=None, position=(wx, wy, wz, 0., 0., 0.)) - - # iren.Render() - - # def ScrollSlice(self, coord): - # if self.orientation == "AXIAL": - # Publisher.sendMessage(('Set scroll position', 'SAGITAL'), - # index=coord[0]) - # Publisher.sendMessage(('Set scroll position', 'CORONAL'), - # index=coord[1]) - # elif self.orientation == "SAGITAL": - # Publisher.sendMessage(('Set scroll position', 'AXIAL'), - # index=coord[2]) - # Publisher.sendMessage(('Set scroll position', 'CORONAL'), - # index=coord[1]) - # elif self.orientation == "CORONAL": - # Publisher.sendMessage(('Set scroll position', 'AXIAL'), - # index=coord[2]) - # Publisher.sendMessage(('Set scroll position', 'SAGITAL'), - # index=coord[0]) + def ChangeTracts(self, pressed: bool) -> None: + # print("Trying to compute tracts") + self.tracts = dtr.compute_and_visualize_tracts(self.tracker, self.seed, self.affine_vtk, pressed) + # mouse_x, mouse_y = iren.GetEventPosition() + # wx, wy, wz = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) + # px, py = self.viewer.get_slice_pixel_coord_by_world_pos(wx, wy, wz) + # coord = self.viewer.calcultate_scroll_position(px, py) + # Publisher.sendMessage('Update cross position', position=(wx, wy, wz)) + # # self.ScrollSlice(coord) + # Publisher.sendMessage('Set ball reference position', position=(wx, wy, wz)) + # Publisher.sendMessage('Co-registered points', arg=None, position=(wx, wy, wz, 0., 0., 0.)) + + # iren.Render() + + # def ScrollSlice(self, coord): + # if self.orientation == "AXIAL": + # Publisher.sendMessage(('Set scroll position', 'SAGITAL'), + # index=coord[0]) + # Publisher.sendMessage(('Set scroll position', 'CORONAL'), + # index=coord[1]) + # elif self.orientation == "SAGITAL": + # Publisher.sendMessage(('Set scroll position', 'AXIAL'), + # index=coord[2]) + # Publisher.sendMessage(('Set scroll position', 'CORONAL'), + # index=coord[1]) + # elif self.orientation == "CORONAL": + # Publisher.sendMessage(('Set scroll position', 'AXIAL'), + # index=coord[2]) + # Publisher.sendMessage(('Set scroll position', 'SAGITAL'), + # index=coord[0]) + class WWWLInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for Window Level & Width functionality. """ - def __init__(self, viewer): + def __init__(self, viewer: Viewer) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.STATE_WL + self.state_code: int = const.STATE_WL - self.viewer = viewer + self.viewer: Viewer = viewer - self.last_x = 0 - self.last_y = 0 + self.last_x: int = 0 + self.last_y: int = 0 - self.acum_achange_window = viewer.slice_.window_width - self.acum_achange_level = viewer.slice_.window_level + self.acum_achange_window: float = viewer.slice_.window_width + self.acum_achange_level: float = viewer.slice_.window_level self.AddObserver("MouseMoveEvent", self.OnWindowLevelMove) self.AddObserver("LeftButtonPressEvent", self.OnWindowLevelClick) - def SetUp(self): + def SetUp(self) -> None: self.viewer.on_wl = True Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) self.viewer.canvas.draw_list.append(self.viewer.wl_text) self.viewer.UpdateCanvas() - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.on_wl = False Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) @@ -650,7 +668,7 @@ def CleanUp(self): self.viewer.canvas.draw_list.remove(self.viewer.wl_text) self.viewer.UpdateCanvas() - def OnWindowLevelMove(self, obj, evt): + def OnWindowLevelMove(self, obj: Any, evt: Any) -> None: if (self.left_pressed): iren = obj.GetInteractor() mouse_x, mouse_y = self.GetMousePosition() @@ -675,7 +693,7 @@ def OnWindowLevelMove(self, obj, evt): Publisher.sendMessage('Update slice viewer') Publisher.sendMessage('Render volume viewer') - def OnWindowLevelClick(self, obj, evt): + def OnWindowLevelClick(self, obj, evt) -> None: iren = obj.GetInteractor() self.last_x, self.last_y = iren.GetLastEventPosition() @@ -687,57 +705,57 @@ class LinearMeasureInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for insert linear measurements. """ - def __init__(self, viewer): + def __init__(self, viewer: Viewer) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.STATE_MEASURE_DISTANCE + self.state_code: int = const.STATE_MEASURE_DISTANCE - self.viewer = viewer - self.orientation = viewer.orientation - self.slice_data = viewer.slice_data + self.viewer: Viewer = viewer + self.orientation: str = viewer.orientation + self.slice_data: SliceData = viewer.slice_data - self.measures = MeasureData() - self.selected = None - self.creating = None + self.measures: MeasureData = MeasureData() + self.selected: Optional[Union[int, Tuple[int, Measure, MeasureRepresentation]]] = None + self.creating: Optional[Tuple[int, Measure, MeasureRepresentation]] = None - self._type = const.LINEAR + self._type: str = const.LINEAR - spacing = self.slice_data.actor.GetInput().GetSpacing() + spacing: Tuple[float, float, float] = self.slice_data.actor.GetInput().GetSpacing() if self.orientation == "AXIAL": - self.radius = min(spacing[1], spacing[2]) * 0.8 - self._ori = const.AXIAL + self.radius: float = min(spacing[1], spacing[2]) * 0.8 + self._ori: str = const.AXIAL elif self.orientation == 'CORONAL': - self.radius = min(spacing[0], spacing[1]) * 0.8 - self._ori = const.CORONAL + self.radius: float = min(spacing[0], spacing[1]) * 0.8 + self._ori: str = const.CORONAL elif self.orientation == 'SAGITAL': - self.radius = min(spacing[1], spacing[2]) * 0.8 - self._ori = const.SAGITAL + self.radius: float = min(spacing[1], spacing[2]) * 0.8 + self._ori: str = const.SAGITAL - self.picker = vtkCellPicker() + self.picker: vtkCellPicker = vtkCellPicker() self.picker.PickFromListOn() self._bind_events() - def _bind_events(self): + def _bind_events(self) -> None: self.AddObserver("LeftButtonPressEvent", self.OnInsertMeasurePoint) self.AddObserver("LeftButtonReleaseEvent", self.OnReleaseMeasurePoint) self.AddObserver("MouseMoveEvent", self.OnMoveMeasurePoint) self.AddObserver("LeaveEvent", self.OnLeaveMeasureInteractor) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) self.picker.PickFromListOff() Publisher.sendMessage("Remove incomplete measurements") - def OnInsertMeasurePoint(self, obj, evt): + def OnInsertMeasurePoint(self, obj, evt) -> None: slice_number = self.slice_data.number x, y, z = self._get_pos_clicked() mx, my = self.GetMousePosition() @@ -763,13 +781,13 @@ def OnInsertMeasurePoint(self, obj, evt): self.viewer.scroll_enabled = False return - selected = self._verify_clicked_display(mx, my) + selected: Optional[Union[int, Tuple[int, Measure, MeasureRepresentation]]] = self._verify_clicked_display(mx, my) if selected: self.selected = selected self.viewer.scroll_enabled = False else: if self.picker.GetViewProp(): - renderer = self.viewer.slice_data.renderer + renderer: vtkRenderer = self.viewer.slice_data.renderer Publisher.sendMessage("Add measurement point", position=(x, y, z), type=self._type, location=ORIENTATIONS[self.orientation], @@ -781,44 +799,65 @@ def OnInsertMeasurePoint(self, obj, evt): slice_number=slice_number, radius=self.radius) - n, (m, mr) = 1, self.measures.measures[self._ori][slice_number][-1] + n: int = 1 + m: Measure = self.measures.measures[self._ori][slice_number][-1][0] + mr: MeasureRepresentation = self.measures.measures[self._ori][slice_number][-1][1] self.creating = n, m, mr self.viewer.UpdateCanvas() self.viewer.scroll_enabled = False - def OnReleaseMeasurePoint(self, obj, evt): + + def OnReleaseMeasurePoint(self, obj: Any, evt: Any) -> None: if self.selected: + n: int + m: Measure + mr: MeasureRepresentation n, m, mr = self.selected - x, y, z = self._get_pos_clicked() - idx = self.measures._list_measures.index((m, mr)) + x: float + y: float + z: float = self._get_pos_clicked() + idx: int = self.measures._list_measures.index((m, mr)) Publisher.sendMessage('Change measurement point position', index=idx, npoint=n, pos=(x, y, z)) self.viewer.UpdateCanvas() self.selected = None self.viewer.scroll_enabled = True + - def OnMoveMeasurePoint(self, obj, evt): - x, y, z = self._get_pos_clicked() + def OnMoveMeasurePoint(self, obj: Any, evt: Any) -> None: + x: float + y: float + z: float = self._get_pos_clicked() if self.selected: + n: int + m: Measure + mr: MeasureRepresentation n, m, mr = self.selected - idx = self.measures._list_measures.index((m, mr)) + idx: int = self.measures._list_measures.index((m, mr)) Publisher.sendMessage('Change measurement point position', index=idx, npoint=n, pos=(x, y, z)) self.viewer.UpdateCanvas() elif self.creating: + n: int + m: Measure + mr: MeasureRepresentation n, m, mr = self.creating - idx = self.measures._list_measures.index((m, mr)) + idx: int = self.measures._list_measures.index((m, mr)) Publisher.sendMessage('Change measurement point position', index=idx, npoint=n, pos=(x, y, z)) self.viewer.UpdateCanvas() else: - mx, my = self.GetMousePosition() + mx: float + my: float = self.GetMousePosition() if self._verify_clicked_display(mx, my): self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_HAND)) else: self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) - def OnLeaveMeasureInteractor(self, obj, evt): + def OnLeaveMeasureInteractor(self, obj: Any, evt: Any) -> None: if self.creating or self.selected: + n: int + m: Measure + mr: MeasureRepresentation n, m, mr = self.creating if not mr.IsComplete(): Publisher.sendMessage("Remove incomplete measurements") @@ -827,80 +866,90 @@ def OnLeaveMeasureInteractor(self, obj, evt): self.viewer.UpdateCanvas() self.viewer.scroll_enabled = True - def _get_pos_clicked(self): + def _get_pos_clicked(self) -> Tuple[float, float, float]: self.picker.AddPickList(self.slice_data.actor) - x, y, z = self.GetPickPosition() + x: float + y: float + z: float = self.GetPickPosition() self.picker.DeletePickList(self.slice_data.actor) return (x, y, z) - def _verify_clicked(self, x, y, z): - slice_number = self.slice_data.number - sx, sy, sz = self.viewer.slice_.spacing + def _verify_clicked(self, x: float, y: float, z: float) -> Optional[Tuple[int, Measure, MeasureRepresentation]]: + slice_number: int = self.slice_data.number + sx: float + sy: float + sz: float = self.viewer.slice_.spacing if self.orientation == "AXIAL": - max_dist = 2 * max(sx, sy) + max_dist: float = 2 * max(sx, sy) elif self.orientation == "CORONAL": - max_dist = 2 * max(sx, sz) + max_dist: float = 2 * max(sx, sz) elif self.orientation == "SAGITAL": - max_dist = 2 * max(sy, sz) + max_dist: float = 2 * max(sy, sz) if slice_number in self.measures.measures[self._ori]: for m, mr in self.measures.measures[self._ori][slice_number]: if mr.IsComplete(): for n, p in enumerate(m.points): - px, py, pz = p - dist = ((px-x)**2 + (py-y)**2 + (pz-z)**2)**0.5 + px: float + py: float + pz: float = p + dist: float = ((px-x)**2 + (py-y)**2 + (pz-z)**2)**0.5 if dist < max_dist: return (n, m, mr) return None - - def _verify_clicked_display(self, x, y, max_dist=5.0): - slice_number = self.slice_data.number - max_dist = max_dist**2 - coord = vtkCoordinate() - if slice_number in self.measures.measures[self._ori]: - for m, mr in self.measures.measures[self._ori][slice_number]: - if mr.IsComplete(): - for n, p in enumerate(m.points): - coord.SetValue(p) - cx, cy = coord.GetComputedDisplayValue(self.viewer.slice_data.renderer) - dist = ((cx-x)**2 + (cy-y)**2) - if dist <= max_dist: - return (n, m, mr) - return None + + + def _verify_clicked_display(self, x: float, y: float, max_dist: float = 5.0) -> Optional[Tuple[int, Measure, MeasureRepresentation]]: + slice_number: int = self.slice_data.number + max_dist: float = max_dist**2 + coord: vtkCoordinate = vtkCoordinate() + if slice_number in self.measures.measures[self._ori]: + for m, mr in self.measures.measures[self._ori][slice_number]: + if mr.IsComplete(): + for n, p in enumerate(m.points): + coord.SetValue(p) + cx: float + cy: float = coord.GetComputedDisplayValue(self.viewer.slice_data.renderer) + dist: float = ((cx-x)**2 + (cy-y)**2) + if dist <= max_dist: + return (n, m, mr) + return None + class AngularMeasureInteractorStyle(LinearMeasureInteractorStyle): - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: LinearMeasureInteractorStyle.__init__(self, viewer) - self._type = const.ANGULAR + self._type: str = const.ANGULAR + + self.state_code: int = const.STATE_MEASURE_ANGLE - self.state_code = const.STATE_MEASURE_ANGLE class DensityMeasureStyle(DefaultInteractorStyle): """ Interactor style responsible for density measurements. """ - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.STATE_MEASURE_DENSITY + self.state_code: int = const.STATE_MEASURE_DENSITY - self.format = 'polygon' + self.format: str = 'polygon' - self._last_measure = None + self._last_measure: Optional[MeasureData] = None - self.viewer = viewer - self.orientation = viewer.orientation - self.slice_data = viewer.slice_data + self.viewer: Any = viewer + self.orientation: Any = viewer.orientation + self.slice_data: Any = viewer.slice_data - self.picker = vtkCellPicker() + self.picker: vtkCellPicker = vtkCellPicker() self.picker.PickFromListOn() - self.measures = MeasureData() + self.measures: MeasureData = MeasureData() self._bind_events() - def _bind_events(self): + def _bind_events(self) -> None: # self.AddObserver("LeftButtonPressEvent", self.OnInsertPoint) # self.AddObserver("LeftButtonReleaseEvent", self.OnReleaseMeasurePoint) # self.AddObserver("MouseMoveEvent", self.OnMoveMeasurePoint) @@ -908,14 +957,14 @@ def _bind_events(self): self.viewer.canvas.subscribe_event('LeftButtonPressEvent', self.OnInsertPoint) self.viewer.canvas.subscribe_event('LeftButtonDoubleClickEvent', self.OnInsertPolygon) - def SetUp(self): + def SetUp(self) -> None: for n in self.viewer.draw_by_slice_number: for i in self.viewer.draw_by_slice_number[n]: if isinstance(i, PolygonDensityMeasure): i.set_interactive(True) self.viewer.canvas.Refresh() - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.canvas.unsubscribe_event('LeftButtonPressEvent', self.OnInsertPoint) self.viewer.canvas.unsubscribe_event('LeftButtonDoubleClickEvent', self.OnInsertPolygon) old_list = self.viewer.draw_by_slice_number @@ -930,7 +979,8 @@ def CleanUp(self): self.viewer.UpdateCanvas() - def _2d_to_3d(self, pos): + + def _2d_to_3d(self, pos: Tuple[int, int]) -> Tuple[float, float, float]: mx, my = pos iren = self.viewer.interactor render = iren.FindPokedRenderer(mx, my) @@ -940,17 +990,17 @@ def _2d_to_3d(self, pos): self.picker.DeletePickList(self.slice_data.actor) return (x, y, z) - def _pick_position(self): + def _pick_position(self) -> Tuple[int, int]: iren = self.viewer.interactor mx, my = self.GetMousePosition() return (mx, my) - def _get_pos_clicked(self): + def _get_pos_clicked(self) -> Tuple[float, float, float]: mouse_x, mouse_y = self.GetMousePosition() position = self.viewer.get_coordinate_cursor(mouse_x, mouse_y, self.picker) return position - def OnInsertPoint(self, evt): + def OnInsertPoint(self, evt: Any) -> None: mouse_x, mouse_y = evt.position print('OnInsertPoint', evt.position) n = self.viewer.slice_data.number @@ -965,20 +1015,20 @@ def OnInsertPoint(self, evt): m.set_point1(pp1) m.set_point2(pp2) m.calc_density() - _new_measure = True + _new_measure: bool = True Publisher.sendMessage("Add density measurement", density_measure=m) elif self.format == 'polygon': if self._last_measure is None: m = PolygonDensityMeasure(self.orientation, n) - _new_measure = True + _new_measure: bool = True else: m = self._last_measure - _new_measure = False + _new_measure: bool = False if m.slice_number != n: self.viewer.draw_by_slice_number[m.slice_number].remove(m) del m m = PolygonDensityMeasure(self.orientation, n) - _new_measure = True + _new_measure: bool = True m.insert_point(pos) @@ -992,8 +1042,9 @@ def OnInsertPoint(self, evt): # m.calc_density() self.viewer.UpdateCanvas() + - def OnInsertPolygon(self, evt): + def OnInsertPolygon(self, evt: Any) -> None: if self.format == 'polygon' and self._last_measure: m = self._last_measure if len(m.points) >= 3: @@ -1004,51 +1055,52 @@ def OnInsertPolygon(self, evt): self._last_measure = None Publisher.sendMessage("Add density measurement", density_measure=m) self.viewer.UpdateCanvas() + class DensityMeasureEllipseStyle(DensityMeasureStyle): - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: DensityMeasureStyle.__init__(self, viewer) - self.state_code = const.STATE_MEASURE_DENSITY_ELLIPSE - self.format = 'ellipse' + self.state_code: str = const.STATE_MEASURE_DENSITY_ELLIPSE + self.format: str = 'ellipse' class DensityMeasurePolygonStyle(DensityMeasureStyle): - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: DensityMeasureStyle.__init__(self, viewer) - self.state_code = const.STATE_MEASURE_DENSITY_POLYGON - self.format = 'polygon' + self.state_code: str = const.STATE_MEASURE_DENSITY_POLYGON + self.format: str = 'polygon' class PanMoveInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for translate the camera. """ - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.STATE_PAN + self.state_code: str = const.STATE_PAN - self.viewer = viewer + self.viewer: Any = viewer self.AddObserver("MouseMoveEvent", self.OnPanMove) self.viewer.interactor.Bind(wx.EVT_LEFT_DCLICK, self.OnUnspan) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.interactor.Unbind(wx.EVT_LEFT_DCLICK) Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) - def OnPanMove(self, obj, evt): + def OnPanMove(self, obj: Any, evt: Any) -> None: if self.left_pressed: obj.Pan() obj.OnRightButtonDown() - def OnUnspan(self, evt): + def OnUnspan(self, evt) -> None: iren = self.viewer.interactor mouse_x, mouse_y = iren.GetLastEventPosition() ren = iren.FindPokedRenderer(mouse_x, mouse_y) @@ -1056,30 +1108,31 @@ def OnUnspan(self, evt): iren.Render() + class SpinInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for spin the camera. """ - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.STATE_SPIN + self.state_code: str = const.STATE_SPIN - self.viewer = viewer + self.viewer: Any = viewer self.AddObserver("MouseMoveEvent", self.OnSpinMove) self.viewer.interactor.Bind(wx.EVT_LEFT_DCLICK, self.OnUnspin) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.interactor.Unbind(wx.EVT_LEFT_DCLICK) Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) - def OnSpinMove(self, obj, evt): + def OnSpinMove(self, obj, evt) -> None: iren = obj.GetInteractor() mouse_x, mouse_y = iren.GetLastEventPosition() ren = iren.FindPokedRenderer(mouse_x, mouse_y) @@ -1089,7 +1142,7 @@ def OnSpinMove(self, obj, evt): obj.Spin() obj.OnRightButtonDown() - def OnUnspin(self, evt): + def OnUnspin(self, evt) -> None: orig_orien = 1 iren = self.viewer.interactor mouse_x, mouse_y = iren.GetLastEventPosition() @@ -1100,36 +1153,37 @@ def OnUnspin(self, evt): iren.Render() + class ZoomInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for zoom with movement of the mouse and the left mouse button clicked. """ - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.STATE_ZOOM + self.state_code: str = const.STATE_ZOOM - self.viewer = viewer + self.viewer: Any = viewer self.AddObserver("MouseMoveEvent", self.OnZoomMoveLeft) self.viewer.interactor.Bind(wx.EVT_LEFT_DCLICK, self.OnUnZoom) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.interactor.Unbind(wx.EVT_LEFT_DCLICK) Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) - def OnZoomMoveLeft(self, obj, evt): + def OnZoomMoveLeft(self, obj: Any, evt: Any) -> None: if self.left_pressed: obj.Dolly() obj.OnRightButtonDown() - def OnUnZoom(self, evt): + def OnUnZoom(self, evt) -> None: mouse_x, mouse_y = self.viewer.interactor.GetLastEventPosition() ren = self.viewer.interactor.FindPokedRenderer(mouse_x, mouse_y) #slice_data = self.get_slice_data(ren) @@ -1143,22 +1197,22 @@ class ZoomSLInteractorStyle(vtkInteractorStyleRubberBandZoom): """ Interactor style responsible for zoom by selecting a region. """ - def __init__(self, viewer): - self.viewer = viewer + def __init__(self, viewer: Any) -> None: + self.viewer: Any = viewer self.viewer.interactor.Bind(wx.EVT_LEFT_DCLICK, self.OnUnZoom) - self.state_code = const.STATE_ZOOM_SL + self.state_code: str = const.STATE_ZOOM_SL - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.interactor.Unbind(wx.EVT_LEFT_DCLICK) Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) - def OnUnZoom(self, evt): + def OnUnZoom(self, evt) -> None: mouse_x, mouse_y = self.viewer.interactor.GetLastEventPosition() ren = self.viewer.interactor.FindPokedRenderer(mouse_x, mouse_y) #slice_data = self.get_slice_data(ren) @@ -1172,31 +1226,31 @@ class ChangeSliceInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for change slice moving the mouse. """ - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.SLICE_STATE_SCROLL + self.state_code: str = const.SLICE_STATE_SCROLL - self.viewer = viewer + self.viewer: Any = viewer self.AddObserver("MouseMoveEvent", self.OnChangeSliceMove) self.AddObserver("LeftButtonPressEvent", self.OnChangeSliceClick) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) - def OnChangeSliceMove(self, evt, obj): + def OnChangeSliceMove(self, evt: Any, obj: Any) -> None: if self.left_pressed: - min = 0 - max = self.viewer.slice_.GetMaxSliceNumber(self.viewer.orientation) + min: int = 0 + max: int = self.viewer.slice_.GetMaxSliceNumber(self.viewer.orientation) - position = self.viewer.interactor.GetLastEventPosition() - scroll_position = self.viewer.scroll.GetThumbPosition() + position: Tuple[int, int] = self.viewer.interactor.GetLastEventPosition() + scroll_position: int = self.viewer.scroll.GetThumbPosition() if (position[1] > self.last_position) and\ (self.acum_achange_slice > min): @@ -1209,32 +1263,32 @@ def OnChangeSliceMove(self, evt, obj): self.viewer.scroll.SetThumbPosition(self.acum_achange_slice) self.viewer.OnScrollBar() - def OnChangeSliceClick(self, evt, obj): - position = self.viewer.interactor.GetLastEventPosition() - self.acum_achange_slice = self.viewer.scroll.GetThumbPosition() - self.last_position = position[1] + def OnChangeSliceClick(self, evt: Any, obj: Any) -> None: + position: Tuple[int, int] = self.viewer.interactor.GetLastEventPosition() + self.acum_achange_slice: int = self.viewer.scroll.GetThumbPosition() + self.last_position: int = position[1] class EditorConfig(metaclass=utils.Singleton): - def __init__(self): - self.operation = const.BRUSH_THRESH - self.cursor_type = const.BRUSH_CIRCLE - self.cursor_size = const.BRUSH_SIZE - self.cursor_unit = 'mm' + def __init__(self) -> None: + self.operation: str = const.BRUSH_THRESH + self.cursor_type: str = const.BRUSH_CIRCLE + self.cursor_size: int = const.BRUSH_SIZE + self.cursor_unit: str = 'mm' class EditorInteractorStyle(DefaultInteractorStyle): - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.SLICE_STATE_EDITOR + self.state_code: str = const.SLICE_STATE_EDITOR - self.viewer = viewer - self.orientation = self.viewer.orientation + self.viewer: Any = viewer + self.orientation: str = self.viewer.orientation - self.config = EditorConfig() + self.config: EditorConfig = EditorConfig() - self.picker = vtkWorldPointPicker() + self.picker: vtkWorldPointPicker = vtkWorldPointPicker() self.AddObserver("EnterEvent", self.OnEnterInteractor) self.AddObserver("LeaveEvent", self.OnLeaveInteractor) @@ -1256,7 +1310,7 @@ def __init__(self, viewer): self._set_cursor() self.viewer.slice_data.cursor.Show(0) - def SetUp(self): + def SetUp(self) -> None: x, y = self.viewer.interactor.ScreenToClient(wx.GetMousePosition()) if self.viewer.interactor.HitTest((x, y)) == wx.HT_WINDOW_INSIDE: self.viewer.slice_data.cursor.Show() @@ -1268,7 +1322,7 @@ def SetUp(self): self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) self.viewer.interactor.Render() - def CleanUp(self): + def CleanUp(self) -> None: Publisher.unsubscribe(self.set_bsize, 'Set edition brush size') Publisher.unsubscribe(self.set_bunit, 'Set edition brush unit') Publisher.unsubscribe(self.set_bformat, 'Set brush format') @@ -1278,22 +1332,23 @@ def CleanUp(self): self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) self.viewer.interactor.Render() - def set_bsize(self, size): + + def set_bsize(self, size: int) -> None: self.config.cursor_size = size self.viewer.slice_data.cursor.SetSize(size) - def set_bunit(self, unit): + def set_bunit(self, unit: str) -> None: self.config.cursor_unit = unit self.viewer.slice_data.cursor.SetUnit(unit) - def set_bformat(self, cursor_format): + def set_bformat(self, cursor_format: str) -> None: self.config.cursor_type = cursor_format self._set_cursor() - def set_boperation(self, operation): + def set_boperation(self, operation: str) -> None: self.config.operation = operation - def _set_cursor(self): + def _set_cursor(self) -> None: if self.config.cursor_type == const.BRUSH_SQUARE: cursor = ca.CursorRectangle() elif self.config.cursor_type == const.BRUSH_CIRCLE: @@ -1302,8 +1357,8 @@ def _set_cursor(self): cursor.SetOrientation(self.orientation) n = self.viewer.slice_data.number coordinates = {"SAGITAL": [n, 0, 0], - "CORONAL": [0, n, 0], - "AXIAL": [0, 0, n]} + "CORONAL": [0, n, 0], + "AXIAL": [0, 0, n]} cursor.SetPosition(coordinates[self.orientation]) spacing = self.viewer.slice_.spacing cursor.SetSpacing(spacing) @@ -1312,26 +1367,27 @@ def _set_cursor(self): cursor.SetUnit(self.config.cursor_unit) self.viewer.slice_data.SetCursor(cursor) - def OnEnterInteractor(self, obj, evt): + def OnEnterInteractor(self, obj: Any, evt: Any) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return self.viewer.slice_data.cursor.Show() self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) self.viewer.interactor.Render() + - def OnLeaveInteractor(self, obj, evt): + def OnLeaveInteractor(self, obj: Any, evt: Any) -> None: self.viewer.slice_data.cursor.Show(0) self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) self.viewer.interactor.Render() - def OnBrushClick(self, obj, evt): + def OnBrushClick(self, obj: Any, evt: Any) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return viewer = self.viewer iren = viewer.interactor - operation = self.config.operation + operation: str = self.config.operation if operation == const.BRUSH_THRESH: if iren.GetControlKey(): if iren.GetShiftKey(): @@ -1371,8 +1427,9 @@ def OnBrushClick(self, obj, evt): # TODO: To create a new function to reload images to viewer. viewer.OnScrollBar() + - def OnBrushMove(self, obj, evt): + def OnBrushMove(self, obj: Any, evt: Any) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return @@ -1396,7 +1453,7 @@ def OnBrushMove(self, obj, evt): operation = const.BRUSH_THRESH_ADD_ONLY elif operation == const.BRUSH_ERASE and iren.GetControlKey(): - operation = const.BRUSH_DRAW + operation: Literal[0] = const.BRUSH_DRAW elif operation == const.BRUSH_DRAW and iren.GetControlKey(): operation = const.BRUSH_ERASE @@ -1412,14 +1469,15 @@ def OnBrushMove(self, obj, evt): slice_data.cursor.SetPosition((wx, wy, wz)) viewer.slice_.edit_mask_pixel(operation, cursor.GetPixels(), - position, radius, viewer.orientation) + position, radius, viewer.orientation) viewer.OnScrollBar(update3D=False) else: viewer.interactor.Render() + - def OnBrushRelease(self, evt, obj): + def OnBrushRelease(self, evt: Any, obj: Any) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return @@ -1428,7 +1486,7 @@ def OnBrushRelease(self, evt, obj): self.viewer._flush_buffer = False self.viewer.slice_.current_mask.modified() - def EOnScrollForward(self, evt, obj): + def EOnScrollForward(self, evt: Any, obj: Any) -> None: iren = self.viewer.interactor viewer = self.viewer if iren.GetControlKey(): @@ -1446,7 +1504,7 @@ def EOnScrollForward(self, evt, obj): else: self.OnScrollForward(obj, evt) - def EOnScrollBackward(self, evt, obj): + def EOnScrollBackward(self, evt: Any, obj: Any) -> None: iren = self.viewer.interactor viewer = self.viewer if iren.GetControlKey(): @@ -1465,42 +1523,44 @@ def EOnScrollBackward(self, evt, obj): self.OnScrollBackward(obj, evt) -class WatershedProgressWindow(object): - def __init__(self, process): + +class WatershedProgressWindow: + def __init__(self, process: Any) -> None: self.process = process - self.title = "InVesalius 3" - self.msg = _("Applying watershed ...") - self.style = wx.PD_APP_MODAL | wx.PD_APP_MODAL | wx.PD_CAN_ABORT + self.title: str = "InVesalius 3" + self.msg: str = _("Applying watershed ...") + self.style: int = wx.PD_APP_MODAL | wx.PD_APP_MODAL | wx.PD_CAN_ABORT - self.dlg = wx.ProgressDialog(self.title, + self.dlg: wx.ProgressDialog = wx.ProgressDialog(self.title, self.msg, - parent = wx.GetApp().GetTopWindow(), - style = self.style) + parent=wx.GetApp().GetTopWindow(), + style=self.style) self.dlg.Bind(wx.EVT_BUTTON, self.Cancel) self.dlg.Show() - def Cancel(self, evt): + def Cancel(self, evt: Any) -> None: self.process.terminate() - def Update(self): + def Update(self) -> None: self.dlg.Pulse() - def Close(self): + def Close(self) -> None: self.dlg.Destroy() + class WatershedConfig(metaclass=utils.Singleton): - def __init__(self): - self.algorithm = "Watershed" - self.con_2d = 4 - self.con_3d = 6 - self.mg_size = 3 - self.use_ww_wl = True - self.operation = BRUSH_FOREGROUND - self.cursor_type = const.BRUSH_CIRCLE - self.cursor_size = const.BRUSH_SIZE - self.cursor_unit = 'mm' + def __init__(self) -> None: + self.algorithm: str = "Watershed" + self.con_2d: int = 4 + self.con_3d: int = 6 + self.mg_size: int = 3 + self.use_ww_wl: bool = True + self.operation: str = BRUSH_FOREGROUND + self.cursor_type: str = const.BRUSH_CIRCLE + self.cursor_size: int = const.BRUSH_SIZE + self.cursor_unit: str = 'mm' Publisher.subscribe(self.set_operation, 'Set watershed operation') Publisher.subscribe(self.set_use_ww_wl, 'Set use ww wl') @@ -1510,42 +1570,43 @@ def __init__(self): Publisher.subscribe(self.set_3dcon, "Set watershed 3d con") Publisher.subscribe(self.set_gaussian_size, "Set watershed gaussian size") - def set_operation(self, operation): + def set_operation(self, operation: str) -> None: self.operation = WATERSHED_OPERATIONS[operation] - def set_use_ww_wl(self, use_ww_wl): + def set_use_ww_wl(self, use_ww_wl: bool) -> None: self.use_ww_wl = use_ww_wl - def set_algorithm(self, algorithm): + def set_algorithm(self, algorithm: str) -> None: self.algorithm = algorithm - def set_2dcon(self, con_2d): + def set_2dcon(self, con_2d: int) -> None: self.con_2d = con_2d - def set_3dcon(self, con_3d): + def set_3dcon(self, con_3d: int) -> None: self.con_3d = con_3d - def set_gaussian_size(self, size): + def set_gaussian_size(self, size: int) -> None: self.mg_size = size -WALGORITHM = {"Watershed": watershed, +WALGORITHM: Dict[str, Any] = {"Watershed": watershed, "Watershed IFT": watershed_ift} -CON2D = {4: 1, 8: 2} -CON3D = {6: 1, 18: 2, 26: 3} +CON2D: Dict[int, int] = {4: 1, 8: 2} +CON3D: Dict[int, int] = {6: 1, 18: 2, 26: 3} + class WaterShedInteractorStyle(DefaultInteractorStyle): - def __init__(self, viewer): + def __init__(self, viewer: Any) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.SLICE_STATE_WATERSHED + self.state_code: int = const.SLICE_STATE_WATERSHED - self.viewer = viewer - self.orientation = self.viewer.orientation - self.matrix = None + self.viewer: Any = viewer + self.orientation: str = self.viewer.orientation + self.matrix: Optional[np.ndarray] = None - self.config = WatershedConfig() + self.config: WatershedConfig = WatershedConfig() - self.picker = vtkWorldPointPicker() + self.picker: vtkWorldPointPicker = vtkWorldPointPicker() self.AddObserver("EnterEvent", self.OnEnterInteractor) self.AddObserver("LeaveEvent", self.OnLeaveInteractor) @@ -1567,8 +1628,8 @@ def __init__(self, viewer): self._set_cursor() self.viewer.slice_data.cursor.Show(0) - def SetUp(self): - mask = self.viewer.slice_.current_mask.matrix + def SetUp(self) -> None: + mask: np.ndarray = self.viewer.slice_.current_mask.matrix self._create_mask() self.viewer.slice_.to_show_aux = 'watershed' self.viewer.OnScrollBar() @@ -1584,7 +1645,7 @@ def SetUp(self): self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) self.viewer.interactor.Render() - def CleanUp(self): + def CleanUp(self) -> None: #self._remove_mask() Publisher.unsubscribe(self.expand_watershed, 'Expand watershed to 3D ' + self.orientation) Publisher.unsubscribe(self.set_bformat, 'Set watershed brush format') @@ -1597,7 +1658,8 @@ def CleanUp(self): self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) self.viewer.interactor.Render() - def _create_mask(self): + + def _create_mask(self) -> None: if self.matrix is None: try: self.matrix = self.viewer.slice_.aux_matrices['watershed'] @@ -1605,58 +1667,58 @@ def _create_mask(self): self.temp_file, self.matrix = self.viewer.slice_.create_temp_mask() self.viewer.slice_.aux_matrices['watershed'] = self.matrix - def _remove_mask(self): + def _remove_mask(self) -> None: if self.matrix is not None: self.matrix = None os.remove(self.temp_file) print("deleting", self.temp_file) - def _set_cursor(self): + def _set_cursor(self) -> None: if self.config.cursor_type == const.BRUSH_SQUARE: - cursor = ca.CursorRectangle() + cursor: ca.CursorRectangle = ca.CursorRectangle() elif self.config.cursor_type == const.BRUSH_CIRCLE: - cursor = ca.CursorCircle() + cursor: ca.CursorCircle = ca.CursorCircle() cursor.SetOrientation(self.orientation) - n = self.viewer.slice_data.number - coordinates = {"SAGITAL": [n, 0, 0], - "CORONAL": [0, n, 0], - "AXIAL": [0, 0, n]} + n: int = self.viewer.slice_data.number + coordinates: Dict[str, List[int]] = {"SAGITAL": [n, 0, 0], + "CORONAL": [0, n, 0], + "AXIAL": [0, 0, n]} cursor.SetPosition(coordinates[self.orientation]) - spacing = self.viewer.slice_.spacing + spacing: Tuple[float, float, float] = self.viewer.slice_.spacing cursor.SetSpacing(spacing) cursor.SetColour(self.viewer._brush_cursor_colour) cursor.SetSize(self.config.cursor_size) cursor.SetUnit(self.config.cursor_unit) self.viewer.slice_data.SetCursor(cursor) - def set_bsize(self, size): + def set_bsize(self, size: int) -> None: self.config.cursor_size = size self.viewer.slice_data.cursor.SetSize(size) - def set_bunit(self, unit): + def set_bunit(self, unit: str) -> None: self.config.cursor_unit = unit self.viewer.slice_data.cursor.SetUnit(unit) - def set_bformat(self, brush_format): + def set_bformat(self, brush_format: str) -> None: self.config.cursor_type = brush_format self._set_cursor() - def OnEnterInteractor(self, obj, evt): + def OnEnterInteractor(self, obj: Any, evt: Any) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return self.viewer.slice_data.cursor.Show() self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_BLANK)) self.viewer.interactor.Render() - def OnLeaveInteractor(self, obj, evt): + def OnLeaveInteractor(self, obj: Any, evt: Any) -> None: self.viewer.slice_data.cursor.Show(0) self.viewer.interactor.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) self.viewer.interactor.Render() - def WOnScrollBackward(self, obj, evt): - iren = self.viewer.interactor - viewer = self.viewer + def WOnScrollBackward(self, obj: Any, evt: Any) -> None: + iren: vtkRenderWindowInteractor = self.viewer.interactor + viewer: Any = self.viewer if iren.GetControlKey(): mouse_x, mouse_y = self.GetMousePosition() render = iren.FindPokedRenderer(mouse_x, mouse_y) @@ -1671,10 +1733,11 @@ def WOnScrollBackward(self, obj, evt): self.viewer.interactor.Render() else: self.OnScrollBackward(obj, evt) + - def WOnScrollForward(self, obj, evt): - iren = self.viewer.interactor - viewer = self.viewer + def WOnScrollForward(self, obj: Any, evt: Any) -> None: + iren: vtkRenderWindowInteractor = self.viewer.interactor + viewer: Any = self.viewer if iren.GetControlKey(): mouse_x, mouse_y = self.GetMousePosition() render = iren.FindPokedRenderer(mouse_x, mouse_y) @@ -1690,12 +1753,12 @@ def WOnScrollForward(self, obj, evt): else: self.OnScrollForward(obj, evt) - def OnBrushClick(self, obj, evt): + def OnBrushClick(self, obj: Any, evt: Any) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return - viewer = self.viewer - iren = viewer.interactor + viewer: Any = self.viewer + iren: vtkRenderWindowInteractor = viewer.interactor viewer._set_editor_cursor_visibility(1) @@ -1716,7 +1779,7 @@ def OnBrushClick(self, obj, evt): if operation == BRUSH_FOREGROUND: if iren.GetControlKey(): - operation = BRUSH_BACKGROUND + operation: int = BRUSH_BACKGROUND elif iren.GetShiftKey(): operation = BRUSH_ERASE elif operation == BRUSH_BACKGROUND: @@ -1725,7 +1788,7 @@ def OnBrushClick(self, obj, evt): elif iren.GetShiftKey(): operation = BRUSH_ERASE - n = self.viewer.slice_data.number + n: int = self.viewer.slice_data.number self.edit_mask_pixel(operation, n, cursor.GetPixels(), position, radius, self.orientation) if self.orientation == 'AXIAL': @@ -1736,13 +1799,14 @@ def OnBrushClick(self, obj, evt): mask = self.matrix[:, :, n] # TODO: To create a new function to reload images to viewer. viewer.OnScrollBar() + - def OnBrushMove(self, obj, evt): + def OnBrushMove(self, obj: Any, evt: Any) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return - viewer = self.viewer - iren = viewer.interactor + viewer: Any = self.viewer + iren: vtkRenderWindowInteractor = viewer.interactor viewer._set_editor_cursor_visibility(1) @@ -1774,7 +1838,7 @@ def OnBrushMove(self, obj, evt): elif iren.GetShiftKey(): operation = BRUSH_ERASE - n = self.viewer.slice_data.number + n: int = self.viewer.slice_data.number self.edit_mask_pixel(operation, n, cursor.GetPixels(), position, radius, self.orientation) if self.orientation == 'AXIAL': @@ -1788,61 +1852,62 @@ def OnBrushMove(self, obj, evt): else: viewer.interactor.Render() + - def OnBrushRelease(self, evt, obj): - n = self.viewer.slice_data.number + def OnBrushRelease(self, evt: Any, obj: Any) -> None: + n: int = self.viewer.slice_data.number self.viewer.slice_.discard_all_buffers() if self.orientation == 'AXIAL': - image = self.viewer.slice_.matrix[n] - mask = self.viewer.slice_.current_mask.matrix[n+1, 1:, 1:] + image: np.ndarray = self.viewer.slice_.matrix[n] + mask: np.ndarray = self.viewer.slice_.current_mask.matrix[n+1, 1:, 1:] self.viewer.slice_.current_mask.matrix[n+1, 0, 0] = 1 - markers = self.matrix[n] + markers: np.ndarray = self.matrix[n] elif self.orientation == 'CORONAL': - image = self.viewer.slice_.matrix[:, n, :] - mask = self.viewer.slice_.current_mask.matrix[1:, n+1, 1:] + image: np.ndarray = self.viewer.slice_.matrix[:, n, :] + mask: np.ndarray = self.viewer.slice_.current_mask.matrix[1:, n+1, 1:] self.viewer.slice_.current_mask.matrix[0, n+1, 0] - markers = self.matrix[:, n, :] + markers: np.ndarray = self.matrix[:, n, :] elif self.orientation == 'SAGITAL': - image = self.viewer.slice_.matrix[:, :, n] - mask = self.viewer.slice_.current_mask.matrix[1: , 1:, n+1] + image: np.ndarray = self.viewer.slice_.matrix[:, :, n] + mask: np.ndarray = self.viewer.slice_.current_mask.matrix[1: , 1:, n+1] self.viewer.slice_.current_mask.matrix[0 , 0, n+1] - markers = self.matrix[:, :, n] + markers: np.ndarray = self.matrix[:, :, n] - ww = self.viewer.slice_.window_width - wl = self.viewer.slice_.window_level + ww: float = self.viewer.slice_.window_width + wl: float = self.viewer.slice_.window_level if BRUSH_BACKGROUND in markers and BRUSH_FOREGROUND in markers: #w_algorithm = WALGORITHM[self.config.algorithm] - bstruct = generate_binary_structure(2, CON2D[self.config.con_2d]) + bstruct: np.ndarray = generate_binary_structure(2, CON2D[self.config.con_2d]) if self.config.use_ww_wl: if self.config.algorithm == 'Watershed': - tmp_image = ndimage.morphological_gradient( - get_LUT_value(image, ww, wl).astype('uint16'), - self.config.mg_size) - tmp_mask = watershed(tmp_image, markers.astype('int16'), bstruct) + tmp_image: np.ndarray = ndimage.morphological_gradient( + get_LUT_value(image, ww, wl).astype('uint16'), + self.config.mg_size) + tmp_mask: np.ndarray = watershed(tmp_image, markers.astype('int16'), bstruct) else: #tmp_image = ndimage.gaussian_filter(get_LUT_value(image, ww, wl).astype('uint16'), self.config.mg_size) #tmp_image = ndimage.morphological_gradient( - #get_LUT_value(image, ww, wl).astype('uint16'), - #self.config.mg_size) - tmp_image = get_LUT_value(image, ww, wl).astype('uint16') + #get_LUT_value(image, ww, wl).astype('uint16'), + #self.config.mg_size) + tmp_image: np.ndarray = get_LUT_value(image, ww, wl).astype('uint16') #markers[markers == 2] = -1 - tmp_mask = watershed_ift(tmp_image, markers.astype('int16'), bstruct) + tmp_mask: np.ndarray = watershed_ift(tmp_image, markers.astype('int16'), bstruct) #markers[markers == -1] = 2 #tmp_mask[tmp_mask == -1] = 2 else: if self.config.algorithm == 'Watershed': - tmp_image = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) - tmp_mask = watershed(tmp_image, markers.astype('int16'), bstruct) + tmp_image: np.ndarray = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) + tmp_mask: np.ndarray = watershed(tmp_image, markers.astype('int16'), bstruct) else: #tmp_image = (image - image.min()).astype('uint16') #tmp_image = ndimage.gaussian_filter(tmp_image, self.config.mg_size) #tmp_image = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) - tmp_image = image - image.min().astype('uint16') - tmp_mask = watershed_ift(tmp_image, markers.astype('int16'), bstruct) + tmp_image: np.ndarray = image - image.min().astype('uint16') + tmp_mask: np.ndarray = watershed_ift(tmp_image, markers.astype('int16'), bstruct) if self.viewer.overwrite_mask: mask[:] = 0 @@ -1857,12 +1922,13 @@ def OnBrushRelease(self, evt, obj): self.viewer.slice_.current_mask.clear_history() # Marking the project as changed - session = ses.Session() + session: ses.Session = ses.Session() session.ChangeProject() Publisher.sendMessage('Reload actual slice') + - def edit_mask_pixel(self, operation, n, index, position, radius, orientation): + def edit_mask_pixel(self, operation: int, n: int, index: np.ndarray, position: int or tuple, radius: float, orientation: str) -> None: if orientation == 'AXIAL': mask = self.matrix[n, :, :] elif orientation == 'CORONAL': @@ -1900,8 +1966,8 @@ def edit_mask_pixel(self, operation, n, index, position, radius, orientation): py = position / mask.shape[1] px = position % mask.shape[1] - cx = index.shape[1] / 2 + 1 - cy = index.shape[0] / 2 + 1 + cx: float = index.shape[1] / 2 + 1 + cy: float = index.shape[0] / 2 + 1 xi = int(px - index.shape[1] + cx) xf = int(xi + index.shape[1]) yi = int(py - index.shape[0] + cy) @@ -1923,7 +1989,7 @@ def edit_mask_pixel(self, operation, n, index, position, radius, orientation): # Verifying if the points is over the image array. if (not 0 <= xi <= mask.shape[1] and not 0 <= xf <= mask.shape[1]) or \ - (not 0 <= yi <= mask.shape[0] and not 0 <= yf <= mask.shape[0]): + (not 0 <= yi <= mask.shape[0] and not 0 <= yf <= mask.shape[0]): return roi_m = mask[yi:yf,xi:xf] @@ -1932,27 +1998,27 @@ def edit_mask_pixel(self, operation, n, index, position, radius, orientation): if roi_m.size: roi_m[index] = operation - def expand_watershed(self): - markers = self.matrix - image = self.viewer.slice_.matrix + def expand_watershed(self) -> None: + markers: np.ndarray = self.matrix + image: np.ndarray = self.viewer.slice_.matrix self.viewer.slice_.do_threshold_to_all_slices() - mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] - ww = self.viewer.slice_.window_width - wl = self.viewer.slice_.window_level + mask: np.ndarray = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + ww: int = self.viewer.slice_.window_width + wl: int = self.viewer.slice_.window_level if BRUSH_BACKGROUND in markers and BRUSH_FOREGROUND in markers: #w_algorithm = WALGORITHM[self.config.algorithm] - bstruct = generate_binary_structure(3, CON3D[self.config.con_3d]) - tfile = tempfile.mktemp() - tmp_mask = np.memmap(tfile, shape=mask.shape, dtype=mask.dtype, - mode='w+') - q = multiprocessing.Queue() - p = multiprocessing.Process(target=watershed_process.do_watershed, args=(image, + bstruct: np.ndarray = generate_binary_structure(3, CON3D[self.config.con_3d]) + tfile: str = tempfile.mktemp() + tmp_mask: np.memmap = np.memmap(tfile, shape=mask.shape, dtype=mask.dtype, + mode='w+') + q: multiprocessing.Queue = multiprocessing.Queue() + p: multiprocessing.Process = multiprocessing.Process(target=watershed_process.do_watershed, args=(image, markers, tfile, tmp_mask.shape, bstruct, self.config.algorithm, self.config.mg_size, self.config.use_ww_wl, wl, ww, q)) - wp = WatershedProgressWindow(p) + wp: WatershedProgressWindow = WatershedProgressWindow(p) p.start() while q.empty() and p.is_alive(): @@ -1974,29 +2040,29 @@ def expand_watershed(self): if q.empty(): return #do_watershed(image, markers, tmp_mask, bstruct, self.config.algorithm, - #self.config.mg_size, self.config.use_ww_wl, wl, ww) + #self.config.mg_size, self.config.use_ww_wl, wl, ww) #if self.config.use_ww_wl: #if self.config.algorithm == 'Watershed': - #tmp_image = ndimage.morphological_gradient( - #get_LUT_value(image, ww, wl).astype('uint16'), - #self.config.mg_size) - #tmp_mask = watershed(tmp_image, markers.astype('int16'), bstruct) + #tmp_image: np.ndarray = ndimage.morphological_gradient( + #get_LUT_value(image, ww, wl).astype('uint16'), + #self.config.mg_size) + #tmp_mask: np.ndarray = watershed(tmp_image, markers.astype('int16'), bstruct) #else: - #tmp_image = get_LUT_value(image, ww, wl).astype('uint16') + #tmp_image: np.ndarray = get_LUT_value(image, ww, wl).astype('uint16') ##tmp_image = ndimage.gaussian_filter(tmp_image, self.config.mg_size) ##tmp_image = ndimage.morphological_gradient( - ##get_LUT_value(image, ww, wl).astype('uint16'), - ##self.config.mg_size) - #tmp_mask = watershed_ift(tmp_image, markers.astype('int16'), bstruct) + ##get_LUT_value(image, ww, wl).astype('uint16'), + ##self.config.mg_size) + #tmp_mask: np.ndarray = watershed_ift(tmp_image, markers.astype('int16'), bstruct) #else: #if self.config.algorithm == 'Watershed': - #tmp_image = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) - #tmp_mask = watershed(tmp_image, markers.astype('int16'), bstruct) + #tmp_image: np.ndarray = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) + #tmp_mask: np.ndarray = watershed(tmp_image, markers.astype('int16'), bstruct) #else: - #tmp_image = (image - image.min()).astype('uint16') + #tmp_image: np.ndarray = (image - image.min()).astype('uint16') ##tmp_image = ndimage.gaussian_filter(tmp_image, self.config.mg_size) ##tmp_image = ndimage.morphological_gradient((image - image.min()).astype('uint16'), self.config.mg_size) - #tmp_mask = watershed_ift(tmp_image, markers.astype('int8'), bstruct) + #tmp_mask: np.ndarray = watershed_ift(tmp_image, markers.astype('int8'), bstruct) if self.viewer.overwrite_mask: mask[:] = 0 @@ -2012,7 +2078,7 @@ def expand_watershed(self): Publisher.sendMessage('Reload actual slice') # Marking the project as changed - session = ses.Session() + session: ses.Session = ses.Session() session.ChangeProject() @@ -2020,23 +2086,23 @@ class ReorientImageInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for image reorientation """ - def __init__(self, viewer): + def __init__(self, viewer: object) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.SLICE_STATE_REORIENT + self.state_code: int = const.SLICE_STATE_REORIENT - self.viewer = viewer + self.viewer: object = viewer - self.line1 = None - self.line2 = None + self.line1: object = None + self.line2: object = None - self.actors = [] + self.actors: list = [] - self._over_center = False - self.dragging = False - self.to_rot = False + self._over_center: bool = False + self.dragging: bool = False + self.to_rot: bool = False - self.picker = vtkWorldPointPicker() + self.picker: object = vtkWorldPointPicker() self.AddObserver("LeftButtonPressEvent",self.OnLeftClick) self.AddObserver("LeftButtonReleaseEvent", self.OnLeftRelease) @@ -2048,26 +2114,26 @@ def __init__(self, viewer): self.viewer.interactor.Bind(wx.EVT_LEFT_DCLICK, self.OnDblClick) - def SetUp(self): + def SetUp(self) -> None: self.draw_lines() Publisher.sendMessage('Hide current mask') Publisher.sendMessage('Reload actual slice') - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.interactor.Unbind(wx.EVT_LEFT_DCLICK) for actor in self.actors: self.viewer.slice_data.renderer.RemoveActor(actor) - self.viewer.slice_.rotations = [0, 0, 0] - self.viewer.slice_.q_orientation = np.array((1, 0, 0, 0)) + self.viewer.slice_.rotations: list = [0, 0, 0] + self.viewer.slice_.q_orientation: np.array = np.array((1, 0, 0, 0)) self._discard_buffers() Publisher.sendMessage('Close reorient dialog') Publisher.sendMessage('Show current mask') - def OnLeftClick(self, obj, evt): + def OnLeftClick(self, obj: object, evt: object) -> None: if self._over_center: - self.dragging = True + self.dragging: bool = True else: x, y = self.GetMousePosition() w, h = self.viewer.interactor.GetSize() @@ -2078,17 +2144,17 @@ def OnLeftClick(self, obj, evt): self.picker.Pick(x, y, 0, self.viewer.slice_data.renderer) x, y, z = self.picker.GetPickPosition() - self.p0 = self.get_image_point_coord(x, y, z) - self.to_rot = True + self.p0: tuple = self.get_image_point_coord(x, y, z) + self.to_rot: bool = True - def OnLeftRelease(self, obj, evt): - self.dragging = False + def OnLeftRelease(self, obj: object, evt: object) -> None: + self.dragging: bool = False if self.to_rot: Publisher.sendMessage('Reload actual slice') - self.to_rot = False + self.to_rot: bool = False - def OnMouseMove(self, obj, evt): + def OnMouseMove(self, obj: object, evt: object) -> None: """ This event is responsible to reorient image, set mouse cursors """ @@ -2102,26 +2168,26 @@ def OnMouseMove(self, obj, evt): mx, my = self.GetMousePosition() # Getting center value - center = self.viewer.slice_.center - coord = vtkCoordinate() + center: tuple = self.viewer.slice_.center + coord: object = vtkCoordinate() coord.SetValue(center) cx, cy = coord.GetComputedDisplayValue(self.viewer.slice_data.renderer) - dist_center = ((mx - cx)**2 + (my - cy)**2)**0.5 + dist_center: float = ((mx - cx)**2 + (my - cy)**2)**0.5 if dist_center <= 15: - self._over_center = True - cursor = wx.Cursor(wx.CURSOR_SIZENESW) + self._over_center: bool = True + cursor: object = wx.Cursor(wx.CURSOR_SIZENESW) else: - self._over_center = False - cursor = wx.Cursor(wx.CURSOR_DEFAULT) + self._over_center: bool = False + cursor: object = wx.Cursor(wx.CURSOR_DEFAULT) self.viewer.interactor.SetCursor(cursor) def OnUpdate(self, obj, evt): w, h = self.viewer.slice_data.renderer.GetSize() - center = self.viewer.slice_.center - coord = vtkCoordinate() + center: tuple = self.viewer.slice_.center + coord: object = vtkCoordinate() coord.SetValue(center) x, y = coord.GetComputedDisplayValue(self.viewer.slice_data.renderer) @@ -2133,9 +2199,9 @@ def OnUpdate(self, obj, evt): self.line2.SetPoint2(x, h, 0) self.line2.Update() - def OnDblClick(self, evt): - self.viewer.slice_.rotations = [0, 0, 0] - self.viewer.slice_.q_orientation = np.array((1, 0, 0, 0)) + def OnDblClick(self, evt: object) -> None: + self.viewer.slice_.rotations: list = [0, 0, 0] + self.viewer.slice_.q_orientation: np.array = np.array((1, 0, 0, 0)) Publisher.sendMessage('Update reorient angles', angles=(0, 0, 0)) @@ -2143,13 +2209,21 @@ def OnDblClick(self, evt): self.viewer.slice_.current_mask.clear_history() Publisher.sendMessage('Reload actual slice') - def _move_center_rot(self): - iren = self.viewer.interactor + def _move_center_rot(self) -> None: + iren: vtkRenderWindowInteractor = self.viewer.interactor + mx: int + my: int mx, my = self.GetMousePosition() + icx: float + icy: float + icz: float icx, icy, icz = self.viewer.slice_.center self.picker.Pick(mx, my, 0, self.viewer.slice_data.renderer) + x: float + y: float + z: float x, y, z = self.picker.GetPickPosition() if self.viewer.orientation == 'AXIAL': @@ -2164,34 +2238,45 @@ def _move_center_rot(self): self.viewer.slice_.current_mask.clear_history() Publisher.sendMessage('Reload actual slice') - def _rotate(self): + def _rotate(self) -> None: # Getting mouse position - iren = self.viewer.interactor + iren: vtkRenderWindowInteractor = self.viewer.interactor + mx: int + my: int mx, my = self.GetMousePosition() + cx: float + cy: float + cz: float cx, cy, cz = self.viewer.slice_.center self.picker.Pick(mx, my, 0, self.viewer.slice_data.renderer) + x: float + y: float + z: float x, y, z = self.picker.GetPickPosition() if self.viewer.orientation == 'AXIAL': - p1 = np.array((y-cy, x-cx)) + p1: np.ndarray = np.array((y-cy, x-cx)) elif self.viewer.orientation == 'CORONAL': - p1 = np.array((z-cz, x-cx)) + p1: np.ndarray = np.array((z-cz, x-cx)) elif self.viewer.orientation == 'SAGITAL': - p1 = np.array((z-cz, y-cy)) - p0 = self.p0 - p1 = self.get_image_point_coord(x, y, z) + p1: np.ndarray = np.array((z-cz, y-cy)) + p0: np.ndarray = self.p0 + p1: np.ndarray = self.get_image_point_coord(x, y, z) - axis = np.cross(p0, p1) - norm = np.linalg.norm(axis) + axis: np.ndarray = np.cross(p0, p1) + norm: float = np.linalg.norm(axis) if norm == 0: return axis = axis / norm - angle = np.arccos(np.dot(p0, p1)/(np.linalg.norm(p0)*np.linalg.norm(p1))) + angle: float = np.arccos(np.dot(p0, p1)/(np.linalg.norm(p0)*np.linalg.norm(p1))) self.viewer.slice_.q_orientation = transformations.quaternion_multiply(self.viewer.slice_.q_orientation, transformations.quaternion_about_axis(angle, axis)) + az: float + ay: float + ax: float az, ay, ax = transformations.euler_from_quaternion(self.viewer.slice_.q_orientation) Publisher.sendMessage('Update reorient angles', angles=(ax, ay, az)) @@ -2201,7 +2286,10 @@ def _rotate(self): Publisher.sendMessage('Reload actual slice %s' % self.viewer.orientation) self.p0 = self.get_image_point_coord(x, y, z) - def get_image_point_coord(self, x, y, z): + def get_image_point_coord(self, x: float, y: float, z: float) -> np.ndarray: + cx: float + cy: float + cz: float cx, cy, cz = self.viewer.slice_.center if self.viewer.orientation == 'AXIAL': z = cz @@ -2210,38 +2298,44 @@ def get_image_point_coord(self, x, y, z): elif self.viewer.orientation == 'SAGITAL': x = cx + x: float + y: float + z: float x, y, z = x-cx, y-cy, z-cz - M = transformations.quaternion_matrix(self.viewer.slice_.q_orientation) - tcoord = np.array((z, y, x, 1)).dot(M) + M: np.ndarray = transformations.quaternion_matrix(self.viewer.slice_.q_orientation) + tcoord: np.ndarray = np.array((z, y, x, 1)).dot(M) tcoord = tcoord[:3]/tcoord[3] # print (z, y, x), tcoord return tcoord - def _set_reorientation_angles(self, angles): + def _set_reorientation_angles(self, angles: Tuple[float, float, float]) -> None: + ax: float + ay: float + az: float ax, ay, az = angles - q = transformations.quaternion_from_euler(az, ay, ax) + q: np.ndarray = transformations.quaternion_from_euler(az, ay, ax) self.viewer.slice_.q_orientation = q self._discard_buffers() self.viewer.slice_.current_mask.clear_history() Publisher.sendMessage('Reload actual slice') - def _create_line(self, x0, y0, x1, y1, color): - line = vtkLineSource() + def _create_line(self, x0: float, y0: float, x1: float, y1: float, color: Tuple[float, float, float]) -> vtkLineSource: + line: vtkLineSource = vtkLineSource() line.SetPoint1(x0, y0, 0) line.SetPoint2(x1, y1, 0) - coord = vtkCoordinate() + coord: vtkCoordinate = vtkCoordinate() coord.SetCoordinateSystemToDisplay() - mapper = vtkPolyDataMapper2D() + mapper: vtkPolyDataMapper2D = vtkPolyDataMapper2D() mapper.SetTransformCoordinate(coord) mapper.SetInputConnection(line.GetOutputPort()) mapper.Update() - actor = vtkActor2D() + actor: vtkActor2D = vtkActor2D() actor.SetMapper(mapper) actor.GetProperty().SetLineWidth(2.0) actor.GetProperty().SetColor(color) @@ -2253,124 +2347,124 @@ def _create_line(self, x0, y0, x1, y1, color): return line - def draw_lines(self): + def draw_lines(self) -> None: if self.viewer.orientation == 'AXIAL': - color1 = (0, 1, 0) - color2 = (0, 0, 1) + color1: tuple[float, float, float] = (0, 1, 0) + color2: tuple[float, float, float] = (0, 0, 1) elif self.viewer.orientation == 'CORONAL': - color1 = (1, 0, 0) - color2 = (0, 0, 1) + color1: tuple[float, float, float] = (1, 0, 0) + color2: tuple[float, float, float] = (0, 0, 1) elif self.viewer.orientation == 'SAGITAL': - color1 = (1, 0, 0) - color2 = (0, 1, 0) + color1: tuple[float, float, float] = (1, 0, 0) + color2: tuple[float, float, float] = (0, 1, 0) - self.line1 = self._create_line(0, 0.5, 1, 0.5, color1) - self.line2 = self._create_line(0.5, 0, 0.5, 1, color2) + self.line1: vtkLineSource = self._create_line(0, 0.5, 1, 0.5, color1) + self.line2: vtkLineSource = self._create_line(0.5, 0, 0.5, 1, color2) - def _discard_buffers(self): + def _discard_buffers(self) -> None: for buffer_ in self.viewer.slice_.buffer_slices.values(): buffer_.discard_vtk_image() buffer_.discard_image() - + class FFillConfig(metaclass=utils.Singleton): - def __init__(self): - self.dlg_visible = False - self.target = "2D" - self.con_2d = 4 - self.con_3d = 6 + def __init__(self) -> None: + self.dlg_visible: bool = False + self.target: str = "2D" + self.con_2d: int = 4 + self.con_3d: int = 6 class FloodFillMaskInteractorStyle(DefaultInteractorStyle): - def __init__(self, viewer): + def __init__(self, viewer: object) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.SLICE_STATE_MASK_FFILL + self.state_code: int = const.SLICE_STATE_MASK_FFILL - self.viewer = viewer - self.orientation = self.viewer.orientation + self.viewer: object = viewer + self.orientation: str = self.viewer.orientation - self.picker = vtkWorldPointPicker() - self.slice_actor = viewer.slice_data.actor - self.slice_data = viewer.slice_data + self.picker: vtkWorldPointPicker = vtkWorldPointPicker() + self.slice_actor: object = viewer.slice_data.actor + self.slice_data: object = viewer.slice_data - self.config = FFillConfig() - self.dlg_ffill = None + self.config: FFillConfig = FFillConfig() + self.dlg_ffill: object = None # InVesalius uses the following values to mark non selected parts in a # mask: # 0 - Threshold # 1 - Manual edition and floodfill # 2 - Watershed - self.t0 = 0 - self.t1 = 2 - self.fill_value = 254 + self.t0: int = 0 + self.t1: int = 2 + self.fill_value: int = 254 - self._dlg_title = _(u"Fill holes") - self._progr_title = _(u"Fill hole") - self._progr_msg = _(u"Filling hole ...") + self._dlg_title: str = _(u"Fill holes") + self._progr_title: str = _(u"Fill hole") + self._progr_msg: str = _(u"Filling hole ...") self.AddObserver("LeftButtonPressEvent", self.OnFFClick) - def SetUp(self): + def SetUp(self) -> None: if not self.config.dlg_visible: - self.config.dlg_visible = True - self.dlg_ffill = dialogs.FFillOptionsDialog(self._dlg_title, self.config) + self.config.dlg_visible: bool = True + self.dlg_ffill: object = dialogs.FFillOptionsDialog(self._dlg_title, self.config) self.dlg_ffill.Show() - def CleanUp(self): + def CleanUp(self) -> None: if (self.dlg_ffill is not None) and (self.config.dlg_visible): - self.config.dlg_visible = False + self.config.dlg_visible: bool = False self.dlg_ffill.Destroy() - self.dlg_ffill = None + self.dlg_ffill: object = None - def OnFFClick(self, obj, evt): + def OnFFClick(self, obj: object, evt: object) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return - viewer = self.viewer + viewer: object = self.viewer iren = viewer.interactor mouse_x, mouse_y = self.GetMousePosition() x, y, z = self.viewer.get_voxel_coord_by_screen_pos(mouse_x, mouse_y, self.picker) - mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + mask: np.ndarray = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] if mask[z, y, x] < self.t0 or mask[z, y, x] > self.t1: return if self.config.target == "3D": - bstruct = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8') + bstruct: np.ndarray = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8') self.viewer.slice_.do_threshold_to_all_slices() - cp_mask = self.viewer.slice_.current_mask.matrix.copy() + cp_mask: np.ndarray = self.viewer.slice_.current_mask.matrix.copy() else: - _bstruct = generate_binary_structure(2, CON2D[self.config.con_2d]) + _bstruct: np.ndarray = generate_binary_structure(2, CON2D[self.config.con_2d]) if self.orientation == 'AXIAL': - bstruct = np.zeros((1, 3, 3), dtype='uint8') + bstruct: np.ndarray = np.zeros((1, 3, 3), dtype='uint8') bstruct[0] = _bstruct elif self.orientation == 'CORONAL': - bstruct = np.zeros((3, 1, 3), dtype='uint8') + bstruct: np.ndarray = np.zeros((3, 1, 3), dtype='uint8') bstruct[:, 0, :] = _bstruct elif self.orientation == 'SAGITAL': - bstruct = np.zeros((3, 3, 1), dtype='uint8') + bstruct: np.ndarray = np.zeros((3, 3, 1), dtype='uint8') bstruct[:, :, 0] = _bstruct if self.config.target == '2D': floodfill.floodfill_threshold(mask, [[x, y, z]], self.t0, self.t1, self.fill_value, bstruct, mask) - b_mask = self.viewer.slice_.buffer_slices[self.orientation].mask - index = self.viewer.slice_.buffer_slices[self.orientation].index + b_mask: np.ndarray = self.viewer.slice_.buffer_slices[self.orientation].mask + index: int = self.viewer.slice_.buffer_slices[self.orientation].index if self.orientation == 'AXIAL': - p_mask = mask[index,:,:].copy() + p_mask: np.ndarray = mask[index,:,:].copy() elif self.orientation == 'CORONAL': - p_mask = mask[:, index, :].copy() + p_mask: np.ndarray = mask[:, index, :].copy() elif self.orientation == 'SAGITAL': - p_mask = mask[:, :, index].copy() + p_mask: np.ndarray = mask[:, :, index].copy() self.viewer.slice_.current_mask.save_history(index, self.orientation, p_mask, b_mask) else: with futures.ThreadPoolExecutor(max_workers=1) as executor: - future = executor.submit(floodfill.floodfill_threshold, mask, [[x, y, z]], self.t0, self.t1, self.fill_value, bstruct, mask) + future: object = executor.submit(floodfill.floodfill_threshold, mask, [[x, y, z]], self.t0, self.t1, self.fill_value, bstruct, mask) - dlg = wx.ProgressDialog(self._progr_title, self._progr_msg, parent=wx.GetApp().GetTopWindow(), style=wx.PD_APP_MODAL) + dlg: wx.ProgressDialog = wx.ProgressDialog(self._progr_title, self._progr_msg, parent=wx.GetApp().GetTopWindow(), style=wx.PD_APP_MODAL) while not future.done(): dlg.Pulse() time.sleep(0.1) @@ -2387,79 +2481,79 @@ def OnFFClick(self, obj, evt): self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask() self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() - self.viewer.slice_.current_mask.was_edited = True + self.viewer.slice_.current_mask.was_edited: bool = True self.viewer.slice_.current_mask.modified(True) Publisher.sendMessage('Reload actual slice') class RemoveMaskPartsInteractorStyle(FloodFillMaskInteractorStyle): - def __init__(self, viewer): + def __init__(self, viewer: object) -> None: FloodFillMaskInteractorStyle.__init__(self, viewer) - self.state_code = const.SLICE_STATE_REMOVE_MASK_PARTS + self.state_code: int = const.SLICE_STATE_REMOVE_MASK_PARTS # InVesalius uses the following values to mark selected parts in a # mask: # 255 - Threshold # 254 - Manual edition and floodfill # 253 - Watershed - self.t0 = 253 - self.t1 = 255 - self.fill_value = 1 + self.t0: int = 253 + self.t1: int = 255 + self.fill_value: int = 1 - self._dlg_title = _(u"Remove parts") - self._progr_title = _(u"Remove part") - self._progr_msg = _(u"Removing part ...") + self._dlg_title: str = _(u"Remove parts") + self._progr_title: str = _(u"Remove part") + self._progr_msg: str = _(u"Removing part ...") class CropMaskConfig(metaclass=utils.Singleton): - def __init__(self): - self.dlg_visible = False + def __init__(self) -> None: + self.dlg_visible: bool = False class CropMaskInteractorStyle(DefaultInteractorStyle): - def __init__(self, viewer): + def __init__(self, viewer: object) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.SLICE_STATE_CROP_MASK + self.state_code: int = const.SLICE_STATE_CROP_MASK - self.viewer = viewer - self.orientation = self.viewer.orientation - self.picker = vtkWorldPointPicker() - self.slice_actor = viewer.slice_data.actor - self.slice_data = viewer.slice_data - self.draw_retangle = None + self.viewer: object = viewer + self.orientation: str = self.viewer.orientation + self.picker: vtkWorldPointPicker = vtkWorldPointPicker() + self.slice_actor: vtkActor = viewer.slice_data.actor + self.slice_data: SliceData = viewer.slice_data + self.draw_retangle: geom.DrawCrop2DRetangle = None - self.config = CropMaskConfig() + self.config: CropMaskConfig = CropMaskConfig() - def __evts__(self): + def __evts__(self) -> None: self.AddObserver("MouseMoveEvent", self.OnMove) self.AddObserver("LeftButtonPressEvent", self.OnLeftPressed) self.AddObserver("LeftButtonReleaseEvent", self.OnReleaseLeftButton) Publisher.subscribe(self.CropMask, "Crop mask") - def OnMove(self, obj, evt): + def OnMove(self, obj, evt) -> None: x, y = self.GetMousePosition() self.draw_retangle.MouseMove(x,y) - def OnLeftPressed(self, obj, evt): + def OnLeftPressed(self, obj, evt) -> None: self.draw_retangle.mouse_pressed = True x, y = self.GetMousePosition() self.draw_retangle.LeftPressed(x,y) - def OnReleaseLeftButton(self, obj, evt): - self.draw_retangle.mouse_pressed = False + def OnReleaseLeftButton(self, obj: object, evt: object) -> None: + self.draw_retangle.mouse_pressed: bool = False self.draw_retangle.ReleaseLeft() - def SetUp(self): - self.draw_retangle = geom.DrawCrop2DRetangle() + def SetUp(self) -> None: + self.draw_retangle: geom.DrawCrop2DRetangle = geom.DrawCrop2DRetangle() self.draw_retangle.SetViewer(self.viewer) self.viewer.canvas.draw_list.append(self.draw_retangle) self.viewer.UpdateCanvas() if not(self.config.dlg_visible): - self.config.dlg_visible = True + self.config.dlg_visible: bool = True - dlg = dialogs.CropOptionsDialog(self.config) + dlg: dialogs.CropOptionsDialog = dialogs.CropOptionsDialog(self.config) dlg.UpdateValues(self.draw_retangle.box.GetLimits()) dlg.Show() @@ -2468,11 +2562,11 @@ def SetUp(self): #Publisher.sendMessage('Hide current mask') #Publisher.sendMessage('Reload actual slice') - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.canvas.draw_list.remove(self.draw_retangle) Publisher.sendMessage('Redraw canvas') - def CropMask(self): + def CropMask(self) -> None: if self.viewer.orientation == "AXIAL": xi, xf, yi, yf, zi, zf = self.draw_retangle.box.GetLimits() @@ -2486,9 +2580,9 @@ def CropMask(self): zf += 1 self.viewer.slice_.do_threshold_to_all_slices() - cp_mask = self.viewer.slice_.current_mask.matrix.copy() + cp_mask: np.ndarray = self.viewer.slice_.current_mask.matrix.copy() - tmp_mask = self.viewer.slice_.current_mask.matrix[zi-1:zf+1, yi-1:yf+1, xi-1:xf+1].copy() + tmp_mask: np.ndarray = self.viewer.slice_.current_mask.matrix[zi-1:zf+1, yi-1:yf+1, xi-1:xf+1].copy() self.viewer.slice_.current_mask.matrix[:] = 1 @@ -2504,81 +2598,81 @@ def CropMask(self): self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask() self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() - self.viewer.slice_.current_mask.was_edited = True + self.viewer.slice_.current_mask.was_edited: bool = True self.viewer.slice_.current_mask.modified(True) Publisher.sendMessage('Reload actual slice') class SelectPartConfig(metaclass=utils.Singleton): - def __init__(self): - self.mask = None - self.con_3d = 6 - self.dlg_visible = False - self.mask_name = '' + def __init__(self) -> None: + self.mask: Mask = None + self.con_3d: int = 6 + self.dlg_visible: bool = False + self.mask_name: str = '' class SelectMaskPartsInteractorStyle(DefaultInteractorStyle): - def __init__(self, viewer): + def __init__(self, viewer: Viewer) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.SLICE_STATE_SELECT_MASK_PARTS + self.state_code: int = const.SLICE_STATE_SELECT_MASK_PARTS - self.viewer = viewer - self.orientation = self.viewer.orientation + self.viewer: Viewer = viewer + self.orientation: int = self.viewer.orientation - self.picker = vtkWorldPointPicker() - self.slice_actor = viewer.slice_data.actor - self.slice_data = viewer.slice_data + self.picker: vtkWorldPointPicker = vtkWorldPointPicker() + self.slice_actor: vtkActor = viewer.slice_data.actor + self.slice_data: SliceData = viewer.slice_data - self.config = SelectPartConfig() - self.dlg = None + self.config: SelectPartConfig = SelectPartConfig() + self.dlg: dialogs.SelectPartsOptionsDialog = None # InVesalius uses the following values to mark selected parts in a # mask: # 255 - Threshold # 254 - Manual edition and floodfill # 253 - Watershed - self.t0 = 253 - self.t1 = 255 - self.fill_value = 254 + self.t0: int = 253 + self.t1: int = 255 + self.fill_value: int = 254 self.AddObserver("LeftButtonPressEvent", self.OnSelect) - def SetUp(self): + def SetUp(self) -> None: if not self.config.dlg_visible: import invesalius.data.mask as mask - default_name = const.MASK_NAME_PATTERN %(mask.Mask.general_index+2) + default_name: str = const.MASK_NAME_PATTERN %(mask.Mask.general_index+2) - self.config.mask_name = default_name - self.config.dlg_visible = True - self.dlg= dialogs.SelectPartsOptionsDialog(self.config) + self.config.mask_name: str = default_name + self.config.dlg_visible: bool = True + self.dlg: dialogs.SelectPartsOptionsDialog = dialogs.SelectPartsOptionsDialog(self.config) self.dlg.Show() - def CleanUp(self): + def CleanUp(self) -> None: if self.dlg is None: return - dialog_return = self.dlg.GetReturnCode() + dialog_return: int = self.dlg.GetReturnCode() if self.config.dlg_visible: - self.config.dlg_visible = False + self.config.dlg_visible: bool = False self.dlg.Destroy() - self.dlg = None + self.dlg: dialogs.SelectPartsOptionsDialog = None if self.config.mask: if dialog_return == wx.OK: - self.config.mask.name = self.config.mask_name + self.config.mask.name: str = self.config.mask_name self.viewer.slice_._add_mask_into_proj(self.config.mask) self.viewer.slice_.SelectCurrentMask(self.config.mask.index) Publisher.sendMessage('Change mask selected', index=self.config.mask.index) del self.viewer.slice_.aux_matrices['SELECT'] - self.viewer.slice_.to_show_aux = '' + self.viewer.slice_.to_show_aux: str = '' Publisher.sendMessage('Reload actual slice') - self.config.mask = None + self.config.mask: Mask = None - def OnSelect(self, obj, evt): + def OnSelect(self, obj, evt) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return @@ -2586,9 +2680,9 @@ def OnSelect(self, obj, evt): mouse_x, mouse_y = self.GetMousePosition() x, y, z = self.viewer.get_voxel_coord_by_screen_pos(mouse_x, mouse_y, self.picker) - mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + mask: np.ndarray = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] - bstruct = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8') + bstruct: np.ndarray = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8') self.viewer.slice_.do_threshold_to_all_slices() if self.config.mask is None: @@ -2599,87 +2693,88 @@ def OnSelect(self, obj, evt): else: floodfill.floodfill_threshold(mask, [[x, y, z]], self.t0, self.t1, self.fill_value, bstruct, self.config.mask.matrix[1:, 1:, 1:]) - self.viewer.slice_.aux_matrices['SELECT'] = self.config.mask.matrix[1:, 1:, 1:] - self.viewer.slice_.to_show_aux = 'SELECT' + self.viewer.slice_.aux_matrices['SELECT']: np.ndarray = self.config.mask.matrix[1:, 1:, 1:] + self.viewer.slice_.to_show_aux: str = 'SELECT' - self.config.mask.was_edited = True + self.config.mask.was_edited: bool = True Publisher.sendMessage('Reload actual slice') - def _create_new_mask(self): - mask = self.viewer.slice_.create_new_mask(show=False, add_to_project=False) - mask.was_edited = True + def _create_new_mask(self) -> None: + mask: Mask = self.viewer.slice_.create_new_mask(show=False, add_to_project=False) + mask.was_edited: bool = True mask.matrix[0, :, :] = 1 mask.matrix[:, 0, :] = 1 mask.matrix[:, :, 0] = 1 - self.config.mask = mask + self.config.mask: Mask = mask class FFillSegmentationConfig(metaclass=utils.Singleton): - def __init__(self): - self.dlg_visible = False - self.dlg = None - self.target = "2D" - self.con_2d = 4 - self.con_3d = 6 + def __init__(self) -> None: + self.dlg_visible: bool = False + self.dlg: dialogs.FFillSegmentationOptionsDialog = None + self.target: str = "2D" + self.con_2d: int = 4 + self.con_3d: int = 6 - self.t0 = None - self.t1 = None + self.t0: int = None + self.t1: int = None - self.fill_value = 254 + self.fill_value: int = 254 - self.method = 'dynamic' + self.method: str = 'dynamic' - self.dev_min = 25 - self.dev_max = 25 + self.dev_min: int = 25 + self.dev_max: int = 25 - self.use_ww_wl = True + self.use_ww_wl: bool = True - self.confid_mult = 2.5 - self.confid_iters = 3 + self.confid_mult: float = 2.5 + self.confid_iters: int =3 class FloodFillSegmentInteractorStyle(DefaultInteractorStyle): - def __init__(self, viewer): + def __init__(self, viewer: vtkImageViewer2) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.SLICE_STATE_FFILL_SEGMENTATION + self.state_code: int = const.SLICE_STATE_FFILL_SEGMENTATION - self.viewer = viewer - self.orientation = self.viewer.orientation + self.viewer: vtkImageViewer2 = viewer + self.orientation: str = self.viewer.orientation - self.picker = vtkWorldPointPicker() - self.slice_actor = viewer.slice_data.actor - self.slice_data = viewer.slice_data + self.picker: vtkWorldPointPicker = vtkWorldPointPicker() + self.slice_actor: vtkImageActor = viewer.slice_data.actor + self.slice_data: SliceData = viewer.slice_data - self.config = FFillSegmentationConfig() + self.config: FFillSegmentationConfig = FFillSegmentationConfig() - self._progr_title = _(u"Region growing") - self._progr_msg = _(u"Segmenting ...") + self._progr_title: str = _(u"Region growing") + self._progr_msg: str = _(u"Segmenting ...") self.AddObserver("LeftButtonPressEvent", self.OnFFClick) - def SetUp(self): + def SetUp(self) -> None: if not self.config.dlg_visible: if self.config.t0 is None: - image = self.viewer.slice_.matrix - _min, _max = image.min(), image.max() + image: np.ndarray = self.viewer.slice_.matrix + _min: float = image.min() + _max: float = image.max() - self.config.t0 = int(_min + (3.0/4.0) * (_max - _min)) - self.config.t1 = int(_max) + self.config.t0: int = int(_min + (3.0/4.0) * (_max - _min)) + self.config.t1: int = int(_max) - self.config.dlg_visible = True - self.config.dlg = dialogs.FFillSegmentationOptionsDialog(self.config) + self.config.dlg_visible: bool = True + self.config.dlg: dialogs.FFillSegmentationOptionsDialog = dialogs.FFillSegmentationOptionsDialog(self.config) self.config.dlg.Show() - def CleanUp(self): + def CleanUp(self) -> None: if (self.config.dlg is not None) and (self.config.dlg_visible): - self.config.dlg_visible = False + self.config.dlg_visible: bool = False self.config.dlg.Destroy() - self.config.dlg = None + self.config.dlg: Optional[dialogs.FFillSegmentationOptionsDialog] = None - def OnFFClick(self, obj, evt): + def OnFFClick(self, obj: Any, evt: Any) -> None: if (self.viewer.slice_.buffer_slices[self.orientation].mask is None): return @@ -2696,45 +2791,45 @@ def OnFFClick(self, obj, evt): self.viewer.slice_.buffer_slices['CORONAL'].discard_vtk_mask() self.viewer.slice_.buffer_slices['SAGITAL'].discard_vtk_mask() - self.viewer.slice_.current_mask.was_edited = True + self.viewer.slice_.current_mask.was_edited: bool = True self.viewer.slice_.current_mask.modified(self.config.target == '3D') Publisher.sendMessage('Reload actual slice') - def do_2d_seg(self): + def do_2d_seg(self) -> None: viewer = self.viewer iren = viewer.interactor mouse_x, mouse_y = self.GetMousePosition() x, y = self.viewer.get_slice_pixel_coord_by_screen_pos(mouse_x, mouse_y, self.picker) - mask = self.viewer.slice_.buffer_slices[self.orientation].mask.copy() - image = self.viewer.slice_.buffer_slices[self.orientation].image + mask: np.ndarray = self.viewer.slice_.buffer_slices[self.orientation].mask.copy() + image: np.ndarray = self.viewer.slice_.buffer_slices[self.orientation].image if self.config.method == 'confidence': dy, dx = image.shape image = image.reshape((1, dy, dx)) mask = mask.reshape((1, dy, dx)) - bstruct = np.array(generate_binary_structure(2, CON2D[self.config.con_2d]), dtype='uint8') - bstruct = bstruct.reshape((1, 3, 3)) + bstruct: np.ndarray = np.array(generate_binary_structure(2, CON2D[self.config.con_2d]), dtype='uint8') + bstruct: np.ndarray = bstruct.reshape((1, 3, 3)) - out_mask = self.do_rg_confidence(image, mask, (x, y, 0), bstruct) + out_mask: np.ndarray = self.do_rg_confidence(image, mask, (x, y, 0), bstruct) else: if self.config.method == 'threshold': - v = image[y, x] - t0 = self.config.t0 - t1 = self.config.t1 + v: float = image[y, x] + t0: int = self.config.t0 + t1: int = self.config.t1 elif self.config.method == 'dynamic': if self.config.use_ww_wl: print("Using WW&WL") - ww = self.viewer.slice_.window_width - wl = self.viewer.slice_.window_level - image = get_LUT_value_255(image, ww, wl) + ww: float = self.viewer.slice_.window_width + wl: float = self.viewer.slice_.window_level + image: np.ndarray = get_LUT_value_255(image, ww, wl) - v = image[y, x] + v: float = image[y, x] - t0 = v - self.config.dev_min - t1 = v + self.config.dev_max + t0: float = v - self.config.dev_min + t1: float = v + self.config.dev_max if image[y, x] < t0 or image[y, x] > t1: @@ -2744,18 +2839,18 @@ def do_2d_seg(self): image = image.reshape((1, dy, dx)) mask = mask.reshape((1, dy, dx)) - out_mask = np.zeros_like(mask) + out_mask: np.ndarray = np.zeros_like(mask) - bstruct = np.array(generate_binary_structure(2, CON2D[self.config.con_2d]), dtype='uint8') - bstruct = bstruct.reshape((1, 3, 3)) + bstruct: np.ndarray = np.array(generate_binary_structure(2, CON2D[self.config.con_2d]), dtype='uint8') + bstruct: np.ndarray = bstruct.reshape((1, 3, 3)) floodfill.floodfill_threshold(image, [[x, y, 0]], t0, t1, 1, bstruct, out_mask) - mask[out_mask.astype('bool')] = self.config.fill_value + mask[out_mask.astype('bool')]: int = self.config.fill_value - index = self.viewer.slice_.buffer_slices[self.orientation].index - b_mask = self.viewer.slice_.buffer_slices[self.orientation].mask - vol_mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + index: int = self.viewer.slice_.buffer_slices[self.orientation].index + b_mask: np.ndarray = self.viewer.slice_.buffer_slices[self.orientation].mask + vol_mask: np.ndarray = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] if self.orientation == 'AXIAL': vol_mask[index, :, :] = mask @@ -2766,44 +2861,44 @@ def do_2d_seg(self): self.viewer.slice_.current_mask.save_history(index, self.orientation, mask, b_mask) - def do_3d_seg(self): + def do_3d_seg(self) -> None: viewer = self.viewer iren = viewer.interactor mouse_x, mouse_y = self.GetMousePosition() x, y, z = self.viewer.get_voxel_coord_by_screen_pos(mouse_x, mouse_y, self.picker) - mask = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] - image = self.viewer.slice_.matrix + mask: np.ndarray = self.viewer.slice_.current_mask.matrix[1:, 1:, 1:] + image: np.ndarray = self.viewer.slice_.matrix if self.config.method != 'confidence': if self.config.method == 'threshold': - v = image[z, y, x] - t0 = self.config.t0 - t1 = self.config.t1 + v: int = image[z, y, x] + t0: int = self.config.t0 + t1: int = self.config.t1 elif self.config.method == 'dynamic': if self.config.use_ww_wl: print("Using WW&WL") - ww = self.viewer.slice_.window_width - wl = self.viewer.slice_.window_level - image = get_LUT_value_255(image, ww, wl) + ww: int = self.viewer.slice_.window_width + wl: int = self.viewer.slice_.window_level + image: np.ndarray = get_LUT_value_255(image, ww, wl) - v = image[z, y, x] + v: int = image[z, y, x] - t0 = v - self.config.dev_min - t1 = v + self.config.dev_max + t0: int = v - self.config.dev_min + t1: int = v + self.config.dev_max if image[z, y, x] < t0 or image[z, y, x] > t1: return - bstruct = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8') + bstruct: np.ndarray = np.array(generate_binary_structure(3, CON3D[self.config.con_3d]), dtype='uint8') self.viewer.slice_.do_threshold_to_all_slices() - cp_mask = self.viewer.slice_.current_mask.matrix.copy() + cp_mask: np.ndarray = self.viewer.slice_.current_mask.matrix.copy() if self.config.method == 'confidence': with futures.ThreadPoolExecutor(max_workers=1) as executor: - future = executor.submit(self.do_rg_confidence, image, mask, (x, y, z), bstruct) + future: object = executor.submit(self.do_rg_confidence, image, mask, (x, y, z), bstruct) self.config.dlg.panel_ffill_progress.Enable() self.config.dlg.panel_ffill_progress.StartTimer() @@ -2813,11 +2908,11 @@ def do_3d_seg(self): time.sleep(0.1) self.config.dlg.panel_ffill_progress.StopTimer() self.config.dlg.panel_ffill_progress.Disable() - out_mask = future.result() + out_mask: np.ndarray = future.result() else: - out_mask = np.zeros_like(mask) + out_mask: np.ndarray = np.zeros_like(mask) with futures.ThreadPoolExecutor(max_workers=1) as executor: - future = executor.submit(floodfill.floodfill_threshold, image, [[x, y, z]], t0, t1, 1, bstruct, out_mask) + future: object = executor.submit(floodfill.floodfill_threshold, image, [[x, y, z]], t0, t1, 1, bstruct, out_mask) self.config.dlg.panel_ffill_progress.Enable() self.config.dlg.panel_ffill_progress.StartTimer() @@ -2835,11 +2930,11 @@ def do_3d_seg(self): def do_rg_confidence(self, image, mask, p, bstruct): x, y, z = p if self.config.use_ww_wl: - ww = self.viewer.slice_.window_width - wl = self.viewer.slice_.window_level - image = get_LUT_value_255(image, ww, wl) - bool_mask = np.zeros_like(mask, dtype='bool') - out_mask = np.zeros_like(mask) + ww: int = self.viewer.slice_.window_width + wl: int = self.viewer.slice_.window_level + image: np.ndarray = get_LUT_value_255(image, ww, wl) + bool_mask: np.ndarray = np.zeros_like(mask, dtype='bool') + out_mask: np.ndarray = np.zeros_like(mask) for k in range(int(z-1), int(z+2)): if k < 0 or k >= bool_mask.shape[0]: @@ -2853,11 +2948,11 @@ def do_rg_confidence(self, image, mask, p, bstruct): bool_mask[k, j, i] = True for i in range(self.config.confid_iters): - var = np.std(image[bool_mask]) - mean = np.mean(image[bool_mask]) + var: float = np.std(image[bool_mask]) + mean: float = np.mean(image[bool_mask]) - t0 = mean - var * self.config.confid_mult - t1 = mean + var * self.config.confid_mult + t0: int = mean - var * self.config.confid_mult + t1: int = mean + var * self.config.confid_mult floodfill.floodfill_threshold(image, [[x, y, z]], t0, t1, 1, bstruct, out_mask) @@ -2868,7 +2963,7 @@ def do_rg_confidence(self, image, mask, p, bstruct): class Styles: - styles = { + styles: Dict[int, Type[InteractorStyle]] = { const.STATE_DEFAULT: DefaultInteractorStyle, const.SLICE_STATE_CROSS: CrossInteractorStyle, const.STATE_WL: WWWLInteractorStyle, @@ -2893,7 +2988,7 @@ class Styles: } @classmethod - def add_style(cls, style_cls, level=1): + def add_style(cls, style_cls: Type[InteractorStyle], level: int = 1) -> int: if style_cls in cls.styles.values(): for style_id in cls.styles: if cls.styles[style_id] == style_cls: @@ -2901,16 +2996,17 @@ def add_style(cls, style_cls, level=1): const.STYLE_LEVEL[style_id] = level return style_id - new_style_id = max(cls.styles) + 1 + new_style_id: int = max(cls.styles) + 1 cls.styles[new_style_id] = style_cls const.SLICE_STYLES.append(new_style_id) const.STYLE_LEVEL[new_style_id] = level return new_style_id @classmethod - def remove_style(cls, style_id): + def remove_style(cls, style_id: int) -> None: del cls.styles[style_id] @classmethod - def get_style(cls, style): + def get_style(cls, style: int) -> Type[InteractorStyle]: return cls.styles[style] + \ No newline at end of file diff --git a/invesalius/data/styles_3d.py b/invesalius/data/styles_3d.py index cc52f849f..8f8e3e381 100644 --- a/invesalius/data/styles_3d.py +++ b/invesalius/data/styles_3d.py @@ -16,7 +16,7 @@ # PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais # detalhes. # -------------------------------------------------------------------------- - +from typing import List, Dict, Any, Type, cast, overload, TYPE_CHECKING import wx from vtkmodules.vtkInteractionStyle import ( vtkInteractorStyleRubberBandZoom, @@ -24,20 +24,21 @@ ) from vtkmodules.vtkRenderingCore import vtkCellPicker, vtkPointPicker, vtkPropPicker + import invesalius.constants as const import invesalius.project as prj from invesalius.pubsub import pub as Publisher -PROP_MEASURE = 0.8 +PROP_MEASURE: float = 0.8 class Base3DInteractorStyle(vtkInteractorStyleTrackballCamera): - def __init__(self, viewer): - self.right_pressed = False - self.left_pressed = False - self.middle_pressed = False + def __init__(self, viewer: wx.Window) -> None: + self.right_pressed: bool = False + self.left_pressed: bool = False + self.middle_pressed: bool = False self.AddObserver("LeftButtonPressEvent", self.OnPressLeftButton) self.AddObserver("LeftButtonReleaseEvent", self.OnReleaseLeftButton) @@ -48,22 +49,22 @@ def __init__(self, viewer): self.AddObserver("MiddleButtonPressEvent", self.OnMiddleButtonPressEvent) self.AddObserver("MiddleButtonReleaseEvent", self.OnMiddleButtonReleaseEvent) - def OnPressLeftButton(self, evt, obj): + def OnPressLeftButton(self, evt: wx.MouseEvent, obj: object) -> None: self.left_pressed = True - def OnReleaseLeftButton(self, evt, obj): + def OnReleaseLeftButton(self, evt: wx.MouseEvent, obj: object) -> None: self.left_pressed = False - def OnPressRightButton(self, evt, obj): + def OnPressRightButton(self, evt: wx.MouseEvent, obj: object) -> None: self.right_pressed = True - def OnReleaseRightButton(self, evt, obj): + def OnReleaseRightButton(self, evt: wx.MouseEvent, obj: object) -> None: self.right_pressed = False - def OnMiddleButtonPressEvent(self, evt, obj): + def OnMiddleButtonPressEvent(self, evt: wx.MouseEvent, obj: object) -> None: self.middle_pressed = True - def OnMiddleButtonReleaseEvent(self, evt, obj): + def OnMiddleButtonReleaseEvent(self, evt: wx.MouseEvent, obj: object) -> None: self.middle_pressed = False @@ -73,11 +74,11 @@ class DefaultInteractorStyle(Base3DInteractorStyle): * Zoom moving mouse with right button pressed; * Change the slices with the scroll. """ - def __init__(self, viewer): + def __init__(self, viewer: wx.Window) -> None: super().__init__(viewer) - self.state_code = const.STATE_DEFAULT + self.state_code: int = const.STATE_DEFAULT - self.viewer = viewer + self.viewer: wx.Window = viewer # Zoom using right button self.AddObserver("LeftButtonPressEvent",self.OnRotateLeftClick) @@ -92,10 +93,10 @@ def __init__(self, viewer): self.AddObserver("MouseWheelBackwardEvent", self.OnScrollBackward) self.AddObserver("EnterEvent", self.OnFocus) - def OnFocus(self, evt, obj): + def OnFocus(self, evt: wx.MouseEvent, obj: object) -> None: self.viewer.SetFocus() - def OnMouseMove(self, evt, obj): + def OnMouseMove(self, evt: wx.MouseEvent, obj: object) -> None: if self.left_pressed: evt.Rotate() evt.OnLeftButtonDown() @@ -108,24 +109,24 @@ def OnMouseMove(self, evt, obj): evt.Pan() evt.OnMiddleButtonDown() - def OnRotateLeftClick(self, evt, obj): + def OnRotateLeftClick(self, evt: wx.MouseEvent, obj: object) -> None: evt.StartRotate() - def OnRotateLeftRelease(self, evt, obj): + def OnRotateLeftRelease(self, evt: wx.MouseEvent, obj: object) -> None: evt.OnLeftButtonUp() evt.EndRotate() - def OnZoomRightClick(self, evt, obj): + def OnZoomRightClick(self, evt: wx.MouseEvent, obj: object) -> None: evt.StartDolly() - def OnZoomRightRelease(self, evt, obj): + def OnZoomRightRelease(self, evt: wx.MouseEvent, obj: object) -> None: evt.OnRightButtonUp() evt.EndDolly() - def OnScrollForward(self, evt, obj): + def OnScrollForward(self, evt: wx.MouseEvent, obj: object) -> None: self.OnMouseWheelForward() - def OnScrollBackward(self, evt, obj): + def OnScrollBackward(self, evt: wx.MouseEvent, obj: object) -> None: self.OnMouseWheelBackward() @@ -134,12 +135,12 @@ class ZoomInteractorStyle(DefaultInteractorStyle): Interactor style responsible for zoom with movement of the mouse and the left mouse button clicked. """ - def __init__(self, viewer): + def __init__(self, viewer: wx.Window) -> None: super().__init__(viewer) - self.state_code = const.STATE_ZOOM + self.state_code: int = const.STATE_ZOOM - self.viewer = viewer + self.viewer: wx.Window = viewer self.RemoveObservers("LeftButtonPressEvent") self.RemoveObservers("LeftButtonReleaseEvent") @@ -149,48 +150,49 @@ def __init__(self, viewer): self.viewer.interactor.Bind(wx.EVT_LEFT_DCLICK, self.OnUnZoom) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.interactor.Unbind(wx.EVT_LEFT_DCLICK) Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) - def OnPressLeftButton(self, evt, obj): + def OnPressLeftButton(self, evt: wx.MouseEvent, obj: object) -> None: self.right_pressed = True - def OnReleaseLeftButton(self, obj, evt): + def OnReleaseLeftButton(self, obj: object, evt: wx.MouseEvent) -> None: self.right_pressed = False - def OnUnZoom(self, evt): + def OnUnZoom(self, evt: wx.MouseEvent) -> None: ren = self.viewer.ren ren.ResetCamera() ren.ResetCameraClippingRange() self.viewer.interactor.Render() + class ZoomSLInteractorStyle(vtkInteractorStyleRubberBandZoom): """ Interactor style responsible for zoom by selecting a region. """ - def __init__(self, viewer): - self.viewer = viewer + def __init__(self, viewer: wx.Window) -> None: + self.viewer: wx.Window = viewer self.viewer.interactor.Bind(wx.EVT_LEFT_DCLICK, self.OnUnZoom) - self.state_code = const.STATE_ZOOM_SL + self.state_code: int = const.STATE_ZOOM_SL - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.interactor.Unbind(wx.EVT_LEFT_DCLICK) Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) - def OnUnZoom(self, evt): + def OnUnZoom(self, evt: wx.MouseEvent) -> None: ren = self.viewer.ren ren.ResetCamera() ren.ResetCameraClippingRange() @@ -201,67 +203,68 @@ class PanMoveInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for translate the camera. """ - def __init__(self, viewer): + def __init__(self, viewer: wx.Window) -> None: super().__init__(viewer) - self.state_code = const.STATE_PAN + self.state_code: int = const.STATE_PAN - self.viewer = viewer + self.viewer: wx.Window = viewer - self.panning = False + self.panning: bool = False self.AddObserver("MouseMoveEvent", self.OnPanMove) self.viewer.interactor.Bind(wx.EVT_LEFT_DCLICK, self.OnUnspan) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: self.viewer.interactor.Unbind(wx.EVT_LEFT_DCLICK) Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) - def OnPressLeftButton(self, evt, obj): + def OnPressLeftButton(self, evt: wx.MouseEvent, obj: object) -> None: self.panning = True - def OnReleaseLeftButton(self, evt, obj): + def OnReleaseLeftButton(self, evt: wx.MouseEvent, obj: object) -> None: self.panning = False - def OnPanMove(self, obj, evt): + def OnPanMove(self, obj: object, evt: wx.MouseEvent) -> None: if self.panning: obj.Pan() obj.OnRightButtonDown() - def OnUnspan(self, evt): + def OnUnspan(self, evt: wx.MouseEvent) -> None: self.viewer.ren.ResetCamera() self.viewer.interactor.Render() + class SpinInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for spin the camera. """ - def __init__(self, viewer): + def __init__(self, viewer: wx.Window) -> None: DefaultInteractorStyle.__init__(self, viewer) - self.state_code = const.STATE_SPIN - self.viewer = viewer - self.spinning = False + self.state_code: int = const.STATE_SPIN + self.viewer: wx.Window = viewer + self.spinning: bool = False self.AddObserver("MouseMoveEvent", self.OnSpinMove) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) - def OnPressLeftButton(self, evt, obj): + def OnPressLeftButton(self, evt: wx.MouseEvent, obj: object) -> None: self.spinning = True - def OnReleaseLeftButton(self, evt, obj): + def OnReleaseLeftButton(self, evt: wx.MouseEvent, obj: object) -> None: self.spinning = False - def OnSpinMove(self, evt, obj): + def OnSpinMove(self, evt: wx.MouseEvent, obj: object) -> None: if self.spinning: evt.Spin() evt.OnRightButtonDown() @@ -271,16 +274,16 @@ class WWWLInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for Window Level & Width functionality. """ - def __init__(self, viewer): + def __init__(self, viewer: wx.Window) -> None: super().__init__(viewer) - self.state_code = const.STATE_WL + self.state_code: int = const.STATE_WL - self.viewer = viewer + self.viewer: wx.Window = viewer - self.last_x = 0 - self.last_y = 0 + self.last_x: int = 0 + self.last_y: int = 0 - self.changing_wwwl = False + self.changing_wwwl: bool = False self.RemoveObservers("LeftButtonPressEvent") self.RemoveObservers("LeftButtonReleaseEvent") @@ -289,20 +292,20 @@ def __init__(self, viewer): self.AddObserver("LeftButtonPressEvent", self.OnWindowLevelClick) self.AddObserver("LeftButtonReleaseEvent", self.OnWindowLevelRelease) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) self.viewer.on_wl = True if self.viewer.raycasting_volume: self.viewer.text.Show() self.viewer.interactor.Render() - def CleanUp(self): + def CleanUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) self.viewer.on_wl = True self.viewer.text.Hide() self.viewer.interactor.Render() - def OnWindowLevelMove(self, obj, evt): + def OnWindowLevelMove(self, obj: object, evt: wx.MouseEvent) -> None: if self.changing_wwwl: mouse_x, mouse_y = self.viewer.get_vtk_mouse_position() diff_x = mouse_x - self.last_x @@ -313,45 +316,51 @@ def OnWindowLevelMove(self, obj, evt): Publisher.sendMessage('Refresh raycasting widget points') Publisher.sendMessage('Render volume viewer') - def OnWindowLevelClick(self, obj, evt): + def OnWindowLevelClick(self, obj: object, evt: wx.MouseEvent) -> None: self.last_x, self.last_y = self.viewer.get_vtk_mouse_position() self.changing_wwwl = True - def OnWindowLevelRelease(self, obj, evt): + def OnWindowLevelRelease(self, obj: object, evt: wx.MouseEvent) -> None: self.changing_wwwl = False + class LinearMeasureInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for insert linear measurements. """ - def __init__(self, viewer): + def __init__(self, viewer: vtkRenderWindowInteractor) -> None: super().__init__(viewer) - self.viewer = viewer - self.state_code = const.STATE_MEASURE_DISTANCE - self.measure_picker = vtkPropPicker() + self.viewer: vtkRenderWindowInteractor = viewer + self.state_code: int = const.STATE_MEASURE_DISTANCE + self.measure_picker: vtkPropPicker = vtkPropPicker() - proj = prj.Project() - self._radius = min(proj.spacing) * PROP_MEASURE + proj: prj.Project = prj.Project() + self._radius: float = min(proj.spacing) * PROP_MEASURE self.RemoveObservers("LeftButtonPressEvent") self.AddObserver("LeftButtonPressEvent", self.OnInsertLinearMeasurePoint) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) Publisher.sendMessage("Remove incomplete measurements") - def OnInsertLinearMeasurePoint(self, obj, evt): + def OnInsertLinearMeasurePoint(self, obj: Any, evt: Any) -> None: + x: float + y: float x,y = self.viewer.get_vtk_mouse_position() self.measure_picker.Pick(x, y, 0, self.viewer.ren) + x: float + y: float + z: float x, y, z = self.measure_picker.GetPickPosition() if self.measure_picker.GetActor(): - self.left_pressed = False + self.left_pressed: bool = False Publisher.sendMessage("Add measurement point", position=(x, y,z), type=const.LINEAR, @@ -359,38 +368,43 @@ def OnInsertLinearMeasurePoint(self, obj, evt): radius=self._radius) self.viewer.interactor.Render() else: - self.left_pressed = True + self.left_pressed: bool = True class AngularMeasureInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for insert linear measurements. """ - def __init__(self, viewer): + def __init__(self, viewer: vtkRenderWindowInteractor) -> None: super().__init__(viewer) - self.viewer = viewer - self.state_code = const.STATE_MEASURE_DISTANCE - self.measure_picker = vtkPropPicker() + self.viewer: vtkRenderWindowInteractor = viewer + self.state_code: int = const.STATE_MEASURE_DISTANCE + self.measure_picker: vtkPropPicker = vtkPropPicker() - proj = prj.Project() - self._radius = min(proj.spacing) * PROP_MEASURE + proj: prj.Project = prj.Project() + self._radius: float = min(proj.spacing) * PROP_MEASURE self.RemoveObservers("LeftButtonPressEvent") self.AddObserver("LeftButtonPressEvent", self.OnInsertAngularMeasurePoint) - def SetUp(self): + def SetUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=True) - def CleanUp(self): + def CleanUp(self) -> None: Publisher.sendMessage('Toggle toolbar item', _id=self.state_code, value=False) Publisher.sendMessage("Remove incomplete measurements") - def OnInsertAngularMeasurePoint(self, obj, evt): + def OnInsertAngularMeasurePoint(self, obj: Any, evt: Any) -> None: + x: float + y: float x,y = self.viewer.get_vtk_mouse_position() self.measure_picker.Pick(x, y, 0, self.viewer.ren) + x: float + y: float + z: float x, y, z = self.measure_picker.GetPickPosition() if self.measure_picker.GetActor(): - self.left_pressed = False + self.left_pressed: bool = False Publisher.sendMessage("Add measurement point", position=(x, y,z), type=const.ANGULAR, @@ -398,63 +412,72 @@ def OnInsertAngularMeasurePoint(self, obj, evt): radius=self._radius) self.viewer.interactor.Render() else: - self.left_pressed = True + self.left_pressed: bool = True class SeedInteractorStyle(DefaultInteractorStyle): """ Interactor style responsible for select sub surfaces. """ - def __init__(self, viewer): + def __init__(self, viewer: vtkRenderWindowInteractor) -> None: super().__init__(viewer) - self.viewer = viewer - self.picker = vtkPointPicker() + self.viewer: vtkRenderWindowInteractor = viewer + self.picker: vtkPointPicker = vtkPointPicker() self.RemoveObservers("LeftButtonPressEvent") self.AddObserver("LeftButtonPressEvent", self.OnInsertSeed) - def OnInsertSeed(self, obj, evt): + def OnInsertSeed(self, obj: Any, evt: Any) -> None: + x: float + y: float x,y = self.viewer.get_vtk_mouse_position() self.picker.Pick(x, y, 0, self.viewer.ren) - point_id = self.picker.GetPointId() + point_id: int = self.picker.GetPointId() if point_id > -1: self.viewer.seed_points.append(point_id) self.viewer.interactor.Render() else: - self.left_pressed = True + self.left_pressed: bool = True + class CrossInteractorStyle(DefaultInteractorStyle): - def __init__(self, viewer): + def __init__(self, viewer: vtkRenderWindowInteractor) -> None: super().__init__(viewer) - self.state_code = const.SLICE_STATE_CROSS - self.picker = vtkCellPicker() + self.state_code: int = const.SLICE_STATE_CROSS + self.picker: vtkCellPicker = vtkCellPicker() self.picker.SetTolerance(1e-3) # self.picker.SetUseCells(True) self.viewer.interactor.SetPicker(self.picker) self.AddObserver("LeftButtonPressEvent", self.OnCrossMouseClick) - def SetUp(self): + def SetUp(self) -> None: print("SetUP") - def CleanUp(self): + def CleanUp(self) -> None: print("CleanUp") - def OnCrossMouseClick(self, obj, evt): + def OnCrossMouseClick(self, obj: Any, evt: Any) -> None: + x: float + y: float x, y = self.viewer.get_vtk_mouse_position() self.picker.Pick(x, y, 0, self.viewer.ren) + x: float + y: float + z: float x, y, z = self.picker.GetPickPosition() if self.picker.GetActor(): - self.viewer.set_camera_position=False + self.viewer.set_camera_position: bool = False Publisher.sendMessage('Update slices position', position=[x, -y, z]) Publisher.sendMessage('Set cross focal point', position=[x, -y, z, None, None, None]) Publisher.sendMessage('Update slice viewer') Publisher.sendMessage('Render volume viewer') - self.viewer.set_camera_position=True + self.viewer.set_camera_position: bool = True + class Styles: - styles = { + styles: Dict[int, Type[DefaultInteractorStyle]] = { const.STATE_DEFAULT: DefaultInteractorStyle, const.STATE_ZOOM: ZoomInteractorStyle, const.STATE_ZOOM_SL: ZoomSLInteractorStyle, @@ -468,26 +491,27 @@ class Styles: } @classmethod - def add_style(cls, style_cls, level=1): + def add_style(cls, style_cls: Type[DefaultInteractorStyle], level: int = 1) -> int: if style_cls in cls.styles.values(): for style_id in cls.styles: if cls.styles[style_id] == style_cls: const.STYLE_LEVEL[style_id] = level return style_id - new_style_id = max(cls.styles) + 1 + new_style_id: int = max(cls.styles) + 1 cls.styles[new_style_id] = style_cls const.STYLE_LEVEL[new_style_id] = level return new_style_id @classmethod - def remove_style(cls, style_id): + def remove_style(cls, style_id: int) -> None: del cls.styles[style_id] @classmethod - def get_style(cls, style): + def get_style(cls, style: int) -> Type[DefaultInteractorStyle]: return cls.styles[style] @classmethod - def has_style(cls, style): + def has_style(cls, style: int) -> bool: return style in cls.styles + diff --git a/invesalius/data/surface_process.py b/invesalius/data/surface_process.py index 12805a8fe..3141650bb 100644 --- a/invesalius/data/surface_process.py +++ b/invesalius/data/surface_process.py @@ -1,6 +1,10 @@ import os import tempfile import weakref +from typing import Optional, Tuple + + + try: import queue @@ -28,7 +32,7 @@ # TODO: Code duplicated from file {imagedata_utils.py}. -def ResampleImage3D(imagedata, value): +def ResampleImage3D(imagedata: vtkImageData, value: float) -> vtkImageData: """ Resample vtkImageData matrix. """ @@ -49,7 +53,7 @@ def ResampleImage3D(imagedata, value): return resample.GetOutput() -def pad_image(image, pad_value, pad_bottom, pad_top): +def pad_image(image: numpy.ndarray, pad_value: int, pad_bottom: bool, pad_top: bool) -> numpy.ndarray: dz, dy, dx = image.shape z_iadd = 0 z_eadd = 0 @@ -68,68 +72,67 @@ def pad_image(image, pad_value, pad_bottom, pad_top): return paded_image -def create_surface_piece(filename, shape, dtype, mask_filename, mask_shape, - mask_dtype, roi, spacing, mode, min_value, max_value, - decimate_reduction, smooth_relaxation_factor, - smooth_iterations, language, flip_image, - from_binary, algorithm, imagedata_resolution, fill_border_holes): +def create_surface_piece(filename: str, shape: tuple, dtype: type, mask_filename: str, mask_shape: tuple, + mask_dtype: type, roi: slice, spacing: tuple, mode: str, min_value: int, max_value: int, + decimate_reduction: float, smooth_relaxation_factor: float, + smooth_iterations: int, language: str, flip_image: bool, + from_binary: bool, algorithm: str, imagedata_resolution: int, fill_border_holes: bool) -> str: - log_path = tempfile.mktemp('vtkoutput.txt') - fow = vtkFileOutputWindow() + log_path: str = tempfile.mktemp('vtkoutput.txt') + fow: vtkFileOutputWindow = vtkFileOutputWindow() fow.SetFileName(log_path) - ow = vtkOutputWindow() + ow: vtkOutputWindow = vtkOutputWindow() ow.SetInstance(fow) - - pad_bottom = (roi.start == 0) - pad_top = (roi.stop >= shape[0]) + pad_bottom: bool = (roi.start == 0) + pad_top: bool = (roi.stop >= shape[0]) if fill_border_holes: - padding = (1, 1, pad_bottom) + padding: tuple = (1, 1, pad_bottom) else: - padding = (0, 0, 0) + padding: tuple = (0, 0, 0) if from_binary: - mask = numpy.memmap(mask_filename, mode='r', - dtype=mask_dtype, - shape=mask_shape) + mask: numpy.memmap = numpy.memmap(mask_filename, mode='r', + dtype=mask_dtype, + shape=mask_shape) if fill_border_holes: - a_mask = pad_image(mask[roi.start + 1: roi.stop + 1, 1:, 1:], 0, pad_bottom, pad_top) + a_mask: numpy.array = pad_image(mask[roi.start + 1: roi.stop + 1, 1:, 1:], 0, pad_bottom, pad_top) else: - a_mask = numpy.array(mask[roi.start + 1: roi.stop + 1, 1:, 1:]) - image = converters.to_vtk(a_mask, spacing, roi.start, "AXIAL", padding=padding) + a_mask: numpy.array = numpy.array(mask[roi.start + 1: roi.stop + 1, 1:, 1:]) + image: vtkImageData = converters.to_vtk(a_mask, spacing, roi.start, "AXIAL", padding=padding) del a_mask else: - image = numpy.memmap(filename, mode='r', dtype=dtype, - shape=shape) - mask = numpy.memmap(mask_filename, mode='r', - dtype=mask_dtype, - shape=mask_shape) + image: numpy.memmap = numpy.memmap(filename, mode='r', dtype=dtype, + shape=shape) + mask: numpy.memmap = numpy.memmap(mask_filename, mode='r', + dtype=mask_dtype, + shape=mask_shape) if fill_border_holes: - a_image = pad_image(image[roi], numpy.iinfo(image.dtype).min, pad_bottom, pad_top) + a_image: numpy.array = pad_image(image[roi], numpy.iinfo(image.dtype).min, pad_bottom, pad_top) else: - a_image = numpy.array(image[roi]) + a_image: numpy.array = numpy.array(image[roi]) # if z_iadd: # a_image[0, 1:-1, 1:-1] = image[0] # if z_eadd: # a_image[-1, 1:-1, 1:-1] = image[-1] if algorithm == u'InVesalius 3.b2': - a_mask = numpy.array(mask[roi.start + 1: roi.stop + 1, 1:, 1:]) + a_mask: numpy.array = numpy.array(mask[roi.start + 1: roi.stop + 1, 1:, 1:]) a_image[a_mask == 1] = a_image.min() - 1 a_image[a_mask == 254] = (min_value + max_value) / 2.0 - image = converters.to_vtk(a_image, spacing, roi.start, "AXIAL", padding=padding) + image: vtkImageData = converters.to_vtk(a_image, spacing, roi.start, "AXIAL", padding=padding) - gauss = vtkImageGaussianSmooth() + gauss: vtkImageGaussianSmooth = vtkImageGaussianSmooth() gauss.SetInputData(image) gauss.SetRadiusFactor(0.3) gauss.ReleaseDataFlagOn() gauss.Update() del image - image = gauss.GetOutput() + image: vtkImageData = gauss.GetOutput() del gauss del a_mask else: @@ -137,13 +140,13 @@ def create_surface_piece(filename, shape, dtype, mask_filename, mask_shape, # origin = -spacing[0], -spacing[1], -spacing[2] # else: # origin = 0, -spacing[1], -spacing[2] - image = converters.to_vtk(a_image, spacing, roi.start, "AXIAL", padding=padding) + image: vtkImageData = converters.to_vtk(a_image, spacing, roi.start, "AXIAL", padding=padding) del a_image # if imagedata_resolution: # image = ResampleImage3D(image, imagedata_resolution) - flip = vtkImageFlip() + flip: vtkImageFlip = vtkImageFlip() flip.SetInputData(image) flip.SetFilteredAxis(1) flip.FlipAboutOriginOn() @@ -156,10 +159,10 @@ def create_surface_piece(filename, shape, dtype, mask_filename, mask_shape, # writer.Write() del image - image = flip.GetOutput() + image: vtkImageData = flip.GetOutput() del flip - contour = vtkContourFilter() + contour: vtkContourFilter = vtkContourFilter() contour.SetInputData(image) if from_binary: contour.SetValue(0, 127) # initial threshold @@ -172,12 +175,12 @@ def create_surface_piece(filename, shape, dtype, mask_filename, mask_shape, contour.ReleaseDataFlagOn() contour.Update() - polydata = contour.GetOutput() + polydata: vtkPolyData = contour.GetOutput() del image del contour - filename = tempfile.mktemp(suffix='_%d_%d.vtp' % (roi.start, roi.stop)) - writer = vtkXMLPolyDataWriter() + filename: str = tempfile.mktemp(suffix='_%d_%d.vtp' % (roi.start, roi.stop)) + writer: vtkXMLPolyDataWriter = vtkXMLPolyDataWriter() writer.SetInputData(polydata) writer.SetFileName(filename) writer.Write() @@ -186,28 +189,27 @@ def create_surface_piece(filename, shape, dtype, mask_filename, mask_shape, print("MY PID MC", os.getpid()) return filename - -def join_process_surface(filenames, algorithm, smooth_iterations, smooth_relaxation_factor, decimate_reduction, keep_largest, fill_holes, options, msg_queue): - def send_message(msg): +def join_process_surface(filenames: list, algorithm: str, smooth_iterations: int, smooth_relaxation_factor: float, decimate_reduction: float, keep_largest: bool, fill_holes: bool, options: dict, msg_queue: queue.Queue) -> None: + def send_message(msg: str) -> None: try: msg_queue.put_nowait(msg) except queue.Full as e: print(e) - log_path = tempfile.mktemp('vtkoutput.txt') - fow = vtkFileOutputWindow() + log_path: str = tempfile.mktemp('vtkoutput.txt') + fow: vtkFileOutputWindow = vtkFileOutputWindow() fow.SetFileName(log_path) - ow = vtkOutputWindow() + ow: vtkOutputWindow = vtkOutputWindow() ow.SetInstance(fow) send_message('Joining surfaces ...') - polydata_append = vtkAppendPolyData() + polydata_append: vtkAppendPolyData = vtkAppendPolyData() for f in filenames: - reader = vtkXMLPolyDataReader() + reader: vtkXMLPolyDataReader = vtkXMLPolyDataReader() reader.SetFileName(f) reader.Update() - polydata = reader.GetOutput() + polydata: vtkPolyData = reader.GetOutput() polydata_append.AddInputData(polydata) del reader @@ -215,16 +217,16 @@ def send_message(msg): polydata_append.Update() # polydata_append.GetOutput().ReleaseDataFlagOn() - polydata = polydata_append.GetOutput() + polydata: vtkPolyData = polydata_append.GetOutput() #polydata.Register(None) # polydata.SetSource(None) del polydata_append send_message('Cleaning surface ...') - clean = vtkCleanPolyData() + clean: vtkCleanPolyData = vtkCleanPolyData() # clean.ReleaseDataFlagOn() # clean.GetOutput().ReleaseDataFlagOn() - clean_ref = weakref.ref(clean) + clean_ref: weakref.ref = weakref.ref(clean) # clean_ref().AddObserver("ProgressEvent", lambda obj,evt: # UpdateProgress(clean_ref(), _("Creating 3D surface..."))) clean.SetInputData(polydata) @@ -232,14 +234,14 @@ def send_message(msg): clean.Update() del polydata - polydata = clean.GetOutput() + polydata: vtkPolyData = clean.GetOutput() # polydata.SetSource(None) del clean if algorithm == 'ca_smoothing': send_message('Calculating normals ...') - normals = vtkPolyDataNormals() - normals_ref = weakref.ref(normals) + normals: vtkPolyDataNormals = vtkPolyDataNormals() + normals_ref: weakref.ref = weakref.ref(normals) # normals_ref().AddObserver("ProgressEvent", lambda obj,evt: # UpdateProgress(normals_ref(), _("Creating 3D surface..."))) normals.SetInputData(polydata) @@ -250,14 +252,14 @@ def send_message(msg): # normals.GetOutput().ReleaseDataFlagOn() normals.Update() del polydata - polydata = normals.GetOutput() + polydata: vtkPolyData = normals.GetOutput() # polydata.SetSource(None) del normals - clean = vtkCleanPolyData() + clean: vtkCleanPolyData = vtkCleanPolyData() # clean.ReleaseDataFlagOn() # clean.GetOutput().ReleaseDataFlagOn() - clean_ref = weakref.ref(clean) + clean_ref: weakref.ref = weakref.ref(clean) # clean_ref().AddObserver("ProgressEvent", lambda obj,evt: # UpdateProgress(clean_ref(), _("Creating 3D surface..."))) clean.SetInputData(polydata) @@ -265,7 +267,7 @@ def send_message(msg): clean.Update() del polydata - polydata = clean.GetOutput() + polydata: vtkPolyData = clean.GetOutput() # polydata.SetSource(None) del clean @@ -291,13 +293,13 @@ def send_message(msg): # else: # #smoother = vtkWindowedSincPolyDataFilter() # send_message('Smoothing ...') - # smoother = vtkSmoothPolyDataFilter() - # smoother_ref = weakref.ref(smoother) + # smoother: vtkSmoothPolyDataFilter = vtkSmoothPolyDataFilter() + # smoother_ref: weakref.ref = weakref.ref(smoother) # # smoother_ref().AddObserver("ProgressEvent", lambda obj,evt: # # UpdateProgress(smoother_ref(), _("Creating 3D surface..."))) # smoother.SetInputData(polydata) - # smoother.SetNumberOfIterations(smooth_iterations) - # smoother.SetRelaxationFactor(smooth_relaxation_factor) + # smoother.SetNumberOfIterations(smooth_iterations: int) + # smoother.SetRelaxationFactor(smooth_relaxation_factor: float) # smoother.SetFeatureAngle(80) # #smoother.SetEdgeAngle(90.0) # #smoother.SetPassBand(0.1) @@ -309,7 +311,7 @@ def send_message(msg): # # smoother.GetOutput().ReleaseDataFlagOn() # smoother.Update() # del polydata - # polydata = smoother.GetOutput() + # polydata: vtkPolyData = smoother.GetOutput() # #polydata.Register(None) # # polydata.SetSource(None) # del smoother @@ -318,11 +320,11 @@ def send_message(msg): if not decimate_reduction: print("Decimating", decimate_reduction) send_message('Decimating ...') - decimation = vtkQuadricDecimation() + decimation: vtkQuadricDecimation = vtkQuadricDecimation() # decimation.ReleaseDataFlagOn() decimation.SetInputData(polydata) decimation.SetTargetReduction(decimate_reduction) - decimation_ref = weakref.ref(decimation) + decimation_ref: weakref.ref = weakref.ref(decimation) # decimation_ref().AddObserver("ProgressEvent", lambda obj,evt: # UpdateProgress(decimation_ref(), _("Creating 3D surface..."))) #decimation.PreserveTopologyOn() @@ -331,7 +333,7 @@ def send_message(msg): # decimation.GetOutput().ReleaseDataFlagOn() decimation.Update() del polydata - polydata = decimation.GetOutput() + polydata: vtkPolyData = decimation.GetOutput() #polydata.Register(None) # polydata.SetSource(None) del decimation @@ -341,16 +343,16 @@ def send_message(msg): if keep_largest: send_message('Finding the largest ...') - conn = vtkPolyDataConnectivityFilter() + conn: vtkPolyDataConnectivityFilter = vtkPolyDataConnectivityFilter() conn.SetInputData(polydata) conn.SetExtractionModeToLargestRegion() - conn_ref = weakref.ref(conn) + conn_ref: weakref.ref = weakref.ref(conn) # conn_ref().AddObserver("ProgressEvent", lambda obj,evt: # UpdateProgress(conn_ref(), _("Creating 3D surface..."))) conn.Update() # conn.GetOutput().ReleaseDataFlagOn() del polydata - polydata = conn.GetOutput() + polydata: vtkPolyData = conn.GetOutput() #polydata.Register(None) # polydata.SetSource(None) del conn @@ -360,27 +362,27 @@ def send_message(msg): #polydata_utils.FillSurfaceHole, we need to review this. if fill_holes: send_message('Filling holes ...') - filled_polydata = vtkFillHolesFilter() + filled_polydata: vtkFillHolesFilter = vtkFillHolesFilter() # filled_polydata.ReleaseDataFlagOn() filled_polydata.SetInputData(polydata) filled_polydata.SetHoleSize(300) - filled_polydata_ref = weakref.ref(filled_polydata) + filled_polydata_ref: weakref.ref = weakref.ref(filled_polydata) # filled_polydata_ref().AddObserver("ProgressEvent", lambda obj,evt: # UpdateProgress(filled_polydata_ref(), _("Creating 3D surface..."))) filled_polydata.Update() # filled_polydata.GetOutput().ReleaseDataFlagOn() del polydata - polydata = filled_polydata.GetOutput() + polydata: vtkPolyData = filled_polydata.GetOutput() #polydata.Register(None) # polydata.SetSource(None) # polydata.DebugOn() del filled_polydata - to_measure = polydata + to_measure: vtkPolyData = polydata - normals = vtkPolyDataNormals() + normals: vtkPolyDataNormals = vtkPolyDataNormals() # normals.ReleaseDataFlagOn() - # normals_ref = weakref.ref(normals) + # normals_ref: weakref.ref = weakref.ref(normals) # normals_ref().AddObserver("ProgressEvent", lambda obj,evt: # UpdateProgress(normals_ref(), _("Creating 3D surface..."))) normals.SetInputData(polydata) @@ -392,16 +394,16 @@ def send_message(msg): # normals.GetOutput().ReleaseDataFlagOn() normals.Update() del polydata - polydata = normals.GetOutput() + polydata: vtkPolyData = normals.GetOutput() #polydata.Register(None) # polydata.SetSource(None) del normals # # Improve performance - # stripper = vtkStripper() + # stripper: vtkStripper = vtkStripper() # # stripper.ReleaseDataFlagOn() - # # stripper_ref = weakref.ref(stripper) + # # stripper_ref: weakref.ref = weakref.ref(stripper) # # stripper_ref().AddObserver("ProgressEvent", lambda obj,evt: # # UpdateProgress(stripper_ref(), _("Creating 3D surface..."))) # stripper.SetInputData(polydata) @@ -410,21 +412,21 @@ def send_message(msg): # # stripper.GetOutput().ReleaseDataFlagOn() # stripper.Update() # del polydata - # polydata = stripper.GetOutput() + # polydata: vtkPolyData = stripper.GetOutput() # #polydata.Register(None) # # polydata.SetSource(None) # del stripper send_message('Calculating area and volume ...') - measured_polydata = vtkMassProperties() + measured_polydata: vtkMassProperties = vtkMassProperties() measured_polydata.SetInputData(to_measure) measured_polydata.Update() - volume = float(measured_polydata.GetVolume()) - area = float(measured_polydata.GetSurfaceArea()) + volume: float = float(measured_polydata.GetVolume()) + area: float = float(measured_polydata.GetSurfaceArea()) del measured_polydata - filename = tempfile.mktemp(suffix='_full.vtp') - writer = vtkXMLPolyDataWriter() + filename: str = tempfile.mktemp(suffix='_full.vtp') + writer: vtkXMLPolyDataWriter = vtkXMLPolyDataWriter() writer.SetInputData(polydata) writer.SetFileName(filename) writer.Write() From 0ac39af76966a1451c304e57cf11dd93d38e14be Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Tue, 18 Apr 2023 19:39:36 +0530 Subject: [PATCH 07/16] add type annotations --- invesalius/data/bases.py | 3 +- invesalius/data/surface.py | 577 +++++++++++++------------- invesalius/data/tracker_connection.py | 375 ++++++++--------- 3 files changed, 478 insertions(+), 477 deletions(-) diff --git a/invesalius/data/bases.py b/invesalius/data/bases.py index 610f5c9af..6c026357a 100644 --- a/invesalius/data/bases.py +++ b/invesalius/data/bases.py @@ -156,8 +156,7 @@ def calculate_fre(fiducials_raw: np.ndarray, fiducials: np.ndarray, ref_mode_id: # # return point_rot -import numpy as np -import transformations as tr + def transform_icp(m_img: np.ndarray, m_icp: np.ndarray) -> np.ndarray: coord_img = [m_img[0, -1], -m_img[1, -1], m_img[2, -1], 1] diff --git a/invesalius/data/surface.py b/invesalius/data/surface.py index d59c93a8a..3de1eb727 100644 --- a/invesalius/data/surface.py +++ b/invesalius/data/surface.py @@ -28,6 +28,8 @@ import time import traceback import weakref +from typing import Union, List, Tuple, Dict, Any, Optional, Callable + import numpy as np try: @@ -45,8 +47,8 @@ vtkTriangleFilter, ) from vtkmodules.vtkCommonCore import (vtkIdList, -vtkPoints, -) + vtkPoints, + ) from vtkmodules.vtkCommonDataModel import ( vtkCellArray, vtkPolyData, @@ -90,42 +92,42 @@ class Surface(): """ Represent both vtkPolyData and associated properties. """ - general_index = -1 - def __init__(self, index=None, name=""): + general_index: int = -1 + def __init__(self, index: Union[int, None] = None, name: str = "") -> None: Surface.general_index += 1 if index is None: - self.index = Surface.general_index + self.index: int = Surface.general_index else: - self.index = index + self.index: int = index Surface.general_index -= 1 - self.polydata = '' - self.colour = '' - self.transparency = const.SURFACE_TRANSPARENCY - self.volume = 0.0 - self.area = 0.0 - self.is_shown = 1 + self.polydata: str = '' + self.colour: str = '' + self.transparency: float = const.SURFACE_TRANSPARENCY + self.volume: float = 0.0 + self.area: float = 0.0 + self.is_shown: int = 1 if not name: - self.name = const.SURFACE_NAME_PATTERN %(self.index+1) + self.name: str = const.SURFACE_NAME_PATTERN %(self.index+1) else: - self.name = name + self.name: str = name - self.filename = None + self.filename: Union[str, None] = None - def SavePlist(self, dir_temp, filelist): + def SavePlist(self, dir_temp: str, filelist: Dict[str, str]) -> str: if self.filename and os.path.exists(self.filename): - filename = u'surface_%d' % self.index - vtp_filename = filename + u'.vtp' - vtp_filepath = self.filename + filename: str = u'surface_%d' % self.index + vtp_filename: str = filename + u'.vtp' + vtp_filepath: str = self.filename else: - filename = u'surface_%d' % self.index - vtp_filename = filename + u'.vtp' - vtp_filepath = tempfile.mktemp() + filename: str = u'surface_%d' % self.index + vtp_filename: str = filename + u'.vtp' + vtp_filepath: str = tempfile.mktemp() pu.Export(self.polydata, vtp_filepath, bin=True) self.filename = vtp_filepath filelist[vtp_filepath] = vtp_filename - surface = {'colour': self.colour[:3], + surface: Dict[str, Union[int, str, float, bool]] = {'colour': self.colour[:3], 'index': self.index, 'name': self.name, 'polydata': vtp_filename, @@ -134,9 +136,9 @@ def SavePlist(self, dir_temp, filelist): 'volume': self.volume, 'area': self.area, } - plist_filename = filename + u'.plist' + plist_filename: str = filename + u'.plist' #plist_filepath = os.path.join(dir_temp, filename + '.plist') - temp_plist = tempfile.mktemp() + temp_plist: str = tempfile.mktemp() with open(temp_plist, 'w+b') as f: plistlib.dump(surface, f) @@ -144,25 +146,26 @@ def SavePlist(self, dir_temp, filelist): return plist_filename - def OpenPList(self, filename): + + def OpenPList(self, filename: str) -> None: with open(filename, 'r+b') as f: sp = plistlib.load(f, fmt=plistlib.FMT_XML) - dirpath = os.path.abspath(os.path.split(filename)[0]) - self.index = sp['index'] - self.name = sp['name'] - self.colour = sp['colour'] - self.transparency = sp['transparency'] - self.is_shown = sp['visible'] - self.volume = sp['volume'] + dirpath: str = os.path.abspath(os.path.split(filename)[0]) + self.index: int = sp['index'] + self.name: str = sp['name'] + self.colour: list = sp['colour'] + self.transparency: float = sp['transparency'] + self.is_shown: bool = sp['visible'] + self.volume: float = sp['volume'] try: - self.area = sp['area'] + self.area: float = sp['area'] except KeyError: - self.area = 0.0 - self.polydata = pu.Import(os.path.join(dirpath, sp['polydata'])) - Surface.general_index = max(Surface.general_index, self.index) + self.area: float = 0.0 + self.polydata: vtkPolyData = pu.Import(os.path.join(dirpath, sp['polydata'])) + Surface.general_index: int = max(Surface.general_index, self.index) - def _set_class_index(self, index): - Surface.general_index = index + def _set_class_index(self, index: int) -> None: + Surface.general_index: int = index # TODO: will be initialized inside control as it is being done? @@ -178,13 +181,13 @@ class SurfaceManager(): - volume_viewer: Sends surface actors as the are created """ - def __init__(self): - self.actors_dict = {} - self.last_surface_index = 0 - self.convert_to_inv = None + def __init__(self) -> None: + self.actors_dict: dict = {} + self.last_surface_index: int = 0 + self.convert_to_inv: bool = None self.__bind_events() - self._default_parameters = { + self._default_parameters: dict = { 'algorithm': 'Default', 'quality': const.DEFAULT_SURFACE_QUALITY, 'fill_holes': False, @@ -194,16 +197,16 @@ def __init__(self): self._load_user_parameters() - def _load_user_parameters(self): - session = ses.Session() + def _load_user_parameters(self) -> None: + session: ses.Session = ses.Session() - surface = session.GetConfig('surface') + surface: dict = session.GetConfig('surface') if surface is not None: self._default_parameters.update(surface) else: session.SetConfig('surface', self._default_parameters) - def __bind_events(self): + def __bind_events(self) -> None: Publisher.subscribe(self.AddNewActor, 'Create surface') Publisher.subscribe(self.GetActor, 'Get Actor') Publisher.subscribe(self.SetActorTransparency, @@ -235,34 +238,34 @@ def __bind_events(self): Publisher.subscribe(self.CreateSurfaceFromPolydata, 'Create surface from polydata') - def OnDuplicate(self, surface_indexes): - proj = prj.Project() - surface_dict = proj.surface_dict + def OnDuplicate(self, surface_indexes: list) -> None: + proj: prj.Project = prj.Project() + surface_dict: dict = proj.surface_dict for index in surface_indexes: - original_surface = surface_dict[index] + original_surface: Surface = surface_dict[index] # compute copy name - name = original_surface.name - names_list = [surface_dict[i].name for i in surface_dict.keys()] - new_name = utl.next_copy_name(name, names_list) + name: str = original_surface.name + names_list: list = [surface_dict[i].name for i in surface_dict.keys()] + new_name: str = utl.next_copy_name(name, names_list) # create new mask - self.CreateSurfaceFromPolydata(polydata = original_surface.polydata, - overwrite = False, - name = new_name, - colour = original_surface.colour, - transparency = original_surface.transparency, - volume = original_surface.volume, - area = original_surface.area) - - def OnRemove(self, surface_indexes): - proj = prj.Project() - - old_dict = self.actors_dict - new_dict = {} + self.CreateSurfaceFromPolydata(polydata=original_surface.polydata, + overwrite=False, + name=new_name, + colour=original_surface.colour, + transparency=original_surface.transparency, + volume=original_surface.volume, + area=original_surface.area) + + def OnRemove(self, surface_indexes: list) -> None: + proj: prj.Project = prj.Project() + + old_dict: dict = self.actors_dict + new_dict: dict = {} if surface_indexes: for index in surface_indexes: proj.RemoveSurface(index) if index in old_dict: - actor = old_dict[index] + actor: vtkActor = old_dict[index] for i in old_dict: if i < index: new_dict[i] = old_dict[i] @@ -274,98 +277,98 @@ def OnRemove(self, surface_indexes): if self.last_surface_index in surface_indexes: if self.actors_dict: - self.last_surface_index = 0 + self.last_surface_index: int = 0 else: - self.last_surface_index = None + self.last_surface_index: None = None - def OnSeedSurface(self, seeds): + def OnSeedSurface(self, seeds: list) -> None: """ Create a new surface, based on the last selected surface, using as reference seeds user add to surface of reference. """ - points_id_list = seeds - index = self.last_surface_index - proj = prj.Project() - surface = proj.surface_dict[index] - - new_polydata = pu.JoinSeedsParts(surface.polydata, - points_id_list) - index = self.CreateSurfaceFromPolydata(new_polydata) + points_id_list: list = seeds + index: int = self.last_surface_index + proj: prj.Project = prj.Project() + surface: Surface = proj.surface_dict[index] + + new_polydata: vtkPolyData = pu.JoinSeedsParts(surface.polydata, + points_id_list) + index: int = self.CreateSurfaceFromPolydata(new_polydata) Publisher.sendMessage('Show single surface', index=index, visibility=True) #self.ShowActor(index, True) - def OnSplitSurface(self): + def OnSplitSurface(self) -> None: """ Create n new surfaces, based on the last selected surface, according to their connectivity. """ - index = self.last_surface_index - proj = prj.Project() - surface = proj.surface_dict[index] + index: int = self.last_surface_index + proj: prj.Project = prj.Project() + surface: Surface = proj.surface_dict[index] - index_list = [] - new_polydata_list = pu.SplitDisconectedParts(surface.polydata) + index_list: list = [] + new_polydata_list: list = pu.SplitDisconectedParts(surface.polydata) for polydata in new_polydata_list: - index = self.CreateSurfaceFromPolydata(polydata) + index: int = self.CreateSurfaceFromPolydata(polydata) index_list.append(index) #self.ShowActor(index, True) Publisher.sendMessage('Show multiple surfaces', index_list=index_list, visibility=True) - def OnLargestSurface(self): + def OnLargestSurface(self) -> None: """ Create a new surface, based on largest part of the last selected surface. """ - index = self.last_surface_index - proj = prj.Project() - surface = proj.surface_dict[index] + index: int = self.last_surface_index + proj: prj.Project = prj.Project() + surface: Surface = proj.surface_dict[index] - new_polydata = pu.SelectLargestPart(surface.polydata) - new_index = self.CreateSurfaceFromPolydata(new_polydata) + new_polydata: vtkPolyData = pu.SelectLargestPart(surface.polydata) + new_index: int = self.CreateSurfaceFromPolydata(new_polydata) Publisher.sendMessage('Show single surface', index=new_index, visibility=True) - def OnImportCustomBinFile(self, filename): - scalar = True + def OnImportCustomBinFile(self, filename: str) -> None: + scalar: bool = True if filename.lower().endswith('.bin'): - polydata = convert_custom_bin_to_vtk(filename) + polydata: vtkPolyData = convert_custom_bin_to_vtk(filename) elif filename.lower().endswith('.stl'): - scalar = False - reader = vtkSTLReader() + scalar: bool = False + reader: vtkSTLReader = vtkSTLReader() if _has_win32api: reader.SetFileName(win32api.GetShortPathName(filename).encode(const.FS_ENCODE)) else: reader.SetFileName(filename.encode(const.FS_ENCODE)) reader.Update() - polydata = reader.GetOutput() - polydata= self.CoverttoMetersPolydata(polydata) + polydata: vtkPolyData = reader.GetOutput() + polydata: vtkPolyData = self.CoverttoMetersPolydata(polydata) if polydata.GetNumberOfPoints() == 0: wx.MessageBox(_("InVesalius was not able to import this surface"), _("Import surface error")) else: - name = os.path.splitext(os.path.split(filename)[-1])[0] + name: str = os.path.splitext(os.path.split(filename)[-1])[0] self.CreateSurfaceFromPolydata(polydata, name=name, scalar=scalar) - def CoverttoMetersPolydata(self, polydata): - idlist = vtkIdList() - points = np.zeros((polydata.GetNumberOfPoints(), 3)) - elements = np.zeros((polydata.GetNumberOfCells(), 3), dtype= np.int32) + def CoverttoMetersPolydata(self, polydata: vtkPolyData) -> vtkPolyData: + idlist: vtkIdList = vtkIdList() + points: np.ndarray = np.zeros((polydata.GetNumberOfPoints(), 3)) + elements: np.ndarray = np.zeros((polydata.GetNumberOfCells(), 3), dtype= np.int32) for i in range(polydata.GetNumberOfPoints()): - x = polydata.GetPoint(i) + x: tuple = polydata.GetPoint(i) points[i] = [j * 1000 for j in x] for i in range(polydata.GetNumberOfCells()): polydata.GetCellPoints(i, idlist) elements[i, 0] = idlist.GetId(0) elements[i, 1] = idlist.GetId(1) elements[i, 2] = idlist.GetId(2) - points_vtk = vtkPoints() - triangles = vtkCellArray() - polydata = vtkPolyData() + points_vtk: vtkPoints = vtkPoints() + triangles: vtkCellArray = vtkCellArray() + polydata: vtkPolyData = vtkPolyData() for i in range(len(points)): points_vtk.InsertNextPoint(points[i]) for i in range(len(elements)): - triangle = vtkTriangle() + triangle: vtkTriangle = vtkTriangle() triangle.GetPointIds().SetId(0, elements[i, 0]) triangle.GetPointIds().SetId(1, elements[i, 1]) triangle.GetPointIds().SetId(2, elements[i, 2]) @@ -377,22 +380,22 @@ def CoverttoMetersPolydata(self, polydata): return polydata - def OnWriteCustomBinFile(self, polydata, filename): - idlist = vtkIdList() - points = np.zeros((polydata.GetNumberOfPoints(), 3)) - elements = np.zeros((polydata.GetNumberOfCells(), 3)) - id = 0 - nop = polydata.GetNumberOfPoints() - noe = polydata.GetNumberOfCells() + def OnWriteCustomBinFile(self, polydata: vtkPolyData, filename: str) -> None: + idlist: vtkIdList = vtkIdList() + points: np.ndarray = np.zeros((polydata.GetNumberOfPoints(), 3)) + elements: np.ndarray = np.zeros((polydata.GetNumberOfCells(), 3)) + id: int = 0 + nop: int = polydata.GetNumberOfPoints() + noe: int = polydata.GetNumberOfCells() for i in range(polydata.GetNumberOfPoints()): - x = polydata.GetPoint(i) + x: list = polydata.GetPoint(i) points[i] = [j / 1000 for j in x] for i in range(polydata.GetNumberOfCells()): polydata.GetCellPoints(i, idlist) elements[i, 0] = idlist.GetId(0) elements[i, 1] = idlist.GetId(1) elements[i, 2] = idlist.GetId(2) - data = {'p': points, 'e': elements} + data: dict = {'p': points, 'e': elements} with open(filename, 'wb') as f: np.array(id, dtype=np.int32).tofile(f) np.array(nop, dtype=np.int32).tofile(f) @@ -400,24 +403,24 @@ def OnWriteCustomBinFile(self, polydata, filename): np.array(data['p'], dtype=np.float32).tofile(f) np.array(data['e'], dtype=np.int32).tofile(f) - def OnImportSurfaceFile(self, filename): + def OnImportSurfaceFile(self, filename: str) -> None: """ Creates a new surface from a surface file (STL, PLY, OBJ or VTP) """ self.CreateSurfaceFromFile(filename) - def CreateSurfaceFromFile(self, filename): - scalar = False + def CreateSurfaceFromFile(self, filename: str) -> None: + scalar: bool = False if filename.lower().endswith('.stl'): - reader = vtkSTLReader() + reader: vtkSTLReader = vtkSTLReader() elif filename.lower().endswith('.ply'): - reader = vtkPLYReader() - scalar = True + reader: vtkPLYReader = vtkPLYReader() + scalar: bool = True elif filename.lower().endswith('.obj'): - reader = vtkOBJReader() + reader: vtkOBJReader = vtkOBJReader() elif filename.lower().endswith('.vtp'): - reader = vtkXMLPolyDataReader() - scalar = True + reader: vtkXMLPolyDataReader = vtkXMLPolyDataReader() + scalar: bool = True else: wx.MessageBox(_("File format not reconized by InVesalius"), _("Import surface error")) return @@ -428,47 +431,47 @@ def CreateSurfaceFromFile(self, filename): reader.SetFileName(filename.encode(const.FS_ENCODE)) reader.Update() - polydata = reader.GetOutput() + polydata: vtkPolyData = reader.GetOutput() if polydata.GetNumberOfPoints() == 0: wx.MessageBox(_("InVesalius was not able to import this surface"), _("Import surface error")) else: - name = os.path.splitext(os.path.split(filename)[-1])[0] + name: str = os.path.splitext(os.path.split(filename)[-1])[0] self.CreateSurfaceFromPolydata(polydata, name=name, scalar=scalar) - def UpdateConvertToInvFlag(self, convert_to_inv=False): + def UpdateConvertToInvFlag(self, convert_to_inv: bool = False) -> None: self.convert_to_inv = convert_to_inv - def CreateSurfaceFromPolydata(self, polydata, overwrite=False, index=None, - name=None, colour=None, transparency=None, - volume=None, area=None, scalar=False): + def CreateSurfaceFromPolydata(self, polydata: vtkPolyData, overwrite: bool = False, index: int = None, + name: str = None, colour: list = None, transparency: float = None, + volume: float = None, area: float = None, scalar: bool = False) -> int: if self.convert_to_inv: # convert between invesalius and world space with shift in the Y coordinate - matrix_shape = sl.Slice().matrix.shape - spacing = sl.Slice().spacing - img_shift = spacing[1] * (matrix_shape[1] - 1) - affine = sl.Slice().affine.copy() + matrix_shape: tuple = sl.Slice().matrix.shape + spacing: tuple = sl.Slice().spacing + img_shift: float = spacing[1] * (matrix_shape[1] - 1) + affine: np.ndarray = sl.Slice().affine.copy() affine[1, -1] -= img_shift - affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(affine) + affine_vtk: vtkMatrix4x4 = vtk_utils.numpy_to_vtkMatrix4x4(affine) - polydata_transform = vtkTransform() + polydata_transform: vtkTransform = vtkTransform() polydata_transform.PostMultiply() polydata_transform.Concatenate(affine_vtk) - transformFilter = vtkTransformPolyDataFilter() + transformFilter: vtkTransformPolyDataFilter = vtkTransformPolyDataFilter() transformFilter.SetTransform(polydata_transform) transformFilter.SetInputData(polydata) transformFilter.Update() - polydata = transformFilter.GetOutput() - self.convert_to_inv = False + polydata: vtkPolyData = transformFilter.GetOutput() + self.convert_to_inv: bool = False - normals = vtkPolyDataNormals() + normals: vtkPolyDataNormals = vtkPolyDataNormals() normals.SetInputData(polydata) normals.SetFeatureAngle(80) normals.AutoOrientNormalsOn() normals.Update() - mapper = vtkPolyDataMapper() + mapper: vtkPolyDataMapper = vtkPolyDataMapper() mapper.SetInputData(normals.GetOutput()) if scalar: mapper.ScalarVisibilityOn() @@ -476,37 +479,37 @@ def CreateSurfaceFromPolydata(self, polydata, overwrite=False, index=None, mapper.ScalarVisibilityOff() # mapper.ImmediateModeRenderingOn() # improve performance - actor = vtkActor() + actor: vtkActor = vtkActor() actor.SetMapper(mapper) actor.GetProperty().SetBackfaceCulling(1) if overwrite: if index is None: - index = self.last_surface_index - surface = Surface(index=index) + index: int = self.last_surface_index + surface: Surface = Surface(index=index) else: - surface = Surface() + surface: Surface = Surface() if not colour: - surface.colour = random.choice(const.SURFACE_COLOUR) + surface.colour: list = random.choice(const.SURFACE_COLOUR) else: - surface.colour = colour - surface.polydata = polydata + surface.colour: list = colour + surface.polydata: vtkPolyData = polydata if transparency: - surface.transparency = transparency + surface.transparency: float = transparency if name: - surface.name = name + surface.name: str = name # Append surface into Project.surface_dict - proj = prj.Project() + proj: prj.Project = prj.Project() if overwrite: proj.ChangeSurface(surface) else: - index = proj.AddSurface(surface) - surface.index = index - self.last_surface_index = index + index: int = proj.AddSurface(surface) + surface.index: int = index + self.last_surface_index: int = index # Set actor colour and transparency actor.GetProperty().SetColor(surface.colour) @@ -514,84 +517,84 @@ def CreateSurfaceFromPolydata(self, polydata, overwrite=False, index=None, if overwrite and self.actors_dict.keys(): try: - old_actor = self.actors_dict[index] + old_actor: vtkActor = self.actors_dict[index] Publisher.sendMessage('Remove surface actor from viewer', actor=old_actor) except KeyError: pass - self.actors_dict[surface.index] = actor + self.actors_dict[surface.index]: vtkActor = actor - session = ses.Session() + session: ses.Session = ses.Session() session.ChangeProject() # The following lines have to be here, otherwise all volumes disappear if not volume or not area: - triangle_filter = vtkTriangleFilter() + triangle_filter: vtkTriangleFilter = vtkTriangleFilter() triangle_filter.SetInputData(polydata) triangle_filter.Update() - measured_polydata = vtkMassProperties() + measured_polydata: vtkMassProperties = vtkMassProperties() measured_polydata.SetInputConnection(triangle_filter.GetOutputPort()) measured_polydata.Update() - volume = measured_polydata.GetVolume() - area = measured_polydata.GetSurfaceArea() - surface.volume = volume - surface.area = area + volume: float = measured_polydata.GetVolume() + area: float = measured_polydata.GetSurfaceArea() + surface.volume: float = volume + surface.area: float = area print(">>>>", surface.volume) else: - surface.volume = volume - surface.area = area + surface.volume: float = volume + surface.area: float = area - self.last_surface_index = surface.index + self.last_surface_index: int = surface.index Publisher.sendMessage('Load surface actor into viewer', actor=actor) Publisher.sendMessage('Update surface info in GUI', surface=surface) return surface.index - def OnCloseProject(self): + def OnCloseProject(self) -> None: self.CloseProject() - def CloseProject(self): + def CloseProject(self) -> None: for index in self.actors_dict: Publisher.sendMessage('Remove surface actor from viewer', actor=self.actors_dict[index]) del self.actors_dict - self.actors_dict = {} + self.actors_dict: dict = {} # restarting the surface index - Surface.general_index = -1 + Surface.general_index: int = -1 - self.affine_vtk = None - self.convert_to_inv = False + self.affine_vtk: vtkMatrix4x4 = None + self.convert_to_inv: bool = False - def OnSelectSurface(self, surface_index): + def OnSelectSurface(self, surface_index: int) -> None: #self.last_surface_index = surface_index # self.actors_dict. - proj = prj.Project() - surface = proj.surface_dict[surface_index] + proj: prj.Project = prj.Project() + surface: Surface = proj.surface_dict[surface_index] Publisher.sendMessage('Update surface info in GUI', surface=surface) - self.last_surface_index = surface_index + self.last_surface_index: int = surface_index # if surface.is_shown: self.ShowActor(surface_index, True) - def OnLoadSurfaceDict(self, surface_dict): + def OnLoadSurfaceDict(self, surface_dict: dict) -> None: for key in surface_dict: - surface = surface_dict[key] + surface: Surface = surface_dict[key] # Map polygonal data (vtkPolyData) to graphics primitives. - normals = vtkPolyDataNormals() + normals: vtkPolyDataNormals = vtkPolyDataNormals() normals.SetInputData(surface.polydata) normals.SetFeatureAngle(80) normals.AutoOrientNormalsOn() # normals.GetOutput().ReleaseDataFlagOn() # Improve performance - stripper = vtkStripper() + stripper: vtkStripper = vtkStripper() stripper.SetInputConnection(normals.GetOutputPort()) stripper.PassThroughCellIdsOn() stripper.PassThroughPointIdsOn() - mapper = vtkPolyDataMapper() + mapper: vtkPolyDataMapper = vtkPolyDataMapper() mapper.SetInputConnection(stripper.GetOutputPort()) mapper.ScalarVisibilityOff() # mapper.ImmediateModeRenderingOn() # improve performance @@ -605,7 +608,7 @@ def OnLoadSurfaceDict(self, surface_dict): actor.GetProperty().SetColor(surface.colour[:3]) actor.GetProperty().SetOpacity(1-surface.transparency) - self.actors_dict[surface.index] = actor + self.actors_dict[surface.index: int] = actor # Send actor by pubsub to viewer's render Publisher.sendMessage('Load surface actor into viewer', actor=actor) @@ -621,58 +624,58 @@ def OnLoadSurfaceDict(self, surface_dict): #### #(mask_index, surface_name, quality, fill_holes, keep_largest) - def _on_complete_surface_creation(self, args, overwrite, surface_name, colour, dialog): - surface_filename, surface_measures = args + def _on_complete_surface_creation(self, args: tuple, overwrite: bool, surface_name: str, colour: list, dialog: object) -> None: + surface_filename, surface_measures: dict = args wx.CallAfter(self._show_surface, surface_filename, surface_measures, overwrite, surface_name, colour, dialog) - def _show_surface(self, surface_filename, surface_measures, overwrite, surface_name, colour, dialog): + def _show_surface(self, surface_filename: str, surface_measures: dict, overwrite: bool, surface_name: str, colour: list, dialog: object) -> None: print(surface_filename, surface_measures) - reader = vtkXMLPolyDataReader() + reader: vtkXMLPolyDataReader = vtkXMLPolyDataReader() reader.SetFileName(surface_filename) reader.Update() - polydata = reader.GetOutput() + polydata: vtkPolyData = reader.GetOutput() # Map polygonal data (vtkPolyData) to graphics primitives. - mapper = vtkPolyDataMapper() + mapper: vtkPolyDataMapper = vtkPolyDataMapper() mapper.SetInputData(polydata) mapper.ScalarVisibilityOff() # mapper.ReleaseDataFlagOn() # mapper.ImmediateModeRenderingOn() # improve performance # Represent an object (geometry & properties) in the rendered scene - actor = vtkActor() + actor: vtkActor = vtkActor() actor.GetProperty().SetBackfaceCulling(1) actor.SetMapper(mapper) del mapper #Create Surface instance if overwrite: - surface = Surface(index = self.last_surface_index) + surface: Surface = Surface(index = self.last_surface_index) else: - surface = Surface(name=surface_name) - surface.colour = colour - surface.polydata = polydata - surface.volume = surface_measures['volume'] - surface.area = surface_measures['area'] + surface: Surface = Surface(name=surface_name) + surface.colour: list = colour + surface.polydata: vtkPolyData = polydata + surface.volume: float = surface_measures['volume'] + surface.area: float = surface_measures['area'] del polydata # Set actor colour and transparency actor.GetProperty().SetColor(colour) actor.GetProperty().SetOpacity(1-surface.transparency) - prop = actor.GetProperty() + prop: vtkProperty = actor.GetProperty() - session = ses.Session() - interpolation = session.GetConfig('surface_interpolation') + session: ses.Session = ses.Session() + interpolation: int = session.GetConfig('surface_interpolation') prop.SetInterpolation(interpolation) - proj = prj.Project() + proj: prj.Project = prj.Project() if overwrite: proj.ChangeSurface(surface) else: - index = proj.AddSurface(surface) - surface.index = index - self.last_surface_index = index + index: int = proj.AddSurface(surface) + surface.index: int = index + self.last_surface_index: int = index session.ChangeProject() @@ -680,29 +683,27 @@ def _show_surface(self, surface_filename, surface_measures, overwrite, surface_n # Send actor by pubsub to viewer's render if overwrite and self.actors_dict.keys(): - old_actor = self.actors_dict[self.last_surface_index] + old_actor: vtkActor = self.actors_dict[self.last_surface_index: int] Publisher.sendMessage('Remove surface actor from viewer', actor=old_actor) # Save actor for future management tasks - self.actors_dict[surface.index] = actor + self.actors_dict[surface.index: int] = actor Publisher.sendMessage('Update surface info in GUI', surface=surface) Publisher.sendMessage('End busy cursor') - dialog.running = False - - def _on_callback_error(self, e, dialog=None): - dialog.running = False - msg = utl.log_traceback(e) - dialog.error = msg + def _on_callback_error(self, e: Exception, dialog: object = None) -> None: + dialog.running: bool = False + msg: str = utl.log_traceback(e) + dialog.error: str = msg - def AddNewActor(self, slice_, mask, surface_parameters): + def AddNewActor(self, slice_: Slice, mask: Mask, surface_parameters: dict) -> None: """ Create surface actor, save into project and send it to viewer. """ if mask.matrix.max() < 127: wx.MessageBox(_("It's not possible to create a surface because there is not any voxel selected on mask"), _("Create surface warning")) return - t_init = time.time() + t_init: float = time.time() matrix = slice_.matrix filename_img = slice_.matrix_filename spacing = slice_.spacing @@ -715,11 +716,11 @@ def AddNewActor(self, slice_, mask, surface_parameters): options = surface_parameters['method']['options'] surface_name = surface_parameters['options']['name'] - quality = surface_parameters['options']['quality'] - fill_holes = surface_parameters['options']['fill'] - keep_largest = surface_parameters['options']['keep_largest'] + quality: int = surface_parameters['options']['quality'] + fill_holes: bool = surface_parameters['options']['fill'] + keep_largest: bool = surface_parameters['options']['keep_largest'] - fill_border_holes = surface_parameters['options'].get('fill_border_holes', True) + fill_border_holes: bool = surface_parameters['options'].get('fill_border_holes', True) print(surface_parameters) @@ -728,18 +729,18 @@ def AddNewActor(self, slice_, mask, surface_parameters): colour = mask.colour[:3] try: - overwrite = surface_parameters['options']['overwrite'] + overwrite: bool = surface_parameters['options']['overwrite'] except KeyError: overwrite = False mask.matrix.flush() if quality in const.SURFACE_QUALITY.keys(): - imagedata_resolution = const.SURFACE_QUALITY[quality][0] - smooth_iterations = const.SURFACE_QUALITY[quality][1] - smooth_relaxation_factor = const.SURFACE_QUALITY[quality][2] - decimate_reduction = const.SURFACE_QUALITY[quality][3] + imagedata_resolution: float = const.SURFACE_QUALITY[quality][0] + smooth_iterations: int = const.SURFACE_QUALITY[quality][1] + smooth_relaxation_factor: float = const.SURFACE_QUALITY[quality][2] + decimate_reduction: float = const.SURFACE_QUALITY[quality][3] - pipeline_size = 4 + pipeline_size: int = 4 if decimate_reduction: pipeline_size += 1 if (smooth_iterations and smooth_relaxation_factor): @@ -753,9 +754,9 @@ def AddNewActor(self, slice_, mask, surface_parameters): language = session.GetConfig('language') if (prj.Project().original_orientation == const.CORONAL): - flip_image = False + flip_image: bool = False else: - flip_image = True + flip_image: bool = True if imagedata_resolution > 0: spacing = tuple([s * imagedata_resolution for s in spacing]) @@ -767,17 +768,17 @@ def AddNewActor(self, slice_, mask, surface_parameters): mask_shape = mask.shape mask_dtype = mask.dtype - n_processors = multiprocessing.cpu_count() + n_processors: int = multiprocessing.cpu_count() - o_piece = 1 - piece_size = 20 + o_piece: int = 1 + piece_size: int = 20 - n_pieces = int(round(matrix.shape[0] / piece_size + 0.5, 0)) + n_pieces: int = int(round(matrix.shape[0] / piece_size + 0.5, 0)) filenames = [] - ctx = multiprocessing.get_context('spawn') + ctx: SpawnContext = multiprocessing.get_context('spawn') pool = ctx.Pool(processes=min(n_pieces, n_processors)) - manager = multiprocessing.Manager() + manager: SyncManager = multiprocessing.Manager() msg_queue = manager.Queue(1) print("Resolution", imagedata_resolution) @@ -813,7 +814,7 @@ def AddNewActor(self, slice_, mask, surface_parameters): time.sleep(0.25) try: - surface_filename, surface_measures = f.get() + surface_filename, surface_measures: dict = f.get() except Exception as e: print(_("InVesalius was not able to create the surface")) print(traceback.print_exc()) @@ -843,24 +844,24 @@ def AddNewActor(self, slice_, mask, surface_parameters): # With GUI else: - sp = dialogs.SurfaceProgressWindow() + sp: dialogs.SurfaceProgressWindow = dialogs.SurfaceProgressWindow() for i in range(n_pieces): - init = i * piece_size - end = init + piece_size + o_piece - roi = slice(init, end) + init: int = i * piece_size + end: int = init + piece_size + o_piece + roi: slice = slice(init, end) print("new_piece", roi) - f = pool.apply_async(surface_process.create_surface_piece, - args = (filename_img, matrix.shape, matrix.dtype, - mask_temp_file, mask_shape, - mask_dtype, roi, spacing, mode, - min_value, max_value, decimate_reduction, - smooth_relaxation_factor, - smooth_iterations, language, flip_image, - algorithm != 'Default', algorithm, - imagedata_resolution, fill_border_holes), - callback=lambda x: filenames.append(x), - error_callback=functools.partial(self._on_callback_error, - dialog=sp)) + f: multiprocessing.pool.ApplyResult = pool.apply_async(surface_process.create_surface_piece, + args = (filename_img, matrix.shape, matrix.dtype, + mask_temp_file, mask_shape, + mask_dtype, roi, spacing, mode, + min_value, max_value, decimate_reduction, + smooth_relaxation_factor, + smooth_iterations, language, flip_image, + algorithm != 'Default', algorithm, + imagedata_resolution, fill_border_holes), + callback=lambda x: filenames.append(x), + error_callback=functools.partial(self._on_callback_error, + dialog=sp)) while len(filenames) != n_pieces: if sp.WasCancelled() or not sp.running: @@ -870,37 +871,37 @@ def AddNewActor(self, slice_, mask, surface_parameters): wx.Yield() if not sp.WasCancelled() or sp.running: - f = pool.apply_async(surface_process.join_process_surface, - args=(filenames, algorithm, smooth_iterations, - smooth_relaxation_factor, - decimate_reduction, keep_largest, - fill_holes, options, msg_queue), - callback=functools.partial(self._on_complete_surface_creation, - overwrite=overwrite, - surface_name=surface_name, - colour=colour, - dialog=sp), - error_callback=functools.partial(self._on_callback_error, - dialog=sp)) + f: multiprocessing.pool.ApplyResult = pool.apply_async(surface_process.join_process_surface, + args=(filenames, algorithm, smooth_iterations, + smooth_relaxation_factor, + decimate_reduction, keep_largest, + fill_holes, options, msg_queue), + callback=functools.partial(self._on_complete_surface_creation, + overwrite=overwrite, + surface_name=surface_name, + colour=colour, + dialog=sp), + error_callback=functools.partial(self._on_callback_error, + dialog=sp)) while sp.running: if sp.WasCancelled(): break time.sleep(0.25) try: - msg = msg_queue.get_nowait() + msg: str = msg_queue.get_nowait() sp.Update(msg) except: sp.Update(None) wx.Yield() - t_end = time.time() + t_end: float = time.time() print("Elapsed time - {}".format(t_end-t_init)) sp.Close() if sp.error: - dlg = GMD.GenericMessageDialog(None, sp.error, - "Exception!", - wx.OK|wx.ICON_ERROR) + dlg: GMD.GenericMessageDialog = GMD.GenericMessageDialog(None, sp.error, + "Exception!", + wx.OK|wx.ICON_ERROR) dlg.ShowModal() del sp @@ -915,18 +916,18 @@ def AddNewActor(self, slice_, mask, surface_parameters): import gc gc.collect() - def GetActor(self, surface_index): + def GetActor(self, surface_index: int) -> None: Publisher.sendMessage('Send Actor', e_field_actor=self.actors_dict[surface_index]) - def UpdateSurfaceInterpolation(self): + def UpdateSurfaceInterpolation(self) -> None: session = ses.Session() - surface_interpolation = session.GetConfig('surface_interpolation') + surface_interpolation: int = session.GetConfig('surface_interpolation') for key in self.actors_dict: self.actors_dict[key].GetProperty().SetInterpolation(surface_interpolation) Publisher.sendMessage('Render volume viewer') - def RemoveActor(self, index): + def RemoveActor(self, index: int) -> None: """ Remove actor, according to given actor index. """ @@ -936,14 +937,14 @@ def RemoveActor(self, index): proj = prj.Project() proj.surface_dict.pop(index) - def OnChangeSurfaceName(self, index, name): + def OnChangeSurfaceName(self, index: int, name: str) -> None: proj = prj.Project() proj.surface_dict[index].name = name - def OnShowSurface(self, index, visibility): + def OnShowSurface(self, index: int, visibility: bool) -> None: self.ShowActor(index, visibility) - def ShowActor(self, index, value): + def ShowActor(self, index: int, value: bool) -> None: """ Show or hide actor, according to given actor index and value. """ @@ -953,7 +954,7 @@ def ShowActor(self, index, value): proj.surface_dict[index].is_shown = value Publisher.sendMessage('Render volume viewer') - def SetActorTransparency(self, surface_index, transparency): + def SetActorTransparency(self, surface_index: int, transparency: float) -> None: """ Set actor transparency (oposite to opacity) according to given actor index and value. @@ -964,7 +965,7 @@ def SetActorTransparency(self, surface_index, transparency): proj.surface_dict[surface_index].transparency = transparency Publisher.sendMessage('Render volume viewer') - def SetActorColour(self, surface_index, colour): + def SetActorColour(self, surface_index: int, colour: list) -> None: """ """ self.actors_dict[surface_index].GetProperty().SetColor(colour[:3]) @@ -973,23 +974,23 @@ def SetActorColour(self, surface_index, colour): proj.surface_dict[surface_index].colour = colour Publisher.sendMessage('Render volume viewer') - def OnExportSurface(self, filename, filetype): - ftype_prefix = { + def OnExportSurface(self, filename: str, filetype: int) -> None: + ftype_prefix: dict = { const.FILETYPE_STL: '.stl', const.FILETYPE_VTP: '.vtp', const.FILETYPE_PLY: '.ply', const.FILETYPE_STL_ASCII: '.stl', } if filetype in ftype_prefix: - temp_file = tempfile.mktemp(suffix=ftype_prefix[filetype]) + temp_file: str = tempfile.mktemp(suffix=ftype_prefix[filetype]) if _has_win32api: utl.touch(temp_file) - _temp_file = temp_file - temp_file = win32api.GetShortPathName(temp_file) + _temp_file: str = temp_file + temp_file: str = win32api.GetShortPathName(temp_file) os.remove(_temp_file) - temp_file = utl.decode(temp_file, const.FS_ENCODE) + temp_file: str = utl.decode(temp_file, const.FS_ENCODE) try: self._export_surface(temp_file, filetype) except ValueError: @@ -1002,11 +1003,11 @@ def OnExportSurface(self, filename, filetype): try: shutil.move(temp_file, filename) except PermissionError as err: - dirpath = os.path.split(filename)[0] + dirpath: str = os.path.split(filename)[0] if wx.GetApp() is None: print(_("It was not possible to export the surface because you don't have permission to write to {} folder: {}".format(dirpath, err))) else: - dlg = dialogs.ErrorMessageBox( + dlg: dialogs.ErrorMessageBox = dialogs.ErrorMessageBox( None, _("Export surface error"), "It was not possible to export the surface because you don't have permission to write to {}:\n{}".format(dirpath, err) @@ -1016,7 +1017,7 @@ def OnExportSurface(self, filename, filetype): os.remove(temp_file) - def _export_surface(self, filename, filetype): + def _export_surface(self, filename: str, filetype: int) -> None: if filetype in (const.FILETYPE_STL, const.FILETYPE_VTP, const.FILETYPE_PLY, @@ -1024,7 +1025,7 @@ def _export_surface(self, filename, filetype): # First we identify all surfaces that are selected # (if any) proj = prj.Project() - polydata_list = [] + polydata_list: list = [] for index in proj.surface_dict: surface = proj.surface_dict[index] @@ -1075,7 +1076,7 @@ def _export_surface(self, filename, filetype): normals.Update() polydata = normals.GetOutput() - filename = filename.encode(const.FS_ENCODE) + filename: bytes = filename.encode(const.FS_ENCODE) writer.SetFileName(filename) writer.SetInputData(polydata) writer.Write() diff --git a/invesalius/data/tracker_connection.py b/invesalius/data/tracker_connection.py index 4fe0ec720..1c264b605 100644 --- a/invesalius/data/tracker_connection.py +++ b/invesalius/data/tracker_connection.py @@ -19,7 +19,7 @@ import sys from wx import ID_OK - +from typing import Any, Dict, List, Optional, Tuple, Union import invesalius.constants as const import invesalius.gui.dialogs as dlg from invesalius import inv_paths @@ -30,47 +30,47 @@ class TrackerConnection(): - def __init__(self, model=None): - self.connection = None - self.configuration = None - self.model = model + def __init__(self, model: object = None) -> None: + self.connection: object = None + self.configuration: object = None + self.model: object = model - def Configure(self): + def Configure(self) -> bool: assert False, "Not implemented" - def Connect(self): + def Connect(self) -> None: assert False, "Not implemented" - def Disconnect(self): + def Disconnect(self) -> None: try: self.connection.Close() - self.connection = False - self.lib_mode = 'wrapper' + self.connection: bool = False + self.lib_mode: str = 'wrapper' print('Tracker disconnected.') except: - self.connection = True - self.lib_mode = 'error' + self.connection: bool = True + self.lib_mode: str = 'error' print('The tracker could not be disconnected.') - def IsConnected(self): + def IsConnected(self) -> object: # TODO: It would be cleaner to compare self.connection to None here; however, currently it can also have # True and False values. Hence, return the connection object as a whole for now. return self.connection - def GetConnection(self): + def GetConnection(self) -> object: # TODO: A nicer API would not expose connection object to outside, but instead use it directly to reply to queries # for coordinates. To be able to do this, code that currently resides in coordinates.py (and that uses the connection # object) would need to be incorporated into TrackerConnection class. return self.connection - def GetLibMode(self): + def GetLibMode(self) -> str: return self.lib_mode - def GetConfiguration(self): + def GetConfiguration(self) -> object: return self.configuration - def SetConfiguration(self, configuration): - self.configuration = configuration + def SetConfiguration(self, configuration: object) -> bool: + self.configuration: object = configuration return True @@ -79,76 +79,77 @@ class OptitrackTrackerConnection(TrackerConnection): Connects to optitrack wrapper from Motive 2.2. Initialize cameras, attach listener, loads Calibration, loads User Profile (Rigid bodies information). """ - def __init__(self, model=None): + def __init__(self, model: object = None) -> None: super().__init__(model) - def Configure(self): - dialog = dlg.SetOptitrackconfigs() + def Configure(self) -> bool: + dialog: dlg.SetOptitrackconfigs = dlg.SetOptitrackconfigs() - status = dialog.ShowModal() - success = status == ID_OK + status: int = dialog.ShowModal() + success: bool = status == ID_OK if success: - calibration, user_profile = dialog.GetValue() - self.configuration = { + calibration: object = dialog.GetValue()[0] + user_profile: object = dialog.GetValue()[1] + self.configuration: dict = { 'calibration': calibration, 'user_profile': user_profile, } else: - self.lib_mode = None + self.lib_mode: str = None dialog.Destroy() return success - def Connect(self): + def Connect(self) -> None: assert self.configuration is not None, "No configuration defined" try: import optitrack - connection = optitrack.optr() + connection: optitrack.optr = optitrack.optr() - calibration = self.configuration['calibration'] - user_profile = self.configuration['user_profile'] + calibration: object = self.configuration['calibration'] + user_profile: object = self.configuration['user_profile'] if connection.Initialize(calibration, user_profile) == 0: connection.Run() # Runs 'Run' function once to update cameras. - lib_mode = 'wrapper' + lib_mode: str = 'wrapper' - self.connection = connection + self.connection: optitrack.optr = connection else: - lib_mode = 'error' + lib_mode: str = 'error' except ImportError: - lib_mode = 'error' + lib_mode: str = 'error' print('Error') - self.lib_mode = lib_mode + self.lib_mode: str = lib_mode - def Disconnect(self): + def Disconnect(self) -> None: super().Disconnect() class ClaronTrackerConnection(TrackerConnection): - def __init__(self, model=None): + def __init__(self, model: object = None) -> None: super().__init__(model) - def Configure(self): + def Configure(self) -> bool: return True - def Connect(self): + def Connect(self) -> None: try: import pyclaron - lib_mode = 'wrapper' - connection = pyclaron.pyclaron() + lib_mode: str = 'wrapper' + connection: pyclaron.pyclaron = pyclaron.pyclaron() - connection.CalibrationDir = inv_paths.MTC_CAL_DIR.encode(const.FS_ENCODE) - connection.MarkerDir = inv_paths.MTC_MAR_DIR.encode(const.FS_ENCODE) - connection.NumberFramesProcessed = 1 - connection.FramesExtrapolated = 0 - connection.PROBE_NAME = const.MTC_PROBE_NAME.encode(const.FS_ENCODE) - connection.REF_NAME = const.MTC_REF_NAME.encode(const.FS_ENCODE) - connection.OBJ_NAME = const.MTC_OBJ_NAME.encode(const.FS_ENCODE) + connection.CalibrationDir: bytes = inv_paths.MTC_CAL_DIR.encode(const.FS_ENCODE) + connection.MarkerDir: bytes = inv_paths.MTC_MAR_DIR.encode(const.FS_ENCODE) + connection.NumberFramesProcessed: int = 1 + connection.FramesExtrapolated: int = 0 + connection.PROBE_NAME: bytes = const.MTC_PROBE_NAME.encode(const.FS_ENCODE) + connection.REF_NAME: bytes = const.MTC_REF_NAME.encode(const.FS_ENCODE) + connection.OBJ_NAME: bytes = const.MTC_OBJ_NAME.encode(const.FS_ENCODE) connection.Initialize() @@ -156,36 +157,36 @@ def Connect(self): connection.Run() print("MicronTracker camera identified.") - self.connection = connection + self.connection: pyclaron.pyclaron = connection except ImportError: - lib_mode = 'error' + lib_mode: str = 'error' print('The ClaronTracker library is not installed.') - self.lib_mode = lib_mode + self.lib_mode: str = lib_mode - def Disconnect(self): + def Disconnect(self) -> None: super().Disconnect() class PolhemusTrackerConnection(TrackerConnection): - def __init__(self, model=None): + def __init__(self, model: str = None) -> None: assert model in ['fastrak', 'isotrak', 'patriot'], "Unsupported model for Polhemus tracker: {}".format(model) super().__init__(model) - def Configure(self): + def Configure(self) -> bool: return True - def ConfigureCOMPort(self): + def ConfigureCOMPort(self) -> bool: dialog = dlg.SetCOMPort(select_baud_rate=False) status = dialog.ShowModal() - success = status == ID_OK + success: bool = status == ID_OK if success: - com_port = dialog.GetCOMPort() - baud_rate = 115200 + com_port: str = dialog.GetCOMPort() + baud_rate: int = 115200 self.configuration = { 'com_port': com_port, @@ -203,11 +204,11 @@ def ConfigureCOMPort(self): # the COM port, and a serial connection is attempted. Unfortunately, that requires # some additional logic in Connect function to support connecting with preset configuration # (namely, by setting 'reconfigure' to False.) - def Connect(self, reconfigure): - connection = None + def Connect(self, reconfigure: bool) -> None: + connection: object = None try: connection = self.PolhemusWrapperConnection() - lib_mode = 'wrapper' + lib_mode: str = 'wrapper' if not connection: print('Could not connect with Polhemus wrapper, trying USB connection...') @@ -227,17 +228,17 @@ def Connect(self, reconfigure): self.connection = connection self.lib_mode = lib_mode - def PolhemusWrapperConnection(self): + def PolhemusWrapperConnection(self) -> object: try: from time import sleep if self.model == 'fastrak': import polhemusFT - connection = polhemusFT.polhemusFT() + connection: object = polhemusFT.polhemusFT() else: import polhemus - connection = polhemus.polhemus() + connection: object = polhemus.polhemus() - success = connection.Initialize() + success: bool = connection.Initialize() if success: # Sequence of runs necessary to throw away unnecessary data @@ -253,16 +254,16 @@ def PolhemusWrapperConnection(self): return connection - def PolhemusSerialConnection(self): + def PolhemusSerialConnection(self) -> object: assert self.configuration is not None, "No configuration defined" import serial - connection = None + connection: object = None try: - com_port = self.configuration['com_port'] - baud_rate = self.configuration['baud_rate'] + com_port: str = self.configuration['com_port'] + baud_rate: int = self.configuration['baud_rate'] connection = serial.Serial( com_port, @@ -283,7 +284,7 @@ def PolhemusSerialConnection(self): connection.write(str.encode("Y")) connection.write(str.encode("P")) - data = connection.readlines() + data: list[bytes] = connection.readlines() if not data: connection = None print('Could not connect to Polhemus serial without error.') @@ -294,8 +295,8 @@ def PolhemusSerialConnection(self): return connection - def PolhemusUSBConnection(self): - connection = None + def PolhemusUSBConnection(self) -> object: + connection: object = None try: import usb.core as uc @@ -313,7 +314,7 @@ def PolhemusUSBConnection(self): pass connection.set_configuration() - endpoint = connection[0][(0, 0)][0] + endpoint: object = connection[0][(0, 0)][0] if self.model == 'fastrak': # Polhemus FASTRAK needs configurations first @@ -335,203 +336,203 @@ def PolhemusUSBConnection(self): return connection - def Disconnect(self): + def Disconnect(self) -> None: try: if self.model == 'isotrak': self.connection.close() - self.lib_mode = 'serial' + self.lib_mode: str = 'serial' else: self.connection.Close() - self.lib_mode = 'wrapper' + self.lib_mode: str = 'wrapper' - self.connection = False + self.connection: bool = False print('Tracker disconnected.') except: - self.connection = True - self.lib_mode = 'error' + self.connection: bool = True + self.lib_mode: str = 'error' print('The tracker could not be disconnected.') class CameraTrackerConnection(TrackerConnection): - def __init__(self, model=None): + def __init__(self, model: str=None) -> None: super().__init__(model) - def Configure(self): + def Configure(self) -> bool: return True - def Connect(self): + def Connect(self) -> None: try: import invesalius.data.camera_tracker as cam - connection = cam.camera() + connection: object = cam.camera() connection.Initialize() print('Connected to camera tracking device.') - lib_mode = 'wrapper' + lib_mode: str = 'wrapper' - self.connection = connection + self.connection: object = connection except: print('Could not connect to camera tracker.') - lib_mode = 'error' + lib_mode: str = 'error' - self.lib_mode = lib_mode + self.lib_mode: str = lib_mode - def Disconnect(self): + def Disconnect(self) -> None: super().Disconnect() class PolarisTrackerConnection(TrackerConnection): - def __init__(self, model=None): + def __init__(self, model: str=None) -> None: super().__init__(model) - def Configure(self): - dialog = dlg.SetNDIconfigs() - status = dialog.ShowModal() + def Configure(self) -> bool: + dialog: object = dlg.SetNDIconfigs() + status: int = dialog.ShowModal() - success = status == ID_OK + success: bool = status == ID_OK if success: - com_port, probe_dir, ref_dir, obj_dir = dialog.GetValue() + com_port, probe_dir, ref_dir, obj_dir: str = dialog.GetValue() - self.configuration = { + self.configuration: dict = { 'com_port': com_port, 'probe_dir': probe_dir, 'ref_dir': ref_dir, 'obj_dir': obj_dir, } else: - self.lib_mode = None + self.lib_mode: str = None print('Could not connect to polaris tracker.') dialog.Destroy() return success - def Connect(self): + def Connect(self) -> None: assert self.configuration is not None, "No configuration defined" try: if sys.platform == 'win32': import pypolaris - connection = pypolaris.pypolaris() + connection: object = pypolaris.pypolaris() else: from pypolaris import pypolaris - connection = pypolaris.pypolaris() + connection: object = pypolaris.pypolaris() - lib_mode = 'wrapper' + lib_mode: str = 'wrapper' - com_port = self.configuration['com_port'].encode(const.FS_ENCODE) - probe_dir = self.configuration['probe_dir'].encode(const.FS_ENCODE) - ref_dir = self.configuration['ref_dir'].encode(const.FS_ENCODE) - obj_dir = self.configuration['obj_dir'].encode(const.FS_ENCODE) + com_port: bytes = self.configuration['com_port'].encode(const.FS_ENCODE) + probe_dir: bytes = self.configuration['probe_dir'].encode(const.FS_ENCODE) + ref_dir: bytes = self.configuration['ref_dir'].encode(const.FS_ENCODE) + obj_dir: bytes = self.configuration['obj_dir'].encode(const.FS_ENCODE) if connection.Initialize(com_port, probe_dir, ref_dir, obj_dir) != 0: - lib_mode = None + lib_mode: str = None print('Could not connect to polaris tracker.') else: print('Connected to polaris tracking device.') - self.connection = connection + self.connection: object = connection except: - lib_mode = 'error' - connection = None + lib_mode: str = 'error' + connection: object = None print('Could not connect to polaris tracker.') - self.lib_mode = lib_mode + self.lib_mode: str = lib_mode - def Disconnect(self): + def Disconnect(self) -> None: super().Disconnect() class PolarisP4TrackerConnection(TrackerConnection): - def __init__(self, model=None): + def __init__(self, model: str=None) -> None: super().__init__(model) - def Configure(self): - dialog = dlg.SetNDIconfigs() - status = dialog.ShowModal() + def Configure(self) -> bool: + dialog: object = dlg.SetNDIconfigs() + status: int = dialog.ShowModal() - success = status == ID_OK + success: bool = status == ID_OK if success: - com_port, probe_dir, ref_dir, obj_dir = dialog.GetValue() + com_port, probe_dir, ref_dir, obj_dir: str = dialog.GetValue() - self.configuration = { + self.configuration: dict = { 'com_port': com_port, 'probe_dir': probe_dir, 'ref_dir': ref_dir, 'obj_dir': obj_dir, } else: - self.lib_mode = None + self.lib_mode: str = None print('Could not connect to Polaris P4 tracker.') dialog.Destroy() return success - def Connect(self): + def Connect(self) -> None: assert self.configuration is not None, "No configuration defined" - connection = None + connection: object = None try: import pypolarisP4 - lib_mode = 'wrapper' - connection = pypolarisP4.pypolarisP4() + lib_mode: str = 'wrapper' + connection: object = pypolarisP4.pypolarisP4() - com_port = self.configuration['com_port'].encode(const.FS_ENCODE) - probe_dir = self.configuration['probe_dir'].encode(const.FS_ENCODE) - ref_dir = self.configuration['ref_dir'].encode(const.FS_ENCODE) - obj_dir = self.configuration['obj_dir'].encode(const.FS_ENCODE) + com_port: bytes = self.configuration['com_port'].encode(const.FS_ENCODE) + probe_dir: bytes = self.configuration['probe_dir'].encode(const.FS_ENCODE) + ref_dir: bytes = self.configuration['ref_dir'].encode(const.FS_ENCODE) + obj_dir: bytes = self.configuration['obj_dir'].encode(const.FS_ENCODE) if connection.Initialize(com_port, probe_dir, ref_dir, obj_dir) != 0: - connection = None - lib_mode = None + connection: object = None + lib_mode: str = None print('Could not connect to Polaris P4 tracker.') else: print('Connect to Polaris P4 tracking device.') except: - lib_mode = 'error' - connection = None + lib_mode: str = 'error' + connection: object = None print('Could not connect to Polaris P4 tracker.') - self.connection = connection - self.lib_mode = lib_mode + self.connection: object = connection + self.lib_mode: str = lib_mode - def Disconnect(self): + def Disconnect(self) -> None: super().Disconnect() class RobotTrackerConnection(TrackerConnection): - def __init__(self, model=None): + def __init__(self, model: str = None) -> None: super().__init__(model) - def Configure(self): + def Configure(self) -> bool: select_tracker_dialog = dlg.SetTrackerDeviceToRobot() - status = select_tracker_dialog.ShowModal() + status: int = select_tracker_dialog.ShowModal() - success = False + success: bool = False if status == ID_OK: - tracker_id = select_tracker_dialog.GetValue() + tracker_id: str = select_tracker_dialog.GetValue() if tracker_id: - connection = CreateTrackerConnection(tracker_id) + connection: TrackerConnection = CreateTrackerConnection(tracker_id) connection.Configure() select_ip_dialog = dlg.SetRobotIP() - status = select_ip_dialog.ShowModal() + status: int = select_ip_dialog.ShowModal() if status == ID_OK: - robot_ip = select_ip_dialog.GetValue() + robot_ip: str = select_ip_dialog.GetValue() - self.configuration = { + self.configuration: dict = { 'tracker_id': tracker_id, 'robot_ip': robot_ip, 'tracker_configuration': connection.GetConfiguration(), } - self.connection = connection + self.connection: TrackerConnection = connection - success = True + success: bool = True select_ip_dialog.Destroy() @@ -539,95 +540,95 @@ def Configure(self): return success - def Connect(self): + def Connect(self) -> None: assert self.configuration is not None, "No configuration defined" - tracker_id = self.configuration['tracker_id'] - robot_ip = self.configuration['robot_ip'] - tracker_configuration = self.configuration['tracker_configuration'] + tracker_id: str = self.configuration['tracker_id'] + robot_ip: str = self.configuration['robot_ip'] + tracker_configuration: dict = self.configuration['tracker_configuration'] if self.connection is None: - self.connection = CreateTrackerConnection(tracker_id) + self.connection: TrackerConnection = CreateTrackerConnection(tracker_id) self.connection.SetConfiguration(tracker_configuration) - Publisher.sendMessage('Connect to robot', robot_IP=robot_ip) + Publisher.sendMessage('Connect to robot', robot_IP = robot_ip) self.connection.Connect() if not self.connection.IsConnected(): print("Failed to connect to tracker.") - def Disconnect(self): + def Disconnect(self) -> None: try: - Publisher.sendMessage('Reset robot', data=None) + Publisher.sendMessage('Reset robot', data = None) self.connection.Disconnect() - self.connection = False + self.connection: bool = False - self.lib_mode = 'wrapper' + self.lib_mode: str = 'wrapper' print('Tracker disconnected.') except: - self.connection = True - self.lib_mode = 'error' + self.connection: bool = True + self.lib_mode: str = 'error' print('The tracker could not be disconnected.') - def GetTrackerId(self): - tracker_id = self.configuration['tracker_id'] + def GetTrackerId(self) -> str: + tracker_id: str = self.configuration['tracker_id'] return tracker_id - def GetConnection(self): + def GetConnection(self) -> object: # XXX: This is a bit convoluted logic, so here's a short explanation: in other cases, self.connection # is the object which can be used to communicate with the tracker directly. However, when using robot, # self.connection is another TrackerConnection object, hence forward the query to that object. return self.connection.GetConnection() - def GetLibMode(self): + def GetLibMode(self) -> str: return self.connection.GetLibMode() - def IsConnected(self): + def IsConnected(self) -> bool: return self.connection and self.connection.IsConnected() - def SetConfiguration(self, configuration): - self.configuration = configuration + def SetConfiguration(self, configuration: dict) -> bool: + self.configuration: dict = configuration return True class DebugTrackerRandomConnection(TrackerConnection): - def __init__(self, model=None): + def __init__(self, model: str = None) -> None: super().__init__(model) - def Configure(self): + def Configure(self) -> bool: return True - def Connect(self): - self.connection = True - self.lib_mode = 'debug' + def Connect(self) -> None: + self.connection: bool = True + self.lib_mode: str = 'debug' print('Debug device (random) started.') - def Disconnect(self): - self.connection = False - self.lib_mode = 'debug' + def Disconnect(self) -> None: + self.connection: bool = False + self.lib_mode: str = 'debug' print('Debug tracker (random) disconnected.') class DebugTrackerApproachConnection(TrackerConnection): - def __init__(self, model=None): + def __init__(self, model: str = None) -> None: super().__init__(model) - def Configure(self): + def Configure(self) -> bool: return True - def Connect(self): - self.connection = True - self.lib_mode = 'debug' + def Connect(self) -> None: + self.connection: bool = True + self.lib_mode: str = 'debug' print('Debug device (approach) started.') - def Disconnect(self): - self.connection = False - self.lib_mode = 'debug' + def Disconnect(self) -> None: + self.connection: bool = False + self.lib_mode: str = 'debug' print('Debug tracker (approach) disconnected.') -TRACKER_CONNECTION_CLASSES = { +TRACKER_CONNECTION_CLASSES: dict = { const.MTC: ClaronTrackerConnection, const.FASTRAK: PolhemusTrackerConnection, const.ISOTRAKII: PolhemusTrackerConnection, @@ -642,29 +643,29 @@ def Disconnect(self): } -def CreateTrackerConnection(tracker_id): +def CreateTrackerConnection(tracker_id: str) -> TrackerConnection: """ Initialize spatial tracker connection for coordinate detection during navigation. :param tracker_id: ID of tracking device. :return spatial tracker connection instance or None if could not open device. """ - tracker_connection_class = TRACKER_CONNECTION_CLASSES[tracker_id] + tracker_connection_class: type = TRACKER_CONNECTION_CLASSES[tracker_id] # XXX: A better solution than to pass a 'model' parameter to the constructor of tracker # connection would be to have separate class for each model, possibly inheriting # the same base class, e.g., in this case, PolhemusTrackerConnection base class, which # would be inherited by FastrakTrackerConnection class, etc. if tracker_id == const.FASTRAK: - model = 'fastrak' + model: str = 'fastrak' elif tracker_id == const.ISOTRAKII: - model = 'isotrak' + model: str = 'isotrak' elif tracker_id == const.PATRIOT: - model = 'patriot' + model: str = 'patriot' else: - model = None + model: str = None - tracker_connection = tracker_connection_class( + tracker_connection: TrackerConnection = tracker_connection_class( model=model ) return tracker_connection From 593a1480f2fad6e8d09d25de43b494bfff658d52 Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Tue, 18 Apr 2023 20:08:15 +0530 Subject: [PATCH 08/16] added type annotations --- invesalius/data/tractography.py | 197 +++++++++++++++++--------------- 1 file changed, 106 insertions(+), 91 deletions(-) diff --git a/invesalius/data/tractography.py b/invesalius/data/tractography.py index 094bd448d..287ef226a 100644 --- a/invesalius/data/tractography.py +++ b/invesalius/data/tractography.py @@ -46,7 +46,7 @@ # np.set_printoptions(suppress=True) -def compute_directions(trk_n, alpha=255): +def compute_directions(trk_n: np.ndarray, alpha: int = 255) -> np.ndarray: """Compute direction of a single tract in each point and return as an RGBA color :param trk_n: nx3 array of doubles (x, y, z) point coordinates composing the tract @@ -67,7 +67,7 @@ def compute_directions(trk_n, alpha=255): return direction.astype(int) -def compute_tubes(trk, direction): +def compute_tubes(trk: np.ndarray, direction: np.ndarray) -> vtkTubeFilter: """Compute and assign colors to a vtkTube for visualization of a single tract :param trk: nx3 array of doubles (x, y, z) point coordinates composing the tract @@ -78,7 +78,7 @@ def compute_tubes(trk, direction): :rtype: vtkTubeFilter """ - numb_points = trk.shape[0] + numb_points: int = trk.shape[0] points = vtkPoints() lines = vtkCellArray() @@ -108,7 +108,7 @@ def compute_tubes(trk, direction): return trk_tube -def create_branch(out_list, n_block): +def create_branch(out_list: list, n_block: int) -> vtkMultiBlockDataSet: """Adds a set of tracts to given position in a given vtkMultiBlockDataSet :param out_list: List of vtkTubeFilters representing the tracts @@ -133,7 +133,7 @@ def create_branch(out_list, n_block): return branch -def compute_tracts(trk_list, n_tract=0, alpha=255): +def compute_tracts(trk_list: list, n_tract: int = 0, alpha: int = 255) -> vtkMultiBlockDataSet: """Convert the list of all computed tracts given by Trekker run and returns a vtkMultiBlockDataSet :param trk_list: List of lists containing the computed tracts and corresponding coordinates @@ -158,7 +158,7 @@ def compute_tracts(trk_list, n_tract=0, alpha=255): return branch -def compute_and_visualize_tracts(trekker, position, affine, affine_vtk, n_tracts_max): +def compute_and_visualize_tracts(trekker, position: list, affine: np.ndarray, affine_vtk: vtkMatrix4x4, n_tracts_max: int): """ Compute tractograms using the Trekker library. :param trekker: Trekker library instance @@ -181,14 +181,14 @@ def compute_and_visualize_tracts(trekker, position, affine, affine_vtk, n_tracts seed_trk = img_utils.convert_world_to_voxel(position, affine) bundle = vtkMultiBlockDataSet() n_branches, n_tracts, count_loop = 0, 0, 0 - n_threads = 2 * const.N_CPU - 1 + n_threads: int = 2 * const.N_CPU - 1 while n_tracts < n_tracts_max: - n_param = 1 + (count_loop % 10) + n_param: int = 1 + (count_loop % 10) # rescale the alpha value that defines the opacity of the branch # the n interval is [1, 10] and the new interval is [51, 255] # the new interval is defined to have no 0 opacity (minimum is 51, i.e., 20%) - alpha = (n_param - 1) * (255 - 51) / (10 - 1) + 51 + alpha: float = (n_param - 1) * (255 - 51) / (10 - 1) + 51 trekker.minFODamp(n_param * 0.01) # print("seed example: {}".format(seed_trk)) @@ -215,7 +215,7 @@ def compute_and_visualize_tracts(trekker, position, affine, affine_vtk, n_tracts class ComputeTractsThread(threading.Thread): # TODO: Remove this class and create a case where no ACT is provided in the class ComputeTractsACTThread - def __init__(self, inp, queues, event, sle): + def __init__(self, inp: list, queues: list, event: threading.Event, sle: float) -> None: """Class (threading) to compute real time tractography data for visualization. Tracts are computed using the Trekker library by Baran Aydogan (https://dmritrekker.github.io/) @@ -243,16 +243,22 @@ def __init__(self, inp, queues, event, sle): self.coord_tracts_queue = queues[0] self.tracts_queue = queues[1] # self.visualization_queue = visualization_queue - self.event = event - self.sle = sle - - def run(self): - - trekker, affine, offset, n_tracts_total, seed_radius, n_threads, act_data, affine_vtk, img_shift = self.inp - # n_threads = n_tracts_total - n_threads = int(n_threads/4) - p_old = np.array([[0., 0., 0.]]) - n_tracts = 0 + self.event: Event = event + self.sle: float = sle + + def run(self) -> None: + + trekker: object + affine: np.ndarray + offset: float + n_tracts_total: int + seed_radius: float + n_threads: int + act_data: np.ndarray + affine_vtk: vtkMatrix4x4 + img_shift: np.ndarray + p_old: np.ndarray = np.array([[0., 0., 0.]]) + n_tracts: int = 0 # Compute the tracts # print('ComputeTractsThread: event {}'.format(self.event.is_set())) @@ -261,7 +267,7 @@ def run(self): # print("Computing tracts") # get from the queue the coordinates, coregistration transformation matrix, and flipped matrix # print("Here") - m_img_flip = self.coord_tracts_queue.get_nowait() + m_img_flip: np.ndarray = self.coord_tracts_queue.get_nowait() # coord, m_img, m_img_flip = self.coord_queue.get_nowait() # print('ComputeTractsThread: get {}'.format(count)) @@ -275,14 +281,14 @@ def run(self): # m_img_flip[1, -1] = -m_img_flip[1, -1] # translate the coordinate along the normal vector of the object/coil - coord_offset = m_img_flip[:3, -1] - offset * m_img_flip[:3, 2] + coord_offset: np.ndarray = m_img_flip[:3, -1] - offset * m_img_flip[:3, 2] # coord_offset = np.array([[27.53, -77.37, 46.42]]) - dist = abs(np.linalg.norm(p_old - np.asarray(coord_offset))) - p_old = coord_offset.copy() + dist: float = abs(np.linalg.norm(p_old - np.asarray(coord_offset))) + p_old: np.ndarray = coord_offset.copy() # print("p_new_shape", coord_offset.shape) # print("m_img_flip_shape", m_img_flip.shape) - seed_trk = img_utils.convert_world_to_voxel(coord_offset, affine) + seed_trk: np.ndarray = img_utils.convert_world_to_voxel(coord_offset, affine) # Juuso's # seed_trk = np.array([[-8.49, -8.39, 2.5]]) # Baran M1 @@ -296,30 +302,30 @@ def run(self): trekker.seed_coordinates(np.repeat(seed_trk, n_threads, axis=0)) # run the trekker, this is the slowest line of code, be careful to just use once! - trk_list = trekker.run() + trk_list: list = trekker.run() if len(trk_list) > 2: # print("dist: {}".format(dist)) if dist >= seed_radius: # when moving the coil further than the seed_radius restart the bundle computation - bundle = vtkMultiBlockDataSet() - n_branches = 0 - branch = compute_tracts(trk_list, n_tract=0, alpha=255) + bundle: vtkMultiBlockDataSet = vtkMultiBlockDataSet() + n_branches: int = 0 + branch: vtkPolyData = compute_tracts(trk_list, n_tract=0, alpha=255) bundle.SetBlock(n_branches, branch) n_branches += 1 - n_tracts = branch.GetNumberOfBlocks() + n_tracts: int = branch.GetNumberOfBlocks() # TODO: maybe keep computing even if reaches the maximum elif dist < seed_radius and n_tracts < n_tracts_total: # compute tracts blocks and add to bungle until reaches the maximum number of tracts - branch = compute_tracts(trk_list, n_tract=0, alpha=255) + branch: vtkPolyData = compute_tracts(trk_list, n_tract=0, alpha=255) if bundle: bundle.SetBlock(n_branches, branch) n_tracts += branch.GetNumberOfBlocks() n_branches += 1 else: - bundle = None + bundle: vtkMultiBlockDataSet = None # rethink if this should be inside the if condition, it may lock the thread if no tracts are found # use no wait to ensure maximum speed and avoid visualizing old tracts in the queue, this might @@ -348,7 +354,7 @@ def run(self): class ComputeTractsACTThread(threading.Thread): - def __init__(self, input_list, queues, event, sleep_thread): + def __init__(self, input_list: list, queues: list, event: threading.Event, sleep_thread: float): """Class (threading) to compute real time tractography data for visualization. Tracts are computed using the Trekker library by Baran Aydogan (https://dmritrekker.github.io/) @@ -377,99 +383,106 @@ def __init__(self, input_list, queues, event, sleep_thread): self.input_list = input_list self.coord_tracts_queue = queues[0] self.tracts_queue = queues[1] - self.event = event - self.sleep_thread = sleep_thread - - def run(self): - - trekker, affine, offset, n_tracts_total, seed_radius, n_threads, act_data, affine_vtk, img_shift = self.input_list - - p_old = np.array([[0., 0., 0.]]) - n_branches, n_tracts, count_loop = 0, 0, 0 - bundle = None - dist_radius = 1.5 + self.event: Event = event + self.sleep_thread: float = sleep_thread + + def run(self) -> None: + trekker: object + affine: np.ndarray + offset: float + n_tracts_total: int + seed_radius: float + n_threads: int + act_data: np.ndarray + affine_vtk: np.ndarray + img_shift: int + + p_old: np.ndarray = np.array([[0., 0., 0.]]) + n_branches: int = 0 + n_tracts: int = 0 + count_loop: int = 0 + bundle: object = None + dist_radius: float = 1.5 # TODO: Try a denser and bigger grid, because it's just a matrix multiplication # maybe 15 mm below the coil offset by default and 5 cm deep # create the rectangular grid to find the gray-white matter boundary - coord_list_w = img_utils.create_grid((-2, 2), (0, 20), offset - 5, 1) + coord_list_w: np.ndarray = img_utils.create_grid((-2, 2), (0, 20), offset - 5, 1) # create the spherical grid to sample around the seed location - samples_in_sphere = img_utils.random_sample_sphere(radius=seed_radius, size=100) - coord_list_sphere = np.hstack([samples_in_sphere, np.ones([samples_in_sphere.shape[0], 1])]).T - m_seed = np.identity(4) + samples_in_sphere: np.ndarray = img_utils.random_sample_sphere(radius=seed_radius, size=100) + coord_list_sphere: np.ndarray = np.hstack([samples_in_sphere, np.ones([samples_in_sphere.shape[0], 1])]).T + m_seed: np.ndarray = np.identity(4) # Compute the tracts while not self.event.is_set(): try: # get from the queue the coordinates, coregistration transformation matrix, and flipped matrix - m_img_flip = self.coord_tracts_queue.get_nowait() + m_img_flip: np.ndarray = self.coord_tracts_queue.get_nowait() - # DEBUG: Uncomment the m_img_flip below so that distance is fixed and tracts keep computing - # m_img_flip[:3, -1] = (5., 10., 12.) - dist = abs(np.linalg.norm(p_old - np.asarray(m_img_flip[:3, -1]))) - p_old = m_img_flip[:3, -1].copy() + dist: float = abs(np.linalg.norm(p_old - np.asarray(m_img_flip[:3, -1]))) + p_old: np.ndarray = m_img_flip[:3, -1].copy() # Uncertainty visualization -- # each tract branch is computed with one minFODamp adjusted from 0.01 to 0.1 # the higher the minFODamp the more the streamlines are faithful to the data, so they become more strict # but also may loose some connections. # the lower the more relaxed streamline also increases the chance of false positives - n_param = 1 + (count_loop % 10) + n_param: int = 1 + (count_loop % 10) # rescale the alpha value that defines the opacity of the branch # the n interval is [1, 10] and the new interval is [51, 255] # the new interval is defined to have no 0 opacity (minimum is 51, i.e., 20%) - alpha = (n_param - 1) * (255 - 51) / (10 - 1) + 51 + alpha: float = (n_param - 1) * (255 - 51) / (10 - 1) + 51 trekker.minFODamp(n_param * 0.01) # --- try: # The original seed location is replaced by the gray-white matter interface that is closest to # the coil center - coord_list_w_tr = m_img_flip @ coord_list_w - coord_offset = grid_offset(act_data, coord_list_w_tr, img_shift) + coord_list_w_tr: np.ndarray = m_img_flip @ coord_list_w + coord_offset: np.ndarray = grid_offset(act_data, coord_list_w_tr, img_shift) except IndexError: # This error might be caused by the coordinate exceeding the image array dimensions. # This happens during navigation because the coil location can have coordinates outside the image # boundaries # Translate the coordinate along the normal vector of the object/coil - coord_offset = m_img_flip[:3, -1] - offset * m_img_flip[:3, 2] + coord_offset: np.ndarray = m_img_flip[:3, -1] - offset * m_img_flip[:3, 2] # --- # Spherical sampling of seed coordinates --- # compute the samples of a sphere centered on seed coordinate offset by the grid # given in the invesalius-vtk space - samples = np.random.choice(coord_list_sphere.shape[1], size=100) - m_seed[:-1, -1] = coord_offset.copy() + samples: np.ndarray = np.random.choice(coord_list_sphere.shape[1], size=100) + m_seed[:-1, -1]: np.ndarray = coord_offset.copy() # translate the spherical grid samples to the coil location in invesalius-vtk space - seed_trk_r_inv = m_seed @ coord_list_sphere[:, samples] + seed_trk_r_inv: np.ndarray = m_seed @ coord_list_sphere[:, samples] - coord_offset_w = np.hstack((coord_offset, 1.0)).reshape([4, 1]) + coord_offset_w: np.ndarray = np.hstack((coord_offset, 1.0)).reshape([4, 1]) try: # Anatomic constrained seed computation --- # find only the samples inside the white matter as with the ACT enabled in the Trekker, # it does not compute any tracts outside the white matter # convert from inveslaius-vtk to mri space - seed_trk_r_mri = seed_trk_r_inv[:3, :].T.astype(int) + np.array([[0, img_shift, 0]], dtype=np.int32) - labs = act_data[seed_trk_r_mri[..., 0], seed_trk_r_mri[..., 1], seed_trk_r_mri[..., 2]] + seed_trk_r_mri: np.ndarray = seed_trk_r_inv[:3, :].T.astype(int) + np.array([[0, img_shift, 0]], dtype=np.int32) + labs: np.ndarray = act_data[seed_trk_r_mri[..., 0], seed_trk_r_mri[..., 1], seed_trk_r_mri[..., 2]] # find all samples in the white matter - labs_id = np.where(labs == 1) + labs_id: np.ndarray = np.where(labs == 1) # Trekker has internal multiprocessing approach done in C. Here the number of available threads - 1 # is given, but in case a large number of tracts is requested, it will compute all in parallel # automatically for a more fluent navigation, better to compute the maximum number the computer can # handle otherwise gets too slow for the multithreading in Python - seed_trk_r_inv_sampled = seed_trk_r_inv[:, labs_id[0][:n_threads]] + seed_trk_r_inv_sampled: np.ndarray = seed_trk_r_inv[:, labs_id[0][:n_threads]] except IndexError: # same as on the grid offset above, if the coil is too far from the mri volume the array indices # are beyond the mri boundaries # in this case use the grid center instead of the spherical samples - seed_trk_r_inv_sampled = coord_offset_w.copy() + seed_trk_r_inv_sampled: np.ndarray = coord_offset_w.copy() # convert to the world coordinate system for trekker - seed_trk_r_world_sampled = np.linalg.inv(affine) @ seed_trk_r_inv_sampled - seed_trk_r_world_sampled = seed_trk_r_world_sampled.T[:, :3] + seed_trk_r_world_sampled: np.ndarray = np.linalg.inv(affine) @ seed_trk_r_inv_sampled + seed_trk_r_world_sampled: np.ndarray = seed_trk_r_world_sampled.T[:, :3] # convert to the world coordinate system for saving in the marker list coord_offset_w = np.linalg.inv(affine) @ coord_offset_w @@ -491,8 +504,9 @@ def run(self): # Currently, it stops to compute tracts when the maximum number of tracts is reached maybe keep # computing even if reaches the maximum if dist >= dist_radius: - bundle = None - n_tracts, n_branches = 0, 0 + bundle: vtkMultiBlockDataSet = None + n_tracts: int = 0 + n_branches: int = 0 # we noticed that usually the navigation lags or crashes when moving the coil location # to reduce the overhead for when the coil is moving, we compute only half the number of tracts @@ -500,40 +514,41 @@ def run(self): # required input is Nx3 array trekker.seed_coordinates(seed_trk_r_world_sampled[::2, :]) # run the trekker, this is the slowest line of code, be careful to just use once! - trk_list = trekker.run() + trk_list: list = trekker.run() # check if any tract was found, otherwise doesn't count if len(trk_list): # a bundle consists for multiple branches and each branch consists of multiple streamlines # every iteration in the main loop adds a branch to the bundle - branch = compute_tracts(trk_list, n_tract=0, alpha=alpha) - n_tracts = branch.GetNumberOfBlocks() + branch: vtkMultiBlockDataSet = compute_tracts(trk_list, n_tract=0, alpha=alpha) + n_tracts: int = branch.GetNumberOfBlocks() # create and add branch to the bundle - bundle = vtkMultiBlockDataSet() + bundle: vtkMultiBlockDataSet = vtkMultiBlockDataSet() bundle.SetBlock(n_branches, branch) - n_branches = 1 + n_branches: int = 1 elif dist < dist_radius and n_tracts < n_tracts_total: # when the coil is fixed in place and the number of tracts is smaller than the total if not bundle: # same as above, when creating the bundle (vtkMultiBlockDataSet) we only compute half the number # of tracts to reduce the overhead - bundle = vtkMultiBlockDataSet() + bundle: vtkMultiBlockDataSet = vtkMultiBlockDataSet() # required input is Nx3 array trekker.seed_coordinates(seed_trk_r_world_sampled[::2, :]) - n_tracts, n_branches = 0, 0 + n_tracts: int = 0 + n_branches: int = 0 else: # if the bundle exists compute all tracts requested # required input is Nx3 array trekker.seed_coordinates(seed_trk_r_world_sampled) - trk_list = trekker.run() + trk_list: list = trekker.run() if len(trk_list): # compute tract blocks and add to bundle until reaches the maximum number of tracts # the alpha changes depending on the parameter set - branch = compute_tracts(trk_list, n_tract=0, alpha=alpha) + branch: vtkMultiBlockDataSet = compute_tracts(trk_list, n_tract=0, alpha=alpha) n_tracts += branch.GetNumberOfBlocks() # add branch to the bundle bundle.SetBlock(n_branches, branch) @@ -557,10 +572,10 @@ def run(self): except queue.Full: self.coord_tracts_queue.task_done() - # sleep required to prevent user interface from being unresponsive - time.sleep(self.sleep_thread) + # sleep required to prevent user interface from being unresponsive + time.sleep(self.sleep_thread) -def set_trekker_parameters(trekker, params): +def set_trekker_parameters(trekker, params: dict) -> list: """Set all user-defined parameters for tractography computation using the Trekker library :param trekker: Trekker instance @@ -590,7 +605,7 @@ def set_trekker_parameters(trekker, params): # check number if number of cores is valid in configuration file, # otherwise use the maximum number of threads which is usually 2*N_CPUS - n_threads = 2 * const.N_CPU - 1 + n_threads: int = 2 * const.N_CPU - 1 if isinstance((params['numb_threads']), int) and params['numb_threads'] <= (2*const.N_CPU-1): n_threads = params['numb_threads'] @@ -599,23 +614,23 @@ def set_trekker_parameters(trekker, params): return trekker, n_threads -def grid_offset(data, coord_list_w_tr, img_shift): +def grid_offset(data: np.ndarray, coord_list_w_tr: np.ndarray, img_shift: int) -> np.ndarray: # convert to int so coordinates can be used as indices in the MRI image space - coord_list_w_tr_mri = coord_list_w_tr[:3, :].T.astype(int) + np.array([[0, img_shift, 0]]) + coord_list_w_tr_mri: np.ndarray = coord_list_w_tr[:3, :].T.astype(int) + np.array([[0, img_shift, 0]]) #FIX: IndexError: index 269 is out of bounds for axis 2 with size 256 # error occurs when running line "labs = data[coord..." # need to check why there is a coordinate outside the MRI bounds # extract the first occurrence of a specific label from the MRI image - labs = data[coord_list_w_tr_mri[..., 0], coord_list_w_tr_mri[..., 1], coord_list_w_tr_mri[..., 2]] - lab_first = np.where(labs == 1) + labs: np.ndarray = data[coord_list_w_tr_mri[..., 0], coord_list_w_tr_mri[..., 1], coord_list_w_tr_mri[..., 2]] + lab_first: np.ndarray = np.where(labs == 1) if not lab_first: - pt_found_inv = None + pt_found_inv: np.ndarray = None else: - pt_found = coord_list_w_tr[:, lab_first[0][0]][:3] + pt_found: np.ndarray = coord_list_w_tr[:, lab_first[0][0]][:3] # convert coordinate back to invesalius 3D space - pt_found_inv = pt_found - np.array([0., img_shift, 0.]) + pt_found_inv: np.ndarray = pt_found - np.array([0., img_shift, 0.]) # lab_first = np.argmax(labs == 1) # if labs[lab_first] == 1: From 682f7720961271e3d014f647eda129ace7202afb Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Tue, 18 Apr 2023 21:45:09 +0530 Subject: [PATCH 09/16] added type annotations --- invesalius/data/volume_widgets.py | 50 +++---- invesalius/data/vtk_utils.py | 199 ++++++++++++++------------- invesalius/data/watershed_process.py | 8 +- 3 files changed, 133 insertions(+), 124 deletions(-) diff --git a/invesalius/data/volume_widgets.py b/invesalius/data/volume_widgets.py index 6c237126d..c33d1481c 100644 --- a/invesalius/data/volume_widgets.py +++ b/invesalius/data/volume_widgets.py @@ -22,8 +22,11 @@ from vtkmodules.vtkRenderingCore import vtkActor, vtkCellPicker, vtkPolyDataMapper -AXIAL, SAGITAL, CORONAL = 0, 1, 2 -PLANE_DATA = {AXIAL: ["z",(0,0,1)], + +AXIAL: int = 0 +SAGITAL: int = 1 +CORONAL: int = 2 +PLANE_DATA: dict = {AXIAL: ["z",(0,0,1)], SAGITAL: ["x", (1,0,0)], CORONAL: ["y", (0,1,0)]} @@ -41,25 +44,25 @@ class Plane(): axial_plane.Show() axial_plane.Update() """ - def __init__(self): - self.orientation = AXIAL + def __init__(self) -> None: + self.orientation: int = AXIAL self.render = None self.iren = None - self.index = 0 + self.index: int = 0 self.source = None self.widget = None self.actor = None - def SetOrientation(self, orientation=AXIAL): + def SetOrientation(self, orientation: int = AXIAL) -> None: self.orientation = orientation - def SetRender(self, render=None): + def SetRender(self, render=None) -> None: self.render = render - def SetInteractor(self, iren=None): + def SetInteractor(self, iren=None) -> None: self.iren = iren - def SetSliceIndex(self, index): + def SetSliceIndex(self, index: int) -> None: self.index = 0 try: self.widget.SetSliceIndex(int(index)) @@ -71,9 +74,9 @@ def SetSliceIndex(self, index): print("send signal - update slice info in panel and in 2d") - def SetInput(self, imagedata): - axes = PLANE_DATA[self.orientation][0] # "x", "y" or "z" - colour = PLANE_DATA[self.orientation][1] + def SetInput(self, imagedata) -> None: + axes: str = PLANE_DATA[self.orientation][0] # "x", "y" or "z" + colour: tuple = PLANE_DATA[self.orientation][1] #if self.orientation == SAGITAL: # spacing = min(imagedata.GetSpacing()) @@ -85,13 +88,13 @@ def SetInput(self, imagedata): # Picker for enabling plane motion. # Allows selection of a cell by shooting a ray into graphics window - picker = vtkCellPicker() + picker: vtkCellPicker = vtkCellPicker() picker.SetTolerance(0.005) picker.PickFromListOn() # 3D widget for reslicing image data. # This 3D widget defines a plane that can be interactively placed in an image volume. - widget = vtkImagePlaneWidget() + widget: vtkImagePlaneWidget = vtkImagePlaneWidget() widget.SetInput(imagedata) widget.SetSliceIndex(self.index) widget.SetPicker(picker) @@ -108,17 +111,17 @@ def SetInput(self, imagedata): prop.SetColor(colour) # Syncronize coloured outline with texture appropriately - source = vtkPlaneSource() + source: vtkPlaneSource = vtkPlaneSource() source.SetOrigin(widget.GetOrigin()) source.SetPoint1(widget.GetPoint1()) source.SetPoint2(widget.GetPoint2()) source.SetNormal(widget.GetNormal()) self.source = source - mapper = vtkPolyDataMapper() + mapper: vtkPolyDataMapper = vtkPolyDataMapper() mapper.SetInput(source.GetOutput()) - actor = vtkActor() + actor: vtkActor = vtkActor() actor.SetMapper(mapper) actor.SetTexture(widget.GetTexture()) actor.VisibilityOff() @@ -126,18 +129,18 @@ def SetInput(self, imagedata): self.render.AddActor(actor) - def Update(self, x=None, y=None): - source = self.source - widget = self.widget + def Update(self, x=None, y=None) -> None: + source: vtkPlaneSource = self.source + widget: vtkImagePlaneWidget = self.widget source.SetOrigin(widget.GetOrigin()) source.SetPoint1(widget.GetPoint1()) source.SetPoint2(widget.GetPoint2()) source.SetNormal(widget.GetNormal()) - def Show(self, show=1): - actor = self.actor - widget = self.widget + def Show(self, show: int =1) -> None: + actor: vtkActor = self.actor + widget: vtkImagePlaneWidget = self.widget if show: actor.VisibilityOn() @@ -145,3 +148,4 @@ def Show(self, show=1): else: actor.VisibilityOff() widget.Off() + diff --git a/invesalius/data/vtk_utils.py b/invesalius/data/vtk_utils.py index 6b0e0efa4..c4e565bd3 100644 --- a/invesalius/data/vtk_utils.py +++ b/invesalius/data/vtk_utils.py @@ -16,8 +16,11 @@ # PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais # detalhes. #-------------------------------------------------------------------------- +from aifc import _aifc_params import os import sys +from typing import Union, Literal +import numpy as np import wx from vtkmodules.vtkCommonMath import vtkMatrix4x4 @@ -40,16 +43,16 @@ class ProgressDialog(object): - def __init__(self, parent, maximum, abort=False): - self.title = "InVesalius 3" - self.msg = _("Loading DICOM files") - self.maximum = maximum - self.current = 0 - self.style = wx.PD_APP_MODAL + def __init__(self, parent: wx.Window, maximum: int, abort: bool = False) -> None: + self.title: str = "InVesalius 3" + self.msg: str = _("Loading DICOM files") + self.maximum: int = maximum + self.current: int = 0 + self.style: int = wx.PD_APP_MODAL if abort: - self.style = wx.PD_APP_MODAL | wx.PD_CAN_ABORT + self.style: int = wx.PD_APP_MODAL | wx.PD_CAN_ABORT - self.dlg = wx.ProgressDialog(self.title, + self.dlg: wx.ProgressDialog = wx.ProgressDialog(self.title, self.msg, maximum = self.maximum, parent = parent, @@ -58,10 +61,10 @@ def __init__(self, parent, maximum, abort=False): self.dlg.Bind(wx.EVT_BUTTON, self.Cancel) self.dlg.SetSize(wx.Size(250,150)) - def Cancel(self, evt): + def Cancel(self, evt: wx.Event) -> None: Publisher.sendMessage("Cancel DICOM load") - def Update(self, value, message): + def Update(self, value: int, message: str): if(int(value) != self.maximum): try: return self.dlg.Update(int(value),message) @@ -72,18 +75,18 @@ def Update(self, value, message): else: return False - def Close(self): + def Close(self) -> None: self.dlg.Destroy() if sys.platform == 'win32': try: import win32api - _has_win32api = True + _has_win32api: bool = True except ImportError: - _has_win32api = False + _has_win32api: bool = False else: - _has_win32api = False + _has_win32api: bool = False # If you are frightened by the code bellow, or think it must have been result of # an identation error, lookup at: @@ -95,28 +98,28 @@ def Close(self): # http://www.ibm.com/developerworks/library/l-prog2.html # http://jjinux.blogspot.com/2006/10/python-modifying-counter-in-closure.html -def ShowProgress(number_of_filters = 1, - dialog_type="GaugeProgress"): +def ShowProgress(number_of_filters: int = 1, + dialog_type: str = "GaugeProgress"): """ To use this closure, do something like this: UpdateProgress = ShowProgress(NUM_FILTERS) UpdateProgress(vtkObject) """ - progress = [0] - last_obj_progress = [0] + progress: list[int] = [0] + last_obj_progress: list[float] = [0] if (dialog_type == "ProgressDialog"): try: - dlg = ProgressDialog(wx.GetApp().GetTopWindow(), 100) + dlg: ProgressDialog = ProgressDialog(wx.GetApp().GetTopWindow(), 100) except (wx._core.PyNoAppError, AttributeError): - return lambda obj, label: 0 + return lambda obj, label : 0 # when the pipeline is larger than 1, we have to consider this object # percentage - number_of_filters = max(number_of_filters, 1) - ratio = (100.0 / number_of_filters) + number_of_filters: int = max(number_of_filters, 1) + ratio: float = (100.0 / number_of_filters) - def UpdateProgress(obj, label=""): + def UpdateProgress(obj: float or vtkObject, label: str = "") -> Union(float , Literal[100]): """ Show progress on GUI according to pipeline execution. """ @@ -124,16 +127,16 @@ def UpdateProgress(obj, label=""): # is necessary verify in case is sending the progress #represented by number in case multiprocess, not vtk object if isinstance(obj, float) or isinstance(obj, int): - obj_progress = obj + obj_progress: float = obj else: - obj_progress = obj.GetProgress() + obj_progress: float = obj.GetProgress() # as it is cummulative, we need to compute the diference, to be # appended on the interface if obj_progress < last_obj_progress[0]: # current obj != previous obj - difference = obj_progress # 0 + difference: float = obj_progress # 0 else: # current obj == previous obj - difference = obj_progress - last_obj_progress[0] + difference: float = obj_progress - last_obj_progress[0] last_obj_progress[0] = obj_progress @@ -154,10 +157,10 @@ def UpdateProgress(obj, label=""): return UpdateProgress class Text(object): - def __init__(self): - self.layer = 99 - self.children = [] - property = vtkTextProperty() + def __init__(self) -> None: + self.layer: int = 99 + self.children: list = [] + property: vtkTextProperty = vtkTextProperty() property.SetFontSize(const.TEXT_SIZE) property.SetFontFamilyToArial() property.BoldOff() @@ -166,33 +169,33 @@ def __init__(self): property.SetJustificationToLeft() property.SetVerticalJustificationToTop() property.SetColor(const.TEXT_COLOUR) - self.property = property + self.property: vtkTextProperty = property - mapper = vtkTextMapper() + mapper: vtkTextMapper = vtkTextMapper() mapper.SetTextProperty(property) - self.mapper = mapper + self.mapper: vtkTextMapper = mapper - actor = vtkActor2D() + actor: vtkActor2D = vtkActor2D() actor.SetMapper(mapper) actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay() actor.PickableOff() - self.actor = actor + self.actor: vtkActor2D = actor self.SetPosition(const.TEXT_POS_LEFT_UP) - def SetColour(self, colour): + def SetColour(self, colour: tuple) -> None: self.property.SetColor(colour) - def ShadowOff(self): + def ShadowOff(self) -> None: self.property.ShadowOff() - def BoldOn(self): + def BoldOn(self) -> None: self.property.BoldOn() - def SetSize(self, size): + def SetSize(self, size: int) -> None: self.property.SetFontSize(size) - def SetValue(self, value): + def SetValue(self, value: str) -> None: if isinstance(value, int) or isinstance(value, float): value = str(value) if sys.platform == 'win32': @@ -208,10 +211,10 @@ def SetValue(self, value): except(UnicodeEncodeError): self.mapper.SetInput(value.encode("utf-8", errors='replace')) - def GetValue(self): + def GetValue(self) -> str: return self.mapper.GetInput() - def SetCoilDistanceValue(self, value): + def SetCoilDistanceValue(self, value: float) -> None: #TODO: Not being used anymore. Can be deleted. if isinstance(value, int) or isinstance(value, float): value = 'Dist: ' + str("{:06.2f}".format(value)) + ' mm' @@ -229,40 +232,40 @@ def SetCoilDistanceValue(self, value): except(UnicodeEncodeError): self.mapper.SetInput(value.encode("utf-8")) - def SetPosition(self, position): + def SetPosition(self, position: tuple) -> None: self.actor.GetPositionCoordinate().SetValue(position[0], position[1]) - def GetPosition(self, position): + def GetPosition(self, position: tuple) -> tuple: self.actor.GetPositionCoordinate().GetValue() - def SetJustificationToRight(self): + def SetJustificationToRight(self) -> None: self.property.SetJustificationToRight() - def SetJustificationToCentered(self): + def SetJustificationToCentered(self) -> None: self.property.SetJustificationToCentered() - def SetVerticalJustificationToBottom(self): + def SetVerticalJustificationToBottom(self) -> None: self.property.SetVerticalJustificationToBottom() - def SetVerticalJustificationToCentered(self): + def SetVerticalJustificationToCentered(self) -> None: self.property.SetVerticalJustificationToCentered() - def Show(self, value=1): + def Show(self, value: int=1) -> None: if value: self.actor.VisibilityOn() else: self.actor.VisibilityOff() - def Hide(self): + def Hide(self) -> None: self.actor.VisibilityOff() class TextZero(object): - def __init__(self): - self.layer = 99 - self.children = [] - property = vtkTextProperty() + def __init__(self) -> None: + self.layer: int = 99 + self.children: list = [] + property: vtkTextProperty = vtkTextProperty() property.SetFontSize(const.TEXT_SIZE_LARGE) property.SetFontFamilyToArial() property.BoldOn() @@ -271,34 +274,34 @@ def __init__(self): property.SetJustificationToLeft() property.SetVerticalJustificationToTop() property.SetColor(const.TEXT_COLOUR) - self.property = property + self.property: vtkTextProperty = property - actor = vtkTextActor() + actor: vtkTextActor = vtkTextActor() actor.GetTextProperty().ShallowCopy(property) actor.GetPositionCoordinate().SetCoordinateSystemToNormalizedDisplay() actor.PickableOff() - self.actor = actor + self.actor: vtkTextActor = actor - self.text = '' - self.position = (0, 0) - self.symbolic_syze = wx.FONTSIZE_MEDIUM - self.bottom_pos = False - self.right_pos = False + self.text: str = '' + self.position: tuple = (0, 0) + self.symbolic_syze: wx.FONTSIZE_MEDIUM = wx.FONTSIZE_MEDIUM + self.bottom_pos: bool = False + self.right_pos: bool = False - def SetColour(self, colour): + def SetColour(self, colour) -> None: self.property.SetColor(colour) - def ShadowOff(self): + def ShadowOff(self) -> None: self.property.ShadowOff() - def SetSize(self, size): + def SetSize(self, size) -> None: self.property.SetFontSize(size) self.actor.GetTextProperty().ShallowCopy(self.property) - def SetSymbolicSize(self, size): - self.symbolic_syze = size + def SetSymbolicSize(self, size) -> None: + self.symbolic_syze: wx.FONTSIZE_MEDIUM = size - def SetValue(self, value): + def SetValue(self, value) -> None: if isinstance(value, int) or isinstance(value, float): value = str(value) if sys.platform == 'win32': @@ -311,44 +314,44 @@ def SetValue(self, value): except(UnicodeEncodeError): self.actor.SetInput(value.encode("utf-8","surrogatepass")) - self.text = value + self.text: str = value - def SetPosition(self, position): - self.position = position + def SetPosition(self, position) -> None: + self.position: tuple = position self.actor.GetPositionCoordinate().SetValue(position[0], position[1]) - def GetPosition(self): + def GetPosition(self) -> tuple: return self.actor.GetPositionCoordinate().GetValue() - def SetJustificationToRight(self): + def SetJustificationToRight(self) -> None: self.property.SetJustificationToRight() - def SetJustificationToCentered(self): + def SetJustificationToCentered(self) -> None: self.property.SetJustificationToCentered() - def SetVerticalJustificationToBottom(self): + def SetVerticalJustificationToBottom(self) -> None: self.property.SetVerticalJustificationToBottom() - def SetVerticalJustificationToCentered(self): + def SetVerticalJustificationToCentered(self) -> None: self.property.SetVerticalJustificationToCentered() - def Show(self, value=1): + def Show(self, value: int=1) -> None: if value: self.actor.VisibilityOn() else: self.actor.VisibilityOff() - def Hide(self): + def Hide(self) -> None: self.actor.VisibilityOff() - def draw_to_canvas(self, gc, canvas): - coord = vtkCoordinate() + def draw_to_canvas(self, gc, canvas) -> None: + coord: vtkCoordinate = vtkCoordinate() coord.SetCoordinateSystemToNormalizedDisplay() coord.SetValue(*self.position) x, y = coord.GetComputedDisplayValue(canvas.evt_renderer) - font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + font: wx.Font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) font.SetSymbolicSize(self.symbolic_syze) font.Scale(canvas.viewer.GetContentScaleFactor()) if self.bottom_pos or self.right_pos: @@ -360,17 +363,17 @@ def draw_to_canvas(self, gc, canvas): canvas.draw_text(self.text, (x, y), font=font) -def numpy_to_vtkMatrix4x4(affine): +def numpy_to_vtkMatrix4x4(affine: np.ndarray) -> vtkMatrix4x4: """ Convert a numpy 4x4 array to a vtk 4x4 matrix :param affine: 4x4 array :return: vtkMatrix4x4 object representing the affine """ # test for type and shape of affine matrix - # assert isinstance(affine, np.ndarray) + assert isinstance(affine, np.ndarray) assert affine.shape == (4, 4) - affine_vtk = vtkMatrix4x4() + affine_vtk: vtkMatrix4x4 = vtkMatrix4x4() for row in range(0, 4): for col in range(0, 4): affine_vtk.SetElement(row, col, affine[row, col]) @@ -379,38 +382,38 @@ def numpy_to_vtkMatrix4x4(affine): # TODO: Use the SurfaceManager >> CreateSurfaceFromFile inside surface.py method instead of duplicating code -def CreateObjectPolyData(filename): +def CreateObjectPolyData(filename: str) -> vtkPolyData: """ Coil for navigation rendered in volume viewer. """ - filename = utils.decode(filename, const.FS_ENCODE) + filename: str = utils.decode(filename, const.FS_ENCODE) if filename: if filename.lower().endswith('.stl'): - reader = vtkSTLReader() + reader: vtkSTLReader = vtkSTLReader() elif filename.lower().endswith('.ply'): - reader = vtkPLYReader() + reader: vtkPLYReader = vtkPLYReader() elif filename.lower().endswith('.obj'): - reader = vtkOBJReader() + reader: vtkOBJReader = vtkOBJReader() elif filename.lower().endswith('.vtp'): - reader = vtkXMLPolyDataReader() + reader: vtkXMLPolyDataReader = vtkXMLPolyDataReader() else: - wx.MessageBox(_("File format not reconized by InVesalius"), _("Import surface error")) + wx.MessageBox(_aifc_params("File format not reconized by InVesalius"), _("Import surface error")) return else: - filename = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") - reader = vtkSTLReader() + filename: str = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") + reader: vtkSTLReader = vtkSTLReader() if _has_win32api: - obj_name = win32api.GetShortPathName(filename).encode(const.FS_ENCODE) + obj_name: bytes = win32api.GetShortPathName(filename).encode(const.FS_ENCODE) else: - obj_name = filename.encode(const.FS_ENCODE) + obj_name: bytes = filename.encode(const.FS_ENCODE) reader.SetFileName(obj_name) reader.Update() - obj_polydata = reader.GetOutput() + obj_polydata: vtkPolyData = reader.GetOutput() if obj_polydata.GetNumberOfPoints() == 0: wx.MessageBox(_("InVesalius was not able to import this surface"), _("Import surface error")) - obj_polydata = None + obj_polydata: vtkPolyData = None return obj_polydata \ No newline at end of file diff --git a/invesalius/data/watershed_process.py b/invesalius/data/watershed_process.py index 339473322..8caa7c2c0 100644 --- a/invesalius/data/watershed_process.py +++ b/invesalius/data/watershed_process.py @@ -1,4 +1,5 @@ import numpy as np +from typing import Tuple from scipy import ndimage from scipy.ndimage import watershed_ift, generate_binary_structure try: @@ -6,8 +7,8 @@ except ImportError: from skimage.morphology import watershed -def get_LUT_value(data, window, level): - shape = data.shape +def get_LUT_value(data: np.ndarray, window: int, level: int) -> np.ndarray: + shape: Tuple[int, ...] = data.shape data_ = data.ravel() data = np.piecewise(data_, [data_ <= (level - 0.5 - (window-1)/2), @@ -17,7 +18,7 @@ def get_LUT_value(data, window, level): return data -def do_watershed(image, markers, tfile, shape, bstruct, algorithm, mg_size, use_ww_wl, wl, ww, q): +def do_watershed(image: np.ndarray, markers: np.ndarray, tfile: str, shape: tuple, bstruct: np.ndarray, algorithm: str, mg_size: int, use_ww_wl: bool, wl: int, ww: int, q: queue.Queue) -> None: mask = np.memmap(tfile, shape=shape, dtype='uint8', mode='r+') if use_ww_wl: @@ -45,3 +46,4 @@ def do_watershed(image, markers, tfile, shape, bstruct, algorithm, mg_size, use mask[:] = tmp_mask mask.flush() q.put(1) + \ No newline at end of file From 5b59e40b808d54b200724aaddaea5c7fa7859bf8 Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Sun, 23 Apr 2023 12:02:09 +0530 Subject: [PATCH 10/16] added type info --- invesalius/data/transformations.py | 427 +++++++++++++++-------------- 1 file changed, 226 insertions(+), 201 deletions(-) diff --git a/invesalius/data/transformations.py b/invesalius/data/transformations.py index afad1e9f6..703ab0053 100644 --- a/invesalius/data/transformations.py +++ b/invesalius/data/transformations.py @@ -198,13 +198,14 @@ import math import numpy +from typing import Tuple, Union, Optional, List, Any, Callable, Dict -__version__ = '2015.07.18' -__docformat__ = 'restructuredtext en' -__all__ = () +__version__: str = '2015.07.18' +__docformat__: str = 'restructuredtext en' +__all__: tuple = () -def identity_matrix(): +def identity_matrix() -> numpy.ndarray: """Return 4x4 identity/unit matrix. >>> I = identity_matrix() @@ -219,7 +220,7 @@ def identity_matrix(): return numpy.identity(4) -def translation_matrix(direction): +def translation_matrix(direction: numpy.ndarray) -> numpy.ndarray: """Return matrix to translate by direction vector. >>> v = numpy.random.random(3) - 0.5 @@ -227,12 +228,12 @@ def translation_matrix(direction): True """ - M = numpy.identity(4) + M: numpy.ndarray = numpy.identity(4) M[:3, 3] = direction[:3] return M -def translation_from_matrix(matrix): +def translation_from_matrix(matrix: numpy.ndarray) -> numpy.ndarray: """Return translation vector from translation matrix. >>> v0 = numpy.random.random(3) - 0.5 @@ -244,7 +245,7 @@ def translation_from_matrix(matrix): return numpy.array(matrix, copy=False)[:3, 3].copy() -def reflection_matrix(point, normal): +def reflection_matrix(point: numpy.ndarray, normal: numpy.ndarray) -> numpy.ndarray: """Return matrix to mirror at plane defined by point and normal vector. >>> v0 = numpy.random.random(4) - 0.5 @@ -264,13 +265,13 @@ def reflection_matrix(point, normal): """ normal = unit_vector(normal[:3]) - M = numpy.identity(4) + M: numpy.ndarray = numpy.identity(4) M[:3, :3] -= 2.0 * numpy.outer(normal, normal) M[:3, 3] = (2.0 * numpy.dot(point[:3], normal)) * normal return M -def reflection_from_matrix(matrix): +def reflection_from_matrix(matrix: numpy.ndarray) -> tuple: """Return mirror plane point and normal vector from reflection matrix. >>> v0 = numpy.random.random(3) - 0.5 @@ -282,24 +283,24 @@ def reflection_from_matrix(matrix): True """ - M = numpy.array(matrix, dtype=numpy.float64, copy=False) + M: numpy.ndarray = numpy.array(matrix, dtype=numpy.float64, copy=False) # normal: unit eigenvector corresponding to eigenvalue -1 w, V = numpy.linalg.eig(M[:3, :3]) i = numpy.where(abs(numpy.real(w) + 1.0) < 1e-8)[0] if not len(i): raise ValueError("no unit eigenvector corresponding to eigenvalue -1") - normal = numpy.real(V[:, i[0]]).squeeze() + normal: numpy.ndarray = numpy.real(V[:, i[0]]).squeeze() # point: any unit eigenvector corresponding to eigenvalue 1 w, V = numpy.linalg.eig(M) i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] if not len(i): raise ValueError("no unit eigenvector corresponding to eigenvalue 1") - point = numpy.real(V[:, i[-1]]).squeeze() + point: numpy.ndarray = numpy.real(V[:, i[-1]]).squeeze() point /= point[3] return point, normal -def rotation_matrix(angle, direction, point=None): +def rotation_matrix(angle: float, direction: list, point: list = None) -> numpy.array: """Return matrix to rotate about axis defined by point and direction. >>> R = rotation_matrix(math.pi/2, [0, 0, 1], [1, 0, 0]) @@ -324,26 +325,26 @@ def rotation_matrix(angle, direction, point=None): True """ - sina = math.sin(angle) - cosa = math.cos(angle) - direction = unit_vector(direction[:3]) + sina: float = math.sin(angle) + cosa: float = math.cos(angle) + direction: list = unit_vector(direction[:3]) # rotation matrix around unit vector - R = numpy.diag([cosa, cosa, cosa]) + R: numpy.array = numpy.diag([cosa, cosa, cosa]) R += numpy.outer(direction, direction) * (1.0 - cosa) direction *= sina R += numpy.array([[ 0.0, -direction[2], direction[1]], [ direction[2], 0.0, -direction[0]], [-direction[1], direction[0], 0.0]]) - M = numpy.identity(4) + M: numpy.array = numpy.identity(4) M[:3, :3] = R if point is not None: # rotation not around origin - point = numpy.array(point[:3], dtype=numpy.float64, copy=False) + point: list = numpy.array(point[:3], dtype=numpy.float64, copy=False) M[:3, 3] = point - numpy.dot(R, point) return M -def rotation_from_matrix(matrix): +def rotation_from_matrix(matrix: numpy.array) -> tuple: """Return rotation angle and axis from rotation matrix. >>> angle = (random.random() - 0.5) * (2*math.pi) @@ -356,34 +357,34 @@ def rotation_from_matrix(matrix): True """ - R = numpy.array(matrix, dtype=numpy.float64, copy=False) - R33 = R[:3, :3] + R: numpy.array = numpy.array(matrix, dtype=numpy.float64, copy=False) + R33: numpy.array = R[:3, :3] # direction: unit eigenvector of R33 corresponding to eigenvalue of 1 - w, W = numpy.linalg.eig(R33.T) - i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] + w, W= numpy.linalg.eig(R33.T) + i: numpy.array = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] if not len(i): raise ValueError("no unit eigenvector corresponding to eigenvalue 1") - direction = numpy.real(W[:, i[-1]]).squeeze() + direction: list = numpy.real(W[:, i[-1]]).squeeze() # point: unit eigenvector of R33 corresponding to eigenvalue of 1 w, Q = numpy.linalg.eig(R) - i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] + i: numpy.array = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] if not len(i): raise ValueError("no unit eigenvector corresponding to eigenvalue 1") - point = numpy.real(Q[:, i[-1]]).squeeze() + point: list = numpy.real(Q[:, i[-1]]).squeeze() point /= point[3] # rotation angle depending on direction - cosa = (numpy.trace(R33) - 1.0) / 2.0 + cosa: float = (numpy.trace(R33) - 1.0) / 2.0 if abs(direction[2]) > 1e-8: - sina = (R[1, 0] + (cosa-1.0)*direction[0]*direction[1]) / direction[2] + sina: float = (R[1, 0] + (cosa-1.0)*direction[0]*direction[1]) / direction[2] elif abs(direction[1]) > 1e-8: - sina = (R[0, 2] + (cosa-1.0)*direction[0]*direction[2]) / direction[1] + sina: float = (R[0, 2] + (cosa-1.0)*direction[0]*direction[2]) / direction[1] else: - sina = (R[2, 1] + (cosa-1.0)*direction[1]*direction[2]) / direction[0] - angle = math.atan2(sina, cosa) + sina: float = (R[2, 1] + (cosa-1.0)*direction[1]*direction[2]) / direction[0] + angle: float = math.atan2(sina, cosa) return angle, direction, point -def scale_matrix(factor, origin=None, direction=None): +def scale_matrix(factor: float, origin: list = None, direction: list = None) -> numpy.array: """Return matrix to scale by factor around origin in direction. Use factor -1 for point symmetry. @@ -402,22 +403,22 @@ def scale_matrix(factor, origin=None, direction=None): """ if direction is None: # uniform scaling - M = numpy.diag([factor, factor, factor, 1.0]) + M: numpy.array = numpy.diag([factor, factor, factor, 1.0]) if origin is not None: M[:3, 3] = origin[:3] M[:3, 3] *= 1.0 - factor else: # nonuniform scaling - direction = unit_vector(direction[:3]) - factor = 1.0 - factor - M = numpy.identity(4) + direction: list = unit_vector(direction[:3]) + factor: float = 1.0 - factor + M: numpy.array = numpy.identity(4) M[:3, :3] -= factor * numpy.outer(direction, direction) if origin is not None: M[:3, 3] = (factor * numpy.dot(origin[:3], direction)) * direction return M -def scale_from_matrix(matrix): +def scale_from_matrix(matrix: numpy.ndarray) -> Tuple[float, numpy.ndarray, numpy.ndarray]: """Return scaling factor, origin and direction from scaling matrix. >>> factor = random.random() * 10 - 5 @@ -435,31 +436,31 @@ def scale_from_matrix(matrix): True """ - M = numpy.array(matrix, dtype=numpy.float64, copy=False) - M33 = M[:3, :3] - factor = numpy.trace(M33) - 2.0 + M: numpy.ndarray = numpy.array(matrix, dtype=numpy.float64, copy=False) + M33: numpy.ndarray = M[:3, :3] + factor: float = numpy.trace(M33) - 2.0 try: # direction: unit eigenvector corresponding to eigenvalue factor w, V = numpy.linalg.eig(M33) - i = numpy.where(abs(numpy.real(w) - factor) < 1e-8)[0][0] - direction = numpy.real(V[:, i]).squeeze() + i: numpy.ndarray = numpy.where(abs(numpy.real(w) - factor) < 1e-8)[0][0] + direction: numpy.ndarray = numpy.real(V[:, i]).squeeze() direction /= vector_norm(direction) except IndexError: # uniform scaling - factor = (factor + 2.0) / 3.0 - direction = None + factor: float = (factor + 2.0) / 3.0 + direction: numpy.ndarray = None # origin: any eigenvector corresponding to eigenvalue 1 w, V = numpy.linalg.eig(M) - i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] + i: numpy.ndarray = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] if not len(i): raise ValueError("no eigenvector corresponding to eigenvalue 1") - origin = numpy.real(V[:, i[-1]]).squeeze() + origin: numpy.ndarray = numpy.real(V[:, i[-1]]).squeeze() origin /= origin[3] return factor, origin, direction -def projection_matrix(point, normal, direction=None, - perspective=None, pseudo=False): +def projection_matrix(point: numpy.ndarray, normal: numpy.ndarray, direction: numpy.ndarray = None, + perspective: numpy.ndarray = None, pseudo: bool = False) -> numpy.ndarray: """Return matrix to project onto plane defined by point and normal. Using either perspective point, projection direction, or none of both. @@ -490,12 +491,12 @@ def projection_matrix(point, normal, direction=None, True """ - M = numpy.identity(4) - point = numpy.array(point[:3], dtype=numpy.float64, copy=False) - normal = unit_vector(normal[:3]) + M: numpy.ndarray = numpy.identity(4) + point: numpy.ndarray = numpy.array(point[:3], dtype=numpy.float64, copy=False) + normal: numpy.ndarray = unit_vector(normal[:3]) if perspective is not None: # perspective projection - perspective = numpy.array(perspective[:3], dtype=numpy.float64, + perspective: numpy.ndarray = numpy.array(perspective[:3], dtype=numpy.float64, copy=False) M[0, 0] = M[1, 1] = M[2, 2] = numpy.dot(perspective-point, normal) M[:3, :3] -= numpy.outer(perspective, normal) @@ -509,8 +510,8 @@ def projection_matrix(point, normal, direction=None, M[3, 3] = numpy.dot(perspective, normal) elif direction is not None: # parallel projection - direction = numpy.array(direction[:3], dtype=numpy.float64, copy=False) - scale = numpy.dot(direction, normal) + direction: numpy.ndarray = numpy.array(direction[:3], dtype=numpy.float64, copy=False) + scale: float = numpy.dot(direction, normal) M[:3, :3] -= numpy.outer(direction, normal) / scale M[:3, 3] = direction * (numpy.dot(point, normal) / scale) else: @@ -520,7 +521,7 @@ def projection_matrix(point, normal, direction=None, return M -def projection_from_matrix(matrix, pseudo=False): +def projection_from_matrix(matrix: numpy.array, pseudo: bool = False) -> Tuple[numpy.array, numpy.array, numpy.array, numpy.array, bool]: """Return projection plane and perspective point from projection matrix. Return values are same as arguments for projection_matrix function: @@ -593,7 +594,7 @@ def projection_from_matrix(matrix, pseudo=False): return point, normal, None, perspective, pseudo -def clip_matrix(left, right, bottom, top, near, far, perspective=False): +def clip_matrix(left: float, right: float, bottom: float, top: float, near: float, far: float, perspective: bool = False) -> numpy.array: """Return matrix to obtain normalized device coordinates from frustum. The frustum bounds are axis-aligned along x (left, right), @@ -645,7 +646,7 @@ def clip_matrix(left, right, bottom, top, near, far, perspective=False): return numpy.array(M) -def shear_matrix(angle, direction, point, normal): +def shear_matrix(angle: float, direction: numpy.array, point: numpy.array, normal: numpy.array) -> numpy.array: """Return matrix to shear by angle along direction vector on shear plane. The shear plane is defined by a point and normal vector. The direction @@ -676,7 +677,7 @@ def shear_matrix(angle, direction, point, normal): return M -def shear_from_matrix(matrix): +def shear_from_matrix(matrix: numpy.array) -> Tuple[float, numpy.array, numpy.array, numpy.array]: """Return shear angle, direction and plane from shear matrix. >>> angle = (random.random() - 0.5) * 4*math.pi @@ -710,7 +711,7 @@ def shear_from_matrix(matrix): direction = numpy.dot(M33 - numpy.identity(3), normal) angle = vector_norm(direction) direction /= angle - angle = math.atan(angle) + angle: float = math.atan(angle) # point: eigenvector corresponding to eigenvalue 1 w, V = numpy.linalg.eig(M) i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0] @@ -721,7 +722,7 @@ def shear_from_matrix(matrix): return angle, direction, point, normal -def decompose_matrix(matrix): +def decompose_matrix(matrix: numpy.array) -> Tuple[numpy.array, list, list, numpy.array, numpy.array]: """Return sequence of transformations from transformation matrix. matrix : array_like @@ -752,29 +753,29 @@ def decompose_matrix(matrix): True """ - M = numpy.array(matrix, dtype=numpy.float64, copy=True).T + M: numpy.array = numpy.array(matrix, dtype=numpy.float64, copy=True).T if abs(M[3, 3]) < _EPS: raise ValueError("M[3, 3] is zero") M /= M[3, 3] - P = M.copy() + P: numpy.array = M.copy() P[:, 3] = 0.0, 0.0, 0.0, 1.0 if not numpy.linalg.det(P): raise ValueError("matrix is singular") - scale = numpy.zeros((3, )) - shear = [0.0, 0.0, 0.0] - angles = [0.0, 0.0, 0.0] + scale: numpy.array = numpy.zeros((3, )) + shear: list = [0.0, 0.0, 0.0] + angles: list = [0.0, 0.0, 0.0] if any(abs(M[:3, 3]) > _EPS): - perspective = numpy.dot(M[:, 3], numpy.linalg.inv(P.T)) + perspective: numpy.array = numpy.dot(M[:, 3], numpy.linalg.inv(P.T)) M[:, 3] = 0.0, 0.0, 0.0, 1.0 else: - perspective = numpy.array([0.0, 0.0, 0.0, 1.0]) + perspective: numpy.array = numpy.array([0.0, 0.0, 0.0, 1.0]) - translate = M[3, :3].copy() + translate: numpy.array = M[3, :3].copy() M[3, :3] = 0.0 - row = M[:3, :3].copy() + row: numpy.array = M[:3, :3].copy() scale[0] = vector_norm(row[0]) row[0] /= scale[0] shear[0] = numpy.dot(row[0], row[1]) @@ -806,8 +807,8 @@ def decompose_matrix(matrix): return scale, shear, angles, translate, perspective -def compose_matrix(scale=None, shear=None, angles=None, translate=None, - perspective=None): +def compose_matrix(scale: numpy.array = None, shear: list = None, angles: list = None, translate: numpy.array = None, + perspective: numpy.array = None) -> numpy.array: """Return transformation matrix from sequence of transformations. This is the inverse of the decompose_matrix function. @@ -831,26 +832,26 @@ def compose_matrix(scale=None, shear=None, angles=None, translate=None, True """ - M = numpy.identity(4) + M: numpy.array = numpy.identity(4) if perspective is not None: - P = numpy.identity(4) + P: numpy.array = numpy.identity(4) P[3, :] = perspective[:4] M = numpy.dot(M, P) if translate is not None: - T = numpy.identity(4) + T: numpy.array = numpy.identity(4) T[:3, 3] = translate[:3] M = numpy.dot(M, T) if angles is not None: - R = euler_matrix(angles[0], angles[1], angles[2], 'sxyz') + R: numpy.array = euler_matrix(angles[0], angles[1], angles[2], 'sxyz') M = numpy.dot(M, R) if shear is not None: - Z = numpy.identity(4) + Z: numpy.array = numpy.identity(4) Z[1, 2] = shear[2] Z[0, 2] = shear[1] Z[0, 1] = shear[0] M = numpy.dot(M, Z) if scale is not None: - S = numpy.identity(4) + S: numpy.array = numpy.identity(4) S[0, 0] = scale[0] S[1, 1] = scale[1] S[2, 2] = scale[2] @@ -859,9 +860,12 @@ def compose_matrix(scale=None, shear=None, angles=None, translate=None, return M -def orthogonalization_matrix(lengths, angles): + + +def orthogonalization_matrix(lengths: 'list[float]', angles: 'list[float]') -> numpy.ndarray: """Return orthogonalization matrix for crystallographic cell coordinates. + Angles are expected in degrees. The de-orthogonalization matrix is the inverse. @@ -885,11 +889,10 @@ def orthogonalization_matrix(lengths, angles): [ a*cosb, b*cosa, c, 0.0], [ 0.0, 0.0, 0.0, 1.0]]) - -def affine_matrix_from_points(v0, v1, shear=True, scale=True, usesvd=True): +def affine_matrix_from_points(v0: numpy.ndarray, v1: numpy.ndarray, shear: bool=True, scale: bool=True, usesvd: bool=True) -> numpy.ndarray: """Return affine transform matrix to register two point sets. - v0 and v1 are shape (ndims, \*) arrays of at least ndims non-homogeneous + v0 and v1 are shape (ndims, *) arrays of at least ndims non-homogeneous coordinates, where ndims is the dimensionality of the coordinate space. If shear is False, a similarity transformation matrix is returned. @@ -930,7 +933,7 @@ def affine_matrix_from_points(v0, v1, shear=True, scale=True, usesvd=True): v0 = numpy.array(v0, dtype=numpy.float64, copy=True) v1 = numpy.array(v1, dtype=numpy.float64, copy=True) - ndims = v0.shape[0] + ndims: int = v0.shape[0] if ndims < 2 or v0.shape[1] < ndims or v0.shape != v1.shape: raise ValueError("input arrays are of wrong shape or type") @@ -995,10 +998,11 @@ def affine_matrix_from_points(v0, v1, shear=True, scale=True, usesvd=True): return M -def superimposition_matrix(v0, v1, scale=False, usesvd=True): + +def superimposition_matrix(v0: numpy.ndarray, v1: numpy.ndarray, scale: bool = False, usesvd: bool = True) -> numpy.ndarray: """Return matrix to transform given 3D point set into second point set. - v0 and v1 are shape (3, \*) or (4, \*) arrays of at least 3 points. + v0 and v1 are shape (3, *) or (4, *) arrays of at least 3 points. The parameters scale and usesvd are explained in the more general affine_matrix_from_points function. @@ -1046,7 +1050,7 @@ def superimposition_matrix(v0, v1, scale=False, usesvd=True): scale=scale, usesvd=usesvd) -def euler_matrix(ai, aj, ak, axes='sxyz'): +def euler_matrix(ai: float, aj: float, ak: float, axes: str or Tuple[int, int, int, int]) -> numpy.ndarray: """Return homogeneous rotation matrix from Euler angles and axis sequence. ai, aj, ak : Euler's roll, pitch and yaw angles @@ -1109,23 +1113,29 @@ def euler_matrix(ai, aj, ak, axes='sxyz'): return M -def euler_from_matrix(matrix, axes='sxyz'): +def euler_from_matrix(matrix: numpy.ndarray, axes: str = 'sxyz') -> tuple: """Return Euler angles from rotation matrix for specified axis sequence. - axes : One of 24 axis sequences as string or encoded tuple + Args: + matrix: A 3x3 numpy array representing the rotation matrix. + axes: One of 24 axis sequences as string or encoded tuple. Note that many Euler angle triplets can describe one matrix. - >>> R0 = euler_matrix(1, 2, 3, 'syxz') - >>> al, be, ga = euler_from_matrix(R0, 'syxz') - >>> R1 = euler_matrix(al, be, ga, 'syxz') - >>> numpy.allclose(R0, R1) - True - >>> angles = (4*math.pi) * (numpy.random.random(3) - 0.5) - >>> for axes in _AXES2TUPLE.keys(): - ... R0 = euler_matrix(axes=axes, *angles) - ... R1 = euler_matrix(axes=axes, *euler_from_matrix(R0, axes)) - ... if not numpy.allclose(R0, R1): print(axes, "failed") + Returns: + A tuple of three floats representing the Euler angles in radians. + + Examples: + >>> R0 = euler_matrix(1, 2, 3, 'syxz') + >>> al, be, ga = euler_from_matrix(R0, 'syxz') + >>> R1 = euler_matrix(al, be, ga, 'syxz') + >>> numpy.allclose(R0, R1) + True + >>> angles = (4*math.pi) * (numpy.random.random(3) - 0.5) + >>> for axes in _AXES2TUPLE.keys(): + ... R0 = euler_matrix(axes=axes, *angles) + ... R1 = euler_matrix(axes=axes, *euler_from_matrix(R0, axes)) + ... if not numpy.allclose(R0, R1): print(axes, "failed") """ try: @@ -1135,29 +1145,29 @@ def euler_from_matrix(matrix, axes='sxyz'): firstaxis, parity, repetition, frame = axes i = firstaxis - j = _NEXT_AXIS[i+parity] - k = _NEXT_AXIS[i-parity+1] + j = _NEXT_AXIS[i + parity] + k = _NEXT_AXIS[i - parity + 1] M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:3, :3] if repetition: - sy = math.sqrt(M[i, j]*M[i, j] + M[i, k]*M[i, k]) + sy: float = math.sqrt(M[i, j] * M[i, j] + M[i, k] * M[i, k]) if sy > _EPS: - ax = math.atan2( M[i, j], M[i, k]) - ay = math.atan2( sy, M[i, i]) - az = math.atan2( M[j, i], -M[k, i]) + ax: float = math.atan2(M[i, j], M[i, k]) + ay: float = math.atan2(sy, M[i, i]) + az: float = math.atan2(M[j, i], -M[k, i]) else: - ax = math.atan2(-M[j, k], M[j, j]) - ay = math.atan2( sy, M[i, i]) + ax = math.atan2(-M[j, k], M[j, j]) + ay = math.atan2(sy, M[i, i]) az = 0.0 else: - cy = math.sqrt(M[i, i]*M[i, i] + M[j, i]*M[j, i]) + cy: float = math.sqrt(M[i, i] * M[i, i] + M[j, i] * M[j, i]) if cy > _EPS: - ax = math.atan2( M[k, j], M[k, k]) - ay = math.atan2(-M[k, i], cy) - az = math.atan2( M[j, i], M[i, i]) + ax = math.atan2(M[k, j], M[k, k]) + ay = math.atan2(-M[k, i], cy) + az = math.atan2(M[j, i], M[i, i]) else: - ax = math.atan2(-M[j, k], M[j, j]) - ay = math.atan2(-M[k, i], cy) + ax = math.atan2(-M[j, k], M[j, j]) + ay = math.atan2(-M[k, i], cy) az = 0.0 if parity: @@ -1167,26 +1177,41 @@ def euler_from_matrix(matrix, axes='sxyz'): return ax, ay, az -def euler_from_quaternion(quaternion, axes='sxyz'): +def euler_from_quaternion(quaternion: list, axes: str = 'sxyz') -> tuple: """Return Euler angles from quaternion for specified axis sequence. - >>> angles = euler_from_quaternion([0.99810947, 0.06146124, 0, 0]) - >>> numpy.allclose(angles, [0.123, 0, 0]) - True + Args: + quaternion: A list of four floats representing the quaternion. + axes: One of 24 axis sequences as string or encoded tuple. + Returns: + A tuple of three floats representing the Euler angles in radians. + + Examples: + >>> angles = euler_from_quaternion([0.99810947, 0.06146124, 0, 0]) + >>> numpy.allclose(angles, [0.123, 0, 0]) + True + """ return euler_from_matrix(quaternion_matrix(quaternion), axes) -def quaternion_from_euler(ai, aj, ak, axes='sxyz'): +def quaternion_from_euler(ai: float, aj: float, ak: float, axes: str = 'sxyz') -> numpy.ndarray: """Return quaternion from Euler angles and axis sequence. - ai, aj, ak : Euler's roll, pitch and yaw angles - axes : One of 24 axis sequences as string or encoded tuple + Args: + ai: Euler's roll angle in radians. + aj: Euler's pitch angle in radians. + ak: Euler's yaw angle in radians. + axes: One of 24 axis sequences as string or encoded tuple. - >>> q = quaternion_from_euler(1, 2, 3, 'ryxz') - >>> numpy.allclose(q, [0.435953, 0.310622, -0.718287, 0.444435]) - True + Returns: + A numpy array of four floats representing the quaternion. + + Examples: + >>> q = quaternion_from_euler(1, 2, 3, 'ryxz') + >>> numpy.allclose(q, [0.435953, 0.310622, -0.718287, 0.444435]) + True """ try: @@ -1196,8 +1221,8 @@ def quaternion_from_euler(ai, aj, ak, axes='sxyz'): firstaxis, parity, repetition, frame = axes i = firstaxis + 1 - j = _NEXT_AXIS[i+parity-1] + 1 - k = _NEXT_AXIS[i-parity] + 1 + j = _NEXT_AXIS[i + parity - 1] + 1 + k = _NEXT_AXIS[i - parity] + 1 if frame: ai, ak = ak, ai @@ -1207,35 +1232,35 @@ def quaternion_from_euler(ai, aj, ak, axes='sxyz'): ai /= 2.0 aj /= 2.0 ak /= 2.0 - ci = math.cos(ai) - si = math.sin(ai) - cj = math.cos(aj) - sj = math.sin(aj) - ck = math.cos(ak) - sk = math.sin(ak) - cc = ci*ck - cs = ci*sk - sc = si*ck - ss = si*sk - - q = numpy.empty((4, )) + ci: float = math.cos(ai) + si: float = math.sin(ai) + cj: float = math.cos(aj) + sj: float = math.sin(aj) + ck: float = math.cos(ak) + sk: float = math.sin(ak) + cc: float = ci * ck + cs: float = ci * sk + sc: float = si * ck + ss: float = si * sk + + q = numpy.empty((4,)) if repetition: - q[0] = cj*(cc - ss) - q[i] = cj*(cs + sc) - q[j] = sj*(cc + ss) - q[k] = sj*(cs - sc) + q[0] = cj * (cc - ss) + q[i] = cj * (cs + sc) + q[j] = sj * (cc + ss) + q[k] = sj * (cs - sc) else: - q[0] = cj*cc + sj*ss - q[i] = cj*sc - sj*cs - q[j] = cj*ss + sj*cc - q[k] = cj*cs - sj*sc + q[0] = cj * cc + sj * ss + q[i] = cj * sc - sj * cs + q[j] = cj * ss + sj * cc + q[k] = cj * cs - sj * sc if parity: q[j] *= -1.0 return q -def quaternion_about_axis(angle, axis): +def quaternion_about_axis(angle: float, axis: 'list[float]') -> numpy.ndarray: """Return quaternion for rotation about axis. >>> q = quaternion_about_axis(0.123, [1, 0, 0]) @@ -1244,14 +1269,14 @@ def quaternion_about_axis(angle, axis): """ q = numpy.array([0.0, axis[0], axis[1], axis[2]]) - qlen = vector_norm(q) + qlen = numpy.linalg.norm(q) if qlen > _EPS: q *= math.sin(angle/2.0) / qlen q[0] = math.cos(angle/2.0) return q -def quaternion_matrix(quaternion): +def quaternion_matrix(quaternion: 'list[float]') -> numpy.ndarray: """Return homogeneous rotation matrix from quaternion. >>> M = quaternion_matrix([0.99810947, 0.06146124, 0, 0]) @@ -1278,7 +1303,7 @@ def quaternion_matrix(quaternion): [ 0.0, 0.0, 0.0, 1.0]]) -def quaternion_from_matrix(matrix, isprecise=False): +def quaternion_from_matrix(matrix: numpy.ndarray, isprecise: bool = False) -> 'list[float]': """Return quaternion from rotation matrix. If isprecise is True, the input matrix is assumed to be a precise rotation @@ -1359,10 +1384,10 @@ def quaternion_from_matrix(matrix, isprecise=False): return q -def quaternion_multiply(quaternion1, quaternion0): +def quaternion_multiply(quaternion1: numpy.ndarray, quaternion0: numpy.ndarray) -> numpy.ndarray: """Return multiplication of two quaternions. - >>> q = quaternion_multiply([4, 1, -2, 3], [8, -5, 6, 7]) + >>> q = quaternion_multiply(numpy.array([4, 1, -2, 3]), numpy.array([8, -5, 6, 7])) >>> numpy.allclose(q, [28, -44, -14, 48]) True @@ -1375,7 +1400,7 @@ def quaternion_multiply(quaternion1, quaternion0): x1*y0 - y1*x0 + z1*w0 + w1*z0], dtype=numpy.float64) -def quaternion_conjugate(quaternion): +def quaternion_conjugate(quaternion: numpy.ndarray) -> numpy.ndarray: """Return conjugate of quaternion. >>> q0 = random_quaternion() @@ -1389,7 +1414,7 @@ def quaternion_conjugate(quaternion): return q -def quaternion_inverse(quaternion): +def quaternion_inverse(quaternion: numpy.ndarray) -> numpy.ndarray: """Return inverse of quaternion. >>> q0 = random_quaternion() @@ -1403,27 +1428,27 @@ def quaternion_inverse(quaternion): return q / numpy.dot(q, q) -def quaternion_real(quaternion): +def quaternion_real(quaternion: numpy.ndarray) -> float: """Return real part of quaternion. - >>> quaternion_real([3, 0, 1, 2]) + >>> quaternion_real(numpy.array([3, 0, 1, 2])) 3.0 """ return float(quaternion[0]) -def quaternion_imag(quaternion): +def quaternion_imag(quaternion: numpy.ndarray) -> numpy.ndarray: """Return imaginary part of quaternion. - >>> quaternion_imag([3, 0, 1, 2]) + >>> quaternion_imag(numpy.array([3, 0, 1, 2])) array([ 0., 1., 2.]) """ return numpy.array(quaternion[1:4], dtype=numpy.float64, copy=True) -def quaternion_slerp(quat0, quat1, fraction, spin=0, shortestpath=True): +def quaternion_slerp(quat0: numpy.ndarray, quat1: numpy.ndarray, fraction: float, spin: int = 0, shortestpath: bool = True) -> numpy.ndarray: """Return spherical linear interpolation between two quaternions. >>> q0 = random_quaternion() @@ -1454,17 +1479,17 @@ def quaternion_slerp(quat0, quat1, fraction, spin=0, shortestpath=True): # invert rotation d = -d numpy.negative(q1, q1) - angle = math.acos(d) + spin * math.pi + angle: float = math.acos(d) + spin * math.pi if abs(angle) < _EPS: return q0 - isin = 1.0 / math.sin(angle) + isin: float = 1.0 / math.sin(angle) q0 *= math.sin((1.0 - fraction) * angle) * isin q1 *= math.sin(fraction * angle) * isin q0 += q1 return q0 -def random_quaternion(rand=None): +def random_quaternion(rand: numpy.ndarray = None) -> numpy.ndarray: """Return uniform random unit quaternion. rand: array like or None @@ -1485,14 +1510,14 @@ def random_quaternion(rand=None): assert len(rand) == 3 r1 = numpy.sqrt(1.0 - rand[0]) r2 = numpy.sqrt(rand[0]) - pi2 = math.pi * 2.0 + pi2: float = math.pi * 2.0 t1 = pi2 * rand[1] t2 = pi2 * rand[2] return numpy.array([numpy.cos(t2)*r2, numpy.sin(t1)*r1, numpy.cos(t1)*r1, numpy.sin(t2)*r2]) -def random_rotation_matrix(rand=None): +def random_rotation_matrix(rand: Optional[List[float]] = None) -> numpy.ndarray: """Return uniform random rotation matrix. rand: array like @@ -1530,20 +1555,20 @@ class Arcball(object): >>> ball.next() """ - def __init__(self, initial=None): + def __init__(self, initial: Optional[numpy.ndarray] = None) -> None: """Initialize virtual trackball control. initial : quaternion or rotation matrix """ - self._axis = None - self._axes = None - self._radius = 1.0 - self._center = [0.0, 0.0] - self._vdown = numpy.array([0.0, 0.0, 1.0]) - self._constrain = False + self._axis: Optional[Tuple[float, float, float]] = None + self._axes: Optional[List[Tuple[float, float, float]]] = None + self._radius: float = 1.0 + self._center: List[float] = [0.0, 0.0] + self._vdown: numpy.ndarray = numpy.array([0.0, 0.0, 1.0]) + self._constrain: bool = False if initial is None: - self._qdown = numpy.array([1.0, 0.0, 0.0, 0.0]) + self._qdown: numpy.ndarray = numpy.array([1.0, 0.0, 0.0, 0.0]) else: initial = numpy.array(initial, dtype=numpy.float64) if initial.shape == (4, 4): @@ -1555,7 +1580,7 @@ def __init__(self, initial=None): raise ValueError("initial not a quaternion or matrix") self._qnow = self._qpre = self._qdown - def place(self, center, radius): + def place(self, center: List[int], radius: float) -> None: """Place Arcball, e.g. when window size changes. center : sequence[2] @@ -1568,7 +1593,7 @@ def place(self, center, radius): self._center[0] = center[0] self._center[1] = center[1] - def setaxes(self, *axes): + def setaxes(self, *axes: Tuple[float, float, float]) -> None: """Set axes to constrain rotations.""" if axes is None: self._axes = None @@ -1576,16 +1601,16 @@ def setaxes(self, *axes): self._axes = [unit_vector(axis) for axis in axes] @property - def constrain(self): + def constrain(self) -> bool: """Return state of constrain to axis mode.""" return self._constrain @constrain.setter - def constrain(self, value): + def constrain(self, value: bool) -> None: """Set state of constrain to axis mode.""" self._constrain = bool(value) - def down(self, point): + def down(self, point: List[int]) -> None: """Set initial cursor window coordinates and pick constrain-axis.""" self._vdown = arcball_map_to_sphere(point, self._center, self._radius) self._qdown = self._qpre = self._qnow @@ -1595,7 +1620,7 @@ def down(self, point): else: self._axis = None - def drag(self, point): + def drag(self, point: List[int]) -> None: """Update current cursor window coordinates.""" vnow = arcball_map_to_sphere(point, self._center, self._radius) if self._axis is not None: @@ -1608,21 +1633,21 @@ def drag(self, point): q = [numpy.dot(self._vdown, vnow), t[0], t[1], t[2]] self._qnow = quaternion_multiply(q, self._qdown) - def next(self, acceleration=0.0): + def next(self, acceleration: float = 0.0) -> None: """Continue rotation in direction of last drag.""" q = quaternion_slerp(self._qpre, self._qnow, 2.0+acceleration, False) self._qpre, self._qnow = self._qnow, q - def matrix(self): + def matrix(self) -> numpy.ndarray: """Return homogeneous rotation matrix.""" return quaternion_matrix(self._qnow) -def arcball_map_to_sphere(point, center, radius): +def arcball_map_to_sphere(point: 'tuple[float, float]', center: 'tuple[float, float]', radius: float) -> numpy.ndarray: """Return unit sphere coordinates from window coordinates.""" - v0 = (point[0] - center[0]) / radius - v1 = (center[1] - point[1]) / radius - n = v0*v0 + v1*v1 + v0: float = (point[0] - center[0]) / radius + v1: float = (center[1] - point[1]) / radius + n: float = v0*v0 + v1*v1 if n > 1.0: # position outside of sphere n = math.sqrt(n) @@ -1631,7 +1656,7 @@ def arcball_map_to_sphere(point, center, radius): return numpy.array([v0, v1, math.sqrt(1.0 - n)]) -def arcball_constrain_to_axis(point, axis): +def arcball_constrain_to_axis(point: numpy.ndarray, axis: numpy.ndarray) -> numpy.ndarray: """Return sphere point perpendicular to axis.""" v = numpy.array(point, dtype=numpy.float64, copy=True) a = numpy.array(axis, dtype=numpy.float64, copy=True) @@ -1647,11 +1672,11 @@ def arcball_constrain_to_axis(point, axis): return unit_vector([-a[1], a[0], 0.0]) -def arcball_nearest_axis(point, axes): +def arcball_nearest_axis(point: numpy.ndarray, axes: 'list[numpy.ndarray]') -> numpy.ndarray: """Return axis, which arc is nearest to point.""" point = numpy.array(point, dtype=numpy.float64, copy=False) nearest = None - mx = -1.0 + mx: float = -1.0 for axis in axes: t = numpy.dot(arcball_constrain_to_axis(point, axis), point) if t > mx: @@ -1661,13 +1686,13 @@ def arcball_nearest_axis(point, axes): # epsilon for testing whether a number is close to zero -_EPS = numpy.finfo(float).eps * 4.0 +_EPS: float = numpy.finfo(float).eps * 4.0 # axis sequences for Euler angles -_NEXT_AXIS = [1, 2, 0, 1] +_NEXT_AXIS: 'list[int]' = [1, 2, 0, 1] # map axes strings to/from tuples of inner axis, parity, repetition, frame -_AXES2TUPLE = { +_AXES2TUPLE: 'dict[str, tuple[int, int, int, int]]' = { 'sxyz': (0, 0, 0, 0), 'sxyx': (0, 0, 1, 0), 'sxzy': (0, 1, 0, 0), 'sxzx': (0, 1, 1, 0), 'syzx': (1, 0, 0, 0), 'syzy': (1, 0, 1, 0), 'syxz': (1, 1, 0, 0), 'syxy': (1, 1, 1, 0), 'szxy': (2, 0, 0, 0), @@ -1677,10 +1702,10 @@ def arcball_nearest_axis(point, axes): 'rzxy': (1, 1, 0, 1), 'ryxy': (1, 1, 1, 1), 'ryxz': (2, 0, 0, 1), 'rzxz': (2, 0, 1, 1), 'rxyz': (2, 1, 0, 1), 'rzyz': (2, 1, 1, 1)} -_TUPLE2AXES = dict((v, k) for k, v in _AXES2TUPLE.items()) +_TUPLE2AXES: 'dict[tuple[int, int, int, int], str]' = dict((v, k) for k, v in _AXES2TUPLE.items()) -def vector_norm(data, axis=None, out=None): +def vector_norm(data: numpy.ndarray, axis: int = None, out: numpy.ndarray = None) -> numpy.ndarray: """Return length, i.e. Euclidean norm, of ndarray along axis. >>> v = numpy.random.random(3) @@ -1719,7 +1744,7 @@ def vector_norm(data, axis=None, out=None): numpy.sqrt(out, out) -def unit_vector(data, axis=None, out=None): +def unit_vector(data: numpy.ndarray, axis: int = None, out: numpy.ndarray = None) -> numpy.ndarray: """Return ndarray normalized by length, i.e. Euclidean norm, along axis. >>> v0 = numpy.random.random(3) @@ -1763,7 +1788,7 @@ def unit_vector(data, axis=None, out=None): return data -def random_vector(size): +def random_vector(size: int) -> numpy.ndarray: """Return array of random doubles in the half-open interval [0.0, 1.0). >>> v = random_vector(10000) @@ -1778,7 +1803,7 @@ def random_vector(size): return numpy.random.random(size) -def vector_product(v0, v1, axis=0): +def vector_product(v0: Union[List[float], numpy.ndarray], v1: Union[List[float], numpy.ndarray], axis: int = 0) -> numpy.ndarray: """Return vector perpendicular to vectors. >>> v = vector_product([2, 0, 0], [0, 3, 0]) @@ -1799,7 +1824,7 @@ def vector_product(v0, v1, axis=0): return numpy.cross(v0, v1, axis=axis) -def angle_between_vectors(v0, v1, directed=True, axis=0): +def angle_between_vectors(v0: Union[List[float], numpy.ndarray], v1: Union[List[float], numpy.ndarray], directed: bool = True, axis: int = 0) -> numpy.ndarray: """Return angle between vectors. If directed is False, the input vectors are interpreted as undirected axes, @@ -1830,7 +1855,7 @@ def angle_between_vectors(v0, v1, directed=True, axis=0): return numpy.arccos(dot if directed else numpy.fabs(dot)) -def inverse_matrix(matrix): +def inverse_matrix(matrix: numpy.ndarray) -> numpy.ndarray: """Return inverse of square transformation matrix. >>> M0 = random_rotation_matrix() @@ -1846,7 +1871,7 @@ def inverse_matrix(matrix): return numpy.linalg.inv(matrix) -def concatenate_matrices(*matrices): +def concatenate_matrices(*matrices: numpy.ndarray) -> numpy.ndarray: """Return concatenation of series of transformation matrices. >>> M = numpy.random.rand(16).reshape((4, 4)) - 0.5 @@ -1862,7 +1887,7 @@ def concatenate_matrices(*matrices): return M -def is_same_transform(matrix0, matrix1): +def is_same_transform(matrix0: numpy.ndarray, matrix1: numpy.ndarray) -> bool: """Return True if two matrices perform same transformation. >>> is_same_transform(numpy.identity(4), numpy.identity(4)) @@ -1878,7 +1903,7 @@ def is_same_transform(matrix0, matrix1): return numpy.allclose(matrix0, matrix1) -def _import_module(name, package=None, warn=True, prefix='_py_', ignore='_'): +def _import_module(name: str, package: str = None, warn: bool = True, prefix: str = '_py_', ignore: str = '_') -> bool: """Try import all public attributes from module into global namespace. Existing attributes with name clashes are renamed with prefix. @@ -1891,9 +1916,9 @@ def _import_module(name, package=None, warn=True, prefix='_py_', ignore='_'): from importlib import import_module try: if not package: - module = import_module(name) + module = __import__(name) else: - module = import_module('.' + name, package=package) + module = __import__('.'.join([package, name]), fromlist=[name]) except ImportError: if warn: warnings.warn("failed to import module %s" % name) From 769dea77ded0778377a44eddbdc59f60d01b799a Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Sun, 23 Apr 2023 19:22:44 +0530 Subject: [PATCH 11/16] added type info --- invesalius/data/viewer_slice.py | 738 ++++++++++++++-------------- invesalius/data/viewer_volume.py | 810 ++++++++++++++++--------------- invesalius/data/volume.py | 468 +++++++++--------- 3 files changed, 1014 insertions(+), 1002 deletions(-) diff --git a/invesalius/data/viewer_slice.py b/invesalius/data/viewer_slice.py index 10a9b9e90..91072df1f 100644 --- a/invesalius/data/viewer_slice.py +++ b/invesalius/data/viewer_slice.py @@ -22,7 +22,7 @@ import itertools import os import tempfile - +from typing import Dict, Any, Union, List, Tuple, Optional, Callable, cast import numpy as np from vtkmodules.vtkFiltersGeneral import vtkCursor3D @@ -71,16 +71,16 @@ if sys.platform == 'win32': try: import win32api - _has_win32api = True + _has_win32api: bool = True except ImportError: - _has_win32api = False + _has_win32api: bool = False else: - _has_win32api = False + _has_win32api: bool = False -ID_TO_TOOL_ITEM = {} -STR_WL = "WL: %d WW: %d" +ID_TO_TOOL_ITEM: Dict[int, Any] = {} +STR_WL: str = "WL: %d WW: %d" -ORIENTATIONS = { +ORIENTATIONS: Dict[str, Union[str, int]] = { "AXIAL": const.AXIAL, "CORONAL": const.CORONAL, "SAGITAL": const.SAGITAL, @@ -88,13 +88,13 @@ class ContourMIPConfig(wx.Panel): - def __init__(self, prnt, orientation): + def __init__(self, prnt: wx.Window, orientation: str) -> None: wx.Panel.__init__(self, prnt) - self.mip_size_spin = InvSpinCtrl(self, -1, value=const.PROJECTION_MIP_SIZE, min_value=1, max_value=240) + self.mip_size_spin: InvSpinCtrl = InvSpinCtrl(self, -1, value=const.PROJECTION_MIP_SIZE, min_value=1, max_value=240) self.mip_size_spin.SetToolTip(wx.ToolTip(_("Number of slices used to compound the visualization."))) self.mip_size_spin.CalcSizeFromTextSize('MMM') - self.border_spin = InvFloatSpinCtrl(self, -1, min_value=0, max_value=10, + self.border_spin: InvFloatSpinCtrl = InvFloatSpinCtrl(self, -1, min_value=0, max_value=10, increment=0.1, value=const.PROJECTION_BORDER_SIZE, digits=1) @@ -107,17 +107,17 @@ def __init__(self, prnt, orientation): # self.border_spin.SetMinSize((5 * w + 10, -1)) # self.border_spin.SetMaxSize((5 * w + 10, -1)) - self.inverted = wx.CheckBox(self, -1, _("Inverted order")) + self.inverted: wx.CheckBox = wx.CheckBox(self, -1, _("Inverted order")) self.inverted.SetToolTip(wx.ToolTip(_("If checked, the slices are" " traversed in descending" " order to compound the" " visualization instead of" " ascending order."))) - txt_mip_size = wx.StaticText(self, -1, _("Number of slices"), style=wx.ALIGN_CENTER_HORIZONTAL) - self.txt_mip_border = wx.StaticText(self, -1, _("Sharpness")) + txt_mip_size: wx.StaticText = wx.StaticText(self, -1, _("Number of slices"), style=wx.ALIGN_CENTER_HORIZONTAL) + self.txt_mip_border: wx.StaticText = wx.StaticText(self, -1, _("Sharpness")) - sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer: wx.BoxSizer = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(txt_mip_size, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2) sizer.Add(self.mip_size_spin, 0) try: @@ -138,8 +138,8 @@ def __init__(self, prnt, orientation): self.Update() self.SetAutoLayout(1) - self.orientation = orientation - self.canvas = None + self.orientation: str = orientation + self.canvas: Any = None self.mip_size_spin.Bind(wx.EVT_SPINCTRL, self.OnSetMIPSize) self.border_spin.Bind(wx.EVT_SPINCTRL, self.OnSetMIPBorder) @@ -147,19 +147,19 @@ def __init__(self, prnt, orientation): Publisher.subscribe(self._set_projection_type, 'Set projection type') - def OnSetMIPSize(self, number_slices): - val = self.mip_size_spin.GetValue() + def OnSetMIPSize(self, number_slices: wx.SpinEvent) -> None: + val: int = self.mip_size_spin.GetValue() Publisher.sendMessage('Set MIP size %s' % self.orientation, number_slices=val) - def OnSetMIPBorder(self, evt): - val = self.border_spin.GetValue() + def OnSetMIPBorder(self, evt: wx.SpinEvent) -> None: + val: float = self.border_spin.GetValue() Publisher.sendMessage('Set MIP border %s' % self.orientation, border_size=val) - def OnCheckInverted(self, evt): - val = self.inverted.GetValue() + def OnCheckInverted(self, evt: wx.CommandEvent) -> None: + val: bool = self.inverted.GetValue() Publisher.sendMessage('Set MIP Invert %s' % self.orientation, invert=val) - def _set_projection_type(self, projection_id): + def _set_projection_type(self, projection_id: int) -> None: if projection_id in (const.PROJECTION_MIDA, const.PROJECTION_CONTOUR_MIDA): self.inverted.Enable() @@ -177,7 +177,7 @@ def _set_projection_type(self, projection_id): class Viewer(wx.Panel): - def __init__(self, prnt, orientation='AXIAL'): + def __init__(self, prnt: wx.Window, orientation: str = 'AXIAL') -> None: wx.Panel.__init__(self, prnt, size=wx.Size(320, 300)) #colour = [255*c for c in const.ORIENTATION_COLOUR[orientation]] @@ -185,73 +185,73 @@ def __init__(self, prnt, orientation='AXIAL'): # Interactor additional style - self._number_slices = const.PROJECTION_MIP_SIZE - self._mip_inverted = False + self._number_slices: int = const.PROJECTION_MIP_SIZE + self._mip_inverted: bool = False - self.style = None - self.last_position_mouse_move = () - self.state = const.STATE_DEFAULT + self.style: Optional[str] = None + self.last_position_mouse_move: Tuple[int, int] = () + self.state: str = const.STATE_DEFAULT - self.overwrite_mask = False + self.overwrite_mask: bool = False # All renderers and image actors in this viewer - self.slice_data_list = [] - self.slice_data = None + self.slice_data_list: List[Any] = [] + self.slice_data: Any = None - self.slice_actor = None - self.interpolation_slice_status = True + self.slice_actor: Optional[Any] = None + self.interpolation_slice_status: bool = True - self.canvas = None + self.canvas: Optional[Any] = None - self.draw_by_slice_number = collections.defaultdict(list) + self.draw_by_slice_number: Dict[int, List[Any]] = collections.defaultdict(list) # The layout from slice_data, the first is number of cols, the second # is the number of rows - self.layout = (1, 1) - self.orientation_texts = [] + self.layout: Tuple[int, int] = (1, 1) + self.orientation_texts: List[Any] = [] - self.measures = measures.MeasureData() - self.actors_by_slice_number = collections.defaultdict(list) - self.renderers_by_slice_number = {} + self.measures: measures.MeasureData = measures.MeasureData() + self.actors_by_slice_number: Dict[int, List[Any]] = collections.defaultdict(list) + self.renderers_by_slice_number: Dict[int, Any] = {} - self.orientation = orientation - self.slice_number = 0 - self.scroll_enabled = True + self.orientation: str = orientation + self.slice_number: int = 0 + self.scroll_enabled: bool = True self.__init_gui() - self._brush_cursor_op = const.DEFAULT_BRUSH_OP - self._brush_cursor_size = const.BRUSH_SIZE - self._brush_cursor_colour = const.BRUSH_COLOUR - self._brush_cursor_type = const.DEFAULT_BRUSH_OP - self.cursor = None - self.wl_text = None - self.on_wl = False - self.on_text = False + self._brush_cursor_op: str = const.DEFAULT_BRUSH_OP + self._brush_cursor_size: int = const.BRUSH_SIZE + self._brush_cursor_colour: Tuple[int, int, int] = const.BRUSH_COLOUR + self._brush_cursor_type: str = const.DEFAULT_BRUSH_OP + self.cursor: Optional[Any] = None + self.wl_text: Optional[Any] = None + self.on_wl: bool = False + self.on_text: bool = False # VTK pipeline and actors self.__config_interactor() - self.cross_actor = vtkActor() + self.cross_actor: vtkActor = vtkActor() self.__bind_events() self.__bind_events_wx() - self._flush_buffer = False + self._flush_buffer: bool = False - def __init_gui(self): - self.interactor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize()) + def __init_gui(self) -> None: + self.interactor: wxVTKRenderWindowInteractor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize()) self.interactor.SetRenderWhenDisabled(True) - scroll = wx.ScrollBar(self, -1, style=wx.SB_VERTICAL) - self.scroll = scroll + scroll: wx.ScrollBar = wx.ScrollBar(self, -1, style=wx.SB_VERTICAL) + self.scroll: wx.ScrollBar = scroll - self.mip_ctrls = ContourMIPConfig(self, self.orientation) + self.mip_ctrls: ContourMIPConfig = ContourMIPConfig(self, self.orientation) self.mip_ctrls.Hide() - sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer: wx.BoxSizer = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(self.interactor, 1, wx.EXPAND) sizer.Add(scroll, 0, wx.EXPAND|wx.GROW) - background_sizer = wx.BoxSizer(wx.VERTICAL) + background_sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL) background_sizer.Add(sizer, 1, wx.EXPAND) #background_sizer.Add(self.mip_ctrls, 0, wx.EXPAND|wx.GROW|wx.ALL, 2) self.SetSizer(background_sizer) @@ -261,20 +261,21 @@ def __init_gui(self): self.Update() self.SetAutoLayout(1) - self.pick = vtkWorldPointPicker() + self.pick: vtkWorldPointPicker = vtkWorldPointPicker() self.interactor.SetPicker(self.pick) - def OnContextMenu(self, evt): + def OnContextMenu(self, evt: wx.ContextMenuEvent) -> None: if (self.last_position_mouse_move ==\ self.interactor.GetLastEventPosition()): self.menu.caller = self self.PopupMenu(self.menu) evt.Skip() - def SetPopupMenu(self, menu): - self.menu = menu + def SetPopupMenu(self, menu: Any) -> None: + self.menu: Any = menu - def SetLayout(self, layout): + + def SetLayout(self, layout: Tuple[int, int]) -> None: self.layout = layout if (layout == (1,1)) and self.on_text: self.ShowTextActors() @@ -286,7 +287,7 @@ def SetLayout(self, layout): self.__configure_renderers() self.__configure_scroll() - def HideTextActors(self, change_status=True): + def HideTextActors(self, change_status: bool=True) -> None: try: self.canvas.draw_list.remove(self.wl_text) except (ValueError, AttributeError): @@ -297,23 +298,23 @@ def HideTextActors(self, change_status=True): if change_status: self.on_text = False - def ShowTextActors(self): + def ShowTextActors(self) -> None: if self.on_wl and self.wl_text: self.canvas.draw_list.append(self.wl_text) [self.canvas.draw_list.append(t) for t in self.orientation_texts] self.UpdateCanvas() self.on_text = True - def __set_layout(self, layout): + def __set_layout(self, layout: Tuple[int, int]) -> None: self.SetLayout(layout) - def __config_interactor(self): + def __config_interactor(self) -> None: style = vtkInteractorStyleImage() interactor = self.interactor interactor.SetInteractorStyle(style) - def SetInteractorStyle(self, state): + def SetInteractorStyle(self, state: str) -> None: cleanup = getattr(self.style, 'CleanUp', None) if cleanup: self.style.CleanUp() @@ -332,7 +333,7 @@ def SetInteractorStyle(self, state): self.state = state - def UpdateWindowLevelValue(self, window, level): + def UpdateWindowLevelValue(self, window: float, level: float) -> None: self.acum_achange_window, self.acum_achange_level = (window, level) self.SetWLText(window, level) @@ -342,27 +343,27 @@ def UpdateWindowLevelValue(self, window, level): Publisher.sendMessage('Update all slice') Publisher.sendMessage('Update clut imagedata widget') - def UpdateWindowLevelText(self, window, level): + def UpdateWindowLevelText(self, window: float, level: float) -> None: self.acum_achange_window, self.acum_achange_level = window, level self.SetWLText(window, level) self.interactor.Render() - def OnClutChange(self, evt): + def OnClutChange(self, evt: Any) -> None: Publisher.sendMessage('Change colour table from background image from widget', - nodes=evt.GetNodes()) + nodes=evt.GetNodes()) slc = sl.Slice() Publisher.sendMessage('Update window level value', - window=slc.window_width, - level=slc.window_level) + window=slc.window_width, + level=slc.window_level) - def SetWLText(self, window_width, window_level): + def SetWLText(self, window_width: float, window_level: float) -> None: value = STR_WL%(window_level, window_width) if (self.wl_text): self.wl_text.SetValue(value) self.canvas.modified = True #self.interactor.Render() - def EnableText(self): + def EnableText(self) -> None: if not (self.wl_text): proj = project.Project() colour = const.ORIENTATION_COLOUR[self.orientation] @@ -416,9 +417,9 @@ def EnableText(self): down_text.SetSymbolicSize(wx.FONTSIZE_LARGE) self.orientation_texts = [left_text, right_text, up_text, - down_text] + down_text] - def RenderTextDirection(self, directions): + def RenderTextDirection(self, directions: List[str]) -> None: # Values are on ccw order, starting from the top: self.up_text.SetValue(directions[0]) self.left_text.SetValue(directions[1]) @@ -426,7 +427,7 @@ def RenderTextDirection(self, directions): self.right_text.SetValue(directions[3]) self.interactor.Render() - def ResetTextDirection(self, cam): + def ResetTextDirection(self, cam: Any) -> None: # Values are on ccw order, starting from the top: if self.orientation == 'AXIAL': values = [_("A"), _("R"), _("P"), _("L")] @@ -438,7 +439,7 @@ def ResetTextDirection(self, cam): self.RenderTextDirection(values) self.interactor.Render() - def UpdateTextDirection(self, cam): + def UpdateTextDirection(self, cam: Any) -> None: croll = cam.GetRoll() if (self.orientation == 'AXIAL'): @@ -449,13 +450,13 @@ def UpdateTextDirection(self, cam): self.RenderTextDirection([_("AL"), _("RA"), _("PR"), _("LP")]) elif(croll > 44 and croll <= 88): - self.RenderTextDirection([_("LA"), _("AR"), _("RP"), _("PL")]) + self.RenderTextDirection([_("LA"), _("AR"), _("RP"), _("PL")]) elif(croll > 89 and croll <= 91): - self.RenderTextDirection([_("L"), _("A"), _("R"), _("P")]) + self.RenderTextDirection([_("L"), _("A"), _("R"), _("P")]) elif(croll > 91 and croll <= 135): - self.RenderTextDirection([_("LP"), _("AL"), _("RA"), _("PR")]) + self.RenderTextDirection([_("LP"), _("AL"), _("RA"), _("PR")]) elif(croll > 135 and croll <= 177): self.RenderTextDirection([_("PL"), _("LA"), _("AR"), _("RP")]) @@ -487,13 +488,13 @@ def UpdateTextDirection(self, cam): self.RenderTextDirection([_("TL"), _("RT"), _("BR"), _("LB")]) elif(croll > 44 and croll <= 88): - self.RenderTextDirection([_("LT"), _("TR"), _("RB"), _("BL")]) + self.RenderTextDirection([_("LT"), _("TR"), _("RB"), _("BL")]) elif(croll > 89 and croll <= 91): - self.RenderTextDirection([_("L"), _("T"), _("R"), _("B")]) + self.RenderTextDirection([_("L"), _("T"), _("R"), _("B")]) elif(croll > 91 and croll <= 135): - self.RenderTextDirection([_("LB"), _("TL"), _("RT"), _("BR")]) + self.RenderTextDirection([_("LB"), _("TL"), _("RT"), _("BR")]) elif(croll > 135 and croll <= 177): self.RenderTextDirection([_("BL"), _("LT"), _("TR"), _("RB")]) @@ -534,13 +535,13 @@ def UpdateTextDirection(self, cam): self.RenderTextDirection([_("AB"), _("TA"), _("PT"), _("BP")]) elif(croll > 44 and croll <= 88): - self.RenderTextDirection([_("BA"), _("AT"), _("TP"), _("PB")]) + self.RenderTextDirection([_("BA"), _("AT"), _("TP"), _("PB")]) elif(croll > 89 and croll <= 91): - self.RenderTextDirection([_("B"), _("A"), _("T"), _("P")]) + self.RenderTextDirection([_("B"), _("A"), _("T"), _("P")]) elif(croll > 91 and croll <= 135): - self.RenderTextDirection([_("BP"), _("AB"), _("TA"), _("PT")]) + self.RenderTextDirection([_("BP"), _("AB"), _("TA"), _("PT")]) elif(croll > 135 and croll <= 177): self.RenderTextDirection([_("PB"), _("BA"), _("AT"), _("TP")]) @@ -555,7 +556,7 @@ def UpdateTextDirection(self, cam): self.RenderTextDirection([_("TP"), _("PB"), _("BA"), _("AT")]) - def Reposition(self, slice_data): + def Reposition(self, slice_data: Any) -> None: """ Based on code of method Zoom in the vtkInteractorStyleRubberBandZoom, the of @@ -563,42 +564,42 @@ def Reposition(self, slice_data): """ ren = slice_data.renderer size = ren.GetSize() - + ren.ResetCamera() ren.GetActiveCamera().Zoom(1.0) self.interactor.Render() - - def ChangeBrushColour(self, colour): - vtk_colour = colour + + def ChangeBrushColour(self, colour: Tuple[int, int, int]) -> None: + vtk_colour: Tuple[int, int, int] = colour self._brush_cursor_colour = vtk_colour if (self.cursor): for slice_data in self.slice_data_list: slice_data.cursor.SetColour(vtk_colour) - - def SetBrushColour(self, colour): - colour_vtk = [colour/float(255) for colour in colour] + + def SetBrushColour(self, colour: Tuple[int, int, int]) -> None: + colour_vtk: list[float] = [colour/float(255) for colour in colour] self._brush_cursor_colour = colour_vtk if self.slice_data.cursor: self.slice_data.cursor.SetColour(colour_vtk) - - def UpdateSlicesPosition(self, position): + + def UpdateSlicesPosition(self, position: Tuple[float, float, float]) -> None: # Get point from base change px, py = self.get_slice_pixel_coord_by_world_pos(*position) - coord = self.calcultate_scroll_position(px, py) + coord: Tuple[int, int, int] = self.calcultate_scroll_position(px, py) # Debugging coordinates. For a 1.0 spacing axis the coord and position is the same, # but for a spacing dimension =! 1, the coord and position are different # print("\nPosition: {}".format(position)) # print("Scroll position: {}".format(coord)) # print("Slice actor bounds: {}".format(self.slice_data.actor.GetBounds())) # print("Scroll from int of position: {}\n".format([round(s) for s in position])) - + # this call did not affect the working code # self.cross.SetFocalPoint(coord) - + # update the image slices in all three orientations self.ScrollSlice(coord) - - def SetCrossFocalPoint(self, position): + + def SetCrossFocalPoint(self, position: List[float]) -> None: """ Sets the cross focal point for all slice panels (axial, coronal, sagittal). This function is also called via pubsub messaging and may receive a list of 6 coordinates. Thus, limiting the number of list elements in the @@ -606,8 +607,8 @@ def SetCrossFocalPoint(self, position): :param position: list of 6 coordinates in vtk world coordinate system wx, wy, wz """ self.cross.SetFocalPoint(position[:3]) - - def ScrollSlice(self, coord): + + def ScrollSlice(self, coord: Tuple[int, int, int]) -> None: if self.orientation == "AXIAL": wx.CallAfter(Publisher.sendMessage, ('Set scroll position', 'SAGITAL'), index=coord[0]) @@ -623,36 +624,36 @@ def ScrollSlice(self, coord): index=coord[2]) wx.CallAfter(Publisher.sendMessage, ('Set scroll position', 'SAGITAL'), index=coord[0]) - - def get_slice_data(self, render): + + def get_slice_data(self, render: Any) -> Any: #for slice_data in self.slice_data_list: #if slice_data.renderer is render: #return slice_data # WARN: Return the only slice_data used in this slice_viewer. return self.slice_data - - def calcultate_scroll_position(self, x, y): + + def calcultate_scroll_position(self, x: float, y: float) -> Tuple[int, int, int]: # Based in the given coord (x, y), returns a list with the scroll positions for each # orientation, being the first position the sagital, second the coronal # and the last, axial. if self.orientation == 'AXIAL': axial = self.slice_data.number - coronal = y - sagital = x - + coronal: float = y + sagital: float = x + elif self.orientation == 'CORONAL': - axial = y + axial: float = y coronal = self.slice_data.number sagital = x - + elif self.orientation == 'SAGITAL': axial = y coronal = x sagital = self.slice_data.number - + return sagital, coronal, axial - - def calculate_matrix_position(self, coord): + + def calculate_matrix_position(self, coord: Tuple[float, float, float]) -> Tuple[int, int]: x, y, z = coord xi, xf, yi, yf, zi, zf = self.slice_data.actor.GetBounds() if self.orientation == 'AXIAL': @@ -665,8 +666,8 @@ def calculate_matrix_position(self, coord): mx = round((y - yi)/self.slice_.spacing[1], 0) my = round((z - zi)/self.slice_.spacing[2], 0) return int(mx), int(my) - - def get_vtk_mouse_position(self): + + def get_vtk_mouse_position(self) -> Tuple[int, int]: """ Get Mouse position inside a wxVTKRenderWindowInteractorself. Return a tuple with X and Y position. @@ -686,13 +687,14 @@ def get_vtk_mouse_position(self): mx *= scale my *= scale return int(mx), int(my) + - def get_coordinate_cursor(self, mx, my, picker=None): + def get_coordinate_cursor(self, mx: int, my: int, picker=None) -> tuple: """ Given the mx, my screen position returns the x, y, z position in world coordinates. - Parameters + Parameters: mx (int): x position. my (int): y position picker: the picker used to get calculate the voxel coordinate. @@ -717,7 +719,7 @@ def get_coordinate_cursor(self, mx, my, picker=None): z = bounds[4] return x, y, z - def get_coordinate_cursor_edition(self, slice_data=None, picker=None): + def get_coordinate_cursor_edition(self, slice_data=None, picker=None) -> tuple: # Find position if slice_data is None: slice_data = self.slice_data @@ -756,7 +758,7 @@ def get_coordinate_cursor_edition(self, slice_data=None, picker=None): return x, y, z - def get_voxel_coord_by_screen_pos(self, mx, my, picker=None): + def get_voxel_coord_by_screen_pos(self, mx: int, my: int, picker=None) -> tuple: """ Given the (mx, my) screen position returns the voxel coordinate of the volume at (that mx, my) position. @@ -778,7 +780,7 @@ def get_voxel_coord_by_screen_pos(self, mx, my, picker=None): return (x, y, z) - def get_voxel_coord_by_world_pos(self, wx, wy, wz): + def get_voxel_coord_by_world_pos(self, wx: float, wy: float, wz: float) -> tuple: """ Given the (x, my) screen position returns the voxel coordinate of the volume at (that mx, my) position. @@ -796,79 +798,88 @@ def get_voxel_coord_by_world_pos(self, wx, wy, wz): x, y, z = self.calcultate_scroll_position(px, py) return (int(x), int(y), int(z)) + - def get_slice_pixel_coord_by_screen_pos(self, mx, my, picker=None): + def get_slice_pixel_coord_by_screen_pos(self, mx: int, my: int, picker: Optional[Any] = None) -> Tuple[int, int]: """ Given the (mx, my) screen position returns the pixel coordinate of the slice at (that mx, my) position. - + Parameters: mx (int): x position. my (int): y position picker: the picker used to get calculate the pixel coordinate. - + Returns: voxel_coordinate (x, y): voxel coordinate inside the matrix. Can be used to access the voxel value inside the matrix. """ if picker is None: picker = self.pick - + wx, wy, wz = self.get_coordinate_cursor(mx, my, picker) x, y = self.get_slice_pixel_coord_by_world_pos(wx, wy, wz) return int(x), int(y) - - def get_slice_pixel_coord_by_world_pos(self, wx, wy, wz): + + + def get_slice_pixel_coord_by_world_pos(self, wx: float, wy: float, wz: float) -> Tuple[float, float]: """ Given the (wx, wy, wz) world position returns the pixel coordinate of the slice at (that mx, my) position. - + Parameters: - mx (int): x position. - my (int): y position - picker: the picker used to get calculate the pixel coordinate. - + wx (float): x position. + wy (float): y position + wz (float): z position + Returns: voxel_coordinate (x, y): voxel coordinate inside the matrix. Can be used to access the voxel value inside the matrix. """ coord = wx, wy, wz px, py = self.calculate_matrix_position(coord) - + return px, py - - def get_coord_inside_volume(self, mx, my, picker=None): + + + def get_coord_inside_volume(self, mx: int, my: int, picker: Optional[Any] = None) -> Tuple[float, float, float]: + """ + Given the (mx, my) screen position returns the world coordinate + of the slice at (that mx, my) position. + + Parameters: + mx (int): x position. + my (int): y position + picker: the picker used to get calculate the pixel coordinate. + + Returns: + coord (x, y, z): world coordinate inside the volume. Can + be used to access the voxel value inside the volume. + """ if picker is None: picker = self.pick - + slice_data = self.slice_data renderer = slice_data.renderer - + coord = self.get_coordinate_cursor(picker) position = slice_data.actor.GetInput().FindPoint(coord) - + if position != -1: coord = slice_data.actor.GetInput().GetPoint(position) - + return coord - - def __bind_events(self): - Publisher.subscribe(self.LoadImagedata, - 'Load slice to viewer') - Publisher.subscribe(self.SetBrushColour, - 'Change mask colour') - Publisher.subscribe(self.UpdateRender, - 'Update slice viewer') - Publisher.subscribe(self.UpdateRender, - 'Update slice viewer %s' % self.orientation) - Publisher.subscribe(self.UpdateCanvas, - 'Redraw canvas') - Publisher.subscribe(self.UpdateCanvas, - 'Redraw canvas %s' % self.orientation) - Publisher.subscribe(self.ChangeSliceNumber, - ('Set scroll position', - self.orientation)) + + + def __bind_events(self) -> None: + Publisher.subscribe(self.LoadImagedata, 'Load slice to viewer') + Publisher.subscribe(self.SetBrushColour, 'Change mask colour') + Publisher.subscribe(self.UpdateRender, 'Update slice viewer') + Publisher.subscribe(self.UpdateRender, 'Update slice viewer %s' % self.orientation) + Publisher.subscribe(self.UpdateCanvas, 'Redraw canvas') + Publisher.subscribe(self.UpdateCanvas, 'Redraw canvas %s' % self.orientation) + Publisher.subscribe(self.ChangeSliceNumber, ('Set scroll position', self.orientation)) # Publisher.subscribe(self.__update_cross_position, # 'Update cross position') # Publisher.subscribe(self.__update_cross_position, @@ -878,85 +889,77 @@ def __bind_events(self): ### # Publisher.subscribe(self.ChangeBrushColour, # 'Add mask') - - Publisher.subscribe(self.UpdateWindowLevelValue, - 'Update window level value') - - Publisher.subscribe(self.UpdateWindowLevelText, - 'Update window level text') - - Publisher.subscribe(self._set_cross_visibility, - 'Set cross visibility') - - Publisher.subscribe(self.__set_layout, - 'Set slice viewer layout') - - Publisher.subscribe(self.OnSetInteractorStyle, - 'Set slice interaction style') + + Publisher.subscribe(self.UpdateWindowLevelValue, 'Update window level value') + + Publisher.subscribe(self.UpdateWindowLevelText, 'Update window level text') + + Publisher.subscribe(self._set_cross_visibility, 'Set cross visibility') + + Publisher.subscribe(self.__set_layout, 'Set slice viewer layout') + + Publisher.subscribe(self.OnSetInteractorStyle, 'Set slice interaction style') Publisher.subscribe(self.OnCloseProject, 'Close project data') - + ##### - Publisher.subscribe(self.OnShowText, - 'Show text actors on viewers') - Publisher.subscribe(self.OnHideText, - 'Hide text actors on viewers') + Publisher.subscribe(self.OnShowText, 'Show text actors on viewers') + Publisher.subscribe(self.OnHideText, 'Hide text actors on viewers') Publisher.subscribe(self.OnExportPicture,'Export picture to file') Publisher.subscribe(self.SetDefaultCursor, 'Set interactor default cursor') - + Publisher.subscribe(self.SetSizeNSCursor, 'Set interactor resize NS cursor') Publisher.subscribe(self.SetSizeWECursor, 'Set interactor resize WE cursor') Publisher.subscribe(self.SetSizeNWSECursor, 'Set interactor resize NSWE cursor') - + Publisher.subscribe(self.AddActors, 'Add actors ' + str(ORIENTATIONS[self.orientation])) Publisher.subscribe(self.RemoveActors, 'Remove actors ' + str(ORIENTATIONS[self.orientation])) Publisher.subscribe(self.OnSwapVolumeAxes, 'Swap volume axes') - + Publisher.subscribe(self.ReloadActualSlice, 'Reload actual slice') Publisher.subscribe(self.ReloadActualSlice, 'Reload actual slice %s' % self.orientation) Publisher.subscribe(self.OnUpdateScroll, 'Update scroll') - - + + # MIP Publisher.subscribe(self.OnSetMIPSize, 'Set MIP size %s' % self.orientation) Publisher.subscribe(self.OnSetMIPBorder, 'Set MIP border %s' % self.orientation) Publisher.subscribe(self.OnSetMIPInvert, 'Set MIP Invert %s' % self.orientation) Publisher.subscribe(self.OnShowMIPInterface, 'Show MIP interface') - + Publisher.subscribe(self.OnSetOverwriteMask, "Set overwrite mask") - + Publisher.subscribe(self.RefreshViewer, "Refresh viewer") Publisher.subscribe(self.SetInterpolatedSlices, "Set interpolated slices") Publisher.subscribe(self.UpdateInterpolatedSlice, "Update Slice Interpolation") - + Publisher.subscribe(self.GetCrossPos, "Set Update cross pos") Publisher.subscribe(self.UpdateCross, "Update cross pos") - - def RefreshViewer(self): + def RefreshViewer(self) -> None: self.Refresh() - def SetDefaultCursor(self): + def SetDefaultCursor(self) -> None: self.interactor.SetCursor(wx.Cursor(wx.CURSOR_DEFAULT)) - def SetSizeNSCursor(self): + def SetSizeNSCursor(self) -> None: self.interactor.SetCursor(wx.Cursor(wx.CURSOR_SIZENS)) - def SetSizeWECursor(self): + def SetSizeWECursor(self) -> None: self.interactor.SetCursor(wx.Cursor(wx.CURSOR_SIZEWE)) - def SetSizeNWSECursor(self): + def SetSizeNWSECursor(self) -> None: if sys.platform.startswith('linux'): self.interactor.SetCursor(wx.Cursor(wx.CURSOR_SIZENWSE)) else: self.interactor.SetCursor(wx.Cursor(wx.CURSOR_SIZING)) - def SetFocus(self): + def SetFocus(self) -> None: Publisher.sendMessage('Set viewer orientation focus', - orientation=self.orientation) + orientation=self.orientation) super().SetFocus() - def OnExportPicture(self, orientation, filename, filetype): - dict = {"AXIAL": const.AXIAL, + def OnExportPicture(self, orientation: str, filename: str, filetype: str) -> None: + dict: dict[str, int] = {"AXIAL": const.AXIAL, "CORONAL": const.CORONAL, "SAGITAL": const.SAGITAL} @@ -964,16 +967,16 @@ def OnExportPicture(self, orientation, filename, filetype): Publisher.sendMessage('Begin busy cursor') if _has_win32api: utils.touch(filename) - win_filename = win32api.GetShortPathName(filename) + win_filename: str = win32api.GetShortPathName(filename) self._export_picture(orientation, win_filename, filetype) else: self._export_picture(orientation, filename, filetype) Publisher.sendMessage('End busy cursor') - def _export_picture(self, id, filename, filetype): + def _export_picture(self, id: str, filename: str, filetype: str) -> None: view_prop_list = [] - dict = {"AXIAL": const.AXIAL, + dict: dict[str, int] = {"AXIAL": const.AXIAL, "CORONAL": const.CORONAL, "SAGITAL": const.SAGITAL} @@ -1022,16 +1025,16 @@ def _export_picture(self, id, filename, filetype): Publisher.sendMessage('End busy cursor') - def OnShowText(self): + def OnShowText(self) -> None: self.ShowTextActors() - def OnHideText(self): + def OnHideText(self) -> None: self.HideTextActors() - def OnCloseProject(self): + def OnCloseProject(self) -> None: self.CloseProject() - def CloseProject(self): + def CloseProject(self) -> None: for slice_data in self.slice_data_list: del slice_data @@ -1052,39 +1055,40 @@ def CloseProject(self): self.cursor = None self.wl_text = None self.pick = vtkWorldPointPicker() + - def OnSetInteractorStyle(self, style): + def OnSetInteractorStyle(self, style: str) -> None: self.SetInteractorStyle(style) - + if (style not in [const.SLICE_STATE_EDITOR, const.SLICE_STATE_WATERSHED]): Publisher.sendMessage('Set interactor default cursor') - - def __bind_events_wx(self): + + def __bind_events_wx(self) -> None: self.scroll.Bind(wx.EVT_SCROLL, self.OnScrollBar) self.scroll.Bind(wx.EVT_SCROLL_THUMBTRACK, self.OnScrollBarRelease) #self.scroll.Bind(wx.EVT_SCROLL_ENDSCROLL, self.OnScrollBarRelease) self.interactor.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) self.interactor.Bind(wx.EVT_RIGHT_UP, self.OnContextMenu) - - def LoadImagedata(self, mask_dict): + + def LoadImagedata(self, mask_dict: dict) -> None: self.SetInput(mask_dict) - - def LoadRenderers(self, imagedata): - number_renderers = self.layout[0] * self.layout[1] - diff = number_renderers - len(self.slice_data_list) + + def LoadRenderers(self, imagedata: Any) -> None: + number_renderers: int = self.layout[0] * self.layout[1] + diff: int = number_renderers - len(self.slice_data_list) if diff > 0: for i in range(diff): slice_data = self.create_slice_window(imagedata) self.slice_data_list.append(slice_data) elif diff < 0: - to_remove = self.slice_data_list[number_renderers::] + to_remove: list[Any] = self.slice_data_list[number_renderers::] for slice_data in to_remove: self.interactor.GetRenderWindow().RemoveRenderer(slice_data.renderer) self.slice_data_list = self.slice_data_list[:number_renderers] - - def __configure_renderers(self): - proportion_x = 1.0 / self.layout[0] - proportion_y = 1.0 / self.layout[1] + + def __configure_renderers(self) -> None: + proportion_x: float = 1.0 / self.layout[0] + proportion_y: float = 1.0 / self.layout[1] # The (0,0) in VTK is in bottom left. So the creation from renderers # must be # in inverted order, from the top left to bottom right w, h = self.interactor.GetRenderWindow().GetSize() @@ -1093,12 +1097,12 @@ def __configure_renderers(self): n = 0 for j in range(self.layout[1]-1, -1, -1): for i in range(self.layout[0]): - slice_xi = i*proportion_x - slice_xf = (i+1)*proportion_x - slice_yi = j*proportion_y - slice_yf = (j+1)*proportion_y - - position = (slice_xi, slice_yi, slice_xf, slice_yf) + slice_xi: float = i*proportion_x + slice_xf: float = (i+1)*proportion_x + slice_yi: float = j*proportion_y + slice_yf: float = (j+1)*proportion_y + + position: tuple[float, float, float, float] = (slice_xi, slice_yi, slice_xf, slice_yf) slice_data = self.slice_data_list[n] slice_data.renderer.SetViewport(position) # Text actor position @@ -1107,22 +1111,22 @@ def __configure_renderers(self): slice_data.SetCursor(self.__create_cursor()) # slice_data.SetSize((w, h)) self.__update_camera(slice_data) - + style = 0 if j == 0: - style = style | sd.BORDER_DOWN + style: int = style | sd.BORDER_DOWN if j == self.layout[1] - 1: style = style | sd.BORDER_UP - + if i == 0: style = style | sd.BORDER_LEFT if i == self.layout[0] - 1: style = style | sd.BORDER_RIGHT - + # slice_data.SetBorderStyle(style) n += 1 - - def __create_cursor(self): + + def __create_cursor(self) -> ca.CursorCircle: cursor = ca.CursorCircle() cursor.SetOrientation(self.orientation) #self.__update_cursor_position([i for i in actor_bound[1::2]]) @@ -1131,22 +1135,22 @@ def __create_cursor(self): cursor.Show(0) self.cursor_ = cursor return cursor - - def SetInput(self, mask_dict): + + def SetInput(self, mask_dict: dict) -> None: self.slice_ = sl.Slice() - - max_slice_number = sl.Slice().GetNumberOfSlices(self.orientation) + + max_slice_number: int = sl.Slice().GetNumberOfSlices(self.orientation) self.scroll.SetScrollbar(wx.SB_VERTICAL, 1, max_slice_number, max_slice_number) - + self.slice_data = self.create_slice_window() self.slice_data.SetCursor(self.__create_cursor()) self.cam = self.slice_data.renderer.GetActiveCamera() self.__build_cross_lines() - + self.canvas = CanvasRendererCTX(self, self.slice_data.renderer, self.slice_data.canvas_renderer, self.orientation) self.canvas.draw_list.append(self.slice_data) - + # Set the slice number to the last slice to ensure the camera if far # enough to show all slices. self.set_slice_number(max_slice_number - 1) @@ -1154,30 +1158,30 @@ def SetInput(self, mask_dict): self.slice_data.renderer.ResetCamera() self.interactor.GetRenderWindow().AddRenderer(self.slice_data.renderer) self.interactor.Render() - + self.EnableText() self.wl_text.Hide() ## Insert cursor self.SetInteractorStyle(const.STATE_DEFAULT) - - def __build_cross_lines(self): + + def __build_cross_lines(self) -> None: renderer = self.slice_data.overlay_renderer - + cross = vtkCursor3D() cross.AllOff() cross.AxesOn() self.cross = cross - + c = vtkCoordinate() c.SetCoordinateSystemToWorld() - + cross_mapper = vtkPolyDataMapper() cross_mapper.SetInputConnection(cross.GetOutputPort()) #cross_mapper.SetTransformCoordinate(c) - + p = vtkProperty() p.SetColor(1, 0, 0) - + cross_actor = vtkActor() cross_actor.SetMapper(cross_mapper) cross_actor.SetProperty(p) @@ -1185,26 +1189,23 @@ def __build_cross_lines(self): # Only the slices are pickable cross_actor.PickableOff() self.cross_actor = cross_actor - + renderer.AddActor(cross_actor) - - # def __update_cross_position(self, arg, position): - # # self.cross.SetFocalPoint(position[:3]) - # self.UpdateSlicesPosition(None, position) - - def _set_cross_visibility(self, visibility): + + def _set_cross_visibility(self, visibility: bool) -> None: self.cross_actor.SetVisibility(visibility) - - def _set_editor_cursor_visibility(self, visibility): + + def _set_editor_cursor_visibility(self, visibility: bool) -> None: for slice_data in self.slice_data_list: slice_data.cursor.actor.SetVisibility(visibility) + - def SetOrientation(self, orientation): + def SetOrientation(self, orientation: int) -> None: self.orientation = orientation for slice_data in self.slice_data_list: self.__update_camera(slice_data) - def create_slice_window(self): + def create_slice_window(self) -> sd.SliceData: renderer = vtkRenderer() renderer.SetLayer(0) cam = renderer.GetActiveCamera() @@ -1229,7 +1230,7 @@ def create_slice_window(self): self.slice_actor = actor # TODO: Create a option to let the user set if he wants to interpolate # the slice images. - + session = ses.Session() if session.GetConfig('slice_interpolation'): actor.InterpolateOff() @@ -1242,15 +1243,15 @@ def create_slice_window(self): slice_data.canvas_renderer = canvas_renderer slice_data.overlay_renderer = overlay_renderer slice_data.actor = actor - # slice_data.SetBorderStyle(sd.BORDER_ALL) + renderer.AddActor(actor) # renderer.AddActor(slice_data.text.actor) # renderer.AddViewProp(slice_data.box_actor) return slice_data - def UpdateInterpolatedSlice(self): - if self.slice_actor != None: + def UpdateInterpolatedSlice(self) -> None: + if self.slice_actor is not None: session = ses.Session() if session.GetConfig('slice_interpolation'): self.slice_actor.InterpolateOff() @@ -1259,17 +1260,17 @@ def UpdateInterpolatedSlice(self): self.interactor.Render() - def SetInterpolatedSlices(self, flag): + def SetInterpolatedSlices(self, flag: bool) -> None: self.interpolation_slice_status = flag - if self.slice_actor != None: + if self.slice_actor is not None: if self.interpolation_slice_status == True: self.slice_actor.InterpolateOn() else: self.slice_actor.InterpolateOff() self.interactor.Render() - def __update_camera(self): - orientation = self.orientation + def __update_camera(self, slice_data: sd.SliceData) -> None: + orientation: str = self.orientation proj = project.Project() orig_orien = proj.original_orientation @@ -1280,21 +1281,22 @@ def __update_camera(self): #self.cam.OrthogonalizeViewUp() self.cam.ParallelProjectionOn() - def __update_display_extent(self, image): + def __update_display_extent(self, image: vtkImageData) -> None: self.slice_data.actor.SetDisplayExtent(image.GetExtent()) self.slice_data.renderer.ResetCameraClippingRange() - def UpdateRender(self): + def UpdateRender(self) -> None: self.interactor.Render() - def UpdateCanvas(self, evt=None): + def UpdateCanvas(self, evt: Any = None) -> None: if self.canvas is not None: self._update_draw_list() self.canvas.modified = True self.interactor.Render() + - def _update_draw_list(self): - cp_draw_list = self.canvas.draw_list[:] + def _update_draw_list(self) -> None: + cp_draw_list: List = self.canvas.draw_list[:] self.canvas.draw_list = [] # Removing all measures @@ -1307,44 +1309,44 @@ def _update_draw_list(self): if m.visible: self.canvas.draw_list.append(mr) - n = self.slice_data.number + n: int = self.slice_data.number self.canvas.draw_list.extend(self.draw_by_slice_number[n]) - def __configure_scroll(self): - actor = self.slice_data_list[0].actor - number_of_slices = self.layout[0] * self.layout[1] - max_slice_number = actor.GetSliceNumberMax()/ \ + def __configure_scroll(self) -> None: + actor: Any = self.slice_data_list[0].actor + number_of_slices: int = self.layout[0] * self.layout[1] + max_slice_number: int = actor.GetSliceNumberMax()/ \ number_of_slices if actor.GetSliceNumberMax()% number_of_slices: max_slice_number += 1 self.scroll.SetScrollbar(wx.SB_VERTICAL, 1, max_slice_number, - max_slice_number) + max_slice_number) self.set_scroll_position(0) @property - def number_slices(self): + def number_slices(self) -> int: return self._number_slices @number_slices.setter - def number_slices(self, val): + def number_slices(self, val: int) -> None: if val != self._number_slices: self._number_slices = val - buffer_ = self.slice_.buffer_slices[self.orientation] + buffer_: Any = self.slice_.buffer_slices[self.orientation] buffer_.discard_buffer() - def set_scroll_position(self, position): + def set_scroll_position(self, position: int) -> None: self.scroll.SetThumbPosition(position) self.OnScrollBar() - def UpdateSlice3D(self, pos): - original_orientation = project.Project().original_orientation - pos = self.scroll.GetThumbPosition() + def UpdateSlice3D(self, pos: int) -> None: + original_orientation: Any = project.Project().original_orientation + pos: int = self.scroll.GetThumbPosition() Publisher.sendMessage('Change slice from slice plane', - orientation=self.orientation, index=pos) + orientation=self.orientation, index=pos) - def OnScrollBar(self, evt=None, update3D=True): - pos = self.scroll.GetThumbPosition() + def OnScrollBar(self, evt: Any = None, update3D: bool = True) -> None: + pos: int = self.scroll.GetThumbPosition() self.set_slice_number(pos) if update3D: self.UpdateSlice3D(pos) @@ -1364,24 +1366,24 @@ def OnScrollBar(self, evt=None, update3D=True): self.slice_.apply_slice_buffer_to_mask(self.orientation) evt.Skip() - def OnScrollBarRelease(self, evt): - pos = self.scroll.GetThumbPosition() + def OnScrollBarRelease(self, evt: Any) -> None: + pos: int = self.scroll.GetThumbPosition() evt.Skip() - def OnKeyDown(self, evt=None, obj=None): - pos = self.scroll.GetThumbPosition() - skip = True + def OnKeyDown(self, evt: Any = None, obj: Any = None) -> None: + pos: int = self.scroll.GetThumbPosition() + skip: bool = True - min = 0 - max = self.slice_.GetMaxSliceNumber(self.orientation) + min: int = 0 + max: int = self.slice_.GetMaxSliceNumber(self.orientation) - projections = {wx.WXK_NUMPAD0 : const.PROJECTION_NORMAL, - wx.WXK_NUMPAD1 : const.PROJECTION_MaxIP, - wx.WXK_NUMPAD2 : const.PROJECTION_MinIP, - wx.WXK_NUMPAD3 : const.PROJECTION_MeanIP, - wx.WXK_NUMPAD4 : const.PROJECTION_MIDA, - wx.WXK_NUMPAD5 : const.PROJECTION_CONTOUR_MIP, - wx.WXK_NUMPAD6 : const.PROJECTION_CONTOUR_MIDA,} + projections: Dict = {wx.WXK_NUMPAD0 : const.PROJECTION_NORMAL, + wx.WXK_NUMPAD1 : const.PROJECTION_MaxIP, + wx.WXK_NUMPAD2 : const.PROJECTION_MinIP, + wx.WXK_NUMPAD3 : const.PROJECTION_MeanIP, + wx.WXK_NUMPAD4 : const.PROJECTION_MIDA, + wx.WXK_NUMPAD5 : const.PROJECTION_CONTOUR_MIP, + wx.WXK_NUMPAD6 : const.PROJECTION_CONTOUR_MIDA,} if self._flush_buffer: self.slice_.apply_slice_buffer_to_mask(self.orientation) @@ -1397,7 +1399,7 @@ def OnKeyDown(self, evt=None, obj=None): skip = False elif (evt.GetKeyCode() == wx.WXK_NUMPAD_ADD): - actual_value = self.mip_ctrls.mip_size_spin.GetValue() + actual_value: int = self.mip_ctrls.mip_size_spin.GetValue() self.mip_ctrls.mip_size_spin.SetValue(actual_value + 1) if self.mip_ctrls.mip_size_spin.GetValue() != actual_value: self.number_slices = self.mip_ctrls.mip_size_spin.GetValue() @@ -1405,7 +1407,7 @@ def OnKeyDown(self, evt=None, obj=None): skip = False elif (evt.GetKeyCode() == wx.WXK_NUMPAD_SUBTRACT): - actual_value = self.mip_ctrls.mip_size_spin.GetValue() + actual_value: int = self.mip_ctrls.mip_size_spin.GetValue() self.mip_ctrls.mip_size_spin.SetValue(actual_value - 1) if self.mip_ctrls.mip_size_spin.GetValue() != actual_value: self.number_slices = self.mip_ctrls.mip_size_spin.GetValue() @@ -1423,12 +1425,13 @@ def OnKeyDown(self, evt=None, obj=None): if evt and skip: evt.Skip() + - def OnScrollForward(self, evt=None, obj=None): + def OnScrollForward(self, evt=None, obj=None) -> None: if not self.scroll_enabled: return - pos = self.scroll.GetThumbPosition() - min = 0 + pos: int = self.scroll.GetThumbPosition() + min: int = 0 if(pos > min): if self._flush_buffer: @@ -1437,11 +1440,11 @@ def OnScrollForward(self, evt=None, obj=None): self.scroll.SetThumbPosition(pos) self.OnScrollBar() - def OnScrollBackward(self, evt=None, obj=None): + def OnScrollBackward(self, evt=None, obj=None) -> None: if not self.scroll_enabled: return - pos = self.scroll.GetThumbPosition() - max = self.slice_.GetMaxSliceNumber(self.orientation) + pos: int = self.scroll.GetThumbPosition() + max: int = self.slice_.GetMaxSliceNumber(self.orientation) if(pos < max): if self._flush_buffer: @@ -1450,23 +1453,23 @@ def OnScrollBackward(self, evt=None, obj=None): self.scroll.SetThumbPosition(pos) self.OnScrollBar() - def OnSetMIPSize(self, number_slices): + def OnSetMIPSize(self, number_slices: int) -> None: self.number_slices = number_slices self.ReloadActualSlice() - def OnSetMIPBorder(self, border_size): + def OnSetMIPBorder(self, border_size: int) -> None: self.slice_.n_border = border_size - buffer_ = self.slice_.buffer_slices[self.orientation] + buffer_: sl.SliceBuffer = self.slice_.buffer_slices[self.orientation] buffer_.discard_buffer() self.ReloadActualSlice() - def OnSetMIPInvert(self, invert): + def OnSetMIPInvert(self, invert: bool) -> None: self._mip_inverted = invert - buffer_ = self.slice_.buffer_slices[self.orientation] + buffer_: sl.SliceBuffer = self.slice_.buffer_slices[self.orientation] buffer_.discard_buffer() self.ReloadActualSlice() - def OnShowMIPInterface(self, flag): + def OnShowMIPInterface(self, flag: bool) -> None: if flag: if not self.mip_ctrls.Shown: self.mip_ctrls.Show() @@ -1477,19 +1480,19 @@ def OnShowMIPInterface(self, flag): self.GetSizer().Detach(self.mip_ctrls) self.Layout() - def OnSetOverwriteMask(self, flag): + def OnSetOverwriteMask(self, flag: bool) -> None: self.overwrite_mask = flag - def set_slice_number(self, index): - max_slice_number = sl.Slice().GetNumberOfSlices(self.orientation) - index = max(index, 0) - index = min(index, max_slice_number - 1) - inverted = self.mip_ctrls.inverted.GetValue() - border_size = self.mip_ctrls.border_spin.GetValue() + def set_slice_number(self, index: int) -> None: + max_slice_number: int = sl.Slice().GetNumberOfSlices(self.orientation) + index: int = max(index, 0) + index: int = min(index, max_slice_number - 1) + inverted: bool = self.mip_ctrls.inverted.GetValue() + border_size: int = self.mip_ctrls.border_spin.GetValue() try: - image = self.slice_.GetSlices(self.orientation, index, - self.number_slices, inverted, - border_size) + image: np.ndarray = self.slice_.GetSlices(self.orientation, index, + self.number_slices, inverted, + border_size) except IndexError: return self.slice_data.actor.SetInputData(image) @@ -1512,34 +1515,35 @@ def set_slice_number(self, index): if self.slice_._type_projection == const.PROJECTION_NORMAL: self.slice_data.SetNumber(index) else: - max_slices = self.slice_.GetMaxSliceNumber(self.orientation) - end = min(max_slices, index + self.number_slices - 1) + max_slices: int = self.slice_.GetMaxSliceNumber(self.orientation) + end: int = min(max_slices, index + self.number_slices - 1) self.slice_data.SetNumber(index, end) self.__update_display_extent(image) self.cross.SetModelBounds(self.slice_data.actor.GetBounds()) self._update_draw_list() - def ChangeSliceNumber(self, index): + def ChangeSliceNumber(self, index: int) -> None: self.scroll.SetThumbPosition(int(index)) - pos = self.scroll.GetThumbPosition() + pos: int = self.scroll.GetThumbPosition() self.set_slice_number(pos) - def ReloadActualSlice(self): - pos = self.scroll.GetThumbPosition() + def ReloadActualSlice(self) -> None: + pos: int = self.scroll.GetThumbPosition() self.set_slice_number(pos) self.interactor.Render() - def OnUpdateScroll(self): - max_slice_number = sl.Slice().GetNumberOfSlices(self.orientation) + def OnUpdateScroll(self) -> None: + max_slice_number: int = sl.Slice().GetNumberOfSlices(self.orientation) self.scroll.SetScrollbar(wx.SB_VERTICAL, 1, max_slice_number, - max_slice_number) + max_slice_number) - def OnSwapVolumeAxes(self, axes): + def OnSwapVolumeAxes(self, axes: Tuple[int, int]) -> None: # Adjusting cursor spacing to match the spacing from the actual slice # orientation - axis0, axis1 = axes + axis0: int = axes[0] + axis1: int = axes[1] cursor = self.slice_data.cursor - spacing = cursor.spacing + spacing: Tuple[float, float, float] = cursor.spacing if (axis0, axis1) == (2, 1): cursor.SetSpacing((spacing[1], spacing[0], spacing[2])) elif (axis0, axis1) == (2, 0): @@ -1548,20 +1552,20 @@ def OnSwapVolumeAxes(self, axes): cursor.SetSpacing((spacing[0], spacing[2], spacing[1])) self.slice_data.renderer.ResetCamera() - - def GetCrossPos(self): - spacing = self.slice_data.actor.GetInput().GetSpacing() - Publisher.sendMessage("Cross focal point", coord = self.cross.GetFocalPoint(), spacing = spacing) - - def UpdateCross(self, coord): + + def GetCrossPos(self) -> None: + spacing: tuple = self.slice_data.actor.GetInput().GetSpacing() + Publisher.sendMessage("Cross focal point", coord=self.cross.GetFocalPoint(), spacing=spacing) + + def UpdateCross(self, coord: tuple) -> None: self.cross.SetFocalPoint(coord) - Publisher.sendMessage('Co-registered points', arg=None, position=(coord[0], coord[1], coord[2], 0., 0., 0.)) + Publisher.sendMessage('Co-registered points', arg=None, position=(coord[0], coord[1], coord[2], 0., 0., 0.)) self.OnScrollBar() self.interactor.Render() - - def AddActors(self, actors, slice_number): + + def AddActors(self, actors: list, slice_number: int) -> None: "Inserting actors" - pos = self.scroll.GetThumbPosition() + pos: int = self.scroll.GetThumbPosition() #try: #renderer = self.renderers_by_slice_number[slice_number] #for actor in actors: @@ -1571,10 +1575,10 @@ def AddActors(self, actors, slice_number): if pos == slice_number: for actor in actors: self.slice_data.renderer.AddActor(actor) - + self.actors_by_slice_number[slice_number].extend(actors) - - def RemoveActors(self, actors, slice_number): + + def RemoveActors(self, actors: list, slice_number: int) -> None: "Remove a list of actors" try: renderer = self.renderers_by_slice_number[slice_number] @@ -1588,39 +1592,39 @@ def RemoveActors(self, actors, slice_number): renderer.RemoveActor(actor) # and remove the actor from the actor's list self.actors_by_slice_number[slice_number].remove(actor) - - def get_actual_mask(self): + + def get_actual_mask(self) -> None: # Returns actual mask. Returns None if there is not a mask or no mask # visible. - mask = self.slice_.current_mask + mask: Any = self.slice_.current_mask return mask - - def get_slice(self): + + def get_slice(self) -> None: return self.slice_ - - def discard_slice_cache(self, all_orientations=False, vtk_cache=True): + + def discard_slice_cache(self, all_orientations: bool = False, vtk_cache: bool = True) -> None: if all_orientations: for orientation in self.slice_.buffer_slices: - buffer_ = self.slice_.buffer_slices[orientation] + buffer_: Any = self.slice_.buffer_slices[orientation] buffer_.discard_image() if vtk_cache: buffer_.discard_vtk_image() else: - buffer_ = self.slice_.buffer_slices[self.orientation] + buffer_: Any = self.slice_.buffer_slices[self.orientation] buffer_.discard_image() if vtk_cache: buffer_.discard_vtk_image() - - def discard_mask_cache(self, all_orientations=False, vtk_cache=True): + + def discard_mask_cache(self, all_orientations: bool = False, vtk_cache: bool = True) -> None: if all_orientations: for orientation in self.slice_.buffer_slices: - buffer_ = self.slice_.buffer_slices[orientation] + buffer_: Any = self.slice_.buffer_slices[orientation] buffer_.discard_mask() if vtk_cache: buffer_.discard_vtk_mask() - + else: - buffer_ = self.slice_.buffer_slices[self.orientation] + buffer_: Any = self.slice_.buffer_slices[self.orientation] buffer_.discard_mask() if vtk_cache: buffer_.discard_vtk_mask() diff --git a/invesalius/data/viewer_volume.py b/invesalius/data/viewer_volume.py index aa557ddd1..4be8c672a 100644 --- a/invesalius/data/viewer_volume.py +++ b/invesalius/data/viewer_volume.py @@ -27,6 +27,7 @@ from numpy.core.umath_tests import inner1d import wx import queue +from typing import List, Tuple, Union,Optional,Dict,Any,Text # TODO: Check that these imports are not used -- vtkLookupTable, vtkMinimalStandardRandomSequence, vtkPoints, vtkUnsignedCharArray from vtkmodules.vtkCommonComputationalGeometry import vtkParametricTorus @@ -87,12 +88,16 @@ from vtkmodules.vtkRenderingOpenGL2 import vtkCompositePolyDataMapper2 from vtkmodules.wx.wxVTKRenderWindowInteractor import wxVTKRenderWindowInteractor + + + from invesalius.pubsub import pub as Publisher import random from scipy.spatial import distance from imageio import imsave + import invesalius.constants as const import invesalius.data.coordinates as dco import invesalius.data.coregistration as dcr @@ -111,38 +116,39 @@ if sys.platform == 'win32': try: import win32api - _has_win32api = True + _has_win32api: bool = True except ImportError: - _has_win32api = False + _has_win32api: bool = False else: - _has_win32api = False + _has_win32api: bool = False -PROP_MEASURE = 0.8 +PROP_MEASURE: float = 0.8 # from invesalius.gui.widgets.canvas_renderer import CanvasRendererCTX, Polygon + class Viewer(wx.Panel): - def __init__(self, parent): - wx.Panel.__init__(self, parent, size=wx.Size(320, 320)) + def __init__(self, parent: wx.Window) -> None: + super().__init__(parent=parent, size=wx.Size(320, 320)) self.SetBackgroundColour(wx.Colour(0, 0, 0)) - self.interaction_style = st.StyleStateManager() + self.interaction_style: st.StyleStateManager = st.StyleStateManager() - self.initial_focus = None + self.initial_focus: None = None - self.static_markers = [] - self.static_arrows = [] - self.style = None + self.static_markers: List = [] + self.static_arrows: List = [] + self.style: None = None - interactor = wxVTKRenderWindowInteractor(self, -1, size = self.GetSize()) - self.interactor = interactor + interactor: wxVTKRenderWindowInteractor = wxVTKRenderWindowInteractor(self, -1, size=self.GetSize()) + self.interactor: wxVTKRenderWindowInteractor = interactor self.interactor.SetRenderWhenDisabled(True) self.enable_style(const.STATE_DEFAULT) - sizer = wx.BoxSizer(wx.VERTICAL) + sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(interactor, 1, wx.EXPAND) - self.sizer = sizer + self.sizer: wx.BoxSizer = sizer self.SetSizer(sizer) self.Layout() @@ -156,28 +162,28 @@ def __init__(self, parent): # that doesn't matter. interactor.Enable(1) - ren = vtkRenderer() - self.ren = ren + ren: vtkRenderer = vtkRenderer() + self.ren: vtkRenderer = ren - canvas_renderer = vtkRenderer() + canvas_renderer: vtkRenderer = vtkRenderer() canvas_renderer.SetLayer(1) canvas_renderer.SetInteractive(0) canvas_renderer.PreserveDepthBufferOn() - self.canvas_renderer = canvas_renderer + self.canvas_renderer: vtkRenderer = canvas_renderer interactor.GetRenderWindow().SetNumberOfLayers(2) interactor.GetRenderWindow().AddRenderer(ren) interactor.GetRenderWindow().AddRenderer(canvas_renderer) - self.raycasting_volume = False + self.raycasting_volume: bool = False - self.onclick = False + self.onclick: bool = False - self.text = vtku.TextZero() + self.text: vtku.TextZero = vtku.TextZero() self.text.SetValue("") self.text.SetPosition(const.TEXT_POS_LEFT_UP) if sys.platform == 'darwin': - font_size = const.TEXT_SIZE_LARGE * self.GetContentScaleFactor() + font_size: float = const.TEXT_SIZE_LARGE * self.GetContentScaleFactor() self.text.SetSize(int(round(font_size, 0))) self.ren.AddActor(self.text.actor) @@ -194,86 +200,86 @@ def __init__(self, parent): # # self.ren.AddActor(axes) - self.slice_plane = None + self.slice_plane: None = None - self.view_angle = None + self.view_angle: None = None self.__bind_events() self.__bind_events_wx() - self.mouse_pressed = 0 - self.on_wl = False + self.mouse_pressed: int = 0 + self.on_wl: bool = False - self.picker = vtkPointPicker() + self.picker: vtkPointPicker = vtkPointPicker() interactor.SetPicker(self.picker) - self.seed_points = [] + self.seed_points: List = [] - self.points_reference = [] + self.points_reference: List = [] - self.measure_picker = vtkPropPicker() + self.measure_picker: vtkPropPicker = vtkPropPicker() #self.measure_picker.SetTolerance(0.005) - self.measures = [] + self.measures: List = [] - self.repositioned_axial_plan = 0 - self.repositioned_sagital_plan = 0 - self.repositioned_coronal_plan = 0 - self.added_actor = 0 + self.repositioned_axial_plan: int = 0 + self.repositioned_sagital_plan: int = 0 + self.repositioned_coronal_plan: int = 0 + self.added_actor: int = 0 - self.camera_state = const.CAM_MODE + self.camera_state: str = const.CAM_MODE - self.nav_status = False + self.nav_status: bool = False - self.ball_actor = None - self.obj_actor = None - self.obj_axes = None - self.obj_name = False - self.show_object = None - self.obj_actor_list = None - self.arrow_actor_list = None - self.pTarget = [0., 0., 0.] + self.ball_actor: None = None + self.obj_actor: None = None + self.obj_axes: None = None + self.obj_name: bool = False + self.show_object: None = None + self.obj_actor_list: None = None + self.arrow_actor_list: None = None + self.pTarget: List[float] = [0., 0., 0.] # self.obj_axes = None - self.x_actor = None - self.y_actor = None - self.z_actor = None - self.mark_actor = None - self.obj_projection_arrow_actor = None - self.object_orientation_torus_actor = None - - self._mode_cross = False - self._to_show_ball = 0 - self._ball_ref_visibility = False - - self.probe = False - self.ref = False - self.obj = False - - self.timer = False - self.index = False - - self.target_coord = None - self.aim_actor = None - self.dummy_coil_actor = None - self.dummy_robot_actor = None - self.target_mode = False - self.polydata = None - self.use_default_object = True - self.anglethreshold = const.COIL_ANGLES_THRESHOLD - self.distthreshold = const.COIL_COORD_THRESHOLD - self.angle_arrow_projection_threshold = const.COIL_ANGLE_ARROW_PROJECTION_THRESHOLD - - self.actor_tracts = None - self.actor_peel = None - self.seed_offset = const.SEED_OFFSET - self.radius_list = vtkIdList() - self.colors_init = vtkUnsignedCharArray() - - self.set_camera_position = True - self.old_coord = np.zeros((6,),dtype=float) + self.x_actor: None = None + self.y_actor: None = None + self.z_actor: None = None + self.mark_actor: None = None + self.obj_projection_arrow_actor: None = None + self.object_orientation_torus_actor: None = None + + self._mode_cross: bool = False + self._to_show_ball: int = 0 + self._ball_ref_visibility: bool = False + + self.probe: bool = False + self.ref: bool = False + self.obj: bool = False + + self.timer: bool = False + self.index: bool = False + + self.target_coord: None = None + self.aim_actor: None = None + self.dummy_coil_actor: None = None + self.dummy_robot_actor: None = None + self.target_mode: bool = False + self.polydata: None = None + self.use_default_object: bool = True + self.anglethreshold: float = const.COIL_ANGLES_THRESHOLD + self.distthreshold: float = const.COIL_COORD_THRESHOLD + self.angle_arrow_projection_threshold: float = const.COIL_ANGLE_ARROW_PROJECTION_THRESHOLD + + self.actor_tracts: None = None + self.actor_peel: None = None + self.seed_offset: int = const.SEED_OFFSET + self.radius_list: vtkIdList = vtkIdList() + self.colors_init: vtkUnsignedCharArray = vtkUnsignedCharArray() + + self.set_camera_position: bool = True + self.old_coord: np.ndarray = np.zeros((6,), dtype=float) self.LoadState() - def __bind_events(self): + def __bind_events(self) -> None: Publisher.subscribe(self.LoadActor, 'Load surface actor into viewer') Publisher.subscribe(self.RemoveActor, @@ -317,23 +323,20 @@ def __bind_events(self): Publisher.subscribe(self.enable_style, 'Enable style') Publisher.subscribe(self.OnDisableStyle, 'Disable style') - Publisher.subscribe(self.OnHideText, - 'Hide text actors on viewers') - + Publisher.subscribe(self.OnHideText, + 'Hide text actors on viewers') + Publisher.subscribe(self.AddActors, 'Add actors ' + str(const.SURFACE)) Publisher.subscribe(self.RemoveActors, 'Remove actors ' + str(const.SURFACE)) - - Publisher.subscribe(self.OnShowText, - 'Show text actors on viewers') + + Publisher.subscribe(self.OnShowText, + 'Show text actors on viewers') Publisher.subscribe(self.OnCloseProject, 'Close project data') Publisher.subscribe(self.RemoveAllActor, 'Remove all volume actors') - Publisher.subscribe(self.OnExportPicture,'Export picture to file') - - Publisher.subscribe(self.OnStartSeed,'Create surface by seeding - start') - Publisher.subscribe(self.OnEndSeed,'Create surface by seeding - end') - + Publisher.subscribe(self.OnExportPicture, 'Export picture to file') + Publisher.subscribe(self.SetStereoMode, 'Set stereo mode') Publisher.subscribe(self.Reposition3DPlane, 'Reposition 3D Plane') @@ -397,35 +400,35 @@ def __bind_events(self): # Related to robot tracking during neuronavigation Publisher.subscribe(self.ActivateRobotMode, 'Robot navigation mode') Publisher.subscribe(self.OnUpdateRobotStatus, 'Update robot status') + + def SaveState(self) -> None: + object_path: Optional[str] = self.obj_name.decode(const.FS_ENCODE) if self.obj_name is not None else None + use_default_object: bool = self.use_default_object - def SaveState(self): - object_path = self.obj_name.decode(const.FS_ENCODE) if self.obj_name is not None else None - use_default_object = self.use_default_object - - state = { + state: Dict[str, Union[str, bool]] = { 'object_path': object_path, 'use_default_object': use_default_object, } - session = ses.Session() + session: ses.Session = ses.Session() session.SetState('viewer', state) - def LoadState(self): - session = ses.Session() - state = session.GetState('viewer') + def LoadState(self) -> None: + session: ses.Session = ses.Session() + state: Optional[Dict[str, Union[str, bool]]] = session.GetState('viewer') if state is None: return - object_path = state['object_path'] - use_default_object = state['use_default_object'] + object_path: Optional[str] = state['object_path'] + use_default_object: bool = state['use_default_object'] - self.obj_name = object_path.encode(const.FS_ENCODE) if object_path is not None else None - self.use_default_object = use_default_object + self.obj_name: Optional[bytes] = object_path.encode(const.FS_ENCODE) if object_path is not None else None + self.use_default_object: bool = use_default_object - self.polydata = pu.LoadPolydata(path=object_path) if object_path is not None else None + self.polydata: Optional[Any] = pu.LoadPolydata(path=object_path) if object_path is not None else None - def get_vtk_mouse_position(self): + def get_vtk_mouse_position(self) -> Tuple[int, int]: """ Get Mouse position inside a wxVTKRenderWindowInteractorself. Return a tuple with X and Y position. @@ -441,12 +444,12 @@ def get_vtk_mouse_position(self): # https://docs.wxpython.org/wx.glcanvas.GLCanvas.html # For now we are doing this only on Mac but it may be needed on # Windows and Linux too. - scale = self.interactor.GetContentScaleFactor() + scale: float = self.interactor.GetContentScaleFactor() mx *= scale my *= scale return int(mx), int(my) - def SetStereoMode(self, mode): + def SetStereoMode(self, mode: str) -> None: ren_win = self.interactor.GetRenderWindow() if mode == const.STEREO_OFF: @@ -474,12 +477,12 @@ def SetStereoMode(self, mode): self.UpdateRender() - def _check_ball_reference(self, style): + def _check_ball_reference(self, style: str) -> None: if style == const.SLICE_STATE_CROSS: - self._mode_cross = True + self._mode_cross: bool = True # self._check_and_set_ball_visibility() #if not self.actor_peel: - self._ball_ref_visibility = True + self._ball_ref_visibility: bool = True #else: # self._ball_ref_visibility = False # if self._to_show_ball: @@ -487,70 +490,73 @@ def _check_ball_reference(self, style): self.CreateBallReference() #self.ball_actor.SetVisibility(1) #else: - # self.ball_actor.SetVisibility(0) + # self.ball_actor.SetVisibility(0) self.UpdateRender() - def _uncheck_ball_reference(self, style): + def _uncheck_ball_reference(self, style: str) -> None: if style == const.SLICE_STATE_CROSS: - self._mode_cross = False + self._mode_cross: bool = False # self.RemoveBallReference() - self._ball_ref_visibility = True + self._ball_ref_visibility: bool = True if self.ball_actor: self.ren.RemoveActor(self.ball_actor) self.ball_actor = None self.UpdateRender() - def OnSensors(self, markers_flag): - probe_id, ref_id, obj_id = markers_flag + def OnSensors(self, markers_flag: Tuple[bool, bool, bool]) -> None: + probe_id: bool = markers_flag[0] + ref_id: bool = markers_flag[1] + obj_id: bool = markers_flag[2] if not self.probe: self.CreateSensorID() if probe_id: - colour1 = (0, 1, 0) + colour1: Tuple[int, int, int] = (0, 1, 0) else: - colour1 = (1, 0, 0) + colour1: Tuple[int, int, int] = (1, 0, 0) if ref_id: - colour2 = (0, 1, 0) + colour2: Tuple[int, int, int] = (0, 1, 0) else: - colour2 = (1, 0, 0) + colour2: Tuple[int, int, int] = (1, 0, 0) if obj_id: - colour3 = (0, 1, 0) + colour3: Tuple[int, int, int] = (0, 1, 0) else: - colour3 = (1, 0, 0) + colour3: Tuple[int, int, int] = (1, 0, 0) self.probe.SetColour(colour1) self.ref.SetColour(colour2) self.obj.SetColour(colour3) - def CreateSensorID(self): - probe = vtku.Text() + def CreateSensorID(self) -> None: + probe: vtku.Text = vtku.Text() probe.SetSize(const.TEXT_SIZE_LARGE) probe.SetPosition((const.X, const.Y)) probe.ShadowOff() probe.SetValue("P") - self.probe = probe + self.probe: Optional[vtku.Text] = probe self.ren.AddActor(probe.actor) - ref = vtku.Text() + ref: vtku.Text = vtku.Text() ref.SetSize(const.TEXT_SIZE_LARGE) ref.SetPosition((const.X+0.04, const.Y)) ref.ShadowOff() ref.SetValue("R") - self.ref = ref + self.ref: Optional[vtku.Text] = ref self.ren.AddActor(ref.actor) - obj = vtku.Text() + obj: vtku.Text = vtku.Text() obj.SetSize(const.TEXT_SIZE_LARGE) obj.SetPosition((const.X+0.08, const.Y)) obj.ShadowOff() obj.SetValue("O") - self.obj = obj + self.obj: Optional[vtku.Text] = obj self.ren.AddActor(obj.actor) self.UpdateRender() + - def OnRemoveSensorsID(self): + def OnRemoveSensorsID(self) -> None: if self.probe: self.ren.RemoveActor(self.probe.actor) self.ren.RemoveActor(self.ref.actor) @@ -558,21 +564,21 @@ def OnRemoveSensorsID(self): self.probe = self.ref = self.obj = False self.UpdateRender() - # def OnShowSurface(self, index, visibility): + # def OnShowSurface(self, index: int, visibility: bool) -> None: # if visibility: # self._to_show_ball += 1 # else: # self._to_show_ball -= 1 # self._check_and_set_ball_visibility() - def OnStartSeed(self): + def OnStartSeed(self) -> None: self.seed_points = [] - def OnEndSeed(self): + def OnEndSeed(self) -> None: Publisher.sendMessage("Create surface from seeds", - seeds=self.seed_points) + seeds=self.seed_points) - def OnExportPicture(self, orientation, filename, filetype): + def OnExportPicture(self, orientation: str, filename: str, filetype: str) -> None: if orientation == const.VOLUME: Publisher.sendMessage('Begin busy cursor') if _has_win32api: @@ -583,7 +589,7 @@ def OnExportPicture(self, orientation, filename, filetype): self._export_picture(orientation, filename, filetype) Publisher.sendMessage('End busy cursor') - def _export_picture(self, id, filename, filetype): + def _export_picture(self, id: int, filename: str, filetype: str) -> None: if filetype == const.FILETYPE_POV: renwin = self.interactor.GetRenderWindow() image = vtkWindowToImageFilter() @@ -622,7 +628,7 @@ def _export_picture(self, id, filename, filetype): wx.MessageBox(_("InVesalius was not able to export this picture"), _("Export picture error")) - def OnCloseProject(self): + def OnCloseProject(self) -> None: if self.raycasting_volume: self.raycasting_volume = False @@ -638,21 +644,21 @@ def OnCloseProject(self): self.interaction_style.Reset() self.SetInteractorStyle(const.STATE_DEFAULT) - def OnHideText(self): + def OnHideText(self) -> None: self.text.Hide() self.UpdateRender() - def OnShowText(self): + def OnShowText(self) -> None: if self.on_wl: self.text.Show() self.UpdateRender() - def AddActors(self, actors): + def AddActors(self, actors: list) -> None: "Inserting actors" for actor in actors: self.ren.AddActor(actor) - def RemoveVolume(self): + def RemoveVolume(self) -> None: volumes = self.ren.GetVolumes() if (volumes.GetNumberOfItems()): self.ren.RemoveVolume(volumes.GetLastProp()) @@ -661,12 +667,12 @@ def RemoveVolume(self): # self._to_show_ball -= 1 # self._check_and_set_ball_visibility() - def RemoveActors(self, actors): + def RemoveActors(self, actors: list) -> None: "Remove a list of actors" for actor in actors: self.ren.RemoveActor(actor) - def AddPointReference(self, position, radius=1, colour=(1, 0, 0)): + def AddPointReference(self, position: tuple, radius: int=1, colour: tuple=(1, 0, 0)) -> None: """ Add a point representation in the given x,y,z position with a optional radius and colour. @@ -689,12 +695,13 @@ def AddPointReference(self, position, radius=1, colour=(1, 0, 0)): self.ren.AddActor(actor) self.points_reference.append(actor) - def RemoveAllPointsReference(self): + def RemoveAllPointsReference(self) -> None: for actor in self.points_reference: self.ren.RemoveActor(actor) self.points_reference = [] + - def RemovePointReference(self, point): + def RemovePointReference(self, point: int) -> None: """ Remove the point reference. The point argument is the position that is added. @@ -702,7 +709,7 @@ def RemovePointReference(self, point): actor = self.points_reference.pop(point) self.ren.RemoveActor(actor) - def SetMarkers(self, markers): + def SetMarkers(self, markers: List[Dict[str, Union[int, float, bool]]]) -> None: """ Set all markers, overwriting the previous markers. """ @@ -737,11 +744,11 @@ def SetMarkers(self, markers): self.UpdateRender() - def AddMarker(self, marker_id, size, colour, position, orientation, arrow_flag): + def AddMarker(self, marker_id: int, size: float, colour: Tuple[float, float, float], position: Tuple[float, float, float], orientation: Tuple[float, float, float], arrow_flag: bool) -> None: """ Markers created by navigation tools and rendered in volume viewer. """ - self.marker_id = marker_id + self.marker_id: int = marker_id position_flip = list(position) position_flip[1] = -position_flip[1] @@ -761,7 +768,7 @@ def AddMarker(self, marker_id, size, colour, position, orientation, arrow_flag): if not self.nav_status: self.UpdateRender() - def add_marker(self, coord, color): + def add_marker(self, coord: Tuple[float, float, float], color: Tuple[float, float, float]) -> vtkActor: """Simplified version for creating a spherical marker in the 3D scene :param coord: @@ -786,29 +793,29 @@ def add_marker(self, coord, color): actor.GetProperty().SetOpacity(1.) return actor - def HideAllMarkers(self, indexes): - ballid = indexes + def HideAllMarkers(self, indexes: int) -> None: + ballid: int = indexes for i in range(0, ballid): self.static_markers[i].SetVisibility(0) if not self.nav_status: self.UpdateRender() - def ShowAllMarkers(self, indexes): - ballid = indexes + def ShowAllMarkers(self, indexes: int) -> None: + ballid: int = indexes for i in range(0, ballid): self.static_markers[i].SetVisibility(1) if not self.nav_status: self.UpdateRender() - def RemoveAllMarkers(self, indexes): - ballid = indexes + def RemoveAllMarkers(self, indexes: int) -> None: + ballid: int = indexes for i in range(0, ballid): self.ren.RemoveActor(self.static_markers[i]) self.static_markers = [] if not self.nav_status: self.UpdateRender() - def RemoveMultipleMarkers(self, indexes): + def RemoveMultipleMarkers(self, indexes: List[int]) -> None: for i in reversed(indexes): self.ren.RemoveActor(self.static_markers[i]) del self.static_markers[i] @@ -816,7 +823,7 @@ def RemoveMultipleMarkers(self, indexes): if not self.nav_status: self.UpdateRender() - def BlinkMarker(self, index): + def BlinkMarker(self, index: int) -> None: if self.timer: self.timer.Stop() self.static_markers[self.index].SetVisibility(1) @@ -826,13 +833,13 @@ def BlinkMarker(self, index): self.timer.Start(500) self.timer_count = 0 - def OnBlinkMarker(self, evt): + def OnBlinkMarker(self, evt: wx.Event) -> None: self.static_markers[self.index].SetVisibility(int(self.timer_count % 2)) if not self.nav_status: self.UpdateRender() self.timer_count += 1 - def StopBlinkMarker(self, index=None): + def StopBlinkMarker(self, index: Optional[int] = None) -> None: if self.timer: self.timer.Stop() if index is None: @@ -841,25 +848,26 @@ def StopBlinkMarker(self, index=None): self.UpdateRender() self.index = False - def SetNewColor(self, index, color): + def SetNewColor(self, index: int, color: Tuple[float, float, float]) -> None: self.static_markers[index].GetProperty().SetColor([round(s / 255.0, 3) for s in color]) if not self.nav_status: self.UpdateRender() + - def OnTargetMarkerTransparency(self, status, index): + def OnTargetMarkerTransparency(self, status: bool, index: int) -> None: if status: self.static_markers[index].GetProperty().SetOpacity(1) # self.staticballs[index].GetProperty().SetOpacity(0.4) else: self.static_markers[index].GetProperty().SetOpacity(1) - def OnUpdateAngleThreshold(self, angle): + def OnUpdateAngleThreshold(self, angle: float) -> None: self.anglethreshold = angle - def OnUpdateDistThreshold(self, dist_threshold): + def OnUpdateDistThreshold(self, dist_threshold: float) -> None: self.distthreshold = dist_threshold - def ActivateTargetMode(self, evt=None, target_mode=None): + def ActivateTargetMode(self, evt=None, target_mode: bool = None) -> None: vtk_colors = vtkNamedColors() self.target_mode = target_mode @@ -950,7 +958,7 @@ def ActivateTargetMode(self, evt=None, target_mode=None): self.obj_actor_list = obj_roll, obj_yaw, obj_pitch self.arrow_actor_list = arrow_roll_z1, arrow_roll_z2, arrow_yaw_y1, arrow_yaw_y2,\ - arrow_pitch_x1, arrow_pitch_x2 + arrow_pitch_x1, arrow_pitch_x2 for ind in self.obj_actor_list: self.ren2.AddActor(ind) @@ -975,14 +983,15 @@ def ActivateTargetMode(self, evt=None, target_mode=None): self.object_orientation_torus_actor.SetVisibility(1) if self.obj_projection_arrow_actor: self.obj_projection_arrow_actor.SetVisibility(1) + - def OnUpdateObjectTargetGuide(self, m_img, coord): + def OnUpdateObjectTargetGuide(self, m_img: np.ndarray, coord: Tuple[float, float, float]) -> None: vtk_colors = vtkNamedColors() if self.target_coord and self.target_mode: - target_dist = distance.euclidean(coord[0:3], - (self.target_coord[0], -self.target_coord[1], self.target_coord[2])) + target_dist: float = distance.euclidean(coord[0:3], + (self.target_coord[0], -self.target_coord[1], self.target_coord[2])) # self.txt.SetCoilDistanceValue(target_dist) self.tdist.SetValue('Distance: ' + str("{:06.2f}".format(target_dist)) + ' mm') @@ -995,48 +1004,48 @@ def OnUpdateObjectTargetGuide(self, m_img, coord): self.ren.GetActiveCamera().Zoom((-0.0404 * target_dist) + 5.0404) if target_dist <= self.distthreshold: - thrdist = True + thrdist: bool = True self.aim_actor.GetProperty().SetDiffuseColor(vtk_colors.GetColor3d('Green')) else: - thrdist = False + thrdist: bool = False self.aim_actor.GetProperty().SetDiffuseColor(vtk_colors.GetColor3d('Yellow')) - m_img_flip = m_img.copy() + m_img_flip: np.ndarray = m_img.copy() m_img_flip[1, -1] = -m_img_flip[1, -1] - distance_to_target_robot = dcr.ComputeRelativeDistanceToTarget(target_coord=self.target_coord, m_img=m_img_flip) + distance_to_target_robot: np.ndarray = dcr.ComputeRelativeDistanceToTarget(target_coord=self.target_coord, m_img=m_img_flip) wx.CallAfter(Publisher.sendMessage, 'Distance to the target', distance=distance_to_target_robot) - distance_to_target = distance_to_target_robot.copy() + distance_to_target: np.ndarray = distance_to_target_robot.copy() if distance_to_target[3] > const.ARROW_UPPER_LIMIT: distance_to_target[3] = const.ARROW_UPPER_LIMIT elif distance_to_target[3] < -const.ARROW_UPPER_LIMIT: distance_to_target[3] = -const.ARROW_UPPER_LIMIT - coordrx_arrow = const.ARROW_SCALE * distance_to_target[3] + coordrx_arrow: float = const.ARROW_SCALE * distance_to_target[3] if distance_to_target[4] > const.ARROW_UPPER_LIMIT: distance_to_target[4] = const.ARROW_UPPER_LIMIT elif distance_to_target[4] < -const.ARROW_UPPER_LIMIT: distance_to_target[4] = -const.ARROW_UPPER_LIMIT - coordry_arrow = const.ARROW_SCALE * distance_to_target[4] + coordry_arrow: float = const.ARROW_SCALE * distance_to_target[4] if distance_to_target[5] > const.ARROW_UPPER_LIMIT: distance_to_target[5] = const.ARROW_UPPER_LIMIT elif distance_to_target[5] < -const.ARROW_UPPER_LIMIT: distance_to_target[5] = -const.ARROW_UPPER_LIMIT - coordrz_arrow = const.ARROW_SCALE * distance_to_target[5] + coordrz_arrow: float = const.ARROW_SCALE * distance_to_target[5] for ind in self.arrow_actor_list: self.ren2.RemoveActor(ind) if self.anglethreshold * const.ARROW_SCALE > coordrx_arrow > -self.anglethreshold * const.ARROW_SCALE: - thrcoordx = True + thrcoordx: bool = True # self.obj_actor_list[0].GetProperty().SetDiffuseColor(vtk_colors.GetColor3d('Green')) self.obj_actor_list[0].GetProperty().SetColor(0, 1, 0) else: - thrcoordx = False + thrcoordx: bool = False # self.obj_actor_list[0].GetProperty().SetDiffuseColor(vtk_colors.GetColor3d('GhostWhite')) self.obj_actor_list[0].GetProperty().SetColor(1, 1, 1) - offset = 5 + offset: int = 5 arrow_roll_x1 = self.CreateArrowActor([-55, -35, offset], [-55, -35, offset - coordrx_arrow]) arrow_roll_x1.RotateX(-60) @@ -1049,15 +1058,15 @@ def OnUpdateObjectTargetGuide(self, m_img, coord): arrow_roll_x2.GetProperty().SetColor(1, 1, 0) if self.anglethreshold * const.ARROW_SCALE > coordrz_arrow > -self.anglethreshold * const.ARROW_SCALE: - thrcoordz = True + thrcoordz: bool = True # self.obj_actor_list[1].GetProperty().SetDiffuseColor(vtk_colors.GetColor3d('Green')) self.obj_actor_list[1].GetProperty().SetColor(0, 1, 0) else: - thrcoordz = False + thrcoordz: bool = False # self.obj_actor_list[1].GetProperty().SetDiffuseColor(vtk_colors.GetColor3d('GhostWhite')) self.obj_actor_list[1].GetProperty().SetColor(1, 1, 1) - offset = -35 + offset: int = -35 arrow_yaw_z1 = self.CreateArrowActor([-55, offset, 0], [-55, offset - coordrz_arrow, 0]) arrow_yaw_z1.SetPosition(0, -150, 0) @@ -1070,22 +1079,22 @@ def OnUpdateObjectTargetGuide(self, m_img, coord): arrow_yaw_z2.GetProperty().SetColor(0, 1, 0) if self.anglethreshold * const.ARROW_SCALE > coordry_arrow > -self.anglethreshold * const.ARROW_SCALE: - thrcoordy = True + thrcoordy: bool = True #self.obj_actor_list[2].GetProperty().SetDiffuseColor(vtk_colors.GetColor3d('Green')) self.obj_actor_list[2].GetProperty().SetColor(0, 1, 0) else: - thrcoordy = False + thrcoordy: bool = False #self.obj_actor_list[2].GetProperty().SetDiffuseColor(vtk_colors.GetColor3d('GhostWhite')) self.obj_actor_list[2].GetProperty().SetColor(1, 1, 1) - offset = 38 + offset: int = 38 arrow_pitch_y1 = self.CreateArrowActor([0, 65, offset], [0, 65, offset + coordry_arrow]) arrow_pitch_y1.SetPosition(0, -300, 0) arrow_pitch_y1.RotateY(90) arrow_pitch_y1.RotateZ(180) arrow_pitch_y1.GetProperty().SetColor(1, 0, 0) - offset = 5 + offset: int = 5 arrow_pitch_y2 = self.CreateArrowActor([0, -55, offset], [0, -55, offset - coordry_arrow]) arrow_pitch_y2.SetPosition(0, -300, 0) arrow_pitch_y2.RotateY(90) @@ -1105,7 +1114,7 @@ def OnUpdateObjectTargetGuide(self, m_img, coord): for ind in self.arrow_actor_list: self.ren2.AddActor(ind) - def OnUpdateTargetCoordinates(self, coord): + def OnUpdateTargetCoordinates(self, coord: Optional[Tuple[float, float, float]]) -> None: if coord is not None: self.target_coord = coord self.target_coord[1] = -self.target_coord[1] @@ -1113,18 +1122,19 @@ def OnUpdateTargetCoordinates(self, coord): Publisher.sendMessage('Target selected', status=True) print("Target updated to coordinates {}".format(coord)) - def RemoveTarget(self): + + def RemoveTarget(self) -> None: self.target_mode = None self.target_coord = None self.RemoveTargetAim() Publisher.sendMessage('Target selected', status=False) - def OnDisableOrEnableCoilTracker(self, status): + def OnDisableOrEnableCoilTracker(self, status: bool) -> None: if not status: self.RemoveTarget() self.DisableCoilTracker() - def CreateVTKObjectMatrix(self, direction, orientation): + def CreateVTKObjectMatrix(self, direction: List[float], orientation: List[float]) -> vtkMatrix4x4: m_img = dco.coordinates_to_transformation_matrix( position=direction, orientation=orientation, @@ -1140,7 +1150,7 @@ def CreateVTKObjectMatrix(self, direction, orientation): return m_img_vtk - def CreateTargetAim(self): + def CreateTargetAim(self) -> None: if self.aim_actor: self.RemoveTargetAim() self.aim_actor = None @@ -1149,7 +1159,7 @@ def CreateTargetAim(self): self.m_img_vtk = self.CreateVTKObjectMatrix(self.target_coord[:3], self.target_coord[3:]) - filename = os.path.join(inv_paths.OBJ_DIR, "aim.stl") + filename: str = os.path.join(inv_paths.OBJ_DIR, "aim.stl") reader = vtkSTLReader() reader.SetFileName(filename) @@ -1212,22 +1222,22 @@ def CreateTargetAim(self): if not self.nav_status: self.UpdateRender() - def RemoveTargetAim(self): + def RemoveTargetAim(self) -> None: self.ren.RemoveActor(self.aim_actor) self.ren.RemoveActor(self.dummy_coil_actor) if not self.nav_status: self.UpdateRender() - def CreateTextDistance(self): + def CreateTextDistance(self) -> None: tdist = vtku.Text() tdist.SetSize(const.TEXT_SIZE_DIST_NAV) tdist.SetPosition((const.X, 1.-const.Y)) tdist.SetVerticalJustificationToBottom() tdist.BoldOn() self.ren.AddActor(tdist.actor) - self.tdist = tdist + self.tdist: Text = tdist - def DisableCoilTracker(self): + def DisableCoilTracker(self) -> None: try: self.ren.SetViewport(0, 0, 1, 1) self.interactor.GetRenderWindow().RemoveRenderer(self.ren2) @@ -1239,11 +1249,11 @@ def DisableCoilTracker(self): except: None - def CreateArrowActor(self, startPoint, endPoint): + def CreateArrowActor(self, startPoint: 'list[float]', endPoint: 'list[float]') -> vtkActor: # Compute a basis - normalizedX = [0 for i in range(3)] - normalizedY = [0 for i in range(3)] - normalizedZ = [0 for i in range(3)] + normalizedX: list[float] = [0.0 for i in range(3)] + normalizedY: list[float] = [0.0 for i in range(3)] + normalizedZ: list[float] = [0.0 for i in range(3)] # The X axis is a vector from start to end math = vtkMath() @@ -1252,7 +1262,7 @@ def CreateArrowActor(self, startPoint, endPoint): math.Normalize(normalizedX) # The Z axis is an arbitrary vector cross X - arbitrary = [0 for i in range(3)] + arbitrary: list[float] = [0.0 for i in range(3)] arbitrary[0] = random.uniform(-10, 10) arbitrary[1] = random.uniform(-10, 10) arbitrary[2] = random.uniform(-10, 10) @@ -1293,8 +1303,8 @@ def CreateArrowActor(self, startPoint, endPoint): return actor_arrow - def CenterOfMass(self): - barycenter = [0.0, 0.0, 0.0] + def CenterOfMass(self) -> 'list[float]': + barycenter: 'list[float]' = [0.0, 0.0, 0.0] proj = prj.Project() try: surface = proj.surface_dict[0].polydata @@ -1313,7 +1323,7 @@ def CenterOfMass(self): return barycenter - def Plane(self, x0, pTarget): + def Plane(self, x0: 'list[float]', pTarget: 'list[float]') -> 'tuple[np.ndarray, np.ndarray]': v3 = np.array(pTarget) - x0 # normal to the plane v3 = v3 / np.linalg.norm(v3) # unit vector @@ -1337,7 +1347,7 @@ def Plane(self, x0, pTarget): return v3, M_plane_inv - def SetCameraTarget(self): + def SetCameraTarget(self) -> None: cam_focus = self.target_coord[0:3] cam = self.ren.GetActiveCamera() @@ -1367,7 +1377,7 @@ def SetCameraTarget(self): cam.SetFocalPoint(cam_focus) cam.SetPosition(cam_pos) - def CreateBallReference(self): + def CreateBallReference(self) -> None: """ Red sphere on volume visualization to reference center of cross in slice planes. @@ -1395,7 +1405,7 @@ def CreateBallReference(self): # def SetCrossFocalPoint(self, position): # self.UpdateCameraBallPosition(None, position) - def UpdateCameraBallPosition(self, position): + def UpdateCameraBallPosition(self, position: Tuple[float, float, float]) -> None: #if not self.actor_peel: coord_flip = list(position[:3]) coord_flip[1] = -coord_flip[1] @@ -1405,7 +1415,7 @@ def UpdateCameraBallPosition(self, position): if not self.nav_status: self.UpdateRender() - def AddObjectActor(self, obj_name): + def AddObjectActor(self, obj_name: str) -> None: """ Coil for navigation rendered in volume viewer. """ @@ -1468,7 +1478,7 @@ def AddObjectActor(self, obj_name): # self.ren.AddActor(self.obj_axes) - def AddObjectOrientationDisk(self, position, orientation, color=[0.0, 0.0, 1.0]): + def AddObjectOrientationDisk(self, position: Tuple[float, float, float], orientation: Tuple[float, float, float], color: List[float] = [0.0, 0.0, 1.0]) -> vtkActor: # Create a disk to show target disk = vtkDiskSource() disk.SetInnerRadius(5) @@ -1488,102 +1498,102 @@ def AddObjectOrientationDisk(self, position, orientation, color=[0.0, 0.0, 1.0]) return disk_actor - def AddTorus(self, position, orientation, color=[0.0, 0.0, 1.0]): - torus = vtkParametricTorus() + def AddTorus(self, position: List[float], orientation: List[float], color: List[float] = [0.0, 0.0, 1.0]) -> vtkActor: + torus: vtkParametricTorus = vtkParametricTorus() torus.SetRingRadius(2) torus.SetCrossSectionRadius(1) - - torusSource = vtkParametricFunctionSource() + + torusSource: vtkParametricFunctionSource = vtkParametricFunctionSource() torusSource.SetParametricFunction(torus) torusSource.Update() - - torusMapper = vtkPolyDataMapper() + + torusMapper: vtkPolyDataMapper = vtkPolyDataMapper() torusMapper.SetInputConnection(torusSource.GetOutputPort()) torusMapper.SetScalarRange(0, 360) - - torusActor = vtkActor() + + torusActor: vtkActor = vtkActor() torusActor.SetMapper(torusMapper) torusActor.GetProperty().SetDiffuseColor(color) torusActor.SetPosition(position) torusActor.SetOrientation(orientation) - + return torusActor - - def CreateActorBall(self, coord_flip, colour=[0.0, 0.0, 1.0], size=2): - ball_ref = vtkSphereSource() + + def CreateActorBall(self, coord_flip: List[float], colour: List[float] = [0.0, 0.0, 1.0], size: int = 2) -> vtkActor: + ball_ref: vtkSphereSource = vtkSphereSource() ball_ref.SetRadius(size) ball_ref.SetCenter(coord_flip) - - mapper = vtkPolyDataMapper() + + mapper: vtkPolyDataMapper = vtkPolyDataMapper() mapper.SetInputConnection(ball_ref.GetOutputPort()) - - prop = vtkProperty() + + prop: vtkProperty = vtkProperty() prop.SetColor(colour) - - actor = vtkActor() + + actor: vtkActor = vtkActor() actor.SetMapper(mapper) actor.SetProperty(prop) actor.PickableOff() - + return actor - - def CreateActorArrow(self, direction, orientation, colour=[0.0, 0.0, 1.0], size=const.ARROW_MARKER_SIZE): - arrow = vtkArrowSource() + + def CreateActorArrow(self, direction: List[float], orientation: List[float], colour: List[float] = [0.0, 0.0, 1.0], size: int = const.ARROW_MARKER_SIZE) -> vtkActor: + arrow: vtkArrowSource = vtkArrowSource() arrow.SetArrowOriginToCenter() arrow.SetTipResolution(40) arrow.SetShaftResolution(40) arrow.SetShaftRadius(0.05) arrow.SetTipRadius(0.15) arrow.SetTipLength(0.35) - - mapper = vtkPolyDataMapper() + + mapper: vtkPolyDataMapper = vtkPolyDataMapper() mapper.SetInputConnection(arrow.GetOutputPort()) - - actor = vtkActor() + + actor: vtkActor = vtkActor() actor.SetMapper(mapper) actor.GetProperty().SetColor(colour) actor.GetProperty().SetLineWidth(5) actor.AddPosition(0, 0, 0) actor.SetScale(size) - - m_img_vtk = self.CreateVTKObjectMatrix(direction, orientation) + + m_img_vtk: vtkMatrix4x4 = self.CreateVTKObjectMatrix(direction, orientation) actor.SetUserMatrix(m_img_vtk) - + return actor - - def ObjectArrowLocation(self, m_img, coord): + + def ObjectArrowLocation(self, m_img: np.ndarray, coord: List[float]) -> Tuple[List[float], List[float], List[float], List[float]]: # m_img[:3, 0] is from posterior to anterior direction of the coil # m_img[:3, 1] is from left to right direction of the coil # m_img[:3, 2] is from bottom to up direction of the coil - vec_length = 70 - m_img_flip = m_img.copy() + vec_length: int = 70 + m_img_flip: np.ndarray = m_img.copy() m_img_flip[1, -1] = -m_img_flip[1, -1] - p1 = m_img_flip[:-1, -1] # coil center - coil_dir = m_img_flip[:-1, 0] - coil_face = m_img_flip[:-1, 1] - - coil_norm = np.cross(coil_dir, coil_face) - p2_norm = p1 - vec_length * coil_norm # point normal to the coil away from the center by vec_length - coil_dir = np.array([coord[3], coord[4], coord[5]]) - + p1: List[float] = m_img_flip[:-1, -1] # coil center + coil_dir: List[float] = m_img_flip[:-1, 0] + coil_face: List[float] = m_img_flip[:-1, 1] + + coil_norm: List[float] = np.cross(coil_dir, coil_face) + p2_norm: List[float] = p1 - vec_length * coil_norm # point normal to the coil away from the center by vec_length + coil_dir: List[float] = [coord[3], coord[4], coord[5]] + return coil_dir, p2_norm, coil_norm, p1 - - - def add_line(self, p1, p2, color=[0.0, 0.0, 1.0]): - line = vtkLineSource() + + + def add_line(self, p1: List[float], p2: List[float], color: List[float] = [0.0, 0.0, 1.0]) -> vtkActor: + line: vtkLineSource = vtkLineSource() line.SetPoint1(p1) line.SetPoint2(p2) - - mapper = vtkPolyDataMapper() + + mapper: vtkPolyDataMapper = vtkPolyDataMapper() mapper.SetInputConnection(line.GetOutputPort()) - - actor = vtkActor() + + actor: vtkActor = vtkActor() actor.SetMapper(mapper) actor.GetProperty().SetColor(color) - + return actor - - def AddPeeledSurface(self, flag, actor): + + def AddPeeledSurface(self, flag: bool, actor: vtkActor) -> None: if self.actor_peel: self.ren.RemoveActor(self.actor_peel) self.ren.RemoveActor(self.object_orientation_torus_actor) @@ -1591,65 +1601,64 @@ def AddPeeledSurface(self, flag, actor): self.actor_peel = None if self.ball_actor: self.ball_actor.SetVisibility(1) - + if flag and actor: self.ren.AddActor(actor) self.actor_peel = actor self.ren.AddActor(self.object_orientation_torus_actor) self.ren.AddActor(self.obj_projection_arrow_actor) - + if not self.nav_status: self.UpdateRender() - - def GetPeelCenters(self, centers, normals): - self.peel_centers = centers - self.peel_normals = normals - - def InitLocatorViewer(self, locator): + + def GetPeelCenters(self, centers: List[float], normals: List[float]) -> None: + self.peel_centers: List[float] = centers + self.peel_normals: List[float] = normals + + def InitLocatorViewer(self, locator) -> None: self.locator = locator - - def RecolorEfieldActor(self): + + def RecolorEfieldActor(self) -> None: self.efield_mesh_normals_viewer.Modified() - - def InitializeColorArray(self): + + def InitializeColorArray(self) -> None: self.colors_init.SetNumberOfComponents(3) self.colors_init.SetName('Colors') - color = 3 * [255.0] + color: List[float] = 3 * [255.0] for i in range(self.efield_mesh.GetNumberOfCells()): self.colors_init.InsertTuple(i, color) - - def ReturnToDefaultColorActor(self): + + def ReturnToDefaultColorActor(self) -> None: self.efield_mesh.GetPointData().SetScalars(self.colors_init) wx.CallAfter(Publisher.sendMessage, 'Initialize color array') self.RecolorEfieldActor() - - def CreateLUTTableForEfield(self, min, max): - lut = vtkLookupTable() + + def CreateLUTTableForEfield(self, min: float, max: float) -> vtkLookupTable: + lut: vtkLookupTable = vtkLookupTable() lut.SetTableRange(min, max) - colorSeries = vtkColorSeries() - seriesEnum = colorSeries.BREWER_SEQUENTIAL_YELLOW_ORANGE_BROWN_9 + colorSeries: vtkColorSeries = vtkColorSeries() + seriesEnum: int = colorSeries.BREWER_SEQUENTIAL_YELLOW_ORANGE_BROWN_9 colorSeries.SetColorScheme(seriesEnum) colorSeries.BuildLookupTable(lut, colorSeries.ORDINAL) return lut - def GetEfieldMaxMin(self, e_field_norms): + def GetEfieldMaxMin(self, e_field_norms: np.ndarray) -> None: self.e_field_norms = e_field_norms - max = np.amax(self.e_field_norms) - min = np.amin(self.e_field_norms) - self.min = min - self.max = max + max_val = np.amax(self.e_field_norms) + min_val = np.amin(self.e_field_norms) + self.min = min_val + self.max = max_val wx.CallAfter(Publisher.sendMessage, 'Update efield vis') - - def GetEfieldActor(self, e_field_actor): + def GetEfieldActor(self, e_field_actor: vtkActor) -> None: self.efield_actor = e_field_actor - def FindPointsAroundRadiusEfield(self, cellId): + def FindPointsAroundRadiusEfield(self, cellId: int) -> None: #radius = vtk.mutable(50) #self.radius_list = vtk.vtkIdList() self.locator_efield.FindPointsWithinRadius(30, self.e_field_mesh_centers.GetPoint(cellId), self.radius_list) - def GetCellIDsfromlistPoints(self, vlist, mesh): + def GetCellIDsfromlistPoints(self, vlist: vtkIdList, mesh) -> list: cell_ids_array = [] pts1 = vtkIdList() for i in range(vlist.GetNumberOfIds()): @@ -1658,7 +1667,7 @@ def GetCellIDsfromlistPoints(self, vlist, mesh): cell_ids_array.append(pts1.GetId(j)) return cell_ids_array - def InitEfield(self, e_field_brain): + def InitEfield(self, e_field_brain: Any) -> None: self.e_field_mesh_normals =e_field_brain.e_field_mesh_normals self.e_field_mesh_centers = e_field_brain.e_field_mesh_centers self.locator_efield = e_field_brain.locator_efield @@ -1676,7 +1685,7 @@ def InitEfield(self, e_field_brain): self.efield_actor.GetProperty().SetBackfaceCulling(1) self.efield_lut = e_field_brain.lut - def ShowEfieldintheintersection(self, intersectingCellIds, p1, coil_norm, coil_dir): + def ShowEfieldintheintersection(self, intersectingCellIds: vtkIdList, p1: np.ndarray, coil_norm: np.ndarray, coil_dir: np.ndarray) -> None: closestDist = 100 # if find intersection , calculate angle and add actors if intersectingCellIds.GetNumberOfIds() != 0: @@ -1694,7 +1703,7 @@ def ShowEfieldintheintersection(self, intersectingCellIds, p1, coil_norm, coil_d else: self.radius_list.Reset() - def OnUpdateEfieldvis(self): + def OnUpdateEfieldvis(self) -> None: if self.radius_list.GetNumberOfIds() != 0: #lut = self.CreateLUTtableforefield(self.min, self.max) @@ -1702,10 +1711,10 @@ def OnUpdateEfieldvis(self): self.colors_init.Fill(255) for h in range(self.radius_list.GetNumberOfIds()): - dcolor = 3 * [0.0] + dcolor: list[float] = 3 * [0.0] index_id = self.radius_list.GetId(h) self.efield_lut.GetColor(self.e_field_norms[index_id], dcolor) - color = 3 * [0.0] + color: list[float] = 3 * [0.0] for j in range(0, 3): color[j] = int(255.0 * dcolor[j]) self.colors_init.InsertTuple(index_id, color) @@ -1714,8 +1723,7 @@ def OnUpdateEfieldvis(self): else: wx.CallAfter(Publisher.sendMessage,'Recolor again') - - def UpdateEfieldPointLocation(self, m_img, coord, queue_IDs): + def UpdateEfieldPointLocation(self, m_img: Any, coord: np.ndarray, queue_IDs: queue.Queue) -> None: #TODO: In the future, remove the "put_nowait" and mesh processing to another module (maybe e_field.py) # this might work because a python instance from the 3D mesh can be edited in the thread. Check how to extract # the instance from the desired mesh for visualization and if it works. Optimally, there should be no @@ -1732,12 +1740,12 @@ def UpdateEfieldPointLocation(self, m_img, coord, queue_IDs): except queue.Full: pass - def GetEnorm(self, enorm): + def GetEnorm(self, enorm: np.ndarray) -> None: self.e_field_norms = enorm wx.CallAfter(Publisher.sendMessage, 'Update efield vis') #self.GetEfieldMaxMin(enorm) - def GetCellIntersection(self, p1, p2, locator): + def GetCellIntersection(self, p1: np.ndarray, p2: np.ndarray, locator) -> vtkIdList: vtk_colors = vtkNamedColors() # This find store the triangles that intersect the coil's normal intersectingCellIds = vtkIdList() @@ -1747,7 +1755,7 @@ def GetCellIntersection(self, p1, p2, locator): locator.FindCellsAlongLine(p1, p2, .001, intersectingCellIds) return intersectingCellIds - def ShowCoilProjection(self, intersectingCellIds, p1, coil_norm, coil_dir): + def ShowCoilProjection(self, intersectingCellIds: vtkIdList, p1: np.ndarray, coil_norm: np.ndarray, coil_dir: np.ndarray) -> None: vtk_colors = vtkNamedColors() closestDist = 50 @@ -1770,7 +1778,7 @@ def ShowCoilProjection(self, intersectingCellIds, p1, coil_norm, coil_dir): #for debbuging self.y_actor = self.add_line(closestPoint, closestPoint + 75 * pointnormal, - vtk_colors.GetColor3d('Yellow')) + vtk_colors.GetColor3d('Yellow')) #self.ren.AddActor(self.y_actor)# remove comment for testing @@ -1800,8 +1808,7 @@ def ShowCoilProjection(self, intersectingCellIds, p1, coil_norm, coil_dir): self.ren.RemoveActor(self.x_actor) self.ball_actor.SetVisibility(1) - - def OnNavigationStatus(self, nav_status, vis_status): + def OnNavigationStatus(self, nav_status: bool, vis_status: list) -> None: self.nav_status = nav_status self.tracts_status = vis_status[1] @@ -1816,10 +1823,10 @@ def OnNavigationStatus(self, nav_status, vis_status): #self.obj_projection_arrow_actor.SetVisibility(self.show_object) self.UpdateRender() - def UpdateSeedOffset(self, data): + def UpdateSeedOffset(self, data: float) -> None: self.seed_offset = data - def UpdateMarkerOffsetState(self, create=False): + def UpdateMarkerOffsetState(self, create: bool=False) -> None: if create: if not self.mark_actor: self.mark_actor = self.add_marker([0., 0., 0.], color=[0., 1., 1.]) @@ -1830,9 +1837,9 @@ def UpdateMarkerOffsetState(self, create=False): self.mark_actor = None self.UpdateRender() - def UpdateObjectOrientation(self, m_img, coord): + def UpdateObjectOrientation(self, m_img: np.ndarray, coord: float) -> None: # print("Update object orientation") - + m_img_flip = m_img.copy() m_img_flip[1, -1] = -m_img_flip[1, -1] @@ -1853,7 +1860,7 @@ def UpdateObjectOrientation(self, m_img, coord): self.y_actor.SetUserMatrix(m_img_vtk) self.z_actor.SetUserMatrix(m_img_vtk) - def UpdateObjectArrowOrientation(self, m_img, coord, flag): + def UpdateObjectArrowOrientation(self, m_img: np.ndarray, coord: float, flag: bool) -> None: [coil_dir, norm, coil_norm, p1 ]= self.ObjectArrowLocation(m_img,coord) if flag: @@ -1864,7 +1871,7 @@ def UpdateObjectArrowOrientation(self, m_img, coord, flag): intersectingCellIds = self.GetCellIntersection(p1, norm, self.locator) self.ShowCoilProjection(intersectingCellIds, p1, coil_norm, coil_dir) - def RemoveObjectActor(self): + def RemoveObjectActor(self) -> None: self.ren.RemoveActor(self.obj_actor) self.ren.RemoveActor(self.x_actor) self.ren.RemoveActor(self.y_actor) @@ -1880,14 +1887,14 @@ def RemoveObjectActor(self): self.obj_projection_arrow_actor = None self.object_orientation_torus_actor = None - def ConfigureObject(self, obj_name=None, polydata=None, use_default_object=True): + def ConfigureObject(self, obj_name: str=None, polydata: Any=None, use_default_object: bool=True) -> None: self.obj_name = obj_name self.polydata = polydata self.use_default_object = use_default_object self.SaveState() - def TrackObject(self, enabled): + def TrackObject(self, enabled: bool) -> None: if enabled: if self.obj_name: if self.obj_actor: @@ -1899,7 +1906,7 @@ def TrackObject(self, enabled): if not self.nav_status: self.UpdateRender() - def ShowObject(self, checked): + def ShowObject(self, checked: bool) -> None: self.show_object = checked if self.obj_actor and not self.show_object: @@ -1914,7 +1921,7 @@ def ShowObject(self, checked): if not self.nav_status: self.UpdateRender() - def OnUpdateTracts(self, root=None, affine_vtk=None, coord_offset=None, coord_offset_w=None): + def OnUpdateTracts(self, root: Any=None, affine_vtk: Any=None, coord_offset: float=None, coord_offset_w: float=None) -> None: mapper = vtkCompositePolyDataMapper2() mapper.SetInputDataObject(root) @@ -1927,20 +1934,21 @@ def OnUpdateTracts(self, root=None, affine_vtk=None, coord_offset=None, coord_of self.mark_actor.SetPosition(coord_offset) self.Refresh() - def OnRemoveTracts(self): + def OnRemoveTracts(self) -> None: if self.actor_tracts: self.ren.RemoveActor(self.actor_tracts) self.actor_tracts = None self.Refresh() - def ActivateRobotMode(self, robot_mode=None): + + def ActivateRobotMode(self, robot_mode: Optional[bool] = None) -> None: if robot_mode: self.ren_robot = vtkRenderer() self.ren_robot.SetLayer(1) self.interactor.GetRenderWindow().AddRenderer(self.ren_robot) self.ren_robot.SetViewport(0.02, 0.82, 0.08, 0.92) - filename = os.path.join(inv_paths.OBJ_DIR, "robot.stl") + filename: str = os.path.join(inv_paths.OBJ_DIR, "robot.stl") reader = vtkSTLReader() reader.SetFileName(filename) @@ -1960,25 +1968,25 @@ def ActivateRobotMode(self, robot_mode=None): else: self.DisableRobotMode() - def OnUpdateRobotStatus(self, robot_status): + def OnUpdateRobotStatus(self, robot_status: bool) -> None: if self.dummy_robot_actor: if robot_status: self.dummy_robot_actor.GetProperty().SetColor(0, 1, 0) else: self.dummy_robot_actor.GetProperty().SetColor(1, 0, 0) - def DisableRobotMode(self): + def DisableRobotMode(self) -> None: if self.dummy_robot_actor: self.ren_robot.RemoveActor(self.dummy_robot_actor) self.interactor.GetRenderWindow().RemoveRenderer(self.ren_robot) self.UpdateRender() - def __bind_events_wx(self): + def __bind_events_wx(self) -> None: #self.Bind(wx.EVT_SIZE, self.OnSize) # self.canvas.subscribe_event('LeftButtonPressEvent', self.on_insert_point) pass - def on_insert_point(self, evt): + def on_insert_point(self, evt: Any) -> None: pos = evt.position self.polygon.append_point(pos) self.canvas.Refresh() @@ -1986,7 +1994,7 @@ def on_insert_point(self, evt): arr = self.canvas.draw_element_to_array([self.polygon,]) imsave('/tmp/polygon.png', arr) - def SetInteractorStyle(self, state): + def SetInteractorStyle(self, state: str) -> None: cleanup = getattr(self.style, 'CleanUp', None) if cleanup: self.style.CleanUp() @@ -2003,9 +2011,9 @@ def SetInteractorStyle(self, state): self.interactor.SetInteractorStyle(style) self.UpdateRender() - self.state = state + self.state: str = state - def enable_style(self, style): + def enable_style(self, style: str) -> None: if styles.Styles.has_style(style): new_state = self.interaction_style.AddState(style) self.SetInteractorStyle(new_state) @@ -2013,19 +2021,19 @@ def enable_style(self, style): new_state = self.interaction_style.RemoveState(style) self.SetInteractorStyle(new_state) - def OnDisableStyle(self, style): + def OnDisableStyle(self, style: str) -> None: new_state = self.interaction_style.RemoveState(style) self.SetInteractorStyle(new_state) - def ResetCamClippingRange(self): + def ResetCamClippingRange(self) -> None: self.ren.ResetCamera() self.ren.ResetCameraClippingRange() - def SetVolumeCameraState(self, camera_state): + def SetVolumeCameraState(self, camera_state: bool) -> None: self.camera_state = camera_state # def SetVolumeCamera(self, arg, position): - def SetVolumeCamera(self, cam_focus): + def SetVolumeCamera(self, cam_focus: np.ndarray) -> None: if self.camera_state: # TODO: exclude dependency on initial focus # cam_focus = np.array(bases.flip_x(position[:3])) @@ -2041,7 +2049,7 @@ def SetVolumeCamera(self, cam_focus): v0n = np.sqrt(inner1d(v0, v0)) if self.show_object: - v1 = (cam_focus[0] - self.pTarget[0], cam_focus[1] - self.pTarget[1], cam_focus[2] - self.pTarget[2]) + v1: tuple[Any, Any, Any] = (cam_focus[0] - self.pTarget[0], cam_focus[1] - self.pTarget[1], cam_focus[2] - self.pTarget[2]) else: v1 = (cam_focus - self.initial_focus) @@ -2058,20 +2066,20 @@ def SetVolumeCamera(self, cam_focus): # self.ren.ResetCameraClippingRange() # self.ren.ResetCamera() - def OnExportSurface(self, filename, filetype): + def OnExportSurface(self, filename: str, filetype: str) -> None: if filetype not in (const.FILETYPE_STL, const.FILETYPE_VTP, const.FILETYPE_PLY, const.FILETYPE_STL_ASCII): if _has_win32api: utils.touch(filename) - win_filename = win32api.GetShortPathName(filename) + win_filename: str = win32api.GetShortPathName(filename) self._export_surface(win_filename, filetype) else: self._export_surface(filename, filetype) - def _export_surface(self, filename, filetype): - fileprefix = filename.split(".")[-2] + def _export_surface(self, filename: str, filetype: str) -> None: + fileprefix: str = filename.split(".")[-2] renwin = self.interactor.GetRenderWindow() if filetype == const.FILETYPE_RIB: @@ -2102,23 +2110,23 @@ def _export_surface(self, filename, filetype): writer.SetInput(renwin) writer.Write() - def OnEnableBrightContrast(self): + def OnEnableBrightContrast(self) -> None: style = self.style style.AddObserver("MouseMoveEvent", self.OnMove) style.AddObserver("LeftButtonPressEvent", self.OnClick) style.AddObserver("LeftButtonReleaseEvent", self.OnRelease) - def OnDisableBrightContrast(self): + def OnDisableBrightContrast(self) -> None: style = vtkInteractorStyleTrackballCamera() self.interactor.SetInteractorStyle(style) self.style = style - def OnSetWindowLevelText(self, ww, wl): + def OnSetWindowLevelText(self, ww: int, wl: int) -> None: if self.raycasting_volume: self.text.SetValue("WL: %d WW: %d"%(wl, ww)) # self.canvas.modified = True - def OnShowRaycasting(self): + def OnShowRaycasting(self) -> None: if not self.raycasting_volume: self.raycasting_volume = True # self._to_show_ball += 1 @@ -2126,24 +2134,24 @@ def OnShowRaycasting(self): if self.on_wl: self.text.Show() - def OnHideRaycasting(self): + def OnHideRaycasting(self) -> None: self.raycasting_volume = False self.text.Hide() # self._to_show_ball -= 1 # self._check_and_set_ball_visibility() - def OnSize(self, evt): + def OnSize(self, evt: Any) -> None: self.UpdateRender() self.Refresh() self.interactor.UpdateWindowUI() self.interactor.Update() evt.Skip() - def ChangeBackgroundColour(self, colour): + def ChangeBackgroundColour(self, colour: Tuple[int, int, int]) -> None: self.ren.SetBackground(colour[:3]) self.UpdateRender() - def LoadActor(self, actor): + def LoadActor(self, actor: Any) -> None: print(actor) self.added_actor = 1 ren = self.ren @@ -2161,7 +2169,7 @@ def LoadActor(self, actor): # self._to_show_ball += 1 # self._check_and_set_ball_visibility() - def RemoveActor(self, actor): + def RemoveActor(self, actor: Any) -> None: utils.debug("RemoveActor") ren = self.ren ren.RemoveActor(actor) @@ -2170,15 +2178,15 @@ def RemoveActor(self, actor): # self._to_show_ball -= 1 # self._check_and_set_ball_visibility() - def RemoveAllActor(self): + def RemoveAllActor(self) -> None: utils.debug("RemoveAllActor") self.ren.RemoveAllProps() Publisher.sendMessage('Render volume viewer') - def LoadSlicePlane(self): + def LoadSlicePlane(self) -> None: self.slice_plane = SlicePlane() - def LoadVolume(self, volume, colour, ww, wl): + def LoadVolume(self, volume: Any, colour: Tuple[int, int, int], ww: int, wl: int) -> None: self.raycasting_volume = True # self._to_show_ball += 1 # self._check_and_set_ball_visibility() @@ -2203,48 +2211,48 @@ def LoadVolume(self, volume, colour, ww, wl): self.UpdateRender() - def UnloadVolume(self, volume): + def UnloadVolume(self, volume) -> None: self.ren.RemoveVolume(volume) del volume self.raycasting_volume = False # self._to_show_ball -= 1 # self._check_and_set_ball_visibility() - - def load_mask_preview(self, mask_3d_actor, flag=True): + + def load_mask_preview(self, mask_3d_actor, flag: bool = True) -> None: if flag: self.ren.AddVolume(mask_3d_actor) else: self.ren.RemoveVolume(mask_3d_actor) - + if self.ren.GetActors().GetNumberOfItems() == 0 and self.ren.GetVolumes().GetNumberOfItems() == 1: self.ren.ResetCamera() self.ren.ResetCameraClippingRange() - - def remove_mask_preview(self, mask_3d_actor): + + def remove_mask_preview(self, mask_3d_actor) -> None: self.ren.RemoveVolume(mask_3d_actor) - - def OnSetViewAngle(self, view): + + def OnSetViewAngle(self, view: int) -> None: self.SetViewAngle(view) - - def SetViewAngle(self, view): + + def SetViewAngle(self, view: int) -> None: cam = self.ren.GetActiveCamera() cam.SetFocalPoint(0,0,0) - + proj = prj.Project() orig_orien = proj.original_orientation - + xv,yv,zv = const.VOLUME_POSITION[const.AXIAL][0][view] xp,yp,zp = const.VOLUME_POSITION[const.AXIAL][1][view] - + cam.SetViewUp(xv,yv,zv) cam.SetPosition(xp,yp,zp) - + self.ren.ResetCameraClippingRange() self.ren.ResetCamera() if not self.nav_status: self.UpdateRender() - - def ShowOrientationCube(self): + + def ShowOrientationCube(self) -> None: cube = vtkAnnotatedCubeActor() cube.GetXMinusFaceProperty().SetColor(1,0,0) cube.GetXPlusFaceProperty().SetColor(1,0,0) @@ -2253,7 +2261,7 @@ def ShowOrientationCube(self): cube.GetZMinusFaceProperty().SetColor(0,0,1) cube.GetZPlusFaceProperty().SetColor(0,0,1) cube.GetTextEdgesProperty().SetColor(0,0,0) - + # anatomic labelling cube.SetXPlusFaceText ("A") cube.SetXMinusFaceText("P") @@ -2261,7 +2269,7 @@ def ShowOrientationCube(self): cube.SetYMinusFaceText("R") cube.SetZPlusFaceText ("S") cube.SetZMinusFaceText("I") - + axes = vtkAxesActor() axes.SetShaftTypeToCylinder() axes.SetTipTypeToCone() @@ -2269,7 +2277,7 @@ def ShowOrientationCube(self): axes.SetYAxisLabelText("Y") axes.SetZAxisLabelText("Z") #axes.SetNormalizedLabelPosition(.5, .5, .5) - + orientation_widget = vtkOrientationMarkerWidget() orientation_widget.SetOrientationMarker(cube) orientation_widget.SetViewport(0.85,0.85,1.0,1.0) @@ -2278,30 +2286,30 @@ def ShowOrientationCube(self): orientation_widget.SetEnabled(1) orientation_widget.On() orientation_widget.InteractiveOff() - - def UpdateRender(self): + + def UpdateRender(self) -> None: self.interactor.Render() - - def SetWidgetInteractor(self, widget=None): + + def SetWidgetInteractor(self, widget = None) -> None: widget.SetInteractor(self.interactor._Iren) - - def AppendActor(self, actor): + + def AppendActor(self, actor) -> None: self.ren.AddActor(actor) - - def Reposition3DPlane(self, plane_label): + + def Reposition3DPlane(self, plane_label: str) -> None: if not(self.added_actor) and not(self.raycasting_volume): if not(self.repositioned_axial_plan) and (plane_label == 'Axial'): self.SetViewAngle(const.VOL_ISO) self.repositioned_axial_plan = 1 - + elif not(self.repositioned_sagital_plan) and (plane_label == 'Sagital'): self.SetViewAngle(const.VOL_ISO) self.repositioned_sagital_plan = 1 - + elif not(self.repositioned_coronal_plan) and (plane_label == 'Coronal'): self.SetViewAngle(const.VOL_ISO) self.repositioned_coronal_plan = 1 - + # def _check_and_set_ball_visibility(self): # #TODO: When creating Raycasting volume and cross is pressed, it is not # # automatically creating the ball reference. @@ -2315,21 +2323,21 @@ def Reposition3DPlane(self, plane_label): # self.interactor.Render() class SlicePlane: - def __init__(self): - project = prj.Project() - self.original_orientation = project.original_orientation + def __init__(self) -> None: + project: prj.Project = prj.Project() + self.original_orientation: str = project.original_orientation self.Create() - self.enabled = False + self.enabled: bool = False self.__bind_evt() - def __bind_evt(self): + def __bind_evt(self) -> None: Publisher.subscribe(self.Enable, 'Enable plane') Publisher.subscribe(self.Disable, 'Disable plane') Publisher.subscribe(self.ChangeSlice, 'Change slice from slice plane') Publisher.subscribe(self.UpdateAllSlice, 'Update all slice') - def Create(self): - plane_x = self.plane_x = vtkImagePlaneWidget() + def Create(self) -> None: + plane_x: vtkImagePlaneWidget = vtkImagePlaneWidget() plane_x.InteractionOff() #Publisher.sendMessage('Input Image in the widget', #(plane_x, 'SAGITAL')) @@ -2341,7 +2349,7 @@ def Create(self): cursor_property = plane_x.GetCursorProperty() cursor_property.SetOpacity(0) - plane_y = self.plane_y = vtkImagePlaneWidget() + plane_y: vtkImagePlaneWidget = vtkImagePlaneWidget() plane_y.DisplayTextOff() #Publisher.sendMessage('Input Image in the widget', #(plane_y, 'CORONAL')) @@ -2350,11 +2358,11 @@ def Create(self): plane_y.SetLeftButtonAction(0) plane_y.SetRightButtonAction(0) plane_y.SetMiddleButtonAction(0) - prop1 = plane_y.GetPlaneProperty() + prop1: vtkProperty = plane_y.GetPlaneProperty() cursor_property = plane_y.GetCursorProperty() cursor_property.SetOpacity(0) - plane_z = self.plane_z = vtkImagePlaneWidget() + plane_z: vtkImagePlaneWidget = vtkImagePlaneWidget() plane_z.InteractionOff() #Publisher.sendMessage('Input Image in the widget', #(plane_z, 'AXIAL')) @@ -2368,22 +2376,22 @@ def Create(self): cursor_property.SetOpacity(0) - prop3 = plane_z.GetPlaneProperty() + prop3: vtkProperty = plane_z.GetPlaneProperty() prop3.SetColor(1, 0, 0) - selected_prop3 = plane_z.GetSelectedPlaneProperty() + selected_prop3: vtkProperty = plane_z.GetSelectedPlaneProperty() selected_prop3.SetColor(1,0,0) - prop1 = plane_x.GetPlaneProperty() + prop1: vtkProperty = plane_x.GetPlaneProperty() prop1.SetColor(0, 0, 1) - selected_prop1 = plane_x.GetSelectedPlaneProperty() + selected_prop1: vtkProperty = plane_x.GetSelectedPlaneProperty() selected_prop1.SetColor(0, 0, 1) - prop2 = plane_y.GetPlaneProperty() + prop2: vtkProperty = plane_y.GetPlaneProperty() prop2.SetColor(0, 1, 0) - selected_prop2 = plane_y.GetSelectedPlaneProperty() + selected_prop2: vtkProperty = plane_y.GetSelectedPlaneProperty() selected_prop2.SetColor(0, 1, 0) Publisher.sendMessage('Set Widget Interactor', widget=plane_x) @@ -2392,7 +2400,7 @@ def Create(self): self.Render() - def Enable(self, plane_label=None): + def Enable(self, plane_label: str = None) -> None: if plane_label: if(plane_label == "Axial"): self.plane_z.On() @@ -2409,7 +2417,7 @@ def Enable(self, plane_label=None): view=const.VOL_ISO) self.Render() - def Disable(self, plane_label=None): + def Disable(self, plane_label: str = None) -> None: if plane_label: if(plane_label == "Axial"): self.plane_z.Off() @@ -2424,10 +2432,10 @@ def Disable(self, plane_label=None): self.Render() - def Render(self): + def Render(self) -> None: Publisher.sendMessage('Render volume viewer') - def ChangeSlice(self, orientation, index): + def ChangeSlice(self, orientation: str, index: int) -> None: if orientation == "CORONAL" and self.plane_y.GetEnabled(): Publisher.sendMessage('Update slice 3D', widget=self.plane_y, @@ -2444,7 +2452,7 @@ def ChangeSlice(self, orientation, index): orientation=orientation) self.Render() - def UpdateAllSlice(self): + def UpdateAllSlice(self) -> None: Publisher.sendMessage('Update slice 3D', widget=self.plane_y, orientation="CORONAL") @@ -2455,7 +2463,7 @@ def UpdateAllSlice(self): widget=self.plane_z, orientation="AXIAL") - def DeletePlanes(self): + def DeletePlanes(self) -> None: del self.plane_x del self.plane_y del self.plane_z diff --git a/invesalius/data/volume.py b/invesalius/data/volume.py index 7b8628bd8..8b1afe784 100644 --- a/invesalius/data/volume.py +++ b/invesalius/data/volume.py @@ -19,6 +19,8 @@ import plistlib import os import weakref + +from typing import Dict, Any,List,Tuple,Optional,Union,Callable from setuptools.extern.packaging.version import Version import numpy @@ -58,7 +60,7 @@ from invesalius import inv_paths -Kernels = { +Kernels: Dict[str, 'list[float]'] = { "Basic Smooth 5x5" : [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 4.0, 4.0, 4.0, 1.0, 1.0, 4.0, 12.0, 4.0, 1.0, @@ -66,7 +68,7 @@ 1.0, 1.0, 1.0, 1.0, 1.0] } -SHADING = { +SHADING: Dict[str, Dict[str, Any]] = { "Default": { "ambient" :0.15, "diffuse" :0.9, @@ -99,23 +101,23 @@ class Volume(): - def __init__(self): - self.config = None - self.exist = None - self.color_transfer = None - self.opacity_transfer_func = None - self.ww = None - self.wl = None - self.curve = 0 - self.plane = None - self.plane_on = False - self.volume = None - self.image = None - self.loaded_image = 0 - self.to_reload = False + def __init__(self) -> None: + self.config: Any = None + self.exist: Any = None + self.color_transfer: Any = None + self.opacity_transfer_func: Any = None + self.ww: Any = None + self.wl: Any = None + self.curve: int = 0 + self.plane: Any = None + self.plane_on: bool = False + self.volume: Any = None + self.image: Any = None + self.loaded_image: int = 0 + self.to_reload: bool = False self.__bind_events() - def __bind_events(self): + def __bind_events(self) -> None: Publisher.subscribe(self.OnHideVolume, 'Hide raycasting volume') Publisher.subscribe(self.OnUpdatePreset, @@ -138,16 +140,16 @@ def __bind_events(self): Publisher.subscribe(self.OnFlipVolume, 'Flip volume') - def ResetRayCasting(self): + def ResetRayCasting(self) -> None: if self.exist: self.exist = None self.LoadVolume() - def OnCloseProject(self): + def OnCloseProject(self) -> None: self.CloseProject() - def CloseProject(self): + def CloseProject(self) -> None: #if self.plane: # self.plane = None # Publisher.sendMessage('Remove surface actor from viewer', self.plane_actor) @@ -179,19 +181,20 @@ def CloseProject(self): self.color_transfer = None Publisher.sendMessage('Render volume viewer') - def OnLoadVolume(self, label): + def OnLoadVolume(self, label: str) -> None: label = label #self.LoadConfig(label) self.LoadVolume() - def OnHideVolume(self): + def OnHideVolume(self) -> None: print('Hide Volume') self.volume.SetVisibility(0) if (self.plane and self.plane_on): self.plane.Disable() Publisher.sendMessage('Render volume viewer') - def OnShowVolume(self): + + def OnShowVolume(self) -> None: print('Show volume') if self.exist: print('Volume exists') @@ -202,12 +205,12 @@ def OnShowVolume(self): else: print('Volume doesnt exit') Publisher.sendMessage('Load raycasting preset', - preset_name=const.RAYCASTING_LABEL) + preset_name=const.RAYCASTING_LABEL) self.LoadConfig() self.LoadVolume() self.exist = 1 - def OnUpdatePreset(self): + def OnUpdatePreset(self) -> None: self.__load_preset_config() if self.config: @@ -246,17 +249,17 @@ def OnUpdatePreset(self): self.color_transfer = None Publisher.sendMessage('Render volume viewer') - def OnFlipVolume(self, axis): + def OnFlipVolume(self, axis: str) -> None: print("Flipping Volume") self.loaded_image = False del self.image self.image = None self.to_reload = True - def __load_preset_config(self): + def __load_preset_config(self) -> None: self.config = prj.Project().raycasting_preset - def __update_colour_table(self): + def __update_colour_table(self) -> None: if self.config['advancedCLUT']: self.Create16bColorTable(self.scale) self.CreateOpacityTable(self.scale) @@ -264,7 +267,7 @@ def __update_colour_table(self): self.Create8bColorTable(self.scale) self.Create8bOpacityTable(self.scale) - def __load_preset(self): + def __load_preset(self) -> None: # Update colour table self.__update_colour_table() @@ -277,14 +280,14 @@ def __load_preset(self): self.SetShading() self.SetTypeRaycasting() - def OnSetCurve(self, curve): + def OnSetCurve(self, curve: int) -> None: self.curve = curve self.CalculateWWWL() ww = self.ww wl = self.wl Publisher.sendMessage('Set volume window and level text', ww=ww, wl=wl) - def OnSetRelativeWindowLevel(self, diff_wl, diff_ww): + def OnSetRelativeWindowLevel(self, diff_wl: float, diff_ww: float) -> None: ww = self.ww + diff_ww wl = self.wl + diff_wl Publisher.sendMessage('Set volume window and level text', ww=ww, wl=wl) @@ -292,11 +295,11 @@ def OnSetRelativeWindowLevel(self, diff_wl, diff_ww): self.ww = ww self.wl = wl - def OnSetWindowLevel(self, ww, wl, curve): + def OnSetWindowLevel(self, ww: float, wl: float, curve: int) -> None: self.curve = curve self.SetWWWL(ww, wl) - def SetWWWL(self, ww, wl): + def SetWWWL(self, ww: float, wl: float) -> None: if self.config['advancedCLUT']: try: curve = self.config['16bitClutCurves'][self.curve] @@ -327,7 +330,7 @@ def SetWWWL(self, ww, wl): self.__update_colour_table() - def CalculateWWWL(self): + def CalculateWWWL(self) -> None: """ Get the window width & level from the selected curve """ @@ -336,15 +339,15 @@ def CalculateWWWL(self): except IndexError: self.curve -= 1 curve = self.config['16bitClutCurves'][self.curve] - first_point = curve[0]['x'] - last_point = curve[-1]['x'] - self.ww = last_point - first_point - self.wl = first_point + self.ww / 2.0 - - def Refresh(self): + first_point: float = curve[0]['x'] + last_point: float = curve[-1]['x'] + self.ww: float = last_point - first_point + self.wl: float = first_point + self.ww / 2.0 + + def Refresh(self) -> None: self.__update_colour_table() - - def Create16bColorTable(self, scale): + + def Create16bColorTable(self, scale: float) -> None: if self.color_transfer: color_transfer = self.color_transfer else: @@ -352,166 +355,166 @@ def Create16bColorTable(self, scale): color_transfer.RemoveAllPoints() curve_table = self.config['16bitClutCurves'] color_table = self.config['16bitClutColors'] - colors = [] + colors: List[Tuple[float, float, float, float]] = [] for i, l in enumerate(curve_table): for j, lopacity in enumerate(l): - gray_level = lopacity['x'] - r = color_table[i][j]['red'] - g = color_table[i][j]['green'] - b = color_table[i][j]['blue'] - + gray_level: float = lopacity['x'] + r: float = color_table[i][j]['red'] + g: float = color_table[i][j]['green'] + b: float = color_table[i][j]['blue'] + colors.append((gray_level, r, g, b)) color_transfer.AddRGBPoint( self.TranslateScale(scale, gray_level), r, g, b) self.color_transfer = color_transfer - - def Create8bColorTable(self, scale): + + def Create8bColorTable(self, scale: float) -> None: if self.color_transfer: color_transfer = self.color_transfer else: color_transfer = vtkColorTransferFunction() color_transfer.RemoveAllPoints() - color_preset = self.config['CLUT'] + color_preset: str = self.config['CLUT'] if color_preset != "No CLUT": - path = os.path.join(inv_paths.RAYCASTING_PRESETS_DIRECTORY, - 'color_list', color_preset + '.plist') + path: str = os.path.join(inv_paths.RAYCASTING_PRESETS_DIRECTORY, + 'color_list', color_preset + '.plist') with open(path, 'rb') as f: p = plistlib.load(f, fmt=plistlib.FMT_XML) - - r = p['Red'] - g = p['Green'] - b = p['Blue'] - colors = list(zip(r,g,b)) + + r: List[float] = p['Red'] + g: List[float] = p['Green'] + b: List[float] = p['Blue'] + colors: List[Tuple[float, float, float]] = list(zip(r,g,b)) else: # Grayscale from black to white colors = [(i, i, i) for i in range(256)] - - ww = self.config['ww'] - wl = self.TranslateScale(scale, self.config['wl']) - init = wl - ww/2.0 - inc = ww / (len(colors) - 1.0) + + ww: float = self.config['ww'] + wl: float = self.TranslateScale(scale, self.config['wl']) + init: float = wl - ww/2.0 + inc: float = ww / (len(colors) - 1.0) for n,rgb in enumerate(colors): color_transfer.AddRGBPoint(init + n * inc, *[i/255.0 for i in rgb]) - + self.color_transfer = color_transfer - - def CreateOpacityTable(self, scale): + + def CreateOpacityTable(self, scale: float) -> None: if self.opacity_transfer_func: opacity_transfer_func = self.opacity_transfer_func else: opacity_transfer_func = vtkPiecewiseFunction() opacity_transfer_func.RemoveAllPoints() curve_table = self.config['16bitClutCurves'] - opacities = [] - - ww = self.config['ww'] - wl = self.config['wl'] - self.ww = ww - self.wl = wl - - l1 = wl - ww/2.0 - l2 = wl + ww/2.0 - - k1 = 0.0 - k2 = 1.0 - + opacities: List[Tuple[float, float]] = [] + + ww: float = self.config['ww'] + wl: float = self.config['wl'] + self.ww: float = ww + self.wl: float = wl + + l1: float = wl - ww/2.0 + l2: float = wl + ww/2.0 + + k1: float = 0.0 + k2: float = 1.0 + opacity_transfer_func.AddSegment(0, 0, 2**16-1, 0) - + for i, l in enumerate(curve_table): for j, lopacity in enumerate(l): - gray_level = lopacity['x'] + gray_level: float = lopacity['x'] #if gray_level <= l1: # opacity = k1 #elif gray_level > l2: # opacity = k2 #else: - opacity = lopacity['y'] + opacity: float = lopacity['y'] opacities.append((gray_level, opacity)) opacity_transfer_func.AddPoint( self.TranslateScale(scale, gray_level), opacity) self.opacity_transfer_func = opacity_transfer_func - - def Create8bOpacityTable(self, scale): + + def Create8bOpacityTable(self, scale: float) -> vtkPiecewiseFunction: if self.opacity_transfer_func: opacity_transfer_func = self.opacity_transfer_func else: opacity_transfer_func = vtkPiecewiseFunction() opacity_transfer_func.RemoveAllPoints() - opacities = [] - - ww = self.config['ww'] - wl = self.TranslateScale(scale, self.config['wl']) - - l1 = wl - ww/2.0 - l2 = wl + ww/2.0 - - self.ww = ww - self.wl = self.config['wl'] - + opacities: List[Tuple[float, float]] = [] + + ww: float = self.config['ww'] + wl: float = self.TranslateScale(scale, self.config['wl']) + + l1: float = wl - ww/2.0 + l2: float = wl + ww/2.0 + + self.ww: float = ww + self.wl: float = self.config['wl'] + opacity_transfer_func.RemoveAllPoints() opacity_transfer_func.AddSegment(0, 0, 2**16-1, 0) - - k1 = 0.0 - k2 = 1.0 - + + k1: float = 0.0 + k2: float = 1.0 + opacity_transfer_func.AddPoint(l1, 0) opacity_transfer_func.AddPoint(l2, 1) - + self.opacity_transfer_func = opacity_transfer_func return opacity_transfer_func - - def GetBackgroundColour(self): - colour = (self.config['backgroundColorRedComponent'], + + def GetBackgroundColour(self) -> Tuple[int, int, int]: + colour: Tuple[int, int, int] = (self.config['backgroundColorRedComponent'], self.config['backgroundColorGreenComponent'], self.config['backgroundColorBlueComponent']) return colour - - def ChangeBackgroundColour(self, colour): + + def ChangeBackgroundColour(self, colour: Tuple[float, float, float]) -> None: if (self.config): self.config['backgroundColorRedComponent'] = colour[0] * 255 self.config['backgroundColorGreenComponent'] = colour[1] * 255 self.config['backgroundColorBlueComponent'] = colour[2] * 255 - - def BuildTable(): + + def BuildTable() -> Tuple[List[Tuple[int, int, int, int]], List[Tuple[int, int]], List[int], bool]: curve_table = p['16bitClutCurves'] color_background = (p['backgroundColorRedComponent'], p['backgroundColorGreenComponent'], p['backgroundColorBlueComponent']) color_background = [i for i in color_background] - opacities = [] - colors = [] - + opacities: List[Tuple[int, int]] = [] + colors: List[Tuple[int, int, int, int]] = [] + for i, l in enumerate(curve_table): for j, lopacity in enumerate(l): gray_level = lopacity['x'] opacity = lopacity['y'] - + opacities.append((gray_level, opacity)) - + r = color_table[i][j]['red'] g = color_table[i][j]['green'] b = color_table[i][j]['blue'] - + colors.append((gray_level, r, g, b)) - + return colors, opacities, color_background, p['useShading'] - - def SetShading(self): + + def SetShading(self) -> None: if self.config['useShading']: self.volume_properties.ShadeOn() else: self.volume_properties.ShadeOff() - shading = SHADING[self.config['shading']] + shading: Dict[str, Any] = SHADING[self.config['shading']] self.volume_properties.SetAmbient(shading['ambient']) self.volume_properties.SetDiffuse(shading['diffuse']) self.volume_properties.SetSpecular(shading['specular']) self.volume_properties.SetSpecularPower(shading['specularPower']) - - def SetTypeRaycasting(self): + + def SetTypeRaycasting(self) -> None: if self.volume_mapper.IsA("vtkFixedPointVolumeRayCastMapper") or self.volume_mapper.IsA("vtkGPUVolumeRayCastMapper"): - + if self.config.get('MIP', False): self.volume_mapper.SetBlendModeToMaximumIntensity() else: @@ -522,13 +525,13 @@ def SetTypeRaycasting(self): else: raycasting_function = vtkVolumeRayCastCompositeFunction() raycasting_function.SetCompositeMethodToInterpolateFirst() - + session = ses.Session() if not session.GetConfig('rendering'): self.volume_mapper.SetVolumeRayCastFunction(raycasting_function) - - def ApplyConvolution(self, imagedata, update_progress = None): - number_filters = len(self.config['convolutionFilters']) + + def ApplyConvolution(self, imagedata, update_progress: Optional[Callable[[vtkImageConvolve, str], None]] = None) -> vtkImageData: + number_filters: int = len(self.config['convolutionFilters']) if number_filters: if not(update_progress): update_progress = vtk_utils.ShowProgress(number_filters) @@ -537,7 +540,7 @@ def ApplyConvolution(self, imagedata, update_progress = None): convolve.SetInputData(imagedata) convolve.SetKernel5x5([i/60.0 for i in Kernels[filter]]) # convolve.ReleaseDataFlagOn() - + convolve_ref = weakref.ref(convolve) convolve_ref().AddObserver("ProgressEvent", lambda obj,evt: @@ -548,153 +551,152 @@ def ApplyConvolution(self, imagedata, update_progress = None): del convolve #convolve.GetOutput().ReleaseDataFlagOn() return imagedata - - def LoadImage(self): + + def LoadImage(self) -> None: slice_data = slice_.Slice() n_array = slice_data.matrix - spacing = slice_data.spacing + spacing: Tuple[float] = slice_data.spacing slice_number = 0 orientation = 'AXIAL' - + image = converters.to_vtk(n_array, spacing, slice_number, orientation) self.image = image + - def LoadVolume(self): - proj = prj.Project() - #image = imagedata_utils.to_vtk(n_array, spacing, slice_number, orientation) - + def LoadVolume(self) -> None: + proj: prj.Project = prj.Project() + # image = imagedata_utils.to_vtk(n_array, spacing, slice_number, orientation) + if not self.loaded_image: self.LoadImage() self.loaded_image = 1 - + image = self.image - - number_filters = len(self.config['convolutionFilters']) - + + number_filters: int = len(self.config['convolutionFilters']) + if (prj.Project().original_orientation == const.AXIAL): - flip_image = True + flip_image: bool = True else: - flip_image = False - - #if (flip_image): - update_progress= vtk_utils.ShowProgress(2 + number_filters) + flip_image: bool = False + + # if (flip_image): + update_progress: Callable[[int], None] = vtk_utils.ShowProgress(2 + number_filters) # Flip original vtkImageData - flip = vtkImageFlip() + flip: vtkImageFlip = vtkImageFlip() flip.SetInputData(image) flip.SetFilteredAxis(1) flip.FlipAboutOriginOn() - # flip.ReleaseDataFlagOn() - - flip_ref = weakref.ref(flip) - flip_ref().AddObserver("ProgressEvent", lambda obj,evt: - update_progress(flip_ref(), "Rendering...")) + # flip.ReleaseDataFlagOn() + + flip_ref: weakref.ref[vtkImageFlip] = weakref.ref(flip) + flip_ref().AddObserver("ProgressEvent", lambda obj, evt: update_progress(flip_ref(), "Rendering...")) flip.Update() image = flip.GetOutput() - - scale = image.GetScalarRange() - self.scale = scale - - cast = vtkImageShiftScale() + + scale: Tuple[float, float] = image.GetScalarRange() + self.scale: Tuple[float, float] = scale + + cast: vtkImageShiftScale = vtkImageShiftScale() cast.SetInputData(image) cast.SetShift(abs(scale[0])) cast.SetOutputScalarTypeToUnsignedShort() - # cast.ReleaseDataFlagOn() - cast_ref = weakref.ref(cast) - cast_ref().AddObserver("ProgressEvent", lambda obj,evt: - update_progress(cast_ref(), "Rendering...")) + # cast.ReleaseDataFlagOn() + cast_ref: weakref.ref[vtkImageShiftScale] = weakref.ref(cast) + cast_ref().AddObserver("ProgressEvent", lambda obj, evt: update_progress(cast_ref(), "Rendering...")) cast.Update() image2 = cast - - self.imagedata = image2 + + self.imagedata: vtkImageData = image2 if self.config['advancedCLUT']: self.Create16bColorTable(scale) self.CreateOpacityTable(scale) else: self.Create8bColorTable(scale) self.Create8bOpacityTable(scale) - + image2 = self.ApplyConvolution(image2.GetOutput(), update_progress) - self.final_imagedata = image2 - + self.final_imagedata: vtkImageData = image2 + # Changed the vtkVolumeRayCast to vtkFixedPointVolumeRayCastMapper # because it's faster and the image is better # TODO: To test if it's true. if const.TYPE_RAYCASTING_MAPPER: - volume_mapper = vtkVolumeRayCastMapper() - #volume_mapper.AutoAdjustSampleDistancesOff() - #volume_mapper.SetInput(image2) - #volume_mapper.SetVolumeRayCastFunction(composite_function) - #volume_mapper.SetGradientEstimator(gradientEstimator) + volume_mapper: vtkVolumeRayCastMapper = vtkVolumeRayCastMapper() + # volume_mapper.AutoAdjustSampleDistancesOff() + # volume_mapper.SetInput(image2) + # volume_mapper.SetVolumeRayCastFunction(composite_function) + # volume_mapper.SetGradientEstimator(gradientEstimator) volume_mapper.IntermixIntersectingGeometryOn() - self.volume_mapper = volume_mapper + self.volume_mapper: vtkVolumeRayCastMapper = volume_mapper else: - session = ses.Session() + session: ses.Session = ses.Session() if not session.GetConfig('rendering'): - volume_mapper = vtkFixedPointVolumeRayCastMapper() - #volume_mapper.AutoAdjustSampleDistancesOff() - self.volume_mapper = volume_mapper + volume_mapper: vtkFixedPointVolumeRayCastMapper = vtkFixedPointVolumeRayCastMapper() + # volume_mapper.AutoAdjustSampleDistancesOff() + self.volume_mapper: vtkFixedPointVolumeRayCastMapper = volume_mapper volume_mapper.IntermixIntersectingGeometryOn() else: - volume_mapper = vtkOpenGLGPUVolumeRayCastMapper() + volume_mapper: vtkOpenGLGPUVolumeRayCastMapper = vtkOpenGLGPUVolumeRayCastMapper() volume_mapper.UseJitteringOn() - self.volume_mapper = volume_mapper - + self.volume_mapper: vtkOpenGLGPUVolumeRayCastMapper = volume_mapper + self.SetTypeRaycasting() volume_mapper.SetInputData(image2) - + # TODO: Look to this - #volume_mapper_hw = vtkVolumeTextureMapper3D() - #volume_mapper_hw.SetInput(image2) - - #Cut Plane - #CutPlane(image2, volume_mapper) - - #self.color_transfer = color_transfer - - volume_properties = vtkVolumeProperty() - #volume_properties.IndependentComponentsOn() + # volume_mapper_hw = vtkVolumeTextureMapper3D() + # volume_mapper_hw.SetInput(image2) + + # Cut Plane + # CutPlane(image2, volume_mapper) + + # self.color_transfer = color_transfer + + volume_properties: vtkVolumeProperty = vtkVolumeProperty() + # volume_properties.IndependentComponentsOn() volume_properties.SetInterpolationTypeToLinear() volume_properties.SetColor(self.color_transfer) - + try: volume_properties.SetScalarOpacity(self.opacity_transfer_func) except NameError: pass - + if not self.volume_mapper.IsA("vtkGPUVolumeRayCastMapper"): # Using these lines to improve the raycasting quality. These values # seems related to the distance from ray from raycasting. # TODO: Need to see values that improve the quality and don't decrease # the performance. 2.0 seems to be a good value to pix_diag - pix_diag = 2.0 + pix_diag: float = 2.0 volume_mapper.SetImageSampleDistance(0.25) volume_mapper.SetSampleDistance(pix_diag / 5.0) volume_properties.SetScalarOpacityUnitDistance(pix_diag) - - self.volume_properties = volume_properties - + + self.volume_properties: vtkVolumeProperty = volume_properties + self.SetShading() - - volume = vtkVolume() + + volume: vtkVolume = vtkVolume() volume.SetMapper(volume_mapper) volume.SetProperty(volume_properties) - self.volume = volume - - colour = self.GetBackgroundColour() - - self.exist = 1 - + self.volume: vtkVolume = volume + + colour: Tuple[float, float, float] = self.GetBackgroundColour() + + self.exist: int = 1 + if self.plane: self.plane.SetVolumeMapper(volume_mapper) - + Publisher.sendMessage('Load volume into viewer', volume=volume, colour=colour, ww=self.ww, wl=self.wl) - + del flip del cast - def OnEnableTool(self, tool_name, flag): + def OnEnableTool(self, tool_name: str, flag: bool) -> None: if tool_name == _("Cut plane"): if self.plane: if flag: @@ -707,9 +709,9 @@ def OnEnableTool(self, tool_name, flag): # self.final_imagedata.Update() self.plane_on = True self.plane = CutPlane(self.final_imagedata, - self.volume_mapper) + self.volume_mapper) - def CalculateHistogram(self): + def CalculateHistogram(self) -> None: image = self.image r = int(image.GetScalarRange()[1] - image.GetScalarRange()[0]) accumulate = vtkImageAccumulate() @@ -723,9 +725,9 @@ def CalculateHistogram(self): init, end = image.GetScalarRange() Publisher.sendMessage('Load histogram', histogram=n_image, init=init, end=end) - + - def TranslateScale(self, scale, value): + def TranslateScale(self, scale: Tuple[int, int], value: int) -> int: #if value < 0: # valor = 2**16 - abs(value) #else: @@ -733,7 +735,7 @@ def TranslateScale(self, scale, value): return value - scale[0] class VolumeMask: - def __init__(self, mask): + def __init__(self, mask) -> None: self.mask = mask self.colour = mask.colour self._volume_mapper = None @@ -742,7 +744,7 @@ def __init__(self, mask): self._piecewise_function = None self._actor = None - def create_volume(self): + def create_volume(self) -> None: if self._actor is None: session = ses.Session() if not session.GetConfig('rendering'): @@ -806,11 +808,11 @@ def create_volume(self): self._actor.SetProperty(self._volume_property) self._actor.Update() - def change_imagedata(self): + def change_imagedata(self) -> None: self._flip.SetInputData(self.mask.imagedata) - def set_colour(self, colour): - self.colour = colour + def set_colour(self, colour: Tuple[int, int, int]) -> None: + self.colour: Tuple[int, int, int] = colour r, g, b = self.colour self._color_transfer.RemoveAllPoints() self._color_transfer.AddRGBPoint(0.0, 0, 0, 0) @@ -818,22 +820,20 @@ def set_colour(self, colour): self._color_transfer.AddRGBPoint(255.0, r, g, b) + class CutPlane: - def __init__(self, img, volume_mapper): + def __init__(self, img: vtkImageData, volume_mapper: vtkVolumeMapper) -> None: self.img = img self.volume_mapper = volume_mapper self.Create() self.__bind_events() - - def __bind_events(self): - Publisher.subscribe(self.Reset, - 'Reset Cut Plane') - Publisher.subscribe(self.Enable, - 'Enable Cut Plane') - Publisher.subscribe(self.Disable, - 'Disable Cut Plane') - - def Create(self): + + def __bind_events(self) -> None: + Publisher.subscribe(self.Reset, 'Reset Cut Plane') + Publisher.subscribe(self.Enable, 'Enable Cut Plane') + Publisher.subscribe(self.Disable, 'Disable Cut Plane') + + def Create(self) -> None: self.plane_widget = plane_widget = vtkImagePlaneWidget() plane_widget.SetInputData(self.img) plane_widget.SetPlaneOrientationToXAxes() @@ -873,11 +873,11 @@ def Create(self): self.p2 = plane_widget.GetPoint2() self.normal = plane_widget.GetNormal() - def SetVolumeMapper(self, volume_mapper): + def SetVolumeMapper(self, volume_mapper: vtkVolumeMapper) -> None: self.volume_mapper = volume_mapper self.volume_mapper.AddClippingPlane(self.plane) - def Update(self, a, b): + def Update(self, a: str, b: str) -> None: plane_source = self.plane_source plane_widget = self.plane_widget plane_source.SetOrigin(plane_widget.GetOrigin()) @@ -888,20 +888,20 @@ def Update(self, a, b): self.plane.SetNormal(plane_source.GetNormal()) self.plane.SetOrigin(plane_source.GetOrigin()) Publisher.sendMessage('Render volume viewer') - - def Enable(self): + + def Enable(self) -> None: self.plane_widget.On() self.plane_actor.VisibilityOn() self.volume_mapper.AddClippingPlane(self.plane) Publisher.sendMessage('Render volume viewer') - - def Disable(self): + + def Disable(self) -> None: self.plane_widget.Off() self.plane_actor.VisibilityOff() self.volume_mapper.RemoveClippingPlane(self.plane) Publisher.sendMessage('Render volume viewer') - - def Reset(self): + + def Reset(self) -> None: plane_source = self.plane_source plane_widget = self.plane_widget plane_source.SetOrigin(self.origin) @@ -911,9 +911,9 @@ def Reset(self): self.plane_actor.VisibilityOn() self.plane.SetNormal(self.normal) self.plane.SetOrigin(self.origin) - Publisher.sendMessage('Render volume viewer') - - def DestroyObjs(self): + Publisher.sendMessage('Render volume viewer') + + def DestroyObjs(self) -> None: Publisher.sendMessage('Remove surface actor from viewer', actor=self.plane_actor) self.Disable() del self.plane_widget From ba066359246a2682954fef5fe9c4645fd0833365 Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Tue, 25 Apr 2023 01:56:06 +0530 Subject: [PATCH 12/16] added type info --- invesalius/presets.py | 44 +++++++-------- invesalius/session.py | 120 +++++++++++++++++++--------------------- invesalius/style.py | 40 +++++++------- invesalius/utils.py | 125 +++++++++++++++++++++--------------------- invesalius/version.py | 26 ++++----- 5 files changed, 172 insertions(+), 183 deletions(-) diff --git a/invesalius/presets.py b/invesalius/presets.py index 2700c83ae..8bd75b8ce 100644 --- a/invesalius/presets.py +++ b/invesalius/presets.py @@ -19,6 +19,7 @@ import glob import os import plistlib +from typing import Dict, List, Tuple import invesalius.constants as const @@ -30,8 +31,8 @@ class Presets(): - def __init__(self): - self.thresh_ct = TwoWaysDictionary({ + def __init__(self) -> None: + self.thresh_ct: TwoWaysDictionary = TwoWaysDictionary({ _("Bone"):(226,3071), _("Soft Tissue"):(-700,225), _("Enamel (Adult)"):(1553,2850), @@ -49,7 +50,7 @@ def __init__(self): _("Custom"):(0, 0) }) - self.thresh_mri = TwoWaysDictionary({ + self.thresh_mri: TwoWaysDictionary = TwoWaysDictionary({ _("Bone"):(1250,4095), _("Soft Tissue"):(324,1249), _("Enamel (Adult)"):(2577,3874), @@ -68,13 +69,13 @@ def __init__(self): }) self.__bind_events() - def __bind_events(self): + def __bind_events(self) -> None: Publisher.subscribe(self.UpdateThresholdModes, 'Update threshold limits list') - def UpdateThresholdModes(self, threshold_range): + def UpdateThresholdModes(self, threshold_range: Tuple[int, int]) -> None: thresh_min, thresh_max = threshold_range - presets_list = (self.thresh_ct, self.thresh_mri) + presets_list: tuple[TwoWaysDictionary, TwoWaysDictionary] = (self.thresh_ct, self.thresh_mri) for presets in presets_list: for key in presets: @@ -83,8 +84,8 @@ def UpdateThresholdModes(self, threshold_range): print(key, t_min, t_max) if (t_min is None) or (t_max is None): # setting custom preset - t_min = thresh_min - t_max = thresh_max + t_min: int = thresh_min + t_max: int = thresh_max t_min = max(t_min, thresh_min) t_max = min(t_max, thresh_max) @@ -100,11 +101,11 @@ def UpdateThresholdModes(self, threshold_range): Publisher.sendMessage('Update threshold limits', threshold_range=(thresh_min, thresh_max)) - def SavePlist(self, filename): + def SavePlist(self, filename: str) -> str: filename = "%s$%s" % (filename, 'presets.plist') - preset = {} + preset: Dict[str, TwoWaysDictionary] = {} - translate_to_en = {_("Bone"):"Bone", + translate_to_en: Dict[str, str] = {_("Bone"):"Bone", _("Soft Tissue"):"Soft Tissue", _("Enamel (Adult)"):"Enamel (Adult)", _("Enamel (Child)"): "Enamel (Child)", @@ -120,11 +121,11 @@ def SavePlist(self, filename): _("Skin Tissue (Child)"):"Skin Tissue (Child)", _("Custom"):"Custom"} - thresh_mri_new = {} + thresh_mri_new: Dict[str, Tuple[int, int]] = {} for name in self.thresh_mri.keys(): thresh_mri_new[translate_to_en[name]] = self.thresh_mri[name] - thresh_ct_new = {} + thresh_ct_new: Dict[str, Tuple[int, int]] = {} for name in self.thresh_ct.keys(): thresh_ct_new[translate_to_en[name]] = self.thresh_ct[name] @@ -134,9 +135,9 @@ def SavePlist(self, filename): plistlib.dump(preset, f) return os.path.split(filename)[1] - def OpenPlist(self, filename): + def OpenPlist(self, filename: str) -> None: - translate_to_x = {"Bone":_("Bone"), + translate_to_x: Dict[str, str] = {"Bone":_("Bone"), "Soft Tissue":_("Soft Tissue"), "Enamel (Adult)":_("Enamel (Adult)"), "Enamel (Child)": _("Enamel (Child)"), @@ -158,11 +159,11 @@ def OpenPlist(self, filename): thresh_mri = p['thresh_mri'].copy() thresh_ct = p['thresh_ct'].copy() - thresh_ct_new = {} + thresh_ct_new: Dict[str, Tuple[int, int]] = {} for name in thresh_ct.keys(): thresh_ct_new[translate_to_x[name]] = thresh_ct[name] - thresh_mri_new = {} + thresh_mri_new: Dict[str, Tuple[int, int]] = {} for name in thresh_mri.keys(): thresh_mri_new[translate_to_x[name]] = thresh_mri[name] @@ -170,17 +171,16 @@ def OpenPlist(self, filename): self.thresh_ct = TwoWaysDictionary(thresh_ct_new) - -def get_wwwl_presets(): - files = glob.glob(os.path.join(inv_paths.RAYCASTING_PRESETS_COLOR_DIRECTORY, '*.plist')) - presets = {} +def get_wwwl_presets() -> Dict[str, str]: + files: List[str] = glob.glob(os.path.join(inv_paths.RAYCASTING_PRESETS_COLOR_DIRECTORY, '*.plist')) + presets: Dict[str, str] = {} for f in files: p = os.path.splitext(os.path.basename(f))[0] presets[p] = f return presets -def get_wwwl_preset_colours(pfile): +def get_wwwl_preset_colours(pfile: str) -> List[Tuple[int, int, int]]: with open(pfile, 'rb') as f: preset = plistlib.load(f, fmt=plistlib.FMT_XML) ncolours = len(preset['Blue']) diff --git a/invesalius/session.py b/invesalius/session.py index bf2aa09f3..f5f9228db 100644 --- a/invesalius/session.py +++ b/invesalius/session.py @@ -32,6 +32,7 @@ from random import randint from threading import Thread from json.decoder import JSONDecodeError +from typing import Any, Dict, List, Optional, Tuple, Union import wx @@ -39,38 +40,37 @@ from invesalius.pubsub import pub as Publisher from invesalius.utils import Singleton, debug, decode, deep_merge_dict +CONFIG_PATH: str = os.path.join(inv_paths.USER_INV_DIR, 'config.json') +OLD_CONFIG_PATH: str = os.path.join(inv_paths.USER_INV_DIR, 'config.cfg') -CONFIG_PATH = os.path.join(inv_paths.USER_INV_DIR, 'config.json') -OLD_CONFIG_PATH = os.path.join(inv_paths.USER_INV_DIR, 'config.cfg') +STATE_PATH: str = os.path.join(inv_paths.USER_INV_DIR, 'state.json') -STATE_PATH = os.path.join(inv_paths.USER_INV_DIR, 'state.json') - -SESSION_ENCODING = 'utf8' +SESSION_ENCODING: str = 'utf8' # Only one session will be initialized per time. Therefore, we use # Singleton design pattern for implementing it class Session(metaclass=Singleton): - def __init__(self): - self.temp_item = False - self.mask_3d_preview = False + def __init__(self) -> None: + self.temp_item: bool = False + self.mask_3d_preview: bool = False - self._config = { + self._config: dict = { 'project_status': 3, 'language': '', 'auto_reload_preview': False, } - self._exited_successfully_last_time = not self._ReadState() + self._exited_successfully_last_time: bool = not self._ReadState() self.__bind_events() - def __bind_events(self): + def __bind_events(self) -> None: Publisher.subscribe(self._Exit, 'Exit') - def CreateConfig(self): + def CreateConfig(self) -> None: import invesalius.constants as const - self._config = { + self._config: dict = { 'mode': const.MODE_RP, 'project_status': const.PROJECT_STATUS_CLOSED, 'debug': False, @@ -86,45 +86,45 @@ def CreateConfig(self): } self.WriteConfigFile() - def CreateState(self): - self._state = {} + def CreateState(self) -> None: + self._state: dict = {} self.WriteStateFile() - def DeleteStateFile(self): + def DeleteStateFile(self) -> None: if os.path.exists(STATE_PATH): os.remove(STATE_PATH) print("Successfully deleted state file.") else: print("State file does not exist.") - def ExitedSuccessfullyLastTime(self): + def ExitedSuccessfullyLastTime(self) -> bool: return self._exited_successfully_last_time - def SetConfig(self, key, value): + def SetConfig(self, key: str, value: any) -> None: self._config[key] = value self.WriteConfigFile() - def GetConfig(self, key, default_value=None): + def GetConfig(self, key: str, default_value: any = None) -> any: if key in self._config: return self._config[key] else: return default_value - def SetState(self, key, value): + def SetState(self, key: str, value: any) -> None: self._state[key] = value self.WriteStateFile() - def GetState(self, key, default_value=None): + def GetState(self, key: str, default_value: any = None) -> any: if key in self._state: return self._state[key] else: return default_value - def IsOpen(self): + def IsOpen(self) -> bool: import invesalius.constants as const return self.GetConfig('project_status') != const.PROJECT_STATUS_CLOSED - def CloseProject(self): + def CloseProject(self) -> None: import invesalius.constants as const debug("Session.CloseProject") self.SetState('project_path', None) @@ -132,7 +132,7 @@ def CloseProject(self): #self.mode = const.MODE_RP self.temp_item = False - def SaveProject(self, path=()): + def SaveProject(self, path: str = ()) -> None: import invesalius.constants as const debug("Session.SaveProject") if path: @@ -143,53 +143,47 @@ def SaveProject(self, path=()): self.SetConfig('project_status', const.PROJECT_STATUS_OPENED) - def ChangeProject(self): - import invesalius.constants as const + + + def ChangeProject(self) -> None: debug("Session.ChangeProject") self.SetConfig('project_status', const.PROJECT_STATUS_CHANGED) - def CreateProject(self, filename): - import invesalius.constants as const - debug("Session.CreateProject") + def CreateProject(self, filename: str) -> None: Publisher.sendMessage('Begin busy cursor') # Set session info tempdir = str(inv_paths.TEMP_DIR) - project_path = (tempdir, filename) + project_path: Tuple[str, str] = (tempdir, filename) self.SetState('project_path', project_path) self.temp_item = True self.SetConfig('project_status', const.PROJECT_STATUS_NEW) - def OpenProject(self, filepath): - import invesalius.constants as const - debug("Session.OpenProject") - + def OpenProject(self, filepath: str) -> None: # Add item to recent projects list - project_path = os.path.split(filepath) + project_path: Tuple[str, str] = os.path.split(filepath) self._add_to_recent_projects(project_path) # Set session info self.SetState('project_path', project_path) self.SetConfig('project_status', const.PROJECT_STATUS_OPENED) - def WriteConfigFile(self): + def WriteConfigFile(self) -> None: self._write_to_json(self._config, CONFIG_PATH) - def WriteStateFile(self): + def WriteStateFile(self) -> None: self._write_to_json(self._state, STATE_PATH) - def _write_to_json(self, config_dict, config_filename): + def _write_to_json(self, config_dict: Dict[str, Any], config_filename: str) -> None: with open(config_filename, 'w') as config_file: json.dump(config_dict, config_file, sort_keys=True, indent=4) - def _add_to_recent_projects(self, item): - import invesalius.constants as const - + def _add_to_recent_projects(self, item: Tuple[str, str]) -> None: # Recent projects list - recent_projects = self.GetConfig('recent_projects') + recent_projects: List[List[str]] = self.GetConfig('recent_projects') item = list(item) # If item exists, remove it from list @@ -200,34 +194,34 @@ def _add_to_recent_projects(self, item): recent_projects.insert(0, item) self.SetConfig('recent_projects', recent_projects[:const.RECENT_PROJECTS_MAXIMUM]) - def _read_config_from_json(self, json_filename): + def _read_config_from_json(self, json_filename: str) -> None: with open(json_filename, 'r') as config_file: - config_dict = json.load(config_file) + config_dict: Dict[str, Any] = json.load(config_file) self._config = deep_merge_dict(self._config.copy(), config_dict) # Do not reading project status from the config file, since there # isn't a recover session tool in InVesalius yet. self.project_status = 3 - def _read_config_from_ini(self, config_filename): + def _read_config_from_ini(self, config_filename: str) -> None: file = codecs.open(config_filename, 'rb', SESSION_ENCODING) config = ConfigParser.ConfigParser() config.readfp(file) file.close() - mode = config.getint('session', 'mode') - debug = config.getboolean('session', 'debug') - debug_efield = config.getboolean('session','debug_efield') - language = config.get('session','language') - last_dicom_folder = config.get('paths','last_dicom_folder') - project_status = config.getint('session', 'status') - surface_interpolation = config.getint('session', 'surface_interpolation') - slice_interpolation = config.getint('session', 'slice_interpolation') - rendering = config.getint('session', 'rendering') - random_id = config.getint('session','random_id') + mode: int = config.getint('session', 'mode') + debug: bool = config.getboolean('session', 'debug') + debug_efield: bool = config.getboolean('session','debug_efield') + language: str = config.get('session','language') + last_dicom_folder: str = config.get('paths','last_dicom_folder') + project_status: int = config.getint('session', 'status') + surface_interpolation: int = config.getint('session', 'surface_interpolation') + slice_interpolation: int = config.getint('session', 'slice_interpolation') + rendering: int = config.getint('session', 'rendering') + random_id: int = config.getint('session','random_id') recent_projects = eval(config.get('project','recent_projects')) - recent_projects = [list(rp) for rp in recent_projects] + recent_projects: list[list[Any]] = [list(rp) for rp in recent_projects] self.SetConfig('mode', mode) self.SetConfig('debug', debug) @@ -245,11 +239,11 @@ def _read_config_from_ini(self, config_filename): #self.SetConfig('project_status', project_status) # if not(sys.platform == 'win32'): - # self.SetConfig('last_dicom_folder', last_dicom_folder.decode('utf-8')) + # self.SetConfig('last_dicom_folder', last_dicom_folder.decode('utf-8')) # TODO: Make also this function private so that it is run when the class constructor is run. # (Compare to _ReadState below.) - def ReadConfig(self): + def ReadConfig(self) -> bool: try: self._read_config_from_json(CONFIG_PATH) except Exception as e1: @@ -262,7 +256,7 @@ def ReadConfig(self): self.WriteConfigFile() return True - def _ReadState(self): + def _ReadState(self) -> bool: success = False if os.path.exists(STATE_PATH): print("Restoring a previous state...") @@ -285,21 +279,21 @@ def _ReadState(self): # Exit-related functions - def StoreSessionDialog(self): + def StoreSessionDialog(self) -> bool: msg = _("Would you like to store the session?") if sys.platform == 'darwin': dialog = wx.MessageDialog(None, "", msg, - wx.ICON_QUESTION | wx.YES_NO) + wx.ICON_QUESTION | wx.YES_NO) else: dialog = wx.MessageDialog(None, msg, "InVesalius 3", - wx.ICON_QUESTION | wx.YES_NO) + wx.ICON_QUESTION | wx.YES_NO) answer = dialog.ShowModal() dialog.Destroy() return answer == wx.ID_YES - def _Exit(self): + def _Exit(self) -> None: if not self.StoreSessionDialog(): self.CloseProject() self.DeleteStateFile() diff --git a/invesalius/style.py b/invesalius/style.py index aba7035cf..4ed55e198 100644 --- a/invesalius/style.py +++ b/invesalius/style.py @@ -70,52 +70,50 @@ class StyleStateManager(object): # don't need to be singleton, only needs to be instantiated inside # (Controller) self.slice_mode = SliceMode() - def __init__(self): + def __init__(self) -> None: self.stack = {} # push default value to stack - self.stack[const.STYLE_LEVEL[const.STATE_DEFAULT]] = \ + self.stack[const.STYLE_LEVEL[const.STATE_DEFAULT]]:str = \ const.STATE_DEFAULT - def AddState(self, state): + def AddState(self, state: str) -> str: - level = const.STYLE_LEVEL[state] - max_level = max(self.stack.keys()) + level: int = const.STYLE_LEVEL[state] + max_level: int = max(self.stack.keys()) # Insert new state into stack self.stack[level] = state - new_max_level = max(self.stack.keys()) + new_max_level: int = max(self.stack.keys()) return self.stack[new_max_level] - def RemoveState(self, state): - level = const.STYLE_LEVEL[state] + def RemoveState(self, state: str) -> str: + level: int = const.STYLE_LEVEL[state] if level in self.stack.keys(): - max_level = max(self.stack.keys()) + max_level: int = max(self.stack.keys()) # Remove item from stack self.stack.pop(level) # New max level - new_max_level = max(self.stack.keys()) + new_max_level: int = max(self.stack.keys()) # Only will affect InVesalius behaviour if the highest # level in stack has been removed if level == max_level: - new_state = self.stack[new_max_level] - + new_state: str = self.stack[new_max_level] return self.stack[new_max_level] - - max_level = max(self.stack.keys()) + max_level: int = max(self.stack.keys()) return self.stack[max_level] - def GetActualState(self): - max_level = max(self.stack.keys()) - state = self.stack[max_level] + def GetActualState(self) -> str: + max_level: int = max(self.stack.keys()) + state: str = self.stack[max_level] return state - def Reset(self): - self.stack = {} - self.stack[const.STYLE_LEVEL[const.STATE_DEFAULT]] = \ - const.STATE_DEFAULT + def Reset(self) -> None: + self.stack: dict = {} + self.stack[const.STYLE_LEVEL[const.STATE_DEFAULT]] = const.STATE_DEFAULT + diff --git a/invesalius/utils.py b/invesalius/utils.py index bd5c24826..58eef71a8 100644 --- a/invesalius/utils.py +++ b/invesalius/utils.py @@ -24,15 +24,16 @@ import math import traceback import collections.abc +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from setuptools.extern.packaging.version import Version from functools import wraps import numpy as np -def format_time(value): - sp1 = value.split(".") - sp2 = value.split(":") +def format_time(value: str) -> str: + sp1: list[str] = value.split(".") + sp2: list[str] = value.split(":") if (len(sp1) == 2) and (len(sp2) == 3): new_value = str(sp2[0]+sp2[1]+ @@ -52,9 +53,9 @@ def format_time(value): return value return time.strftime("%H:%M:%S",data) -def format_date(value): +def format_date(value: str) -> str: - sp1 = value.split(".") + sp1: list[str] = value.split(".") try: if (len(sp1) > 1): @@ -71,7 +72,7 @@ def format_date(value): except(ValueError): return "" -def debug(error_str): +def debug(error_str: str) -> None: """ Redirects output to file, or to the terminal This should be used in the place of "print" @@ -81,7 +82,7 @@ def debug(error_str): #if session.GetConfig('debug'): print(error_str) -def next_copy_name(original_name, names_list): +def next_copy_name(original_name: str, names_list: 'list[str]') -> str: """ Given original_name of an item and a list of existing names, builds up the name of a copy, keeping the pattern: @@ -91,15 +92,15 @@ def next_copy_name(original_name, names_list): """ # is there only one copy, unnumbered? if original_name.endswith(" copy"): - first_copy = original_name + first_copy: str = original_name last_index = -1 else: - parts = original_name.rpartition(" copy#") + parts: tuple[str, str, str] = original_name.rpartition(" copy#") # is there any copy, might be numbered? if parts[0] and parts[-1]: # yes, lets check if it ends with a number if isinstance(eval(parts[-1]), int): - last_index = int(parts[-1]) - 1 + last_index: int = int(parts[-1]) - 1 first_copy="%s copy"%parts[0] # no... well, so will build the copy name from zero else: @@ -123,22 +124,22 @@ def next_copy_name(original_name, names_list): got_new_name = False while not got_new_name: last_index += 1 - next_copy = "%s#%d"%(first_copy, last_index+1) + next_copy: str = "%s#%d"%(first_copy, last_index+1) if not (next_copy in names_list): got_new_name = True return next_copy -def new_name_by_pattern(pattern): +def new_name_by_pattern(pattern: str) -> str: from invesalius.project import Project proj = Project() - mask_dict = proj.mask_dict + mask_dict: TwoWaysDictionary = proj.mask_dict names_list = [i.name for i in mask_dict.values() if i.name.startswith(pattern + "_")] - count = len(names_list) + 1 + count: int = len(names_list) + 1 return "{}_{}".format(pattern, count) -def VerifyInvalidPListCharacter(text): +def VerifyInvalidPListCharacter(text: str) -> bool: #print text #text = unicode(text) @@ -156,12 +157,13 @@ def VerifyInvalidPListCharacter(text): #http://www.garyrobinson.net/2004/03/python_singleto.html # Gary Robinson class Singleton(type): - def __init__(cls,name,bases,dic): - super(Singleton,cls).__init__(name,bases,dic) - cls.instance=None - def __call__(cls,*args,**kw): + def __init__(cls: type, name: str, bases: tuple, dic: dict) -> None: + super().__init__(name, bases, dic) + cls.instance = None + + def __call__(cls: type, *args: tuple, **kw: dict) -> type: if cls.instance is None: - cls.instance=super(Singleton,cls).__call__(*args,**kw) + cls.instance = super().__call__(*args, **kw) return cls.instance # Another possible implementation @@ -176,46 +178,48 @@ class TwoWaysDictionary(dict): Dictionary that can be searched based on a key or on a item. The idea is to be able to search for the key given the item it maps. """ - def __init__(self, items=[]): - dict.__init__(self, items) - def get_key(self, value): + def __init__(self, items: list = []) -> None: + super().__init__(items) + + def get_key(self, value: any) -> any: """ Find the key (first) with the given value """ return self.get_keys(value)[0] - def get_keys(self, value): + def get_keys(self, value: any) -> list: """ Find the key(s) as a list given a value. """ return [item[0] for item in self.items() if item[1] == value] - def remove(self, key): + def remove(self, key: any) -> None: try: self.pop(key) except TypeError: debug("TwoWaysDictionary: no item") - def get_value(self, key): + def get_value(self, key: any) -> any: """ Find the value given a key. """ return self[key] -def frange(start, end=None, inc=None): + +def frange(start: float, end: float = None, inc: float = None) -> list: "A range function, that accepts float increments." - if end == None: + if end is None: end = start + 0.0 start = 0.0 - if (inc == None) or (inc == 0): + if (inc is None) or (inc == 0): inc = 1.0 L = [] - while 1: - next = start + len(L) * inc + while True: + next: float = start + len(L) * inc if inc > 0 and next >= end: break elif inc < 0 and next <= end: @@ -224,9 +228,7 @@ def frange(start, end=None, inc=None): return L - - -def calculate_resizing_tofitmemory(x_size,y_size,n_slices,byte): +def calculate_resizing_tofitmemory(x_size: int, y_size: int, n_slices: int, byte: int) -> float: """ Predicts the percentage (between 0 and 1) to resize the image to fit the memory, giving the following information: @@ -249,9 +251,9 @@ def calculate_resizing_tofitmemory(x_size,y_size,n_slices,byte): try: if (psutil.version_info>=(0,6,0)): - ram_free = psutil.virtual_memory().available - ram_total = psutil.virtual_memory().total - swap_free = psutil.swap_memory().free + ram_free: int = psutil.virtual_memory().available + ram_total: int = psutil.virtual_memory().total + swap_free: int = psutil.swap_memory().free else: ram_free = psutil.phymem_usage().free + psutil.cached_phymem() + psutil.phymem_buffers() ram_total = psutil.phymem_usage().total @@ -282,13 +284,13 @@ def calculate_resizing_tofitmemory(x_size,y_size,n_slices,byte): if (swap_free>ram_total): swap_free=ram_total resize = (float((ram_free+0.5*swap_free)/imagesize)) - resize=math.sqrt(resize) # this gives the "resize" for each axis x and y + resize: float=math.sqrt(resize) # this gives the "resize" for each axis x and y if (resize>1): resize=1 return round(resize,2) -def predict_memory(nfiles, x, y, p): +def predict_memory(nfiles: int, x: int, y: int, p: int) -> 'tuple[int, int]': """ Predict how much memory will be used, giving the following information: @@ -296,9 +298,9 @@ def predict_memory(nfiles, x, y, p): x, y: dicom image size p: bits allocated for each pixel sample """ - m = nfiles * (x * y * p) + m: int = nfiles * (x * y * p) #physical_memory in Byte - physical_memory = get_physical_memory() + physical_memory: int = get_physical_memory() if (sys.platform == 'win32'): @@ -308,7 +310,7 @@ def predict_memory(nfiles, x, y, p): #case occupy more than 300 MB image is reduced to 1.5, #and 25 MB each image is resized 0.04. if (m >= 314859200): - porcent = 1.5 + (m - 314859200) / 26999999 * 0.04 + porcent: float = 1.5 + (m - 314859200) / 26999999 * 0.04 else: return (x, y) else: #64 bits architecture @@ -369,7 +371,7 @@ def predict_memory(nfiles, x, y, p): # return str(bytes) + ' bytes' -def get_physical_memory(): +def get_physical_memory() -> int: """ Return physical memory in bytes """ @@ -378,17 +380,13 @@ def get_physical_memory(): sg.close() return int(mem.total()) - - -def get_system_encoding(): +def get_system_encoding() -> str: if (sys.platform == 'win32'): return locale.getdefaultlocale()[1] else: return 'utf-8' - - -def UpdateCheck(): +def UpdateCheck() -> None: try: from urllib.parse import urlencode from urllib.request import urlopen, Request @@ -398,7 +396,7 @@ def UpdateCheck(): import wx import invesalius.session as ses - def _show_update_info(): + def _show_update_info(url: str) -> None: from invesalius.gui import dialogs msg=_("A new version of InVesalius is available. Do you want to open the download website now?") title=_("Invesalius Update") @@ -420,8 +418,8 @@ def _show_update_info(): # Fetch update data from server import invesalius.constants as const url = "https://www.cti.gov.br/dt3d/invesalius/update/checkupdate.php" - headers = { 'User-Agent' : 'Mozilla/5.0 (compatible; MSIE 5.5; Windows NT)' } - data = {'update_protocol_version' : '1', + headers: Dict[str, str] = { 'User-Agent' : 'Mozilla/5.0 (compatible; MSIE 5.5; Windows NT)' } + data: Dict[str, Union[str, int]] = {'update_protocol_version' : '1', 'invesalius_version' : const.INVESALIUS_VERSION, 'platform' : sys.platform, 'architecture' : platform.architecture()[0], @@ -447,7 +445,7 @@ def _show_update_info(): wx.CallAfter(wx.CallLater, 1000, _show_update_info) -def vtkarray_to_numpy(m): +def vtkarray_to_numpy(m: Any) -> np.ndarray: nm = np.zeros((4, 4)) for i in range(4): for j in range(4): @@ -455,48 +453,47 @@ def vtkarray_to_numpy(m): return nm -def touch(fname): +def touch(fname: str) -> None: with open(fname, 'a'): pass -def decode(text, encoding, *args): +def decode(text: bytes, encoding: str, *args: Any) -> str: try: return text.decode(encoding, *args) except AttributeError: return text -def encode(text, encoding, *args): +def encode(text: str, encoding: str, *args: Any) -> bytes: try: return text.encode(encoding, *args) except AttributeError: return text -def timing(f): +def timing(f: Any) -> Any: @wraps(f) - def wrapper(*args, **kwargs): - start = time.time() + def wrapper(*args: Any, **kwargs: Any) -> Any: + start: float = time.time() result = f(*args, **kwargs) - end = time.time() + end: float = time.time() print('{} elapsed time: {}'.format(f.__name__, end-start)) return result return wrapper -def log_traceback(ex): +def log_traceback(ex: Exception) -> str: if hasattr(ex, '__traceback__'): ex_traceback = ex.__traceback__ else: _, _, ex_traceback = sys.exc_info() - tb_lines = [line.rstrip('\n') for line in + tb_lines: list[str] = [line.rstrip('\n') for line in traceback.format_exception(ex.__class__, ex, ex_traceback)] return ''.join(tb_lines) - -def deep_merge_dict(d, u): +def deep_merge_dict(d: Dict[str, Any], u: Dict[str, Any]) -> Dict[str, Any]: for k, v in u.items(): if isinstance(v, collections.abc.Mapping): d[k] = deep_merge_dict(d.get(k, {}), v) diff --git a/invesalius/version.py b/invesalius/version.py index 4b7deafee..51a1c1b55 100644 --- a/invesalius/version.py +++ b/invesalius/version.py @@ -25,42 +25,42 @@ import os.path import re -def get_svn_revision(path=None): +def get_svn_revision(path: str = None) -> str: """ Returns the SVN revision in the form pspb-XXXX, where XXXX is the revision number. Returns pspb-unknown if anything goes wrong, such as an unexpected format of internal SVN files. - + If path is provided, it should be a directory whose SVN info you want to inspect. If it's not provided, this will use the current directory. """ - rev = None + rev: str = None if path is None: - path = os.curdir - entries_path = '%s/.svn/entries' % path - + path: str = os.curdir + entries_path: str = '%s/.svn/entries' % path + try: - entries = open(entries_path, 'r').read() + entries: str = open(entries_path, 'r').read() except IOError: pass else: # Versions >= 7 of the entries file are flat text. The first line is # the version number. The next set of digits after 'dir' is the revision. if re.match('(\d+)', entries): - rev_match = re.search('\d+\s+dir\s+(\d+)', entries) + rev_match: re.Match = re.search('\d+\s+dir\s+(\d+)', entries) if rev_match: - rev = rev_match.groups()[0] + rev: str = rev_match.groups()[0] # Older XML versions of the file specify revision as an attribute of # the first entries node. else: from xml.dom import minidom - dom = minidom.parse(entries_path) - rev = dom.getElementsByTagName('entry')[0].getAttribute('revision') - + dom: minidom.Document = minidom.parse(entries_path) + rev: str = dom.getElementsByTagName('entry')[0].getAttribute('revision') + if rev: return 'pspb-%s' % rev return 'pspb-unknown' - + From 08545520825aefe1293dab67d6c9e50c98ed68f7 Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Wed, 26 Apr 2023 23:36:20 +0530 Subject: [PATCH 13/16] type info added --- invesalius/expanduser.py | 12 +-- invesalius/i18n.py | 7 +- invesalius/inv_paths.py | 66 +++++++------ invesalius/math_utils.py | 17 ++-- invesalius/net/remote_control.py | 25 ++--- invesalius/net/utils.py | 17 ++-- invesalius/plugins.py | 13 +-- invesalius/pubsub/pub.py | 24 ++--- .../segmentation/deep_learning/model.py | 97 +++++++++---------- .../segmentation/deep_learning/utils.py | 19 ++-- 10 files changed, 152 insertions(+), 145 deletions(-) diff --git a/invesalius/expanduser.py b/invesalius/expanduser.py index 8aac381e6..179aca19e 100644 --- a/invesalius/expanduser.py +++ b/invesalius/expanduser.py @@ -6,30 +6,30 @@ from ctypes import windll, wintypes class GUID(ctypes.Structure): - _fields_ = [ + _fields_: 'list[tuple[str, type]]' = [ ('Data1', wintypes.DWORD), ('Data2', wintypes.WORD), ('Data3', wintypes.WORD), ('Data4', wintypes.BYTE * 8) ] - def __init__(self, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8): + def __init__(self, l: int, w1: int, w2: int, b1: int, b2: int, b3: int, b4: int, b5: int, b6: int, b7: int, b8: int) -> None: """Create a new GUID.""" self.Data1 = l self.Data2 = w1 self.Data3 = w2 self.Data4[:] = (b1, b2, b3, b4, b5, b6, b7, b8) - def __repr__(self): + def __repr__(self) -> str: b1, b2, b3, b4, b5, b6, b7, b8 = self.Data4 return 'GUID(%x-%x-%x-%x%x%x%x%x%x%x%x)' % ( self.Data1, self.Data2, self.Data3, b1, b2, b3, b4, b5, b6, b7, b8) # constants to be used according to the version on shell32 -CSIDL_PROFILE = 40 -FOLDERID_Profile = GUID(0x5E6C858F, 0x0E22, 0x4760, 0x9A, 0xFE, 0xEA, 0x33, 0x17, 0xB6, 0x71, 0x73) +CSIDL_PROFILE: int = 40 +FOLDERID_Profile: GUID = GUID(0x5E6C858F, 0x0E22, 0x4760, 0x9A, 0xFE, 0xEA, 0x33, 0x17, 0xB6, 0x71, 0x73) -def expand_user(): +def expand_user() -> str: # get the function that we can find from Vista up, not the one in XP get_folder_path = getattr(windll.shell32, 'SHGetKnownFolderPath', None) #import pdb; pdb.set_trace() diff --git a/invesalius/i18n.py b/invesalius/i18n.py index c222bd94c..559b05e92 100644 --- a/invesalius/i18n.py +++ b/invesalius/i18n.py @@ -31,7 +31,7 @@ import invesalius.utils as utl -def GetLocales(): +def GetLocales() -> dict: """Return a dictionary which defines supported languages""" d = utl.TwoWaysDictionary ({'zh_TW': u'中文', 'en': u'English', @@ -52,7 +52,7 @@ def GetLocales(): 'be': u'Беларуская',}) return d -def GetLocaleOS(): +def GetLocaleOS() -> str: """Return language of the operating system.""" if sys.platform == 'darwin': #The app can't get the location then it has to set @@ -63,7 +63,7 @@ def GetLocaleOS(): return locale.getdefaultlocale()[0] -def InstallLanguage(language): +def InstallLanguage(language: str) -> callable: file_path = os.path.split(__file__)[0] abs_file_path = os.path.abspath(file_path + os.sep + "..") @@ -87,3 +87,4 @@ def InstallLanguage(language): except TypeError: lang.install() return lang.gettext + diff --git a/invesalius/inv_paths.py b/invesalius/inv_paths.py index cbb96ddcb..a8308c1f7 100644 --- a/invesalius/inv_paths.py +++ b/invesalius/inv_paths.py @@ -21,41 +21,42 @@ import shutil import sys import tempfile +from typing import List -HOME_DIR = pathlib.Path().home() -CONF_DIR = pathlib.Path(os.environ.get("XDG_CONFIG_HOME", HOME_DIR.joinpath(".config"))) -USER_INV_DIR = CONF_DIR.joinpath("invesalius") -USER_PRESET_DIR = USER_INV_DIR.joinpath("presets") -USER_LOG_DIR = USER_INV_DIR.joinpath("logs") -USER_DL_WEIGHTS = USER_INV_DIR.joinpath("deep_learning/weights/") -USER_RAYCASTING_PRESETS_DIRECTORY = USER_PRESET_DIR.joinpath("raycasting") -TEMP_DIR = tempfile.gettempdir() +HOME_DIR: pathlib.Path = pathlib.Path().home() +CONF_DIR: pathlib.Path = pathlib.Path(os.environ.get("XDG_CONFIG_HOME", HOME_DIR.joinpath(".config"))) +USER_INV_DIR: pathlib.Path = CONF_DIR.joinpath("invesalius") +USER_PRESET_DIR: pathlib.Path = USER_INV_DIR.joinpath("presets") +USER_LOG_DIR: pathlib.Path = USER_INV_DIR.joinpath("logs") +USER_DL_WEIGHTS: pathlib.Path = USER_INV_DIR.joinpath("deep_learning/weights/") +USER_RAYCASTING_PRESETS_DIRECTORY: pathlib.Path = USER_PRESET_DIR.joinpath("raycasting") +TEMP_DIR: str = tempfile.gettempdir() -USER_PLUGINS_DIRECTORY = USER_INV_DIR.joinpath("plugins") +USER_PLUGINS_DIRECTORY: pathlib.Path = USER_INV_DIR.joinpath("plugins") -OLD_USER_INV_DIR = HOME_DIR.joinpath(".invesalius") -OLD_USER_PRESET_DIR = OLD_USER_INV_DIR.joinpath("presets") -OLD_USER_LOG_DIR = OLD_USER_INV_DIR.joinpath("logs") +OLD_USER_INV_DIR: pathlib.Path = HOME_DIR.joinpath(".invesalius") +OLD_USER_PRESET_DIR: pathlib.Path = OLD_USER_INV_DIR.joinpath("presets") +OLD_USER_LOG_DIR: pathlib.Path = OLD_USER_INV_DIR.joinpath("logs") -INV_TOP_DIR = pathlib.Path(__file__).parent.parent.resolve() +INV_TOP_DIR: pathlib.Path = pathlib.Path(__file__).parent.parent.resolve() -PLUGIN_DIRECTORY = INV_TOP_DIR.joinpath("plugins") +PLUGIN_DIRECTORY: pathlib.Path = INV_TOP_DIR.joinpath("plugins") -ICON_DIR = INV_TOP_DIR.joinpath("icons") -SAMPLE_DIR = INV_TOP_DIR.joinpath("samples") -DOC_DIR = INV_TOP_DIR.joinpath("docs") -RAYCASTING_PRESETS_DIRECTORY = INV_TOP_DIR.joinpath("presets", "raycasting") -RAYCASTING_PRESETS_COLOR_DIRECTORY = INV_TOP_DIR.joinpath( +ICON_DIR: pathlib.Path = INV_TOP_DIR.joinpath("icons") +SAMPLE_DIR: pathlib.Path = INV_TOP_DIR.joinpath("samples") +DOC_DIR: pathlib.Path = INV_TOP_DIR.joinpath("docs") +RAYCASTING_PRESETS_DIRECTORY: pathlib.Path = INV_TOP_DIR.joinpath("presets", "raycasting") +RAYCASTING_PRESETS_COLOR_DIRECTORY: pathlib.Path = INV_TOP_DIR.joinpath( "presets", "raycasting", "color_list" ) -MODELS_DIR = INV_TOP_DIR.joinpath("ai") +MODELS_DIR: pathlib.Path = INV_TOP_DIR.joinpath("ai") # Inside the windows executable if hasattr(sys, "frozen") and ( sys.frozen == "windows_exe" or sys.frozen == "console_exe" ): - abs_path = INV_TOP_DIR.parent.resolve() + abs_path: pathlib.Path = INV_TOP_DIR.parent.resolve() ICON_DIR = abs_path.joinpath("icons") SAMPLE_DIR = INV_TOP_DIR.joinpath("samples") DOC_DIR = INV_TOP_DIR.joinpath("docs") @@ -76,17 +77,17 @@ ) # Navigation paths -OBJ_DIR = str(INV_TOP_DIR.joinpath("navigation", "objects")) +OBJ_DIR: str = str(INV_TOP_DIR.joinpath("navigation", "objects")) -MTC_CAL_DIR = str(INV_TOP_DIR.joinpath("navigation", "mtc_files", "CalibrationFiles")) -MTC_MAR_DIR = str(INV_TOP_DIR.joinpath("navigation", "mtc_files", "Markers")) +MTC_CAL_DIR: str = str(INV_TOP_DIR.joinpath("navigation", "mtc_files", "CalibrationFiles")) +MTC_MAR_DIR: str = str(INV_TOP_DIR.joinpath("navigation", "mtc_files", "Markers")) -NDI_MAR_DIR_PROBE = str(INV_TOP_DIR.joinpath("navigation", "ndi_files", "Markers", "8700340.rom")) -NDI_MAR_DIR_REF = str(INV_TOP_DIR.joinpath("navigation", "ndi_files", "Markers", "8700339.rom")) -NDI_MAR_DIR_OBJ = str(INV_TOP_DIR.joinpath("navigation", "ndi_files", "Markers", "8700338.rom")) +NDI_MAR_DIR_PROBE: str = str(INV_TOP_DIR.joinpath("navigation", "ndi_files", "Markers", "8700340.rom")) +NDI_MAR_DIR_REF: str = str(INV_TOP_DIR.joinpath("navigation", "ndi_files", "Markers", "8700339.rom")) +NDI_MAR_DIR_OBJ: str = str(INV_TOP_DIR.joinpath("navigation", "ndi_files", "Markers", "8700338.rom")) -OPTITRACK_CAL_DIR = str(INV_TOP_DIR.joinpath("navigation", "optitrack_files", "Calibration.cal")) -OPTITRACK_USERPROFILE_DIR = str(INV_TOP_DIR.joinpath("navigation", "optitrack_files", "UserProfile.motive")) +OPTITRACK_CAL_DIR: str = str(INV_TOP_DIR.joinpath("navigation", "optitrack_files", "Calibration.cal")) +OPTITRACK_USERPROFILE_DIR: str = str(INV_TOP_DIR.joinpath("navigation", "optitrack_files", "UserProfile.motive")) # MAC App if not os.path.exists(ICON_DIR): ICON_DIR = INV_TOP_DIR.parent.parent.joinpath("icons").resolve() @@ -94,7 +95,7 @@ DOC_DIR = INV_TOP_DIR.parent.parent.joinpath("docs").resolve() -def create_conf_folders(): +def create_conf_folders() -> None: USER_INV_DIR.mkdir(parents=True, exist_ok=True) USER_PRESET_DIR.mkdir(parents=True, exist_ok=True) USER_LOG_DIR.mkdir(parents=True, exist_ok=True) @@ -102,10 +103,11 @@ def create_conf_folders(): USER_PLUGINS_DIRECTORY.mkdir(parents=True, exist_ok=True) -def copy_old_files(): +def copy_old_files() -> List[str]: + copied_files: List[str] = [] for f in OLD_USER_INV_DIR.glob("*"): if f.is_file(): - print( + copied_files.append( shutil.copy( f, USER_INV_DIR.joinpath( diff --git a/invesalius/math_utils.py b/invesalius/math_utils.py index 25ecd8cc5..30523a8a9 100644 --- a/invesalius/math_utils.py +++ b/invesalius/math_utils.py @@ -2,8 +2,10 @@ import math import numpy as np +from typing import List, Tuple -def calculate_distance(p1, p2): + +def calculate_distance(p1: Tuple[float, float], p2: Tuple[float, float]) -> float: """ Calculates the euclidian distance between p1 and p2 points. @@ -16,7 +18,7 @@ def calculate_distance(p1, p2): return math.sqrt(sum([(j-i)**2 for i,j in zip(p1, p2)])) -def calculate_angle(v1, v2): +def calculate_angle(v1: Tuple[float, float], v2: Tuple[float, float]) -> float: """ Calculates the angle formed between vector v1 and v2. @@ -27,11 +29,11 @@ def calculate_angle(v1, v2): 90.0 """ cos_ = np.dot(v1, v2)/(np.linalg.norm(v1)*np.linalg.norm(v2)) - angle = math.degrees(math.acos(cos_)) + angle: float = math.degrees(math.acos(cos_)) return angle -def calc_ellipse_area(a, b): +def calc_ellipse_area(a: float, b: float) -> float: """ Calculates the area of the ellipse with the given a and b radius. @@ -46,7 +48,7 @@ def calc_ellipse_area(a, b): return np.pi * a * b -def calc_polygon_area(points): +def calc_polygon_area(points: List[Tuple[float, float]]) -> float: """ Calculates the area from the polygon formed by given the points. @@ -69,13 +71,14 @@ def calc_polygon_area(points): True """ area = 0.0 - j = len(points) - 1 + j: int = len(points) - 1 for i in range(len(points)): area += (points[j][0]+points[i][0]) * (points[j][1]-points[i][1]) j = i - area = abs(area / 2.0) + area: float = abs(area / 2.0) return area + if __name__ == '__main__': import doctest doctest.testmod() diff --git a/invesalius/net/remote_control.py b/invesalius/net/remote_control.py index 699c31e3d..8e7948c28 100644 --- a/invesalius/net/remote_control.py +++ b/invesalius/net/remote_control.py @@ -26,22 +26,22 @@ from invesalius.pubsub import pub as Publisher class RemoteControl: - def __init__(self, remote_host): - self._remote_host = remote_host - self._connected = False - self._sio = None + def __init__(self, remote_host: str) -> None: + self._remote_host: str = remote_host + self._connected: bool = False + self._sio: typing.Optional[socketio.Client] = None - def _on_connect(self): + def _on_connect(self) -> None: print("Connected to {}".format(self._remote_host)) self._connected = True - def _on_disconnect(self): + def _on_disconnect(self) -> None: print("Disconnected") self._connected = False - def _to_neuronavigation(self, msg): - topic = msg["topic"] - data = msg["data"] + def _to_neuronavigation(self, msg: dict) -> None: + topic: str = msg["topic"] + data: dict = msg["data"] if data is None: data = {} @@ -51,12 +51,12 @@ def _to_neuronavigation(self, msg): **data ) - def _to_neuronavigation_wrapper(self, msg): + def _to_neuronavigation_wrapper(self, msg: dict) -> None: # wx.CallAfter wrapping is needed to make messages that update WxPython UI work properly, as the # Socket.IO listener runs inside a thread. (See WxPython and thread-safety for more information.) wx.CallAfter(self._to_neuronavigation, msg) - def connect(self): + def connect(self) -> None: self._sio = socketio.Client() self._sio.on('connect', self._on_connect) @@ -70,7 +70,7 @@ def connect(self): print("Connecting...") time.sleep(1.0) - def _emit(topic, data): + def _emit(topic: str, data: dict) -> None: #print("Emitting data {} to topic {}".format(data, topic)) try: if isinstance(topic, str): @@ -84,3 +84,4 @@ def _emit(topic, data): pass Publisher.add_sendMessage_hook(_emit) + diff --git a/invesalius/net/utils.py b/invesalius/net/utils.py index 787c29fb8..4cb7170c9 100644 --- a/invesalius/net/utils.py +++ b/invesalius/net/utils.py @@ -6,12 +6,12 @@ import os import shutil -def download_url_to_file(url: str, dst: pathlib.Path, hash: str = None, callback: typing.Callable[[float], None] = None): - file_size = None - total_downloaded = 0 +def download_url_to_file(url: str, dst: pathlib.Path, hash: str = None, callback: typing.Callable[[float], None] = None) -> None: + file_size: typing.Optional[int] = None + total_downloaded: int = 0 if hash is not None: - calc_hash = hashlib.sha256() - req = Request(url) + calc_hash: hashlib.sha256 = hashlib.sha256() + req: Request = Request(url) response = urlopen(req) meta = response.info() if hasattr(meta, "getheaders"): @@ -22,10 +22,10 @@ def download_url_to_file(url: str, dst: pathlib.Path, hash: str = None, callback if content_length is not None and len(content_length) > 0: file_size = int(content_length[0]) dst.parent.mkdir(parents=True, exist_ok=True) - f = tempfile.NamedTemporaryFile(delete=False, dir=dst.parent) + f: tempfile.NamedTemporaryFile = tempfile.NamedTemporaryFile(delete=False, dir=dst.parent) try: while True: - buffer = response.read(8192) + buffer: bytes = response.read(8192) if len(buffer) == 0: break total_downloaded += len(buffer) @@ -36,7 +36,7 @@ def download_url_to_file(url: str, dst: pathlib.Path, hash: str = None, callback callback(100 * total_downloaded/file_size) f.close() if hash is not None: - digest = calc_hash.hexdigest() + digest: str = calc_hash.hexdigest() if digest != hash: raise RuntimeError(f'Invalid hash value (expected "{hash}", got "{digest}")') shutil.move(f.name, dst) @@ -44,3 +44,4 @@ def download_url_to_file(url: str, dst: pathlib.Path, hash: str = None, callback f.close() if os.path.exists(f.name): os.remove(f.name) + diff --git a/invesalius/plugins.py b/invesalius/plugins.py index 61f14634b..b1b4b05ce 100644 --- a/invesalius/plugins.py +++ b/invesalius/plugins.py @@ -23,13 +23,14 @@ import pathlib import sys from itertools import chain +from typing import Dict, Any from invesalius.pubsub import pub as Publisher from invesalius import inv_paths -def import_source(module_name, module_file_path): +def import_source(module_name: str, module_file_path: str) -> Any: module_spec = importlib.util.spec_from_file_location(module_name, module_file_path) module = importlib.util.module_from_spec(module_spec) module_spec.loader.exec_module(module) @@ -37,14 +38,14 @@ def import_source(module_name, module_file_path): class PluginManager: - def __init__(self): - self.plugins = {} + def __init__(self) -> None: + self.plugins: Dict[str, Any] = {} self.__bind_pubsub_evt() - def __bind_pubsub_evt(self): + def __bind_pubsub_evt(self) -> None: Publisher.subscribe(self.load_plugin, "Load plugin") - def find_plugins(self): + def find_plugins(self) -> None: self.plugins = {} for p in chain( glob.glob(str(inv_paths.PLUGIN_DIRECTORY.joinpath("**/plugin.json")), recursive=True), @@ -69,7 +70,7 @@ def find_plugins(self): Publisher.sendMessage("Add plugins menu items", items=self.plugins) - def load_plugin(self, plugin_name): + def load_plugin(self, plugin_name: str) -> None: if plugin_name in self.plugins: plugin_module = import_source( plugin_name, self.plugins[plugin_name]["folder"].joinpath("__init__.py") diff --git a/invesalius/pubsub/pub.py b/invesalius/pubsub/pub.py index 8f0d41f0e..194546d19 100644 --- a/invesalius/pubsub/pub.py +++ b/invesalius/pubsub/pub.py @@ -17,12 +17,12 @@ # detalhes. #-------------------------------------------------------------------------- -from typing import Callable +from typing import Callable, Any, Tuple from pubsub import pub as Publisher from pubsub.core.listener import UserListener -__all__ = [ +__all__: Tuple[str, ...] = ( # subscribing 'subscribe', 'unsubscribe', @@ -33,13 +33,12 @@ # adding hooks 'add_sendMessage_hook' -] - -Hook = Callable[[str, dict], None] +) +Hook: Callable[[str, dict], None] sendMessage_hook: Hook = None -def add_sendMessage_hook(hook: Hook): +def add_sendMessage_hook(hook: Hook) -> None: """Add a hook for sending messages. The hook is a function that takes the topic name as the first parameter and the message dict as the second parameter, and returns None. @@ -49,7 +48,7 @@ def add_sendMessage_hook(hook: Hook): global sendMessage_hook sendMessage_hook = hook -def subscribe(listener: UserListener, topicName: str, **curriedArgs): +def subscribe(listener: UserListener, topicName: str, **curriedArgs: Any) -> Tuple[UserListener, bool]: """Subscribe to a topic. :param listener: @@ -59,13 +58,13 @@ def subscribe(listener: UserListener, topicName: str, **curriedArgs): subscribedListener, success = Publisher.subscribe(listener, topicName, **curriedArgs) return subscribedListener, success -def unsubscribe(*args, **kwargs): +def unsubscribe(*args: Any, **kwargs: Any) -> None: """Unsubscribe from a topic. """ Publisher.unsubscribe(*args, **kwargs) -def sendMessage(topicName: str, **msgdata): +def sendMessage(topicName: str, **msgdata: Any) -> None: """Send a message in a given topic. :param topicName: @@ -75,7 +74,7 @@ def sendMessage(topicName: str, **msgdata): if sendMessage_hook is not None: sendMessage_hook(topicName, msgdata) -def sendMessage_no_hook(topicName: str, **msgdata): +def sendMessage_no_hook(topicName: str, **msgdata: Any) -> None: """Send a message in a given topic, but do not call the hook. :param topicName: @@ -83,5 +82,6 @@ def sendMessage_no_hook(topicName: str, **msgdata): """ Publisher.sendMessage(topicName, **msgdata) -AUTO_TOPIC = Publisher.AUTO_TOPIC -ALL_TOPICS = Publisher.ALL_TOPICS +AUTO_TOPIC: str = Publisher.AUTO_TOPIC +ALL_TOPICS: str = Publisher.ALL_TOPICS + diff --git a/invesalius/segmentation/deep_learning/model.py b/invesalius/segmentation/deep_learning/model.py index 29e390249..1f19b9a5a 100644 --- a/invesalius/segmentation/deep_learning/model.py +++ b/invesalius/segmentation/deep_learning/model.py @@ -3,101 +3,100 @@ import torch import torch.nn as nn -SIZE = 48 +SIZE: int = 48 class Unet3D(nn.Module): - # Based on https://github.com/mateuszbuda/brain-segmentation-pytorch/blob/master/unet.py - def __init__(self, in_channels=1, out_channels=1, init_features=8): + def __init__(self, in_channels: int = 1, out_channels: int = 1, init_features: int = 8) -> None: super().__init__() - features = init_features + features: int = init_features - self.encoder1 = self._block( + self.encoder1: nn.Sequential = self._block( in_channels, features=features, padding=2, name="enc1" ) - self.pool1 = nn.MaxPool3d(kernel_size=2, stride=2) + self.pool1: nn.MaxPool3d = nn.MaxPool3d(kernel_size=2, stride=2) - self.encoder2 = self._block( + self.encoder2: nn.Sequential = self._block( features, features=features * 2, padding=2, name="enc2" ) - self.pool2 = nn.MaxPool3d(kernel_size=2, stride=2) + self.pool2: nn.MaxPool3d = nn.MaxPool3d(kernel_size=2, stride=2) - self.encoder3 = self._block( + self.encoder3: nn.Sequential = self._block( features * 2, features=features * 4, padding=2, name="enc3" ) - self.pool3 = nn.MaxPool3d(kernel_size=2, stride=2) + self.pool3: nn.MaxPool3d = nn.MaxPool3d(kernel_size=2, stride=2) - self.encoder4 = self._block( + self.encoder4: nn.Sequential = self._block( features * 4, features=features * 8, padding=2, name="enc4" ) - self.pool4 = nn.MaxPool3d(kernel_size=2, stride=2) + self.pool4: nn.MaxPool3d = nn.MaxPool3d(kernel_size=2, stride=2) - self.bottleneck = self._block( + self.bottleneck: nn.Sequential = self._block( features * 8, features=features * 16, padding=2, name="bottleneck" ) - self.upconv4 = nn.ConvTranspose3d( + self.upconv4: nn.ConvTranspose3d = nn.ConvTranspose3d( features * 16, features * 8, kernel_size=4, stride=2, padding=1 ) - self.decoder4 = self._block( + self.decoder4: nn.Sequential = self._block( features * 16, features=features * 8, padding=2, name="dec4" ) - self.upconv3 = nn.ConvTranspose3d( + self.upconv3: nn.ConvTranspose3d = nn.ConvTranspose3d( features * 8, features * 4, kernel_size=4, stride=2, padding=1 ) - self.decoder3 = self._block( + self.decoder3: nn.Sequential = self._block( features * 8, features=features * 4, padding=2, name="dec4" ) - self.upconv2 = nn.ConvTranspose3d( + self.upconv2: nn.ConvTranspose3d = nn.ConvTranspose3d( features * 4, features * 2, kernel_size=4, stride=2, padding=1 ) - self.decoder2 = self._block( + self.decoder2: nn.Sequential = self._block( features * 4, features=features * 2, padding=2, name="dec4" ) - self.upconv1 = nn.ConvTranspose3d( + self.upconv1: nn.ConvTranspose3d = nn.ConvTranspose3d( features * 2, features, kernel_size=4, stride=2, padding=1 ) - self.decoder1 = self._block( + self.decoder1: nn.Sequential = self._block( features * 2, features=features, padding=2, name="dec4" ) - self.conv = nn.Conv3d( + self.conv: nn.Conv3d = nn.Conv3d( in_channels=features, out_channels=out_channels, kernel_size=1 ) - def forward(self, img): - enc1 = self.encoder1(img) - enc2 = self.encoder2(self.pool1(enc1)) - enc3 = self.encoder3(self.pool2(enc2)) - enc4 = self.encoder4(self.pool3(enc3)) + def forward(self, img: torch.Tensor) -> torch.Tensor: + enc1: torch.Tensor = self.encoder1(img) + enc2: torch.Tensor = self.encoder2(self.pool1(enc1)) + enc3: torch.Tensor = self.encoder3(self.pool2(enc2)) + enc4: torch.Tensor = self.encoder4(self.pool3(enc3)) - bottleneck = self.bottleneck(self.pool4(enc4)) + bottleneck: torch.Tensor = self.bottleneck(self.pool4(enc4)) - upconv4 = self.upconv4(bottleneck) - dec4 = torch.cat((upconv4, enc4), dim=1) - dec4 = self.decoder4(dec4) + upconv4: torch.Tensor = self.upconv4(bottleneck) + dec4: torch.Tensor = torch.cat((upconv4, enc4), dim=1) + dec4: torch.Tensor = self.decoder4(dec4) - upconv3 = self.upconv3(dec4) - dec3 = torch.cat((upconv3, enc3), dim=1) - dec3 = self.decoder3(dec3) + upconv3: torch.Tensor = self.upconv3(dec4) + dec3: torch.Tensor = torch.cat((upconv3, enc3), dim=1) + dec3: torch.Tensor = self.decoder3(dec3) - upconv2 = self.upconv2(dec3) - dec2 = torch.cat((upconv2, enc2), dim=1) - dec2 = self.decoder2(dec2) + upconv2: torch.Tensor = self.upconv2(dec3) + dec2: torch.Tensor = torch.cat((upconv2, enc2), dim=1) + dec2: torch.Tensor = self.decoder2(dec2) - upconv1 = self.upconv1(dec2) - dec1 = torch.cat((upconv1, enc1), dim=1) - dec1 = self.decoder1(dec1) + upconv1: torch.Tensor = self.upconv1(dec2) + dec1: torch.Tensor = torch.cat((upconv1, enc1), dim=1) + dec1: torch.Tensor = self.decoder1(dec1) - conv = self.conv(dec1) + conv: torch.Tensor = self.conv(dec1) - sigmoid = torch.sigmoid(conv) + sigmoid: torch.Tensor = torch.sigmoid(conv) return sigmoid - def _block(self, in_channels, features, padding=1, kernel_size=5, name="block"): + def _block(self, in_channels: int, features: int, padding: int = 1, kernel_size: int = 5, name: str = "block") -> nn.Sequential: return nn.Sequential( OrderedDict( ( @@ -130,16 +129,16 @@ def _block(self, in_channels, features, padding=1, kernel_size=5, name="block"): ) -def main(): +def main() -> None: import torchviz - dev = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") - model = Unet3D() + dev: torch.device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") + model: Unet3D = Unet3D() model.to(dev) model.eval() print(next(model.parameters()).is_cuda) # True - img = torch.randn(1, SIZE, SIZE, SIZE, 1).to(dev) - out = model(img) - dot = torchviz.make_dot(out, params=dict(model.named_parameters()), show_attrs=True, show_saved=True) + img: torch.Tensor = torch.randn(1, SIZE, SIZE, SIZE, 1).to(dev) + out: torch.Tensor = model(img) + dot: torchviz.Digraph = torchviz.make_dot(out, params=dict(model.named_parameters()), show_attrs=True, show_saved=True) dot.render("unet", format="png") torch.save(model, "model.pth") print(dot) diff --git a/invesalius/segmentation/deep_learning/utils.py b/invesalius/segmentation/deep_learning/utils.py index 3bc12b6ef..3eb0402a2 100644 --- a/invesalius/segmentation/deep_learning/utils.py +++ b/invesalius/segmentation/deep_learning/utils.py @@ -1,30 +1,31 @@ import os import pathlib import sys +from typing import Dict, Union -def prepare_plaidml(): +def prepare_plaidml() -> None: # Linux if installed plaidml with pip3 install --user if sys.platform.startswith("linux"): - local_user_plaidml = pathlib.Path("~/.local/share/plaidml/").expanduser().absolute() + local_user_plaidml: pathlib.Path = pathlib.Path("~/.local/share/plaidml/").expanduser().absolute() if local_user_plaidml.exists(): os.environ["RUNFILES_DIR"] = str(local_user_plaidml) os.environ["PLAIDML_NATIVE_PATH"] = str(pathlib.Path("~/.local/lib/libplaidml.so").expanduser().absolute()) # Mac if using python3 from homebrew elif sys.platform == "darwin": - local_user_plaidml = pathlib.Path("/usr/local/share/plaidml") + local_user_plaidml: pathlib.Path = pathlib.Path("/usr/local/share/plaidml") if local_user_plaidml.exists(): os.environ["RUNFILES_DIR"] = str(local_user_plaidml) os.environ["PLAIDML_NATIVE_PATH"] = str(pathlib.Path("/usr/local/lib/libplaidml.dylib").expanduser().absolute()) elif sys.platform == "win32": if 'VIRTUAL_ENV' in os.environ: - local_user_plaidml = pathlib.Path(os.environ["VIRTUAL_ENV"]).joinpath("share/plaidml") - plaidml_dll = pathlib.Path(os.environ["VIRTUAL_ENV"]).joinpath("library/bin/plaidml.dll") + local_user_plaidml: pathlib.Path = pathlib.Path(os.environ["VIRTUAL_ENV"]).joinpath("share/plaidml") + plaidml_dll: pathlib.Path = pathlib.Path(os.environ["VIRTUAL_ENV"]).joinpath("library/bin/plaidml.dll") if local_user_plaidml.exists(): os.environ["RUNFILES_DIR"] = str(local_user_plaidml) if plaidml_dll.exists(): os.environ["PLAIDML_NATIVE_PATH"] = str(plaidml_dll) -def prepare_ambient(backend, device_id, use_gpu): +def prepare_ambient(backend: str, device_id: str, use_gpu: bool) -> None: if backend.lower() == 'plaidml': os.environ["KERAS_BACKEND"] = "plaidml.keras.backend" os.environ["PLAIDML_DEVICE_IDS"] = device_id @@ -39,14 +40,12 @@ def prepare_ambient(backend, device_id, use_gpu): else: raise TypeError("Wrong backend") - - -def get_plaidml_devices(gpu=False): +def get_plaidml_devices(gpu: bool = False) -> Dict[str, str]: prepare_plaidml() import plaidml - ctx = plaidml.Context() + ctx: Context = plaidml.Context() plaidml.settings._setup_for_test(plaidml.settings.user_settings) plaidml.settings.experimental = True devices, _ = plaidml.devices(ctx, limit=100, return_all=True) From d4eba6e515fdc3dbc544b44555afe205d923303f Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Thu, 27 Apr 2023 00:10:12 +0530 Subject: [PATCH 14/16] added type info --- invesalius/gui/task_generic.py | 29 +++--- invesalius/navigation/robot.py | 37 ++++---- invesalius/net/dicom.py | 121 +++++++++++++------------- invesalius/net/neuronavigation_api.py | 33 +++---- invesalius/net/pedal_connection.py | 44 +++++----- invesalius/net/remote_control.py | 3 +- 6 files changed, 137 insertions(+), 130 deletions(-) diff --git a/invesalius/gui/task_generic.py b/invesalius/gui/task_generic.py index 6668528a4..f0e5070a0 100644 --- a/invesalius/gui/task_generic.py +++ b/invesalius/gui/task_generic.py @@ -23,12 +23,13 @@ import wx import wx.lib.hyperlink as hl + class TaskPanel(wx.Panel): """ This panel works as a "frame", drawing a white margin arround the panel that really matters (InnerTaskPanel). """ - def __init__(self, parent): + def __init__(self, parent: wx.Window) -> None: # note: don't change this class!!! wx.Panel.__init__(self, parent) @@ -43,13 +44,14 @@ def __init__(self, parent): self.Update() self.SetAutoLayout(1) + class InnerTaskPanel(wx.Panel): - def __init__(self, parent): + def __init__(self, parent: wx.Window) -> None: wx.Panel.__init__(self, parent) - self.SetBackgroundColour(wx.Colour(255,255,255)) + self.SetBackgroundColour(wx.Colour(255, 255, 255)) self.SetAutoLayout(1) - + # Build GUI self.__init_gui() @@ -57,41 +59,42 @@ def __init__(self, parent): self.__bind_events() self.__bind_wx_events() - def __init_gui(self): + def __init_gui(self) -> None: """ Build widgets in current panel """ # Create widgets to be inserted in this panel - link_test = hl.HyperLinkCtrl(self, -1, _("Testing...")) + link_test: hl.HyperLinkCtrl = hl.HyperLinkCtrl(self, -1, _("Testing...")) link_test.SetUnderlines(False, False, False) link_test.SetColours("BLACK", "BLACK", "BLACK") link_test.AutoBrowse(False) link_test.UpdateLink() - self.link_test = link_test + self.link_test: hl.HyperLinkCtrl = link_test # Add line sizers into main sizer - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(link_test, 0, wx.GROW|wx.EXPAND) + sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(link_test, 0, wx.GROW | wx.EXPAND) self.SetSizer(sizer) self.Fit() - def __bind_events(self): + def __bind_events(self) -> None: """ Bind pubsub events """ # Example: ps.Publisher().subscribe("Test") pass - def __bind_wx_events(self): + def __bind_wx_events(self) -> None: """ Bind wx general events """ # Example: self.Bind(wx.EVT_BUTTON, self.OnButton) self.link_test.Bind(hl.EVT_HYPERLINK_LEFT, self.OnTest) - - def OnTest(self, event): + + def OnTest(self, event: wx.CommandEvent) -> None: """ Describe what this method does """ event.Skip() + diff --git a/invesalius/navigation/robot.py b/invesalius/navigation/robot.py index 62d2cd636..5d807629d 100644 --- a/invesalius/navigation/robot.py +++ b/invesalius/navigation/robot.py @@ -21,6 +21,7 @@ import numpy as np import wx +from typing import Any, Dict, List, Optional import invesalius.constants as const import invesalius.gui.dialogs as dlg @@ -33,32 +34,32 @@ # functionality should be gathered here. class Robot(): - def __init__(self, tracker): + def __init__(self, tracker: Any) -> None: self.tracker = tracker - self.matrix_tracker_to_robot = None + self.matrix_tracker_to_robot: Optional[np.ndarray] = None - success = self.LoadState() + success: bool = self.LoadState() if success: self.InitializeRobot() self.__bind_events() - def __bind_events(self): + def __bind_events(self) -> None: Publisher.subscribe(self.AbortRobotConfiguration, 'Dialog robot destroy') - def SaveState(self): - matrix_tracker_to_robot = self.matrix_tracker_to_robot.tolist() + def SaveState(self) -> None: + matrix_tracker_to_robot: List[List[float]] = self.matrix_tracker_to_robot.tolist() - state = { + state: Dict[str, List[List[float]]] = { 'tracker_to_robot': matrix_tracker_to_robot, } - session = ses.Session() + session: ses.Session = ses.Session() session.SetState('robot', state) - def LoadState(self): - session = ses.Session() - state = session.GetState('robot') + def LoadState(self) -> bool: + session: ses.Session = ses.Session() + state: Optional[Dict[str, List[List[float]]]] = session.GetState('robot') if state is None: return False @@ -66,12 +67,12 @@ def LoadState(self): self.matrix_tracker_to_robot = np.array(state['tracker_to_robot']) return True - def ConfigureRobot(self): - self.robot_coregistration_dialog = dlg.RobotCoregistrationDialog(self.tracker) + def ConfigureRobot(self) -> bool: + self.robot_coregistration_dialog: dlg.RobotCoregistrationDialog = dlg.RobotCoregistrationDialog(self.tracker) # Show dialog and store relevant output values. - status = self.robot_coregistration_dialog.ShowModal() - matrix_tracker_to_robot = self.robot_coregistration_dialog.GetValue() + status: int = self.robot_coregistration_dialog.ShowModal() + matrix_tracker_to_robot: Optional[np.ndarray] = self.robot_coregistration_dialog.GetValue() # Destroy the dialog. self.robot_coregistration_dialog.Destroy() @@ -86,13 +87,13 @@ def ConfigureRobot(self): return True - def AbortRobotConfiguration(self): + def AbortRobotConfiguration(self) -> None: if self.robot_coregistration_dialog: self.robot_coregistration_dialog.Destroy() - def InitializeRobot(self): + def InitializeRobot(self) -> None: Publisher.sendMessage('Robot navigation mode', robot_mode=True) Publisher.sendMessage('Load robot transformation matrix', data=self.matrix_tracker_to_robot.tolist()) - def DisconnectRobot(self): + def DisconnectRobot(self) -> None: Publisher.sendMessage('Robot navigation mode', robot_mode=False) diff --git a/invesalius/net/dicom.py b/invesalius/net/dicom.py index 9ef858ffd..4b096c908 100644 --- a/invesalius/net/dicom.py +++ b/invesalius/net/dicom.py @@ -3,68 +3,67 @@ class DicomNet: - def __init__(self): - self.address = '' - self.port = '' - self.aetitle_call = '' - self.aetitle = '' - self.search_word = '' - self.search_type = 'patient' - - def __call__(self): + def __init__(self) -> None: + self.address: str = '' + self.port: str = '' + self.aetitle_call: str = '' + self.aetitle: str = '' + self.search_word: str = '' + self.search_type: str = 'patient' + + def __call__(self) -> "DicomNet": return self - def SetHost(self, address): + def SetHost(self, address: str) -> None: self.address = address - def SetPort(self, port): + def SetPort(self, port: str) -> None: self.port = port - def SetAETitleCall(self, name): + def SetAETitleCall(self, name: str) -> None: self.aetitle_call = name - def SetAETitle(self, ae_title): + def SetAETitle(self, ae_title: str) -> None: self.aetitle = ae_title - def SetSearchWord(self, word): + def SetSearchWord(self, word: str) -> None: self.search_word = word - def SetSearchType(self, stype): + def SetSearchType(self, stype: str) -> None: self.search_type = stype - def GetValueFromDICOM(self, ret, tag): - value = str(ret.GetDataElement(gdcm.Tag(tag[0],\ + def GetValueFromDICOM(self, ret: gdcm.DataSet, tag: 'tuple[int, int]') -> str: + value: str = str(ret.GetDataElement(gdcm.Tag(tag[0],\ tag[1])).GetValue()) if value == 'None' and tag != (0x0008,0x103E): value = '' return value - def RunCEcho(self): - cnf = gdcm.CompositeNetworkFunctions() + def RunCEcho(self) -> bool: + cnf: gdcm.CompositeNetworkFunctions = gdcm.CompositeNetworkFunctions() return cnf.CEcho(self.address, int(self.port),\ self.aetitle, self.aetitle_call) - def RunCFind(self): - - tags = [(0x0010, 0x0010), (0x0010, 0x1010), (0x0010,0x0040), (0x0008,0x1030),\ + def RunCFind(self) -> 'dict[str, dict[str, dict[str, str]]]': + tags: list[tuple[int, int]] = [(0x0010, 0x0010), (0x0010, 0x1010), (0x0010,0x0040), (0x0008,0x1030),\ (0x0008,0x0060), (0x0008,0x0022), (0x0008,0x0080), (0x0010,0x0030),\ (0x0008,0x0050), (0x0008,0x0090), (0x0008,0x103E), (0x0008,0x0033),\ (0x0008,0x0032), (0x0020,0x000d)] - ds = gdcm.DataSet() + ds: gdcm.DataSet = gdcm.DataSet() for tag in tags: - tg = gdcm.Tag(tag[0], tag[1]) + tg: gdcm.Tag = gdcm.Tag(tag[0], tag[1]) - de = gdcm.DataElement(tg) + de: gdcm.DataElement = gdcm.DataElement(tg) if self.search_type == 'patient' and tag == (0x0010, 0x0010): - bit_size = len(self.search_word) + 1 + bit_size: int = len(self.search_word) + 1 de.SetByteValue(str(self.search_word + '*'), gdcm.VL(bit_size)) else: de.SetByteValue('*', gdcm.VL(1)) @@ -72,20 +71,20 @@ def RunCFind(self): ds.Insert(de) - cnf = gdcm.CompositeNetworkFunctions() - theQuery = cnf.ConstructQuery(gdcm.ePatientRootType, gdcm.eImageOrFrame, ds) - ret = gdcm.DataSetArrayType() + cnf: gdcm.CompositeNetworkFunctions = gdcm.CompositeNetworkFunctions() + theQuery: gdcm.BaseRootQuery = cnf.ConstructQuery(gdcm.ePatientRootType, gdcm.eImageOrFrame, ds) + ret: gdcm.DataSetArrayType = gdcm.DataSetArrayType() cnf.CFind(self.address, int(self.port), theQuery, ret, self.aetitle,\ self.aetitle_call) - patients = {} + patients: dict[str, dict[str, dict[str, str]]] = {} - exist_images = False - c = 0 + exist_images: bool = False + c: int = 0 for i in range(0,ret.size()): - patient_id = str(ret[i].GetDataElement(gdcm.Tag(0x0010, 0x0020)).GetValue()) - serie_id = str(ret[i].GetDataElement(gdcm.Tag(0x0020, 0x000e)).GetValue()) + patient_id: str = str(ret[i].GetDataElement(gdcm.Tag(0x0010, 0x0020)).GetValue()) + serie_id: str = str(ret[i].GetDataElement(gdcm.Tag(0x0020, 0x000e)).GetValue()) if not(patient_id in patients.keys()): patients[patient_id] = {} @@ -93,22 +92,22 @@ def RunCFind(self): if not(serie_id in patients[patient_id]): - rt = ret[i] - - name = self.GetValueFromDICOM(rt, (0x0010, 0x0010)) - age = self.GetValueFromDICOM(rt, (0x0010, 0x1010)) - gender = self.GetValueFromDICOM(rt, (0x0010,0x0040)) - study_description = self.GetValueFromDICOM(rt, (0x0008,0x1030)) - modality = self.GetValueFromDICOM(rt, (0x0008,0x0060)) - institution = self.GetValueFromDICOM(rt, (0x0008,0x0080)) - date_of_birth = utils.format_date(self.GetValueFromDICOM(rt, (0x0010,0x0030))) - acession_number = self.GetValueFromDICOM(rt, (0x0008,0x0050)) - ref_physician = self.GetValueFromDICOM(rt, (0x0008,0x0090)) - serie_description = self.GetValueFromDICOM(rt, (0x0008,0x103E)) - acquisition_time = utils.format_time(self.GetValueFromDICOM(rt, (0x0008,0x0032))) - acquisition_date = utils.format_date(self.GetValueFromDICOM(rt, (0x0008,0x0022))) - - teste = self.GetValueFromDICOM(rt, (0x0020,0x000d)) + rt: gdcm.DataSet = ret[i] + + name: str = self.GetValueFromDICOM(rt, (0x0010, 0x0010)) + age: str = self.GetValueFromDICOM(rt, (0x0010, 0x1010)) + gender: str = self.GetValueFromDICOM(rt, (0x0010,0x0040)) + study_description: str = self.GetValueFromDICOM(rt, (0x0008,0x1030)) + modality: str = self.GetValueFromDICOM(rt, (0x0008,0x0060)) + institution: str = self.GetValueFromDICOM(rt, (0x0008,0x0080)) + date_of_birth: str = utils.format_date(self.GetValueFromDICOM(rt, (0x0010,0x0030))) + acession_number: str = self.GetValueFromDICOM(rt, (0x0008,0x0050)) + ref_physician: str = self.GetValueFromDICOM(rt, (0x0008,0x0090)) + serie_description: str = self.GetValueFromDICOM(rt, (0x0008,0x103E)) + acquisition_time: str = utils.format_time(self.GetValueFromDICOM(rt, (0x0008,0x0032))) + acquisition_date: str = utils.format_date(self.GetValueFromDICOM(rt, (0x0008,0x0022))) + + teste: str = self.GetValueFromDICOM(rt, (0x0020,0x000d)) patients[patient_id][serie_id] = {'name':name, 'age':age, 'gender':gender,\ 'study_description':study_description,\ @@ -128,21 +127,19 @@ def RunCFind(self): return patients - def RunCMove(self, values): - - ds = gdcm.DataSet() + def RunCMove(self, values: 'list[str]') -> None: + ds: gdcm.DataSet = gdcm.DataSet() #for v in values: + tg_patient: gdcm.Tag = gdcm.Tag(0x0010, 0x0020) + tg_serie: gdcm.Tag = gdcm.Tag(0x0020, 0x000e) - tg_patient = gdcm.Tag(0x0010, 0x0020) - tg_serie = gdcm.Tag(0x0020, 0x000e) - - de_patient = gdcm.DataElement(tg_patient) - de_serie = gdcm.DataElement(tg_serie) + de_patient: gdcm.DataElement = gdcm.DataElement(tg_patient) + de_serie: gdcm.DataElement = gdcm.DataElement(tg_serie) - patient_id = str(values[0]) - serie_id = str(values[1]) + patient_id: str = str(values[0]) + serie_id: str = str(values[1]) de_patient.SetByteValue(patient_id, gdcm.VL(len(patient_id))) de_serie.SetByteValue(serie_id, gdcm.VL(len(serie_id))) @@ -151,8 +148,8 @@ def RunCMove(self, values): ds.Insert(de_serie) - cnf = gdcm.CompositeNetworkFunctions() - theQuery = cnf.ConstructQuery(gdcm.ePatientRootType, gdcm.eImageOrFrame, ds) + cnf: gdcm.CompositeNetworkFunctions = gdcm.CompositeNetworkFunctions() + theQuery: gdcm.BaseRootQuery = cnf.ConstructQuery(gdcm.ePatientRootType, gdcm.eImageOrFrame, ds) #ret = gdcm.DataSetArrayType() """ diff --git a/invesalius/net/neuronavigation_api.py b/invesalius/net/neuronavigation_api.py index 449a0322e..34f1cf0ea 100644 --- a/invesalius/net/neuronavigation_api.py +++ b/invesalius/net/neuronavigation_api.py @@ -44,9 +44,9 @@ class NeuronavigationApi(metaclass=Singleton): If connection object is not given or it is None, do not do the updates. """ - N_VERTICES_IN_POLYGON = 3 + N_VERTICES_IN_POLYGON: int = 3 - def __init__(self, connection=None): + def __init__(self, connection: any = None) -> None: if connection is not None: self.assert_valid(connection) @@ -55,23 +55,23 @@ def __init__(self, connection=None): self.connection = connection - def _hasmethod(self, obj, name): + def _hasmethod(self, obj: any, name: str) -> bool: return hasattr(obj, name) and callable(getattr(obj, name)) - def assert_valid(self, connection): + def assert_valid(self, connection: any) -> None: assert self._hasmethod(connection, 'update_coil_at_target') assert self._hasmethod(connection, 'update_coil_pose') assert self._hasmethod(connection, 'update_focus') assert self._hasmethod(connection, 'set_callback__set_markers') - def __bind_events(self): + def __bind_events(self) -> None: Publisher.subscribe(self.update_coil_at_target, 'Coil at target') #Publisher.subscribe(self.update_focus, 'Set cross focal point') Publisher.subscribe(self.update_target_orientation, 'Update target orientation') # Functions for InVesalius to send updates. - def update_target_orientation(self, target_id, orientation): + def update_target_orientation(self, target_id: int, orientation: np.ndarray) -> None: if self.connection is not None: self.connection.update_target_orientation( target_id=target_id, @@ -85,21 +85,21 @@ def update_target_orientation(self, target_id, orientation): # would require changing 'Set cross focal point' publishers and subscribers # throughout the code. # - def update_focus(self, position): + def update_focus(self, position: np.ndarray) -> None: if self.connection is not None: self.connection.update_focus( position=position[:3], orientation=position[3:], ) - def update_coil_pose(self, position, orientation): + def update_coil_pose(self, position: np.ndarray, orientation: np.ndarray) -> None: if self.connection is not None: self.connection.update_coil_pose( position=position, orientation=orientation, ) - def update_coil_mesh(self, polydata): + def update_coil_mesh(self, polydata: any) -> None: if self.connection is not None: wrapped = dataset_adapter.WrapDataObject(polydata) @@ -125,13 +125,13 @@ def update_coil_mesh(self, polydata): polygons=polygons, ) - def update_coil_at_target(self, state): + def update_coil_at_target(self, state: bool) -> None: if self.connection is not None: self.connection.update_coil_at_target( state=state ) - def update_efield(self, position, orientation, T_rot): + def update_efield(self, position: np.ndarray, orientation: np.ndarray, T_rot: np.ndarray) -> any: if self.connection is not None: return self.connection.update_efield( position=position, @@ -142,11 +142,11 @@ def update_efield(self, position, orientation, T_rot): # Functions for InVesalius to receive updates via callbacks. - def __set_callbacks(self, connection): + def __set_callbacks(self, connection: any) -> None: connection.set_callback__set_markers(self.set_markers) connection.set_callback__open_orientation_dialog(self.open_orientation_dialog) - def add_pedal_callback(self, name, callback, remove_when_released=False): + def add_pedal_callback(self, name: str, callback: any, remove_when_released: bool = False) -> None: if self.connection is not None: self.connection.add_pedal_callback( name=name, @@ -154,12 +154,13 @@ def add_pedal_callback(self, name, callback, remove_when_released=False): remove_when_released=remove_when_released, ) - def remove_pedal_callback(self, name): + def remove_pedal_callback(self, name: str) -> None: if self.connection is not None: self.connection.remove_pedal_callback(name=name) - def open_orientation_dialog(self, target_id): + def open_orientation_dialog(self, target_id: int) -> None: wx.CallAfter(Publisher.sendMessage, 'Open marker orientation dialog', marker_id=target_id) - def set_markers(self, markers): + def set_markers(self, markers: any) -> None: wx.CallAfter(Publisher.sendMessage, 'Set markers', markers=markers) + diff --git a/invesalius/net/pedal_connection.py b/invesalius/net/pedal_connection.py index ccabddc68..9100a1581 100644 --- a/invesalius/net/pedal_connection.py +++ b/invesalius/net/pedal_connection.py @@ -19,12 +19,14 @@ import time from threading import Thread +from typing import List, Dict, Any, Union, Optional, Callable import mido from invesalius.pubsub import pub as Publisher from invesalius.utils import Singleton + class PedalConnection(Thread, metaclass=Singleton): """ Connect to the trigger pedal via MIDI, and allow adding callbacks for the pedal @@ -32,26 +34,27 @@ class PedalConnection(Thread, metaclass=Singleton): Started by calling PedalConnection().start() """ - def __init__(self): + + def __init__(self) -> None: Thread.__init__(self) self.daemon = True - self.in_use = False + self.in_use: bool = False - self._midi_in = None - self._active_inputs = None - self._callback_infos = [] + self._midi_in: Optional[mido.MidiFile] = None + self._active_inputs: Optional[str] = None + self._callback_infos: List[Dict[str, Any]] = [] - def _midi_to_pedal(self, msg): + def _midi_to_pedal(self, msg: mido.Message) -> None: # TODO: At this stage, interpret all note_on messages as the pedal being pressed, # and note_off messages as the pedal being released. Later, use the correct # message types and be more stringent about the messages. # if msg.type == 'note_on': - state = True + state: bool = True elif msg.type == 'note_off': - state = False + state: bool = False else: print("Unknown message type received from MIDI device") @@ -59,16 +62,16 @@ def _midi_to_pedal(self, msg): Publisher.sendMessage('Pedal state changed', state=state) for callback_info in self._callback_infos: - callback = callback_info['callback'] + callback: Callable[[bool], None] = callback_info['callback'] callback(state) if not state: self._callback_infos = [callback_info for callback_info in self._callback_infos if not callback_info['remove_when_released']] - def _connect_if_disconnected(self): + def _connect_if_disconnected(self) -> None: if self._midi_in is None and len(self._midi_inputs) > 0: - self._active_input = self._midi_inputs[0] - self._midi_in = mido.open_input(self._active_input) + self._active_input: str = self._midi_inputs[0] + self._midi_in: mido.MidiFile = mido.open_input(self._active_input) self._midi_in._rt.ignore_types(False, False, False) self._midi_in.callback = self._midi_to_pedal @@ -76,7 +79,7 @@ def _connect_if_disconnected(self): print("Connected to MIDI device") - def _check_disconnected(self): + def _check_disconnected(self) -> None: if self._midi_in is not None: if self._active_input not in self._midi_inputs: self._midi_in = None @@ -85,26 +88,27 @@ def _check_disconnected(self): print("Disconnected from MIDI device") - def _update_midi_inputs(self): - self._midi_inputs = mido.get_input_names() + def _update_midi_inputs(self) -> None: + self._midi_inputs: List[str] = mido.get_input_names() - def is_connected(self): + def is_connected(self) -> bool: return self._midi_in is not None - def add_callback(self, name, callback, remove_when_released=False): + def add_callback(self, name: str, callback: Callable[[bool], None], remove_when_released: Optional[bool] = False) -> None: self._callback_infos.append({ 'name': name, 'callback': callback, 'remove_when_released': remove_when_released, }) - def remove_callback(self, name): + def remove_callback(self, name: str) -> None: self._callback_infos = [callback_info for callback_info in self._callback_infos if callback_info['name'] != name] - def run(self): - self.in_use = True + def run(self) -> None: + self.in_use: bool = True while True: self._update_midi_inputs() self._check_disconnected() self._connect_if_disconnected() time.sleep(1.0) + diff --git a/invesalius/net/remote_control.py b/invesalius/net/remote_control.py index 8e7948c28..0b5ec411b 100644 --- a/invesalius/net/remote_control.py +++ b/invesalius/net/remote_control.py @@ -22,6 +22,7 @@ import socketio import wx +from typing import Optional from invesalius.pubsub import pub as Publisher @@ -29,7 +30,7 @@ class RemoteControl: def __init__(self, remote_host: str) -> None: self._remote_host: str = remote_host self._connected: bool = False - self._sio: typing.Optional[socketio.Client] = None + self._sio: Optional[socketio.Client] = None def _on_connect(self) -> None: print("Connected to {}".format(self._remote_host)) From 598bed1347557c5d930c1314c7866d5c9f02a307 Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Thu, 27 Apr 2023 13:58:03 +0530 Subject: [PATCH 15/16] added type info --- invesalius/gui/deep_learning_seg_dialog.py | 196 +++++++++++---------- invesalius/gui/default_tasks.py | 145 ++++++++------- invesalius/gui/widgets/inv_spinctrl.py | 127 ++++++------- 3 files changed, 235 insertions(+), 233 deletions(-) diff --git a/invesalius/gui/deep_learning_seg_dialog.py b/invesalius/gui/deep_learning_seg_dialog.py index ce34fea5e..39e42a263 100644 --- a/invesalius/gui/deep_learning_seg_dialog.py +++ b/invesalius/gui/deep_learning_seg_dialog.py @@ -18,24 +18,26 @@ from invesalius.pubsub import pub as Publisher from invesalius.segmentation.deep_learning import segment, utils -HAS_THEANO = bool(importlib.util.find_spec("theano")) -HAS_PLAIDML = bool(importlib.util.find_spec("plaidml")) -PLAIDML_DEVICES = {} -TORCH_DEVICES = {} +from typing import Dict, List, Tuple, Union, Any, Optional, Callable + +HAS_THEANO: bool = bool(importlib.util.find_spec("theano")) +HAS_PLAIDML: bool = bool(importlib.util.find_spec("plaidml")) +PLAIDML_DEVICES: Dict[str, str] = {} +TORCH_DEVICES: Dict[str, str] = {} try: import torch - HAS_TORCH = True + HAS_TORCH: bool = True except ImportError: - HAS_TORCH = False + HAS_TORCH: bool = False if HAS_TORCH: - TORCH_DEVICES = {} + TORCH_DEVICES: Dict[str, str] = {} if torch.cuda.is_available(): for i in range(torch.cuda.device_count()): - name = torch.cuda.get_device_name() - device_id = f"cuda:{i}" + name: str = torch.cuda.get_device_name() + device_id: str = f"cuda:{i}" TORCH_DEVICES[name] = device_id TORCH_DEVICES["CPU"] = "cpu" @@ -53,13 +55,13 @@ class DeepLearningSegmenterDialog(wx.Dialog): def __init__( self, - parent, - title, - has_torch=True, - has_plaidml=True, - has_theano=True, - segmenter=None - ): + parent: wx.Window, + title: str, + has_torch: bool = True, + has_plaidml: bool = True, + has_theano: bool = True, + segmenter: Union[None, Any] = None + ) -> None: wx.Dialog.__init__( self, parent, @@ -67,26 +69,26 @@ def __init__( title=title, style=wx.DEFAULT_DIALOG_STYLE | wx.FRAME_FLOAT_ON_PARENT, ) - backends = [] + backends: List[str] = [] if HAS_TORCH and has_torch: backends.append("Pytorch") if HAS_PLAIDML and has_plaidml: backends.append("PlaidML") if HAS_THEANO and has_theano: backends.append("Theano") - self.segmenter = segmenter + self.segmenter: Union[None, Any] = segmenter # self.pg_dialog = None - self.torch_devices = TORCH_DEVICES - self.plaidml_devices = PLAIDML_DEVICES + self.torch_devices: Dict[str, str] = TORCH_DEVICES + self.plaidml_devices: Dict[str, str] = PLAIDML_DEVICES - self.ps = None - self.segmented = False - self.mask = None + self.ps: Union[None, Any] = None + self.segmented: bool = False + self.mask: Union[None, Any] = None - self.overlap_options = (0, 10, 25, 50) - self.default_overlap = 50 + self.overlap_options: Tuple[int, int, int, int] = (0, 10, 25, 50) + self.default_overlap: int = 50 - self.cb_backends = wx.ComboBox( + self.cb_backends: wx.ComboBox = wx.ComboBox( self, wx.ID_ANY, choices=backends, @@ -95,33 +97,33 @@ def __init__( ) w, h = self.CalcSizeFromTextSize("MM" * (1 + max(len(i) for i in backends))) self.cb_backends.SetMinClientSize((w, -1)) - self.chk_use_gpu = wx.CheckBox(self, wx.ID_ANY, _("Use GPU")) + self.chk_use_gpu: wx.CheckBox = wx.CheckBox(self, wx.ID_ANY, _("Use GPU")) if HAS_TORCH or HAS_PLAIDML: if HAS_TORCH: - choices = list(self.torch_devices.keys()) - value = choices[0] + choices: List[str] = list(self.torch_devices.keys()) + value: str = choices[0] else: - choices = list(self.plaidml_devices.keys()) - value = choices[0] - self.lbl_device = wx.StaticText(self, -1, _("Device")) - self.cb_devices = wx.ComboBox( + choices: List[str] = list(self.plaidml_devices.keys()) + value: str = choices[0] + self.lbl_device: wx.StaticText = wx.StaticText(self, -1, _("Device")) + self.cb_devices: wx.ComboBox = wx.ComboBox( self, wx.ID_ANY, choices=choices, value=value, style=wx.CB_DROPDOWN | wx.CB_READONLY, ) - self.sld_threshold = wx.Slider(self, wx.ID_ANY, 75, 0, 100) + self.sld_threshold: wx.Slider = wx.Slider(self, wx.ID_ANY, 75, 0, 100) w, h = self.CalcSizeFromTextSize("M" * 20) self.sld_threshold.SetMinClientSize((w, -1)) - self.txt_threshold = wx.TextCtrl(self, wx.ID_ANY, "") + self.txt_threshold: wx.TextCtrl = wx.TextCtrl(self, wx.ID_ANY, "") w, h = self.CalcSizeFromTextSize("MMMMM") self.txt_threshold.SetMinClientSize((w, -1)) - self.chk_new_mask = wx.CheckBox(self, wx.ID_ANY, _("Create new mask")) + self.chk_new_mask: wx.CheckBox = wx.CheckBox(self, wx.ID_ANY, _("Create new mask")) self.chk_new_mask.SetValue(True) - self.chk_apply_wwwl = wx.CheckBox(self, wx.ID_ANY, _("Apply WW&WL")) + self.chk_apply_wwwl: wx.CheckBox = wx.CheckBox(self, wx.ID_ANY, _("Apply WW&WL")) self.chk_apply_wwwl.SetValue(False) - self.overlap = wx.RadioBox( + self.overlap: wx.RadioBox = wx.RadioBox( self, -1, _("Overlap"), @@ -129,37 +131,38 @@ def __init__( style=wx.NO_BORDER | wx.HORIZONTAL, ) self.overlap.SetSelection(self.overlap_options.index(self.default_overlap)) - self.progress = wx.Gauge(self, -1) - self.lbl_progress_caption = wx.StaticText(self, -1, _("Elapsed time:")) - self.lbl_time = wx.StaticText(self, -1, _("00:00:00")) - self.btn_segment = wx.Button(self, wx.ID_ANY, _("Segment")) - self.btn_stop = wx.Button(self, wx.ID_ANY, _("Stop")) + self.progress: wx.Gauge = wx.Gauge(self, -1) + self.lbl_progress_caption: wx.StaticText = wx.StaticText(self, -1, _("Elapsed time:")) + self.lbl_time: wx.StaticText = wx.StaticText(self, -1, _("00:00:00")) + self.btn_segment: wx.Button = wx.Button(self, wx.ID_ANY, _("Segment")) + self.btn_stop: wx.Button = wx.Button(self, wx.ID_ANY, _("Stop")) self.btn_stop.Disable() - self.btn_close = wx.Button(self, wx.ID_CLOSE) + self.btn_close: wx.Button = wx.Button(self, wx.ID_CLOSE) self.txt_threshold.SetValue("{:3d}%".format(self.sld_threshold.GetValue())) - self.elapsed_time_timer = wx.Timer() + self.elapsed_time_timer: wx.Timer = wx.Timer() self.__do_layout() self.__set_events() - def __do_layout(self): - main_sizer = wx.BoxSizer(wx.VERTICAL) - sizer_3 = wx.BoxSizer(wx.HORIZONTAL) - sizer_backends = wx.BoxSizer(wx.HORIZONTAL) - label_1 = wx.StaticText(self, wx.ID_ANY, _("Backend")) + + def __do_layout(self) -> None: + main_sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL) + sizer_3: wx.BoxSizer = wx.BoxSizer(wx.HORIZONTAL) + sizer_backends: wx.BoxSizer = wx.BoxSizer(wx.HORIZONTAL) + label_1: wx.StaticText = wx.StaticText(self, wx.ID_ANY, _("Backend")) sizer_backends.Add(label_1, 0, wx.ALIGN_CENTER, 0) sizer_backends.Add(self.cb_backends, 1, wx.LEFT, 5) main_sizer.Add(sizer_backends, 0, wx.ALL | wx.EXPAND, 5) main_sizer.Add(self.chk_use_gpu, 0, wx.ALL, 5) - sizer_devices = wx.BoxSizer(wx.HORIZONTAL) + sizer_devices: wx.BoxSizer = wx.BoxSizer(wx.HORIZONTAL) if HAS_TORCH or HAS_PLAIDML: sizer_devices.Add(self.lbl_device, 0, wx.ALIGN_CENTER, 0) sizer_devices.Add(self.cb_devices, 1, wx.LEFT, 5) main_sizer.Add(sizer_devices, 0, wx.ALL | wx.EXPAND, 5) main_sizer.Add(self.overlap, 0, wx.ALL | wx.EXPAND, 5) - label_5 = wx.StaticText(self, wx.ID_ANY, _("Level of certainty")) + label_5: wx.StaticText = wx.StaticText(self, wx.ID_ANY, _("Level of certainty")) main_sizer.Add(label_5, 0, wx.ALL, 5) sizer_3.Add( self.sld_threshold, @@ -172,11 +175,11 @@ def __do_layout(self): main_sizer.Add(self.chk_apply_wwwl, 0, wx.EXPAND | wx.ALL, 5) main_sizer.Add(self.chk_new_mask, 0, wx.EXPAND | wx.ALL, 5) main_sizer.Add(self.progress, 0, wx.EXPAND | wx.ALL, 5) - time_sizer = wx.BoxSizer(wx.HORIZONTAL) + time_sizer: wx.BoxSizer = wx.BoxSizer(wx.HORIZONTAL) time_sizer.Add(self.lbl_progress_caption, 0, wx.EXPAND, 0) time_sizer.Add(self.lbl_time, 1, wx.EXPAND | wx.LEFT, 5) main_sizer.Add(time_sizer, 0, wx.EXPAND | wx.ALL, 5) - sizer_buttons = wx.BoxSizer(wx.HORIZONTAL) + sizer_buttons: wx.BoxSizer = wx.BoxSizer(wx.HORIZONTAL) sizer_buttons.Add(self.btn_close, 0, wx.ALIGN_BOTTOM | wx.ALL, 5) sizer_buttons.Add(self.btn_stop, 0, wx.ALIGN_BOTTOM | wx.ALL, 5) sizer_buttons.Add(self.btn_segment, 0, wx.ALIGN_BOTTOM | wx.ALL, 5) @@ -185,7 +188,7 @@ def __do_layout(self): main_sizer.Fit(self) main_sizer.SetSizeHints(self) - self.main_sizer = main_sizer + self.main_sizer: wx.BoxSizer = main_sizer self.OnSetBackend() self.HideProgress() @@ -193,7 +196,7 @@ def __do_layout(self): self.Layout() self.Centre() - def __set_events(self): + def __set_events(self) -> None: self.cb_backends.Bind(wx.EVT_COMBOBOX, self.OnSetBackend) self.sld_threshold.Bind(wx.EVT_SCROLL, self.OnScrollThreshold) self.txt_threshold.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) @@ -203,23 +206,23 @@ def __set_events(self): self.elapsed_time_timer.Bind(wx.EVT_TIMER, self.OnTickTimer) self.Bind(wx.EVT_CLOSE, self.OnClose) - def apply_segment_threshold(self): - threshold = self.sld_threshold.GetValue() / 100.0 + def apply_segment_threshold(self) -> None: + threshold: float = self.sld_threshold.GetValue() / 100.0 if self.ps is not None: self.ps.apply_segment_threshold(threshold) slc.Slice().discard_all_buffers() Publisher.sendMessage("Reload actual slice") - def CalcSizeFromTextSize(self, text): - dc = wx.WindowDC(self) + def CalcSizeFromTextSize(self, text: str) -> Tuple[int, int]: + dc: wx.WindowDC = wx.WindowDC(self) dc.SetFont(self.GetFont()) width, height = dc.GetTextExtent(text) return width, height - def OnSetBackend(self, evt=None): + def OnSetBackend(self, evt: Optional[wx.Event] = None) -> None: if self.cb_backends.GetValue().lower() == "pytorch": if HAS_TORCH: - choices = list(self.torch_devices.keys()) + choices: List[str] = list(self.torch_devices.keys()) self.cb_devices.Clear() self.cb_devices.SetItems(choices) self.cb_devices.SetValue(choices[0]) @@ -228,7 +231,7 @@ def OnSetBackend(self, evt=None): self.chk_use_gpu.Hide() elif self.cb_backends.GetValue().lower() == "plaidml": if HAS_PLAIDML: - choices = list(self.plaidml_devices.keys()) + choices: List[str] = list(self.plaidml_devices.keys()) self.cb_devices.Clear() self.cb_devices.SetItems(choices) self.cb_devices.SetValue(choices[0]) @@ -244,14 +247,14 @@ def OnSetBackend(self, evt=None): self.main_sizer.Fit(self) self.main_sizer.SetSizeHints(self) - def OnScrollThreshold(self, evt): - value = self.sld_threshold.GetValue() + def OnScrollThreshold(self, evt: wx.ScrollEvent) -> None: + value: int = self.sld_threshold.GetValue() self.txt_threshold.SetValue("{:3d}%".format(self.sld_threshold.GetValue())) if self.segmented: self.apply_segment_threshold() - def OnKillFocus(self, evt): - value = self.txt_threshold.GetValue() + def OnKillFocus(self, evt: wx.FocusEvent) -> None: + value: str = self.txt_threshold.GetValue() value = value.replace("%", "") try: value = int(value) @@ -262,39 +265,40 @@ def OnKillFocus(self, evt): if self.segmented: self.apply_segment_threshold() + - def OnSegment(self, evt): + def OnSegment(self, evt: Event) -> None: self.ShowProgress() - self.t0 = time.time() + self.t0: float = time.time() self.elapsed_time_timer.Start(1000) - image = slc.Slice().matrix - backend = self.cb_backends.GetValue() + image: np.ndarray = slc.Slice().matrix + backend: str = self.cb_backends.GetValue() if backend.lower() == "pytorch": try: - device_id = self.torch_devices[self.cb_devices.GetValue()] + device_id: Union[str, int] = self.torch_devices[self.cb_devices.GetValue()] except (KeyError, AttributeError): device_id = "cpu" else: try: - device_id = self.plaidml_devices[self.cb_devices.GetValue()] + device_id: str = self.plaidml_devices[self.cb_devices.GetValue()] except (KeyError, AttributeError): device_id = "llvm_cpu.0" - apply_wwwl = self.chk_apply_wwwl.GetValue() - create_new_mask = self.chk_new_mask.GetValue() - use_gpu = self.chk_use_gpu.GetValue() - prob_threshold = self.sld_threshold.GetValue() / 100.0 + apply_wwwl: bool = self.chk_apply_wwwl.GetValue() + create_new_mask: bool = self.chk_new_mask.GetValue() + use_gpu: bool = self.chk_use_gpu.GetValue() + prob_threshold: float = self.sld_threshold.GetValue() / 100.0 self.btn_close.Disable() self.btn_stop.Enable() self.btn_segment.Disable() self.chk_new_mask.Disable() - window_width = slc.Slice().window_width - window_level = slc.Slice().window_level + window_width: float = slc.Slice().window_width + window_level: float = slc.Slice().window_level - overlap = self.overlap_options[self.overlap.GetSelection()] + overlap: str = self.overlap_options[self.overlap.GetSelection()] try: - self.ps = self.segmenter( + self.ps: multiprocessing.Process = self.segmenter( image, create_new_mask, backend, @@ -309,7 +313,7 @@ def OnSegment(self, evt): except (multiprocessing.ProcessError, OSError, ValueError) as err: self.OnStop(None) self.HideProgress() - dlg = dialogs.ErrorMessageBox( + dlg: dialogs.ErrorMessageBox = dialogs.ErrorMessageBox( None, "It was not possible to start brain segmentation because:" + "\n" @@ -319,7 +323,7 @@ def OnSegment(self, evt): ) dlg.ShowModal() - def OnStop(self, evt): + def OnStop(self, evt: Event) -> None: if self.ps is not None: self.ps.terminate() self.btn_close.Enable() @@ -328,11 +332,11 @@ def OnStop(self, evt): self.chk_new_mask.Enable() self.elapsed_time_timer.Stop() - def OnBtnClose(self, evt): + def OnBtnClose(self, evt: Event) -> None: self.Close() - def AfterSegment(self): - self.segmented = True + def AfterSegment(self) -> None: + self.segmented: bool = True self.btn_close.Enable() self.btn_stop.Disable() self.btn_segment.Disable() @@ -340,19 +344,19 @@ def AfterSegment(self): self.elapsed_time_timer.Stop() self.apply_segment_threshold() - def SetProgress(self, progress): + def SetProgress(self, progress: float) -> None: self.progress.SetValue(int(progress * 100)) wx.GetApp().Yield() - def OnTickTimer(self, evt): - fmt = "%H:%M:%S" + def OnTickTimer(self, evt: Event) -> None: + fmt: str = "%H:%M:%S" self.lbl_time.SetLabel(time.strftime(fmt, time.gmtime(time.time() - self.t0))) if self.ps is not None: if not self.ps.is_alive() and self.ps.exception is not None: error, traceback = self.ps.exception self.OnStop(None) self.HideProgress() - dlg = dialogs.ErrorMessageBox( + dlg: dialogs.ErrorMessageBox = dialogs.ErrorMessageBox( None, "Brain segmentation error", "It was not possible to use brain segmentation because:" @@ -365,14 +369,14 @@ def OnTickTimer(self, evt): dlg.ShowModal() return - progress = self.ps.get_completion() + progress: float = self.ps.get_completion() if progress == np.Inf: progress = 1 self.AfterSegment() progress = max(0, min(progress, 1)) self.SetProgress(float(progress)) - def OnClose(self, evt): + def OnClose(self, evt: Event) -> None: # self.segmenter.stop = True self.btn_stop.Disable() self.btn_segment.Enable() @@ -385,14 +389,14 @@ def OnClose(self, evt): self.Destroy() - def HideProgress(self): + def HideProgress(self) -> None: self.progress.Hide() self.lbl_progress_caption.Hide() self.lbl_time.Hide() self.main_sizer.Fit(self) self.main_sizer.SetSizeHints(self) - def ShowProgress(self): + def ShowProgress(self) -> None: self.progress.Show() self.lbl_progress_caption.Show() self.lbl_time.Show() @@ -401,7 +405,7 @@ def ShowProgress(self): class BrainSegmenterDialog(DeepLearningSegmenterDialog): - def __init__(self, parent): + def __init__(self, parent: wx.Window) -> None: super().__init__( parent=parent, title=_("Brain segmentation"), @@ -413,7 +417,7 @@ def __init__(self, parent): class TracheaSegmenterDialog(DeepLearningSegmenterDialog): - def __init__(self, parent): + def __init__(self, parent: wx.Window) -> None: super().__init__( parent=parent, title=_("Trachea segmentation"), diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index e664cdcde..96bfda80e 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -18,10 +18,14 @@ #-------------------------------------------------------------------------- import wx +from io import BytesIO +from typing import Tuple, Union, List, Optional, Any, Dict, Callable, Type + try: import wx.lib.agw.foldpanelbar as fpb except ModuleNotFoundError: import wx.lib.foldpanelbar as fpb + from invesalius.pubsub import pub as Publisher import invesalius.constants as const @@ -34,16 +38,15 @@ import invesalius.gui.task_tools as tools import invesalius.gui.task_navigator as navigator -FPB_DEFAULT_STYLE = 2621440 +FPB_DEFAULT_STYLE: int = 2621440 -def GetCollapsedIconData(): - return \ -b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +def GetCollapsedIconData() -> bytes: + return b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ \x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ \x00\x01\x8eIDAT8\x8d\xa5\x93-n\xe4@\x10\x85?g\x03\n6lh)\xc4\xd2\x12\xc3\x81\ \xd6\xa2I\x90\x154\xb9\x81\x8f1G\xc8\x11\x16\x86\xcd\xa0\x99F\xb3A\x91\xa1\ -\xc9J&\x96L"5lX\xcc\x0bl\xf7v\xb2\x7fZ\xa5\x98\xebU\xbdz\xf5\\\x9deW\x9f\xf8\ -H\\\xbfO|{y\x9dT\x15P\x04\x01\x01UPUD\x84\xdb/7YZ\x9f\xa5\n\xce\x97aRU\x8a\ +\xc9J&\x96L"5lX\xcc\x0bl\xf7v\xb2\x7fZ\xa5\x98\xebU\xbdz\xf5\\x9deW\x9f\xf8\ +H\\xbfO|{y\x9dT\x15P\x04\x01\x01UPUD\x84\xdb/7YZ\x9f\xa5\n\xce\x97aRU\x8a\ \xdc`\xacA\x00\x04P\xf0!0\xf6\x81\xa0\xf0p\xff9\xfb\x85\xe0|\x19&T)K\x8b\x18\ \xf9\xa3\xe4\xbe\xf3\x8c^#\xc9\xd5\n\xa8*\xc5?\x9a\x01\x8a\xd2b\r\x1cN\xc3\ \x14\t\xce\x97a\xb2F0Ks\xd58\xaa\xc6\xc5\xa6\xf7\xdfya\xe7\xbdR\x13M2\xf9\ @@ -57,30 +60,28 @@ def GetCollapsedIconData(): 0)\xba>\x83\xd5\xb97o\xe0\xaf\x04\xff\x13?\x00\xd2\xfb\xa9`z\xac\x80w\x00\ \x00\x00\x00IEND\xaeB`\x82' -def GetCollapsedIconBitmap(): +def GetCollapsedIconBitmap() -> wx.Bitmap: return wx.Bitmap(GetCollapsedIconImage()) -def GetCollapsedIconImage(): - from io import BytesIO - stream = BytesIO(GetCollapsedIconData()) +def GetCollapsedIconImage() -> wx.Image: + stream: BytesIO = BytesIO(GetCollapsedIconData()) return wx.Image(stream) -def GetExpandedIconData(): - return \ -b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +def GetExpandedIconData() -> bytes: + return b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ \x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ \x00\x01\x9fIDAT8\x8d\x95\x93\xa1\x8e\xdc0\x14EO\xb2\xc4\xd0\xd2\x12\xb7(mI\ \xa4%V\xd1lQT4[4-\x9a\xfe\xc1\xc2|\xc6\xc2~BY\x83:A3E\xd3\xa0*\xa4\xd2\x90H!\ \x95\x0c\r\r\x1fK\x81g\xb2\x99\x84\xb4\x0fY\xd6\xbb\xc7\xf7>=\'Iz\xc3\xbcv\ \xfbn\xb8\x9c\x15 \xe7\xf3\xc7\x0fw\xc9\xbc7\x99\x03\x0e\xfbn0\x99F+\x85R\ -\x80RH\x10\x82\x08\xde\x05\x1ef\x90+\xc0\xe1\xd8\ryn\xd0Z-\\A\xb4\xd2\xf7\ +\x80RH\x10\x82\x08\xde\x05\x1ef\x90+\xc0\xe1\xd8\ryn\xd0Z-\A\xb4\xd2\xf7\ \x9e\xfbwoF\xc8\x088\x1c\xbbae\xb3\xe8y&\x9a\xdf\xf5\xbd\xe7\xfem\x84\xa4\ \x97\xccYf\x16\x8d\xdb\xb2a]\xfeX\x18\xc9s\xc3\xe1\x18\xe7\x94\x12cb\xcc\xb5\ \xfa\xb1l8\xf5\x01\xe7\x84\xc7\xb2Y@\xb2\xcc0\x02\xb4\x9a\x88%\xbe\xdc\xb4\ \x9e\xb6Zs\xaa74\xadg[6\x88<\xb7]\xc6\x14\x1dL\x86\xe6\x83\xa0\x81\xba\xda\ \x10\x02x/\xd4\xd5\x06\r\x840!\x9c\x1fM\x92\xf4\x86\x9f\xbf\xfe\x0c\xd6\x9ae\ \xd6u\x8d \xf4\xf5\x165\x9b\x8f\x04\xe1\xc5\xcb\xdb$\x05\x90\xa97@\x04lQas\ -\xcd*7\x14\xdb\x9aY\xcb\xb8\\\xe9E\x10|\xbc\xf2^\xb0E\x85\xc95_\x9f\n\xaa/\ +\xcd*7\x14\xdb\x9aY\xcb\xb8\\xe9E\x10|\xbc\xf2^\xb0E\x85\xc95_\x9f\n\xaa/\ \x05\x10\x81\xce\xc9\xa8\xf6> wx.Bitmap: return wx.Bitmap(GetExpandedIconImage()) -def GetExpandedIconImage(): - from io import BytesIO - stream = BytesIO(GetExpandedIconData()) +def GetExpandedIconImage() -> wx.Image: + stream: BytesIO = BytesIO(GetExpandedIconData()) return wx.Image(stream) + # Main panel class Panel(wx.Panel): - def __init__(self, parent): - wx.Panel.__init__(self, parent, pos=wx.Point(5, 5), - size=wx.Size(280, 656)) + def __init__(self, parent: wx.Window) -> None: + super().__init__(parent, pos=wx.Point(5, 5), size=wx.Size(280, 656)) + + gbs: wx.GridBagSizer = wx.GridBagSizer(5, 5) - #sizer = wx.BoxSizer(wx.VERTICAL) - gbs = wx.GridBagSizer(5,5) - #sizer.Add(UpperTaskPanel(self), 5, wx.EXPAND|wx.GROW) gbs.Add(UpperTaskPanel(self), (0, 0), flag=wx.EXPAND) #sizer.Add(LowerTaskPanel(self), 3, wx.EXPAND|wx.GROW) @@ -116,10 +115,10 @@ def __init__(self, parent): gbs.AddGrowableRow(0, 1) gbs.AddGrowableRow(1, 0) - sizer = wx.BoxSizer(wx.VERTICAL) + sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(gbs, 1, wx.EXPAND) - self.gbs = gbs + self.gbs: wx.GridBagSizer = gbs #self.SetSizerAndFit(sizer) self.SetSizer(sizer) @@ -128,43 +127,41 @@ def __init__(self, parent): # Lower fold panel class LowerTaskPanel(wx.Panel): - def __init__(self, parent): - wx.Panel.__init__(self, parent, size=(150, -1)) - fold_panel = fpb.FoldPanelBar(self, -1, + def __init__(self, parent: wx.Window) -> None: + super().__init__(parent, size=(150, -1)) + fold_panel: fpb.FoldPanelBar = fpb.FoldPanelBar(self, -1, style=FPB_DEFAULT_STYLE, agwStyle=fpb.FPB_COLLAPSE_TO_BOTTOM) - self.fold_panel = fold_panel + self.fold_panel: fpb.FoldPanelBar = fold_panel - self.enable_items = [] - self.overwrite = False + self.enable_items: List[fpb.FoldPanel] = [] + self.overwrite: bool = False - gbs = wx.GridBagSizer(5,5) + gbs: wx.GridBagSizer = wx.GridBagSizer(5, 5) gbs.AddGrowableCol(0, 1) - self.gbs = gbs + self.gbs: wx.GridBagSizer = gbs - sizer = wx.BoxSizer(wx.VERTICAL) + sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(gbs, 1, wx.GROW|wx.EXPAND) self.SetSizer(sizer) - image_list = wx.ImageList(16,16) + image_list: wx.ImageList = wx.ImageList(16, 16) image_list.Add(GetExpandedIconBitmap()) image_list.Add(GetCollapsedIconBitmap()) # Fold 1 - Data - item = fold_panel.AddFoldPanel(_("Data"), collapsed=False, + item: fpb.FoldPanel = fold_panel.AddFoldPanel(_("Data"), collapsed=False, foldIcons=image_list) - style = fold_panel.GetCaptionStyle(item) - col = style.GetFirstColour() + style: fpb.CaptionBarStyle = fold_panel.GetCaptionStyle(item) + col: wx.Colour = style.GetFirstColour() self.enable_items.append(item) - #npanel = wx.Panel(self, -1) - self.npanel = nb.NotebookPanel(item) + self.npanel: nb.NotebookPanel = nb.NotebookPanel(item) self.__calc_best_size(self.npanel) - fold_panel.AddFoldPanelWindow(item, self.npanel, #fpb.FPB_ALIGN_WIDTH, #Spacing= 0, - leftSpacing=0, rightSpacing=0) + fold_panel.AddFoldPanelWindow(item, self.npanel, leftSpacing=0, rightSpacing=0) gbs.AddGrowableRow(0, 1) gbs.Add(fold_panel, (0, 0), flag=wx.EXPAND) @@ -178,12 +175,12 @@ def __init__(self, parent): self.__bind_events() - def __calc_best_size(self, panel): - parent = panel.GetParent() + def __calc_best_size(self, panel: wx.Panel) -> None: + parent: wx.Window = panel.GetParent() panel.Reparent(self) - gbs = self.gbs - fold_panel = self.fold_panel + gbs: wx.GridBagSizer = self.gbs + fold_panel: fpb.FoldPanelBar = self.fold_panel # Calculating the size gbs.AddGrowableRow(1, 1) @@ -192,7 +189,7 @@ def __calc_best_size(self, panel): gbs.Add(panel, (1, 0), flag=wx.EXPAND) gbs.Layout() self.Fit() - size = panel.GetSize() + size: wx.Size = panel.GetSize() gbs.Remove(1) gbs.Remove(0) @@ -202,32 +199,32 @@ def __calc_best_size(self, panel): panel.SetInitialSize(size) self.SetInitialSize(self.GetSize()) - def __bind_events(self): + def __bind_events(self) -> None: Publisher.subscribe(self.OnEnableState, "Enable state project") - def OnEnableState(self, state): + def OnEnableState(self, state: bool) -> None: if state: self.SetStateProjectOpen() else: self.SetStateProjectClose() - def SetStateProjectClose(self): + def SetStateProjectClose(self) -> None: for item in self.enable_items: item.Disable() - def SetStateProjectOpen(self): + def SetStateProjectOpen(self) -> None: for item in self.enable_items: item.Enable() - def ResizeFPB(self): - sizeNeeded = self.fold_panel.GetPanelsLength(0, 0)[2] + def ResizeFPB(self) -> None: + sizeNeeded: int = self.fold_panel.GetPanelsLength(0, 0)[2] self.fold_panel.SetMinSize((self.fold_panel.GetSize()[0], sizeNeeded )) self.fold_panel.SetSize((self.fold_panel.GetSize()[0], sizeNeeded)) # Upper fold panel class UpperTaskPanel(wx.Panel): - def __init__(self, parent): + def __init__(self, parent: wx.Window) -> None: wx.Panel.__init__(self, parent) fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, wx.DefaultSize,FPB_DEFAULT_STYLE, @@ -237,9 +234,9 @@ def __init__(self, parent): image_list.Add(GetExpandedIconBitmap()) image_list.Add(GetCollapsedIconBitmap()) - - self.enable_items = [] - self.overwrite = False + self.enable_items: List[fpb.FoldPanel] = [] + self.overwrite: bool = False + self.navigation_mode_status: bool = False session = ses.Session() mode = session.GetConfig('mode') @@ -247,14 +244,14 @@ def __init__(self, parent): print("Mode: ", mode) if mode == const.MODE_RP: - tasks = [(_("Load data"), importer.TaskPanel), + tasks: List[Tuple[str, wx.Panel]] = [(_("Load data"), importer.TaskPanel), (_("Select region of interest"), slice_.TaskPanel), (_("Configure 3D surface"), surface.TaskPanel), (_("Export data"), exporter.TaskPanel) ] elif mode == const.MODE_NAVIGATOR: - tasks = [(_("Load data"), importer.TaskPanel), + tasks: List[Tuple[str, wx.Panel]] = [(_("Load data"), importer.TaskPanel), (_("Select region of interest"), slice_.TaskPanel), (_("Configure 3D surface"), surface.TaskPanel), (_("Export data"), exporter.TaskPanel), @@ -264,7 +261,7 @@ def __init__(self, parent): (name, panel) = tasks[i] # Create panel - item = fold_panel.AddFoldPanel("%d. %s"%(i+1, name), + item: fpb.FoldPanel = fold_panel.AddFoldPanel("%d. %s"%(i+1, name), collapsed=True, foldIcons=image_list) style = fold_panel.GetCaptionStyle(item) @@ -286,9 +283,9 @@ def __init__(self, parent): # It is used as reference to set mouse cursor related to # slice editor. if name == _("Select region of interest"): - self.__id_slice = item.GetId() + self.__id_slice: int = item.GetId() elif name == _("Configure 3D surface"): - self.__id_surface = item.GetId() + self.__id_surface: int = item.GetId() fold_panel.Expand(fold_panel.GetFoldPanel(0)) self.fold_panel = fold_panel @@ -302,7 +299,7 @@ def __init__(self, parent): self.SetStateProjectClose() self.__bind_events() - def __bind_events(self): + def __bind_events(self) -> None: self.fold_panel.Bind(fpb.EVT_CAPTIONBAR, self.OnFoldPressCaption) Publisher.subscribe(self.OnEnableState, "Enable state project") Publisher.subscribe(self.OnOverwrite, 'Create surface from index') @@ -310,23 +307,23 @@ def __bind_events(self): Publisher.subscribe(self.OnFoldExport, 'Fold export task') Publisher.subscribe(self.SetNavigationMode, "Set navigation mode") - def OnOverwrite(self, surface_parameters): + def OnOverwrite(self, surface_parameters: dict) -> None: self.overwrite = surface_parameters['options']['overwrite'] - def OnFoldSurface(self): + def OnFoldSurface(self) -> None: if not self.overwrite: self.fold_panel.Expand(self.fold_panel.GetFoldPanel(2)) - def OnFoldExport(self): + def OnFoldExport(self) -> None: self.fold_panel.Expand(self.fold_panel.GetFoldPanel(3)) - def OnEnableState(self, state): + def OnEnableState(self, state: bool) -> None: if state: self.SetStateProjectOpen() else: self.SetStateProjectClose() - def SetNavigationMode(self, status): + def SetNavigationMode(self, status: bool) -> None: self.navigation_mode_status = status name = _("Navigation system") panel = navigator.TaskPanel @@ -365,17 +362,17 @@ def SetNavigationMode(self, status): self.sizer.Layout() - def SetStateProjectClose(self): + def SetStateProjectClose(self) -> None: self.fold_panel.Expand(self.fold_panel.GetFoldPanel(0)) for item in self.enable_items: item.Disable() - def SetStateProjectOpen(self): + def SetStateProjectOpen(self) -> None: self.fold_panel.Expand(self.fold_panel.GetFoldPanel(1)) for item in self.enable_items: item.Enable() - def OnFoldPressCaption(self, evt): + def OnFoldPressCaption(self, evt: fpb.FoldPanelEvent) -> None: id = evt.GetTag().GetId() closed = evt.GetFoldStatus() @@ -391,7 +388,7 @@ def OnFoldPressCaption(self, evt): evt.Skip() wx.CallAfter(self.ResizeFPB) - def ResizeFPB(self): + def ResizeFPB(self) -> None: sizeNeeded = self.fold_panel.GetPanelsLength(0, 0)[2] self.fold_panel.SetMinSize((self.fold_panel.GetSize()[0], sizeNeeded )) self.fold_panel.SetSize((self.fold_panel.GetSize()[0], sizeNeeded)) diff --git a/invesalius/gui/widgets/inv_spinctrl.py b/invesalius/gui/widgets/inv_spinctrl.py index ebcfb2800..5f888cf16 100644 --- a/invesalius/gui/widgets/inv_spinctrl.py +++ b/invesalius/gui/widgets/inv_spinctrl.py @@ -19,22 +19,23 @@ import decimal import wx +from typing import Optional class InvSpinCtrl(wx.Panel): def __init__( self, - parent, - id=wx.ID_ANY, - value=0, - min_value=1, - max_value=100, - increment=1, - spin_button=True, - unit='', - size=wx.DefaultSize, - style=wx.TE_RIGHT, - ): + parent: wx.Window, + id: int = wx.ID_ANY, + value: int = 0, + min_value: int = 1, + max_value: int = 100, + increment: int = 1, + spin_button: bool = True, + unit: str = '', + size: wx.Size = wx.DefaultSize, + style: int = wx.TE_RIGHT, + ) -> None: super().__init__(parent, id, size=size) self._textctrl = wx.TextCtrl(self, -1, style=style) @@ -43,13 +44,13 @@ def __init__( else: self._spinbtn = None - self._value = 0 - self._last_value = 0 - self._min_value = 0 - self._max_value = 100 - self._increment = 1 + self._value: int = 0 + self._last_value: int = 0 + self._min_value: int = 0 + self._max_value: int = 100 + self._increment: int = 1 - self.unit = unit + self.unit: str = unit self.SetMin(min_value) self.SetMax(max_value) @@ -66,34 +67,34 @@ def __init__( self.__bind_events() - def __bind_events(self): + def __bind_events(self) -> None: self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel) self._textctrl.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) if self._spinbtn: self._spinbtn.Bind(wx.EVT_SPIN_UP, self.OnSpinUp) self._spinbtn.Bind(wx.EVT_SPIN_DOWN, self.OnSpinDown) - def SetIncrement(self, increment): + def SetIncrement(self, increment: int) -> None: self._increment = increment - def SetMin(self, min_value): + def SetMin(self, min_value: int) -> None: self._min_value = min_value self.SetValue(self._value) self.CalcSizeFromTextSize() - def SetMax(self, max_value): + def SetMax(self, max_value: int) -> None: self._max_value = max_value self.SetValue(self._value) self.CalcSizeFromTextSize() - def SetRange(self, min_value, max_value): + def SetRange(self, min_value: int, max_value: int) -> None: self.SetMin(min_value) self.SetMax(max_value) - def GetValue(self): + def GetValue(self) -> int: return self._value - def SetValue(self, value): + def SetValue(self, value: int) -> None: try: value = int(value) except ValueError: @@ -110,14 +111,14 @@ def SetValue(self, value): self._last_value = self._value - def GetUnit(self): + def GetUnit(self) -> str: return self.unit - def SetUnit(self, unit): + def SetUnit(self, unit: str) -> None: self.unit = unit self.SetValue(self.GetValue()) - def CalcSizeFromTextSize(self, text=None): + def CalcSizeFromTextSize(self, text: str = None) -> None: # To calculate best width to spinctrl if text is None: text = "{}".format( @@ -138,7 +139,7 @@ def CalcSizeFromTextSize(self, text=None): spinb.Destroy() width += spinb_width - if wx.Platform == "__WXMAC": + if wx.Platform == "__WXMAC__": height = max(height, spin_height, spinb_height) else: height = spin_height @@ -148,7 +149,7 @@ def CalcSizeFromTextSize(self, text=None): self.SetMinSize((width, height)) self.Layout() - def OnMouseWheel(self, evt): + def OnMouseWheel(self, evt: wx.MouseEvent) -> None: r = evt.GetWheelRotation() if r > 0: self.SetValue(self.GetValue() + self._increment) @@ -157,23 +158,23 @@ def OnMouseWheel(self, evt): self.raise_event() evt.Skip() - def OnKillFocus(self, evt): + def OnKillFocus(self, evt: wx.FocusEvent) -> None: value = self._textctrl.GetValue() self.SetValue(value) self.raise_event() evt.Skip() - def OnSpinDown(self, evt): + def OnSpinDown(self, evt: wx.CommandEvent) -> None: self.SetValue(self.GetValue() - self._increment) self.raise_event() evt.Skip() - def OnSpinUp(self, evt): + def OnSpinUp(self, evt: wx.CommandEvent) -> None: self.SetValue(self.GetValue() + self._increment) self.raise_event() evt.Skip() - def raise_event(self): + def raise_event(self) -> None: event = wx.PyCommandEvent(wx.EVT_SPINCTRL.typeId, self.GetId()) self.GetEventHandler().ProcessEvent(event) @@ -181,17 +182,17 @@ def raise_event(self): class InvFloatSpinCtrl(wx.Panel): def __init__( self, - parent, - id=wx.ID_ANY, - value=0.0, - min_value=1.0, - max_value=100.0, - increment=0.1, - digits=1, - spin_button=True, - size=wx.DefaultSize, - style=wx.TE_RIGHT, - ): + parent: wx.Window, + id: int = wx.ID_ANY, + value: float = 0.0, + min_value: float = 1.0, + max_value: float = 100.0, + increment: float = 0.1, + digits: int = 1, + spin_button: bool = True, + size: wx.Size = wx.DefaultSize, + style: int = wx.TE_RIGHT, + ) -> None: super().__init__(parent, id, size=size) self._textctrl = wx.TextCtrl(self, -1, style=style) @@ -200,7 +201,7 @@ def __init__( else: self._spinbtn = None - self._digits = digits + self._digits: int = digits self._dec_context = decimal.Context(prec=digits) self._value = decimal.Decimal("0", self._dec_context) @@ -224,19 +225,19 @@ def __init__( self.__bind_events() - def __bind_events(self): + def __bind_events(self) -> None: self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel) self._textctrl.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) if self._spinbtn: self._spinbtn.Bind(wx.EVT_SPIN_UP, self.OnSpinUp) self._spinbtn.Bind(wx.EVT_SPIN_DOWN, self.OnSpinDown) - def _to_decimal(self, value): + def _to_decimal(self, value: float) -> decimal.Decimal: if not isinstance(value, str): value = "{:.{digits}f}".format(value, digits=self._digits) return decimal.Decimal(value, self._dec_context) - def SetDigits(self, digits): + def SetDigits(self, digits: int) -> None: self._digits = digits self._dec_context = decimal.Context(prec=digits) @@ -245,25 +246,25 @@ def SetDigits(self, digits): self.SetMax(self._max_value) self.SetValue(self._value) - def SetIncrement(self, increment): + def SetIncrement(self, increment: float) -> None: self._increment = self._to_decimal(increment) - def SetMin(self, min_value): + def SetMin(self, min_value: float) -> None: self._min_value = self._to_decimal(min_value) self.SetValue(self._value) - def SetMax(self, max_value): + def SetMax(self, max_value: float) -> None: self._max_value = self._to_decimal(max_value) self.SetValue(self._value) - def SetRange(self, min_value, max_value): + def SetRange(self, min_value: float, max_value: float) -> None: self.SetMin(min_value) self.SetMax(max_value) - def GetValue(self): + def GetValue(self) -> float: return float(self._value) - def SetValue(self, value): + def SetValue(self, value: float) -> None: try: value = self._to_decimal(value) except decimal.InvalidOperation: @@ -275,11 +276,11 @@ def SetValue(self, value): if value > self._max_value: value = self._max_value - self._value = value + self._value: float = value self._textctrl.SetValue("{}".format(self._value)) - self._last_value = self._value + self._last_value: float = self._value - def CalcSizeFromTextSize(self, text=None): + def CalcSizeFromTextSize(self, text: str = None) -> None: # To calculate best width to spinctrl if text is None: text = "{}".format( @@ -304,7 +305,7 @@ def CalcSizeFromTextSize(self, text=None): spinb.Destroy() width += spinb_width - if wx.Platform == "__WXMAC": + if wx.Platform == "__WXMAC__": height = max(height, spin_height, spinb_height) else: height = spin_height @@ -314,7 +315,7 @@ def CalcSizeFromTextSize(self, text=None): self.SetMinSize((width, height)) self.Layout() - def OnMouseWheel(self, evt): + def OnMouseWheel(self, evt: wx.MouseEvent) -> None: r = evt.GetWheelRotation() if r > 0: self.SetValue(self._value + self._increment) @@ -323,22 +324,22 @@ def OnMouseWheel(self, evt): self.raise_event() evt.Skip() - def OnKillFocus(self, evt): + def OnKillFocus(self, evt: wx.FocusEvent) -> None: value = self._textctrl.GetValue() self.SetValue(value) self.raise_event() evt.Skip() - def OnSpinDown(self, evt): + def OnSpinDown(self, evt: wx.SpinEvent) -> None: self.SetValue(self._value - self._increment) self.raise_event() evt.Skip() - def OnSpinUp(self, evt): + def OnSpinUp(self, evt: wx.SpinEvent) -> None: self.SetValue(self._value + self._increment) self.raise_event() evt.Skip() - def raise_event(self): + def raise_event(self) -> None: event = wx.PyCommandEvent(wx.EVT_SPINCTRL.typeId, self.GetId()) self.GetEventHandler().ProcessEvent(event) From 5306b6590a4afb2e420837d103fa6e9073e6b7ee Mon Sep 17 00:00:00 2001 From: abhay-ahirkar Date: Thu, 27 Apr 2023 15:50:02 +0530 Subject: [PATCH 16/16] added type info --- invesalius/gui/import_bitmap_panel.py | 167 +++++++++++++------------- invesalius/gui/language_dialog.py | 68 ++++++----- 2 files changed, 121 insertions(+), 114 deletions(-) diff --git a/invesalius/gui/import_bitmap_panel.py b/invesalius/gui/import_bitmap_panel.py index db616ac0d..99d709501 100644 --- a/invesalius/gui/import_bitmap_panel.py +++ b/invesalius/gui/import_bitmap_panel.py @@ -20,12 +20,14 @@ import wx.gizmos as gizmos from invesalius.pubsub import pub as Publisher import wx.lib.splitter as spl +from typing import Any, List, Tuple, Union, Optional import invesalius.constants as const import invesalius.gui.dialogs as dlg import invesalius.gui.bitmap_preview_panel as bpp import invesalius.reader.bitmap_reader as bpr from invesalius.gui.dialogs import ImportBitmapParameters as dialogs + myEVT_SELECT_SERIE = wx.NewEventType() EVT_SELECT_SERIE = wx.PyEventBinder(myEVT_SELECT_SERIE, 1) @@ -38,26 +40,29 @@ myEVT_SELECT_SERIE_TEXT = wx.NewEventType() EVT_SELECT_SERIE_TEXT = wx.PyEventBinder(myEVT_SELECT_SERIE_TEXT, 1) + class SelectEvent(wx.PyCommandEvent): - def __init__(self , evtType, id): + def __init__(self , evtType: wx.EventType, id: int) -> None: super(SelectEvent, self).__init__(evtType, id) + self.SelectedID = None + self.data = None - def GetSelectID(self): + def GetSelectID(self) -> int: return self.SelectedID - def SetSelectedID(self, id): - self.SelectedID = id + def SetSelectedID(self, id: int) -> None: + self.SelectedID: int = id - def GetItemData(self): + def GetItemData(self) -> Any: return self.data - def SetItemData(self, data): + def SetItemData(self, data: Any) -> None: self.data = data class Panel(wx.Panel): - def __init__(self, parent): - wx.Panel.__init__(self, parent, pos=wx.Point(5, 5)) + def __init__(self, parent: wx.Window) -> None: + super().__init__(parent, pos=wx.Point(5, 5)) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(InnerPanel(self), 1, wx.EXPAND|wx.GROW|wx.ALL, 5) @@ -72,9 +77,8 @@ def __init__(self, parent): # Inner fold panel class InnerPanel(wx.Panel): - def __init__(self, parent): - wx.Panel.__init__(self, parent, pos=wx.Point(5, 5)) - + def __init__(self, parent: wx.Window) -> None: + super().__init__(parent, pos=wx.Point(5, 5)) self.patients = [] self.first_image_selection = None self.last_image_selection = None @@ -82,7 +86,7 @@ def __init__(self, parent): self._bind_events() self._bind_pubsubevt() - def _init_ui(self): + def _init_ui(self) -> None: splitter = spl.MultiSplitterWindow(self, style=wx.SP_LIVE_UPDATE) splitter.SetOrientation(wx.VERTICAL) self.splitter = splitter @@ -123,70 +127,70 @@ def _init_ui(self): self.Update() self.SetAutoLayout(1) - def _bind_pubsubevt(self): + def _bind_pubsubevt(self) -> None: Publisher.subscribe(self.ShowBitmapPreview, "Load import bitmap panel") Publisher.subscribe(self.GetSelectedImages ,"Selected Import Images") - def ShowBitmapPreview(self, data): + def ShowBitmapPreview(self, data: Any) -> None: #self.patients.extend(dicom_groups) self.text_panel.Populate(data) - def GetSelectedImages(self, selection): - self.first_image_selection = selection[0] - self.last_image_selection = selection[1] + def GetSelectedImages(self, selection: List[int]) -> None: + self.first_image_selection: int = selection[0] + self.last_image_selection: int = selection[1] - def _bind_events(self): + def _bind_events(self) -> None: self.Bind(EVT_SELECT_SLICE, self.OnSelectSlice) self.Bind(EVT_SELECT_PATIENT, self.OnSelectPatient) self.btn_ok.Bind(wx.EVT_BUTTON, self.OnClickOk) self.btn_cancel.Bind(wx.EVT_BUTTON, self.OnClickCancel) self.text_panel.Bind(EVT_SELECT_SERIE_TEXT, self.OnDblClickTextPanel) - def OnSelectSlice(self, evt): + def OnSelectSlice(self, evt: wx.Event) -> None: pass - def OnSelectPatient(self, evt): + def OnSelectPatient(self, evt: wx.Event) -> None: pass - def OnDblClickTextPanel(self, evt): + def OnDblClickTextPanel(self, evt: wx.Event) -> None: pass - def OnClickOk(self, evt): + def OnClickOk(self, evt: wx.Event) -> None: parm = dlg.ImportBitmapParameters() parm.SetInterval(self.combo_interval.GetSelection()) parm.ShowModal() - def OnClickCancel(self, evt): + def OnClickCancel(self, evt: wx.Event) -> None: Publisher.sendMessage("Cancel DICOM load") class TextPanel(wx.Panel): - def __init__(self, parent): + def __init__(self, parent: wx.Window) -> None: wx.Panel.__init__(self, parent, -1) - self.parent = parent + self.parent: wx.Window = parent - self._selected_by_user = True - self.idserie_treeitem = {} - self.treeitem_idpatient = {} + self._selected_by_user: bool = True + self.idserie_treeitem: dict = {} + self.treeitem_idpatient: dict = {} - self.selected_items = None - self.shift_pressed = False + self.selected_items: Optional[List[wx.TreeItemId]] = None + self.shift_pressed: bool = False self.__init_gui() self.__bind_events_wx() self.__bind_pubsub_evt() - def __bind_pubsub_evt(self): + def __bind_pubsub_evt(self) -> None: Publisher.subscribe(self.SelectSeries, 'Select series in import panel') - def __bind_events_wx(self): + def __bind_events_wx(self) -> None: self.Bind(wx.EVT_SIZE, self.OnSize) self.Bind(wx.EVT_CHAR_HOOK, self.OnKeyPress) - def __init_gui(self): - tree = gizmos.TreeListCtrl(self, -1, style = + def __init_gui(self) -> None: + tree: gizmos.TreeListCtrl = gizmos.TreeListCtrl(self, -1, style = wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.TR_ROW_LINES @@ -206,26 +210,26 @@ def __init_gui(self): tree.SetColumnWidth(1, 60) tree.SetColumnWidth(2, 130) - self.root = tree.AddRoot(_("InVesalius Database")) - self.tree = tree + self.root: wx.TreeItemId = tree.AddRoot(_("InVesalius Database")) + self.tree: gizmos.TreeListCtrl = tree - def OnKeyPress(self, evt): - key_code = evt.GetKeyCode() + def OnKeyPress(self, evt: wx.KeyEvent) -> None: + key_code: int = evt.GetKeyCode() if key_code == wx.WXK_DELETE or key_code == wx.WXK_NUMPAD_DELETE: for selected_item in self.selected_items: if selected_item != self.tree.GetRootItem(): - text_item = self.tree.GetItemText(selected_item) + text_item: str = self.tree.GetItemText(selected_item) - index = bpr.BitmapData().GetIndexByPath(text_item) + index: int = bpr.BitmapData().GetIndexByPath(text_item) bpr.BitmapData().RemoveFileByPath(text_item) - data_size = len(bpr.BitmapData().GetData()) + data_size: int = len(bpr.BitmapData().GetData()) if index >= 0 and index < data_size: Publisher.sendMessage('Set bitmap in preview panel', pos=index) @@ -243,13 +247,13 @@ def OnKeyPress(self, evt): evt.Skip() - def SelectSeries(self, group_index): + def SelectSeries(self, group_index: Any) -> None: pass - def Populate(self, data): - tree = self.tree + def Populate(self, data: List[Tuple[Any]]) -> None: + tree: gizmos.TreeListCtrl = self.tree for value in data: - parent = tree.AppendItem(self.root, value[0]) + parent: wx.TreeItemId = tree.AppendItem(self.root, value[0]) self.tree.SetItemText(parent, value[2], 1) self.tree.SetItemText(parent, value[5], 2) @@ -259,49 +263,49 @@ def Populate(self, data): Publisher.sendMessage('Load bitmap into import panel', data=data) - def OnSelChanged(self, evt): - self.selected_items = self.tree.GetSelections() - item = self.selected_items[-1] + def OnSelChanged(self, evt: wx.TreeEvent) -> None: + self.selected_items: List[wx.TreeItemId] = self.tree.GetSelections() + item: wx.TreeItemId = self.selected_items[-1] if self._selected_by_user: - text_item = self.tree.GetItemText(item) - index = bpr.BitmapData().GetIndexByPath(text_item) + text_item: str = self.tree.GetItemText(item) + index: int = bpr.BitmapData().GetIndexByPath(text_item) Publisher.sendMessage('Set bitmap in preview panel', pos=index) evt.Skip() - def OnActivate(self, evt): - item = evt.GetItem() - group = self.tree.GetItemPyData(item) - my_evt = SelectEvent(myEVT_SELECT_SERIE_TEXT, self.GetId()) + def OnActivate(self, evt: wx.TreeEvent) -> None: + item: wx.TreeItemId = evt.GetItem() + group: Any = self.tree.GetItemPyData(item) + my_evt: SelectEvent = SelectEvent(myEVT_SELECT_SERIE_TEXT, self.GetId()) my_evt.SetItemData(group) self.GetEventHandler().ProcessEvent(my_evt) - def OnSize(self, evt): + def OnSize(self, evt: wx.SizeEvent) -> None: self.tree.SetSize(self.GetSize()) evt.Skip() - def SelectSerie(self, serie): - self._selected_by_user = False - item = self.idserie_treeitem[serie] + def SelectSerie(self, serie: Any) -> None: + self._selected_by_user: bool = False + item: wx.TreeItemId = self.idserie_treeitem[serie] self.tree.SelectItem(item) - self._selected_by_user = True + self._selected_by_user: bool = True - def GetSelection(self): + def GetSelection(self) -> Any: """Get selected item""" - item = self.tree.GetSelection() - group = self.tree.GetItemPyData(item) + item: wx.TreeItemId = self.tree.GetSelection() + group: Any = self.tree.GetItemPyData(item) return group class ImagePanel(wx.Panel): - def __init__(self, parent): - wx.Panel.__init__(self, parent, -1) + def __init__(self, parent: wx.Window) -> None: + super().__init__(parent, -1) self._init_ui() self._bind_events() - def _init_ui(self): + def _init_ui(self) -> None: splitter = spl.MultiSplitterWindow(self, style=wx.SP_LIVE_UPDATE) splitter.SetOrientation(wx.HORIZONTAL) self.splitter = splitter @@ -326,20 +330,20 @@ def _init_ui(self): self.Update() self.SetAutoLayout(1) - def _bind_events(self): + def _bind_events(self) -> None: self.text_panel.Bind(EVT_SELECT_SLICE, self.OnSelectSlice) - def OnSelectSlice(self, evt): + def OnSelectSlice(self, evt: wx.Event) -> None: self.image_panel.bitmap_preview.ShowSlice(evt.GetSelectID()) evt.Skip() - def SetSerie(self, serie): + def SetSerie(self, serie: List[Tuple[str, str]]) -> None: self.image_panel.bitmap_preview.SetDicomGroup(serie) class SeriesPanel(wx.Panel): - def __init__(self, parent): - wx.Panel.__init__(self, parent, -1) + def __init__(self, parent: wx.Window) -> None: + super().__init__(parent, -1) self.thumbnail_preview = bpp.BitmapPreviewSeries(self) @@ -357,23 +361,23 @@ def __init__(self, parent): self.__bind_evt() self._bind_gui_evt() - def __bind_evt(self): + def __bind_evt(self) -> None: Publisher.subscribe(self.SetBitmapFiles, 'Load bitmap into import panel') - def _bind_gui_evt(self): + def _bind_gui_evt(self) -> None: self.thumbnail_preview.Bind(bpp.EVT_CLICK_SERIE, self.OnSelectSerie) - def GetSelectedImagesRange(self): + def GetSelectedImagesRange(self) -> List[int]: return [self.bitmap_preview.first_selected, self.dicom_preview_last_selection] - def SetBitmapFiles(self, data): + def SetBitmapFiles(self, data: List[Tuple[str, str]]) -> None: bitmap = data self.thumbnail_preview.Show(1) self.thumbnail_preview.SetBitmapFiles(bitmap) self.Update() - def OnSelectSerie(self, evt): + def OnSelectSerie(self, evt: wx.Event) -> None: data = evt.GetItemData() my_evt = SelectEvent(myEVT_SELECT_SERIE, self.GetId()) my_evt.SetSelectedID(evt.GetSelectID()) @@ -384,7 +388,7 @@ def OnSelectSerie(self, evt): self.Show() self.Update() - def OnSelectSlice(self, evt): + def OnSelectSlice(self, evt: wx.Event) -> None: my_evt = SelectEvent(myEVT_SELECT_SLICE, self.GetId()) my_evt.SetSelectedID(evt.GetSelectID()) my_evt.SetItemData(evt.GetItemData()) @@ -393,15 +397,15 @@ def OnSelectSlice(self, evt): class SlicePanel(wx.Panel): - def __init__(self, parent): - wx.Panel.__init__(self, parent, -1) + def __init__(self, parent: wx.Window) -> None: + super().__init__(parent, -1) self.__init_gui() self.__bind_evt() - def __bind_evt(self): + def __bind_evt(self) -> None: Publisher.subscribe(self.SetBitmapFiles, 'Load bitmap into import panel') - def __init_gui(self): + def __init_gui(self) -> None: self.SetBackgroundColour((255,255,255)) self.bitmap_preview = bpp.SingleImagePreview(self) @@ -414,7 +418,8 @@ def __init_gui(self): self.SetAutoLayout(1) self.sizer = sizer - def SetBitmapFiles(self, data): + def SetBitmapFiles(self, data: List[Tuple[str, str]]) -> None: self.bitmap_preview.SetBitmapFiles(data) self.sizer.Layout() self.Update() + diff --git a/invesalius/gui/language_dialog.py b/invesalius/gui/language_dialog.py index 26a097274..212246ffd 100644 --- a/invesalius/gui/language_dialog.py +++ b/invesalius/gui/language_dialog.py @@ -28,56 +28,58 @@ import invesalius.i18n as i18n -file_path = os.path.split(__file__)[0] +from typing import List, Tuple, Union, Optional + +file_path: str = os.path.split(__file__)[0] if hasattr(sys, "frozen") and ( sys.frozen == "windows_exe" or sys.frozen == "console_exe" ): - abs_file_path = os.path.abspath( + abs_file_path: str = os.path.abspath( file_path + os.sep + ".." + os.sep + ".." + os.sep + ".." + os.sep + ".." ) - ICON_DIR = os.path.abspath(os.path.join(abs_file_path, "icons")) + ICON_DIR: str = os.path.abspath(os.path.join(abs_file_path, "icons")) else: - ICON_DIR = os.path.abspath(os.path.join(file_path, "..", "..", "icons")) + ICON_DIR: str = os.path.abspath(os.path.join(file_path, "..", "..", "icons")) # MAC App if not os.path.exists(ICON_DIR): - ICON_DIR = os.path.abspath( + ICON_DIR: str = os.path.abspath( os.path.join(file_path, "..", "..", "..", "..", "..", "icons") ) class ComboBoxLanguage: - def __init__(self, parent): + def __init__(self, parent: wx.Window) -> None: """Initialize combobox bitmap""" # Retrieve locales dictionary - dict_locales = i18n.GetLocales() + dict_locales: dict = i18n.GetLocales() # Retrieve locales names and sort them - self.locales = dict_locales.values() - self.locales = sorted(self.locales) + self.locales: List[str] = list(dict_locales.values()) + self.locales.sort() # Retrieve locales keys (eg: pt_BR for Portuguese(Brazilian)) - self.locales_key = [dict_locales.get_key(value) for value in self.locales] + self.locales_key: List[str] = [dict_locales.get_key(value)[0] for value in self.locales] # Find out OS locale - self.os_locale = i18n.GetLocaleOS() + self.os_locale: str = i18n.GetLocaleOS() try: - os_lang = self.os_locale[0:2] + os_lang: str = self.os_locale[0:2] except TypeError: - os_lang = None + os_lang: str = None # Default selection will be English - selection = self.locales_key.index("en") + selection: int = self.locales_key.index("en") # Create bitmap combo self.bitmapCmb = bitmapCmb = BitmapComboBox(parent, style=wx.CB_READONLY) for key in self.locales_key: # Based on composed flag filename, get bitmap - filepath = os.path.join(ICON_DIR, "%s.png" % (key)) - bmp = wx.Bitmap(filepath, wx.BITMAP_TYPE_PNG) + filepath: str = os.path.join(ICON_DIR, "%s.png" % (key)) + bmp: wx.Bitmap = wx.Bitmap(filepath, wx.BITMAP_TYPE_PNG) # Add bitmap and info to Combo bitmapCmb.Append(dict_locales[key], bmp, key) # Set default combo item if available on the list @@ -85,10 +87,10 @@ def __init__(self, parent): selection = self.locales_key.index(key) bitmapCmb.SetSelection(selection) - def GetComboBox(self): + def GetComboBox(self) -> wx.BitmapComboBox: return self.bitmapCmb - def GetLocalesKey(self): + def GetLocalesKey(self) -> List[str]: return self.locales_key @@ -97,7 +99,7 @@ class LanguageDialog(wx.Dialog): exist chcLanguage that list language EN and PT. The language selected is writing in the config.ini""" - def __init__(self, parent=None, startApp=None): + def __init__(self, parent: wx.Window = None, startApp: bool = None) -> None: super(LanguageDialog, self).__init__(parent, title="") self.__TranslateMessage__() self.SetTitle(_("Language selection")) @@ -138,27 +140,27 @@ def __init__(self, parent=None, startApp=None): # selection = self.locales_key.index(key) # bitmapCmb.SetSelection(selection) - def GetComboBox(self): + def GetComboBox(self) -> wx.BitmapComboBox: return self.bitmapCmb - def __init_gui(self): - self.txtMsg = wx.StaticText(self, -1, label=_("Choose user interface language")) + def __init_gui(self) -> None: + self.txtMsg: wx.StaticText = wx.StaticText(self, -1, label=_("Choose user interface language")) - btnsizer = wx.StdDialogButtonSizer() + btnsizer: wx.StdDialogButtonSizer = wx.StdDialogButtonSizer() - btn = wx.Button(self, wx.ID_OK) + btn: wx.Button = wx.Button(self, wx.ID_OK) btn.SetDefault() btnsizer.AddButton(btn) - btn = wx.Button(self, wx.ID_CANCEL) + btn: wx.Button = wx.Button(self, wx.ID_CANCEL) btnsizer.AddButton(btn) btnsizer.Realize() # self.__init_combobox_bitmap__() - self.cmb = ComboBoxLanguage(self) - self.bitmapCmb = self.cmb.GetComboBox() + self.cmb: ComboBoxLanguage = ComboBoxLanguage(self) + self.bitmapCmb: wx.BitmapComboBox = self.cmb.GetComboBox() - sizer = wx.BoxSizer(wx.VERTICAL) + sizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.txtMsg, 0, wx.EXPAND | wx.ALL, 5) sizer.Add(self.bitmapCmb, 0, wx.EXPAND | wx.ALL, 5) sizer.Add(btnsizer, 0, wx.EXPAND | wx.ALL, 5) @@ -169,14 +171,14 @@ def __init_gui(self): self.Update() self.SetAutoLayout(1) - def GetSelectedLanguage(self): + def GetSelectedLanguage(self) -> str: """Return String with Selected Language""" - self.locales_key = self.cmb.GetLocalesKey() + self.locales_key: List[str] = self.cmb.GetLocalesKey() return self.locales_key[self.bitmapCmb.GetSelection()] - def __TranslateMessage__(self): + def __TranslateMessage__(self) -> None: """Translate Messages of the Window""" - os_language = i18n.GetLocaleOS() + os_language: str = i18n.GetLocaleOS() if os_language[0:2] == "pt": _ = i18n.InstallLanguage("pt_BR") @@ -185,7 +187,7 @@ def __TranslateMessage__(self): else: _ = i18n.InstallLanguage("en") - def Cancel(self, event): + def Cancel(self, event: wx.Event) -> None: """Close Frm_Language""" self.Close() event.Skip()