From c425a022c310ef3527615f114a49d5e397bfd769 Mon Sep 17 00:00:00 2001 From: hrobarts Date: Tue, 11 Nov 2025 14:52:44 +0000 Subject: [PATCH 1/5] Calculate euler angles for parallel tilted geometry --- Wrappers/Python/cil/plugins/tigre/Geometry.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 358b5bca0e..c88769062d 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -18,6 +18,7 @@ from cil.framework.labels import AcquisitionType, AngleUnit import numpy as np +from scipy.spatial.transform import Rotation try: from tigre.utilities.geometry import Geometry @@ -36,6 +37,32 @@ def getTIGREGeometry(ig, ag): if ag.config.angles.angle_unit == AngleUnit.DEGREE: angles *= (np.pi/180.) + if ag.geom_type == 'parallel' and ag.system_description == 'advanced': + ag_cil = ag.copy() + ag_cil.config.system.align_reference_frame('cil') # geometry in CIL frame + untilted_rotation_axis = ag_cil.config.system.rotation_axis.direction + untilted_rotation_axis /= np.linalg.norm(untilted_rotation_axis) + + tilted_rotation_axis = ag.config.system.rotation_axis.direction + tilted_rotation_axis /= np.linalg.norm(tilted_rotation_axis) + + v = np.cross(untilted_rotation_axis, tilted_rotation_axis) + v_norm = np.linalg.norm(v) + c = float(np.dot(untilted_rotation_axis, tilted_rotation_axis)) + + # if tilted and untilted rotation axes are not parallel calculate Euler angles + if v_norm > 1e-12: + theta = np.arctan2(v_norm, c) + rotation_matrix = Rotation.from_rotvec(theta * v / v_norm) + euler_angles = [] + for angle in angles: + R1 = Rotation.from_euler("z", angle, degrees=False) + combined = rotation_matrix * R1 + euler_angles.append(combined.as_euler("ZYZ", degrees=False)) + tg.euler_angles = np.array(euler_angles, dtype=np.float32) + + return tg, tg.euler_angles + #convert CIL to TIGRE angles s angles = -(angles + np.pi/2 +tg.theta ) @@ -89,13 +116,14 @@ def __init__(self, ig, ag): self.mode = 'cone' else: - if ag_in.system_description == 'advanced': - raise NotImplementedError ("CIL cannot use TIGRE to process parallel geometries with tilted axes") self.DSO = clearance_len self.DSD = 2*clearance_len self.mode = 'parallel' + # if ag_in.system_description == 'advanced': + # raise NotImplementedError ("CIL cannot use TIGRE to process parallel geometries with tilted axes") + # number of voxels (vx) self.nVoxel = np.array( [ig.voxel_num_z, ig.voxel_num_y, ig.voxel_num_x] ) # size of each voxel (mm) From 9ba6f5dbd543c2cfeffce59ec63ca63de13a461b Mon Sep 17 00:00:00 2001 From: hrobarts Date: Wed, 21 Jan 2026 11:59:49 +0000 Subject: [PATCH 2/5] Use rotation matrices not scipy euler methods --- Wrappers/Python/cil/plugins/tigre/Geometry.py | 60 +++++++++++++++++-- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 0fc1ccd66b..9971388549 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -18,7 +18,6 @@ from cil.framework.labels import AcquisitionType, AngleUnit import numpy as np -from scipy.spatial.transform import Rotation try: from tigre.utilities.geometry import Geometry @@ -53,12 +52,12 @@ def getTIGREGeometry(ig, ag): # if tilted and untilted rotation axes are not parallel calculate Euler angles if v_norm > 1e-12: theta = np.arctan2(v_norm, c) - rotation_matrix = Rotation.from_rotvec(theta * v / v_norm) + rotation_matrix = rotation_matrix_from_vec(theta * v / v_norm) euler_angles = [] for angle in angles: - R1 = Rotation.from_euler("z", angle, degrees=False) - combined = rotation_matrix * R1 - euler_angles.append(combined.as_euler("ZYZ", degrees=False)) + R1 = rot_z(angle) + combined = rotation_matrix @ R1 + euler_angles.append(euler_from_matrix_zyz(combined)) tg.euler_angles = np.array(euler_angles, dtype=np.float32) return tg, tg.euler_angles @@ -75,6 +74,57 @@ def getTIGREGeometry(ig, ag): angles[i] = a return tg, angles + +def rotation_matrix_from_vec(rotvec): + rotvec = np.asarray(rotvec, dtype=float) + theta = np.linalg.norm(rotvec) + + diag = np.array([[1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.]]) + + if theta == 0.0: + return diag + + axis = rotvec / theta + x, y, z = axis + K = np.array([[ 0, -z, y], + [ z, 0, -x], + [-y, x, 0] + ]) + + R = (diag + + np.sin(theta) * K + + (1 - np.cos(theta)) * (K @ K) + ) + + return R + +def rot_z(angle): + c, s = np.cos(angle), np.sin(angle) + return np.array([[c, -s, 0], + [s, c, 0], + [0, 0, 1]]) + +def euler_from_matrix_zyz(R, eps=1e-12): + """ + Equivalent to scipy Rotation.as_euler("ZYZ", degrees=False) + """ + R = np.asarray(R, dtype=float) + + beta = np.arccos(np.clip(R[2, 2], -1.0, 1.0)) + + if abs(np.sin(beta)) > eps: + alpha = np.arctan2(R[1, 2], R[0, 2]) + gamma = np.arctan2(R[2, 1], -R[2, 0]) + else: + alpha = np.arctan2(R[0, 1], R[0, 0]) + gamma = 0.0 + + alpha = (alpha + np.pi) % (2*np.pi) - np.pi + gamma = (gamma + np.pi) % (2*np.pi) - np.pi + + return np.array([alpha, beta, gamma]) class TIGREGeometry(Geometry): From 1bd91a7e111646ee25520a6e278e655ca9f07e2a Mon Sep 17 00:00:00 2001 From: hrobarts Date: Mon, 16 Feb 2026 17:52:48 +0000 Subject: [PATCH 3/5] Make Euler angles calculate from roll, pitch, yaw --- Wrappers/Python/cil/plugins/tigre/Geometry.py | 80 ++++++++++--------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 9971388549..6d28b214f9 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -37,44 +37,41 @@ def getTIGREGeometry(ig, ag): angles *= (np.pi/180.) if ag.geom_type == 'parallel' and ag.system_description == 'advanced': - ag_cil = ag.copy() - ag_cil.config.system.align_reference_frame('cil') # geometry in CIL frame - untilted_rotation_axis = ag_cil.config.system.rotation_axis.direction - untilted_rotation_axis /= np.linalg.norm(untilted_rotation_axis) - - tilted_rotation_axis = ag.config.system.rotation_axis.direction - tilted_rotation_axis /= np.linalg.norm(tilted_rotation_axis) + angles = calculate_euler_angles(angles, tg) + tg.rotDetector = np.array((0.0, 0.0, 0.0)) + + else: + # don't do this if using Euler angles because the tigre conversion + # is already applied in calculate_euler_angles - v = np.cross(untilted_rotation_axis, tilted_rotation_axis) - v_norm = np.linalg.norm(v) - c = float(np.dot(untilted_rotation_axis, tilted_rotation_axis)) + #convert CIL to TIGRE angles s + angles = -(angles + np.pi/2 +tg.theta ) - # if tilted and untilted rotation axes are not parallel calculate Euler angles - if v_norm > 1e-12: - theta = np.arctan2(v_norm, c) - rotation_matrix = rotation_matrix_from_vec(theta * v / v_norm) - euler_angles = [] - for angle in angles: - R1 = rot_z(angle) - combined = rotation_matrix @ R1 - euler_angles.append(euler_from_matrix_zyz(combined)) - tg.euler_angles = np.array(euler_angles, dtype=np.float32) - - return tg, tg.euler_angles - - #convert CIL to TIGRE angles s - angles = -(angles + np.pi/2 +tg.theta ) - - #angles in range -pi->pi - for i, a in enumerate(angles): - while a < -np.pi: - a += 2 * np.pi - while a >= np.pi: - a -= 2 * np.pi - angles[i] = a + #angles in range -pi->pi + for i, a in enumerate(angles): + while a < -np.pi: + a += 2 * np.pi + while a >= np.pi: + a -= 2 * np.pi + angles[i] = a return tg, angles +def calculate_euler_angles(angles, tg): + roll, pitch, yaw = tg.rotDetector + + # R_detector = rot_y(-yaw) @ rot_x(-pitch) @ rot_z(-roll) + R_detector = rot_z(yaw) @ rot_y(pitch) @ rot_x(roll) + + euler_angles = [] + for angle in angles: + R_combined = R_detector @ rot_z(angle) + R_tigre = rot_z(-np.pi/2 - tg.theta) @ R_combined + euler_angles.append(euler_from_matrix_zyz(R_tigre)) + euler_angles = np.array(euler_angles, dtype=np.float32) + + return euler_angles + def rotation_matrix_from_vec(rotvec): rotvec = np.asarray(rotvec, dtype=float) theta = np.linalg.norm(rotvec) @@ -100,6 +97,19 @@ def rotation_matrix_from_vec(rotvec): return R +def rot_x(angle): + c, s = np.cos(angle), np.sin(angle) + return np.array([[1, 0, 0], + [0, c, -s], + [0, s, c]]) + + +def rot_y(angle): + c, s = np.cos(angle), np.sin(angle) + return np.array([[ c, 0, s], + [ 0, 1, 0], + [-s, 0, c]]) + def rot_z(angle): c, s = np.cos(angle), np.sin(angle) return np.array([[c, -s, 0], @@ -107,9 +117,7 @@ def rot_z(angle): [0, 0, 1]]) def euler_from_matrix_zyz(R, eps=1e-12): - """ - Equivalent to scipy Rotation.as_euler("ZYZ", degrees=False) - """ + R = np.asarray(R, dtype=float) beta = np.arccos(np.clip(R[2, 2], -1.0, 1.0)) From d5f35e8783adc97d3e6d0e7b55ae5ebe940ceb60 Mon Sep 17 00:00:00 2001 From: hrobarts Date: Tue, 17 Feb 2026 08:58:08 +0000 Subject: [PATCH 4/5] Add comments --- Wrappers/Python/cil/plugins/tigre/Geometry.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 6d28b214f9..be4120b04b 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -38,11 +38,12 @@ def getTIGREGeometry(ig, ag): if ag.geom_type == 'parallel' and ag.system_description == 'advanced': angles = calculate_euler_angles(angles, tg) + # reset the detector rotation to 0 as the rotation is now included in the Euler angles tg.rotDetector = np.array((0.0, 0.0, 0.0)) else: # don't do this if using Euler angles because the tigre conversion - # is already applied in calculate_euler_angles + # is applied in calculate_euler_angles #convert CIL to TIGRE angles s angles = -(angles + np.pi/2 +tg.theta ) @@ -60,12 +61,12 @@ def getTIGREGeometry(ig, ag): def calculate_euler_angles(angles, tg): roll, pitch, yaw = tg.rotDetector - # R_detector = rot_y(-yaw) @ rot_x(-pitch) @ rot_z(-roll) R_detector = rot_z(yaw) @ rot_y(pitch) @ rot_x(roll) euler_angles = [] for angle in angles: R_combined = R_detector @ rot_z(angle) + # convert to tigre angles R_tigre = rot_z(-np.pi/2 - tg.theta) @ R_combined euler_angles.append(euler_from_matrix_zyz(R_tigre)) euler_angles = np.array(euler_angles, dtype=np.float32) @@ -182,9 +183,6 @@ def __init__(self, ig, ag): self.DSD = 2*clearance_len self.mode = 'parallel' - # if ag_in.system_description == 'advanced': - # raise NotImplementedError ("CIL cannot use TIGRE to process parallel geometries with tilted axes") - # number of voxels (vx) self.nVoxel = np.array( [ig.voxel_num_z, ig.voxel_num_y, ig.voxel_num_x] ) # size of each voxel (mm) From 4615f7d319dd250c03f37705f322db00b28f1a3f Mon Sep 17 00:00:00 2001 From: Gemma Fardell Date: Fri, 19 Jun 2026 16:37:51 +0000 Subject: [PATCH 5/5] Update rotations --- Wrappers/Python/cil/plugins/tigre/Geometry.py | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/Wrappers/Python/cil/plugins/tigre/Geometry.py b/Wrappers/Python/cil/plugins/tigre/Geometry.py index 99e1273df1..71be506a91 100644 --- a/Wrappers/Python/cil/plugins/tigre/Geometry.py +++ b/Wrappers/Python/cil/plugins/tigre/Geometry.py @@ -35,27 +35,22 @@ def getTIGREGeometry(ig, ag): if ag.config.angles.angle_unit == AngleUnit.DEGREE: angles *= (np.pi/180.) + # convert CIL to TIGRE angles + angles += np.pi/2 + tg.theta + angles *= -1 + + for i, a in enumerate(angles): + while a < -np.pi: + a += 2 * np.pi + while a >= np.pi: + a -= 2 * np.pi + angles[i] = a + if ag.geom_type == 'parallel' and ag.system_description == 'advanced': angles = calculate_euler_angles(angles, tg) # reset the detector rotation to 0 as the rotation is now included in the Euler angles tg.rotDetector = np.array((0.0, 0.0, 0.0)) - else: - # don't do this if using Euler angles because the tigre conversion - # is applied in calculate_euler_angles - - #convert CIL to TIGRE angles s - angles += np.pi/2 + tg.theta - angles *= -1 - - #angles in range -pi->pi - for i, a in enumerate(angles): - while a < -np.pi: - a += 2 * np.pi - while a >= np.pi: - a -= 2 * np.pi - angles[i] = a - return tg, angles def calculate_euler_angles(angles, tg): @@ -65,9 +60,7 @@ def calculate_euler_angles(angles, tg): euler_angles = [] for angle in angles: - R_combined = R_detector @ rot_z(angle) - # convert to tigre angles - R_tigre = rot_z(-np.pi/2 - tg.theta) @ R_combined + R_tigre = rot_z(angle) @ R_detector euler_angles.append(euler_from_matrix_zyz(R_tigre)) euler_angles = np.array(euler_angles, dtype=np.float32)