SeekTransformModule.py 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
  1. import os
  2. import numpy as np
  3. import scipy
  4. from scipy.spatial.distance import cdist
  5. from scipy.spatial.transform import Rotation as R
  6. import slicer
  7. import itertools
  8. from DICOMLib import DICOMUtils
  9. from collections import deque
  10. import vtk
  11. from slicer.ScriptedLoadableModule import *
  12. import qt
  13. import matplotlib.pyplot as plt
  14. import csv
  15. import time
  16. #exec(open("C:/Users/lkomar/Documents/Prostata/FirstTryRegister.py").read())
  17. class SeekTransformModule(ScriptedLoadableModule):
  18. """
  19. Module description shown in the module panel.
  20. """
  21. def __init__(self, parent):
  22. ScriptedLoadableModule.__init__(self, parent)
  23. self.parent.title = "Seek Transform module"
  24. self.parent.categories = ["Image Processing"]
  25. self.parent.contributors = ["Luka Komar (Onkološki Inštitut Ljubljana, Fakulteta za Matematiko in Fiziko Ljubljana)"]
  26. self.parent.helpText = "This module applies rigid transformations to CBCT volumes based on reference CT volumes."
  27. self.parent.acknowledgementText = "Supported by doc. Primož Peterlin & prof. Andrej Studen"
  28. class SeekTransformModuleWidget(ScriptedLoadableModuleWidget):
  29. """
  30. GUI of the module.
  31. """
  32. def setup(self):
  33. ScriptedLoadableModuleWidget.setup(self)
  34. # Dropdown menu za izbiro metode
  35. self.rotationMethodComboBox = qt.QComboBox()
  36. self.rotationMethodComboBox.addItems(["Kabsch", "Horn", "Iterative Closest Point (Horn)"])
  37. self.layout.addWidget(self.rotationMethodComboBox)
  38. # Checkboxi za transformacije
  39. self.rotationCheckBox = qt.QCheckBox("Rotation")
  40. self.rotationCheckBox.setChecked(True)
  41. self.layout.addWidget(self.rotationCheckBox)
  42. self.translationCheckBox = qt.QCheckBox("Translation")
  43. self.translationCheckBox.setChecked(True)
  44. self.layout.addWidget(self.translationCheckBox)
  45. self.scalingCheckBox = qt.QCheckBox("Scaling")
  46. self.scalingCheckBox.setChecked(True)
  47. self.layout.addWidget(self.scalingCheckBox)
  48. self.writefileCheckBox = qt.QCheckBox("Write distances to csv file")
  49. self.writefileCheckBox.setChecked(True)
  50. self.layout.addWidget(self.writefileCheckBox)
  51. # Load button
  52. self.applyButton = qt.QPushButton("Find markers and transform")
  53. self.applyButton.toolTip = "Finds markers, computes optimal rigid transform and applies it to CBCT volumes."
  54. self.applyButton.enabled = True
  55. self.layout.addWidget(self.applyButton)
  56. # Connect button to logic
  57. self.applyButton.connect('clicked(bool)', self.onApplyButton)
  58. self.layout.addStretch(1)
  59. def onApplyButton(self):
  60. logic = MyTransformModuleLogic()
  61. selectedMethod = self.rotationMethodComboBox.currentText # izberi metodo izračuna rotacije
  62. # Preberi stanje checkboxov
  63. applyRotation = self.rotationCheckBox.isChecked()
  64. applyTranslation = self.translationCheckBox.isChecked()
  65. applyScaling = self.scalingCheckBox.isChecked()
  66. writefilecheck = self.writefileCheckBox.isChecked()
  67. # Pokliči logiko z izbranimi nastavitvami
  68. logic.run(selectedMethod, applyRotation, applyTranslation, applyScaling, writefilecheck)
  69. class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
  70. """
  71. Core logic of the module.
  72. """
  73. def run(self, selectedMethod, applyRotation, applyTranslation, applyScaling, writefilecheck):
  74. start_time = time.time()
  75. print("Calculating...")
  76. def group_points(points, threshold):
  77. # Function to group points that are close to each other
  78. grouped_points = []
  79. while points:
  80. point = points.pop() # Take one point from the list
  81. group = [point] # Start a new group
  82. # Find all points close to this one
  83. distances = cdist([point], points) # Calculate distances from this point to others
  84. close_points = [i for i, dist in enumerate(distances[0]) if dist < threshold]
  85. # Add the close points to the group
  86. group.extend([points[i] for i in close_points])
  87. # Remove the grouped points from the list
  88. points = [point for i, point in enumerate(points) if i not in close_points]
  89. # Add the group to the result
  90. grouped_points.append(group)
  91. return grouped_points
  92. def region_growing(image_data, seed, intensity_threshold, max_distance):
  93. dimensions = image_data.GetDimensions()
  94. visited = set()
  95. region = []
  96. queue = deque([seed])
  97. while queue:
  98. x, y, z = queue.popleft()
  99. if (x, y, z) in visited:
  100. continue
  101. visited.add((x, y, z))
  102. voxel_value = image_data.GetScalarComponentAsDouble(x, y, z, 0)
  103. if voxel_value >= intensity_threshold:
  104. region.append((x, y, z))
  105. # Add neighbors within bounds
  106. for dx, dy, dz in [(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)]:
  107. nx, ny, nz = x + dx, y + dy, z + dz
  108. if 0 <= nx < dimensions[0] and 0 <= ny < dimensions[1] and 0 <= nz < dimensions[2]:
  109. if (nx, ny, nz) not in visited:
  110. queue.append((nx, ny, nz))
  111. return region
  112. def compute_optimal_scaling_per_axis(moving_points, fixed_points):
  113. """Computes optimal scaling factors for each axis (X, Y, Z) to align moving points (CBCT) to fixed points (CT).
  114. Args:
  115. moving_points (list of lists): List of (x, y, z) moving points (CBCT).
  116. fixed_points (list of lists): List of (x, y, z) fixed points (CT).
  117. Returns:
  118. tuple: Scaling factors (sx, sy, sz).
  119. """
  120. moving_points_np = np.array(moving_points)
  121. fixed_points_np = np.array(fixed_points)
  122. # Compute centroids
  123. centroid_moving = np.mean(moving_points_np, axis=0)
  124. centroid_fixed = np.mean(fixed_points_np, axis=0)
  125. # Compute absolute distances of each point from its centroid along each axis
  126. distances_moving = np.abs(moving_points_np - centroid_moving)
  127. distances_fixed = np.abs(fixed_points_np - centroid_fixed)
  128. # Compute scaling factors as the ratio of mean absolute distances per axis
  129. scale_factors = np.mean(distances_fixed, axis=0) / np.mean(distances_moving, axis=0)
  130. return tuple(scale_factors)
  131. def compute_scaling(cbct_points, scaling_factors):
  132. """Applies non-uniform scaling to CBCT points.
  133. Args:
  134. cbct_points (list of lists): List of (x, y, z) points.
  135. scaling_factors (tuple): Scaling factors (sx, sy, sz) for each axis.
  136. Returns:
  137. np.ndarray: Scaled CBCT points.
  138. """
  139. sx, sy, sz = scaling_factors # Extract scaling factors
  140. scaling_matrix = np.diag([sx, sy, sz]) # Create diagonal scaling matrix
  141. cbct_points_np = np.array(cbct_points) # Convert to numpy array
  142. scaled_points = cbct_points_np @ scaling_matrix.T # Apply scaling
  143. return scaled_points.tolist() # Convert back to list
  144. def compute_Kabsch_rotation(moving_points, fixed_points):
  145. """
  146. Computes the optimal rotation matrix to align moving_points to fixed_points.
  147. Parameters:
  148. moving_points (list or ndarray): List of points to be rotated CBCT
  149. fixed_points (list or ndarray): List of reference points CT
  150. Returns:
  151. ndarray: Optimal rotation matrix.
  152. """
  153. assert len(moving_points) == len(fixed_points), "Point lists must be the same length."
  154. # Convert to numpy arrays
  155. moving = np.array(moving_points)
  156. fixed = np.array(fixed_points)
  157. # Compute centroids
  158. centroid_moving = np.mean(moving, axis=0)
  159. centroid_fixed = np.mean(fixed, axis=0)
  160. # Center the points
  161. moving_centered = moving - centroid_moving
  162. fixed_centered = fixed - centroid_fixed
  163. # Compute covariance matrix
  164. H = np.dot(moving_centered.T, fixed_centered)
  165. # SVD decomposition
  166. U, _, Vt = np.linalg.svd(H)
  167. Rotate_optimal = np.dot(Vt.T, U.T)
  168. # Correct improper rotation (reflection)
  169. if np.linalg.det(Rotate_optimal) < 0:
  170. Vt[-1, :] *= -1
  171. Rotate_optimal = np.dot(Vt.T, U.T)
  172. return Rotate_optimal
  173. def compute_Horn_rotation(moving_points, fixed_points):
  174. """
  175. Computes the optimal rotation matrix using quaternions.
  176. Parameters:
  177. moving_points (list or ndarray): List of points to be rotated.
  178. fixed_points (list or ndarray): List of reference points.
  179. Returns:
  180. ndarray: Optimal rotation matrix.
  181. """
  182. assert len(moving_points) == len(fixed_points), "Point lists must be the same length."
  183. moving = np.array(moving_points)
  184. fixed = np.array(fixed_points)
  185. # Compute centroids
  186. centroid_moving = np.mean(moving, axis=0)
  187. centroid_fixed = np.mean(fixed, axis=0)
  188. # Center the points
  189. moving_centered = moving - centroid_moving
  190. fixed_centered = fixed - centroid_fixed
  191. # Construct the cross-dispersion matrix
  192. M = np.dot(moving_centered.T, fixed_centered)
  193. # Construct the N matrix for quaternion solution
  194. A = M - M.T
  195. delta = np.array([A[1, 2], A[2, 0], A[0, 1]])
  196. trace = np.trace(M)
  197. N = np.zeros((4, 4))
  198. N[0, 0] = trace
  199. N[1:, 0] = delta
  200. N[0, 1:] = delta
  201. N[1:, 1:] = M + M.T - np.eye(3) * trace
  202. # Compute the eigenvector corresponding to the maximum eigenvalue
  203. eigvals, eigvecs = np.linalg.eigh(N)
  204. q_optimal = eigvecs[:, np.argmax(eigvals)] # Optimal quaternion
  205. # Convert quaternion to rotation matrix
  206. w, x, y, z = q_optimal
  207. R = np.array([
  208. [1 - 2*(y**2 + z**2), 2*(x*y - z*w), 2*(x*z + y*w)],
  209. [2*(x*y + z*w), 1 - 2*(x**2 + z**2), 2*(y*z - x*w)],
  210. [2*(x*z - y*w), 2*(y*z + x*w), 1 - 2*(x**2 + y**2)]
  211. ])
  212. return R
  213. def icp_algorithm(moving_points, fixed_points, max_iterations=100, tolerance=1e-5):
  214. """
  215. Iterative Closest Point (ICP) algorithm to align moving_points to fixed_points.
  216. Parameters:
  217. moving_points (list or ndarray): List of points to be aligned.
  218. fixed_points (list or ndarray): List of reference points.
  219. max_iterations (int): Maximum number of iterations.
  220. tolerance (float): Convergence tolerance.
  221. Returns:
  222. ndarray: Transformed moving points.
  223. ndarray: Optimal rotation matrix.
  224. ndarray: Optimal translation vector.
  225. """
  226. # Convert to numpy arrays
  227. moving = np.array(moving_points)
  228. fixed = np.array(fixed_points)
  229. # Initialize transformation
  230. R = np.eye(3) # Identity matrix for rotation
  231. t = np.zeros(3) # Zero vector for translation
  232. prev_error = np.inf # Initialize previous error to a large value
  233. for iteration in range(max_iterations):
  234. # Step 1: Find the nearest neighbors (correspondences)
  235. distances = np.linalg.norm(moving[:, np.newaxis] - fixed, axis=2)
  236. nearest_indices = np.argmin(distances, axis=1)
  237. nearest_points = fixed[nearest_indices]
  238. # Step 2: Compute the optimal rotation and translation
  239. R_new = compute_Horn_rotation(moving, nearest_points)
  240. centroid_moving = np.mean(moving, axis=0)
  241. centroid_fixed = np.mean(nearest_points, axis=0)
  242. t_new = centroid_fixed - np.dot(R_new, centroid_moving)
  243. # Step 3: Apply the transformation
  244. moving = np.dot(moving, R_new.T) + t_new
  245. # Update the cumulative transformation
  246. R = np.dot(R_new, R)
  247. t = np.dot(R_new, t) + t_new
  248. # Step 4: Check for convergence
  249. mean_error = np.mean(np.linalg.norm(moving - nearest_points, axis=1))
  250. if np.abs(prev_error - mean_error) < tolerance:
  251. print(f"ICP converged after {iteration + 1} iterations.")
  252. break
  253. prev_error = mean_error
  254. else:
  255. print(f"ICP reached maximum iterations ({max_iterations}).")
  256. return moving, R, t
  257. def match_points(cbct_points, ct_points, auto_weights=True, fallback_if_worse=True, normalize_lengths=True, normalize_angles=False, min_distance=5):
  258. def side_lengths(points):
  259. lengths = [
  260. np.linalg.norm(points[0] - points[1]),
  261. np.linalg.norm(points[1] - points[2]),
  262. np.linalg.norm(points[2] - points[0])
  263. ]
  264. return lengths
  265. def triangle_angles(points):
  266. a = np.linalg.norm(points[1] - points[2])
  267. b = np.linalg.norm(points[0] - points[2])
  268. c = np.linalg.norm(points[0] - points[1])
  269. angle_A = np.arccos(np.clip((b**2 + c**2 - a**2) / (2 * b * c), -1.0, 1.0))
  270. angle_B = np.arccos(np.clip((a**2 + c**2 - b**2) / (2 * a * c), -1.0, 1.0))
  271. angle_C = np.pi - angle_A - angle_B
  272. return [angle_A, angle_B, angle_C]
  273. def normalize(vec):
  274. norm = np.linalg.norm(vec)
  275. return [v / norm for v in vec] if norm > 0 else vec
  276. def permutation_score(perm, ct_lengths, ct_angles, w_len, w_ang):
  277. perm_lengths = side_lengths(perm)
  278. perm_angles = triangle_angles(perm)
  279. # Filter za minimum razdalje
  280. if min(perm_lengths) < min_distance:
  281. return float('inf')
  282. lengths_1 = normalize(perm_lengths) if normalize_lengths else perm_lengths
  283. lengths_2 = normalize(ct_lengths) if normalize_lengths else ct_lengths
  284. angles_1 = normalize(perm_angles) if normalize_angles else perm_angles
  285. angles_2 = normalize(ct_angles) if normalize_angles else ct_angles
  286. score_len = sum(abs(a - b) for a, b in zip(lengths_1, lengths_2))
  287. score_ang = sum(abs(a - b) for a, b in zip(angles_1, angles_2))
  288. return w_len * score_len + w_ang * score_ang
  289. cbct_points = list(cbct_points)
  290. ct_lengths = side_lengths(np.array(ct_points))
  291. ct_angles = triangle_angles(np.array(ct_points))
  292. if auto_weights:
  293. var_len = np.var(ct_lengths)
  294. var_ang = np.var(ct_angles)
  295. total_var = var_len + var_ang + 1e-6
  296. weight_length = (1 - var_len / total_var)
  297. weight_angle = (1 - var_ang / total_var)
  298. else:
  299. weight_length = 0.5
  300. weight_angle = 0.5
  301. best_perm = None
  302. best_score = float('inf')
  303. #print(f"CT points: {ct_points}")
  304. for perm in itertools.permutations(cbct_points):
  305. perm = np.array(perm)
  306. score = permutation_score(perm, ct_lengths, ct_angles, weight_length, weight_angle)
  307. #print(f"Perm: {perm.tolist()} -> Score: {score:.4f}")
  308. if score < best_score:
  309. best_score = score
  310. best_perm = perm
  311. #print("CT centroid:", np.mean(ct_points, axis=0))
  312. #print("CBCT centroid (best perm):", np.mean(best_perm, axis=0))
  313. if fallback_if_worse:
  314. original_score = permutation_score(np.array(cbct_points), ct_lengths, ct_angles, weight_length, weight_angle)
  315. #print("Original score: ", original_score)
  316. if original_score <= best_score:
  317. #print("Fallback to original points due to worse score of the permutation.")
  318. return list(cbct_points)
  319. return list(best_perm)
  320. def compute_translation(moving_points, fixed_points, rotation_matrix):
  321. """
  322. Computes the translation vector to align moving_points to fixed_points given a rotation matrix.
  323. Parameters:
  324. moving_points (list or ndarray): List of points to be translated.
  325. fixed_points (list or ndarray): List of reference points.
  326. rotation_matrix (ndarray): Rotation matrix.
  327. Returns:
  328. ndarray: Translation vector.
  329. """
  330. # Convert to numpy arrays
  331. moving = np.array(moving_points)
  332. fixed = np.array(fixed_points)
  333. # Compute centroids
  334. centroid_moving = np.mean(moving, axis=0)
  335. centroid_fixed = np.mean(fixed, axis=0)
  336. # Compute translation
  337. translation = centroid_fixed - np.dot(centroid_moving, rotation_matrix)
  338. return translation
  339. def create_vtk_transform(rotation_matrix, translation_vector, study_name=None, cbct_volume_name=None):
  340. """
  341. Creates a vtkTransform from a rotation matrix and a translation vector.
  342. """
  343. # Create a 4x4 transformation matrix
  344. transform_matrix = np.eye(4) # Start with an identity matrix
  345. transform_matrix[:3, :3] = rotation_matrix # Set rotation part
  346. transform_matrix[:3, 3] = translation_vector # Set translation part
  347. global latest_transform_matrix
  348. # Save to global variable
  349. latest_transform_matrix = transform_matrix.copy()
  350. if study_name and cbct_volume_name:
  351. save_transform_matrix(transform_matrix, study_name, cbct_volume_name)
  352. # Convert to vtkMatrix4x4
  353. vtk_matrix = vtk.vtkMatrix4x4()
  354. for i in range(4):
  355. for j in range(4):
  356. vtk_matrix.SetElement(i, j, transform_matrix[i, j])
  357. print("Transform matrix:")
  358. for i in range(4):
  359. print(" ".join(f"{vtk_matrix.GetElement(i, j):.6f}" for j in range(4)))
  360. # Create vtkTransform and set the matrix
  361. transform = vtk.vtkTransform()
  362. transform.SetMatrix(vtk_matrix)
  363. return transform
  364. def save_transform_matrix(matrix, study_name, cbct_volume_name):
  365. """
  366. Appends the given 4x4 matrix to a text file under the given study folder.
  367. """
  368. base_folder = "Transformacijske matrike"
  369. study_folder = os.path.join(base_folder, study_name)
  370. os.makedirs(study_folder, exist_ok=True) # Create folders if they don't exist
  371. filename = os.path.join(study_folder, f"{cbct_volume_name}.txt")
  372. with open(filename, "a") as f:
  373. f.write("Transformacija:\n")
  374. for row in matrix:
  375. f.write(" ".join(f"{elem:.6f}" for elem in row) + "\n")
  376. f.write("\n") # Dodaj prazen vrstico med transformacijami
  377. def detect_points_region_growing(volume_name, yesCbct, create_marker, intensity_threshold=3000, x_min=90, x_max=380, y_min=190, y_max=380, z_min=50, z_max=140, max_distance=9, centroid_merge_threshold=5):
  378. volume_node = slicer.util.getNode(volume_name)
  379. if not volume_node:
  380. raise RuntimeError(f"Volume {volume_name} not found.")
  381. image_data = volume_node.GetImageData()
  382. matrix = vtk.vtkMatrix4x4()
  383. volume_node.GetIJKToRASMatrix(matrix)
  384. dimensions = image_data.GetDimensions()
  385. #detected_regions = []
  386. if yesCbct: #je cbct ali ct?
  387. valid_x_min, valid_x_max = 0, dimensions[0] - 1
  388. valid_y_min, valid_y_max = 0, dimensions[1] - 1
  389. valid_z_min, valid_z_max = 0, dimensions[2] - 1
  390. else:
  391. valid_x_min, valid_x_max = max(x_min, 0), min(x_max, dimensions[0] - 1)
  392. valid_y_min, valid_y_max = max(y_min, 0), min(y_max, dimensions[1] - 1)
  393. valid_z_min, valid_z_max = max(z_min, 0), min(z_max, dimensions[2] - 1)
  394. visited = set()
  395. def grow_region(x, y, z):
  396. if (x, y, z) in visited:
  397. return None
  398. voxel_value = image_data.GetScalarComponentAsDouble(x, y, z, 0)
  399. if voxel_value < intensity_threshold:
  400. return None
  401. region = region_growing(image_data, (x, y, z), intensity_threshold, max_distance=max_distance)
  402. if region:
  403. for point in region:
  404. visited.add(tuple(point))
  405. return region
  406. return None
  407. regions = []
  408. for z in range(valid_z_min, valid_z_max + 1):
  409. for y in range(valid_y_min, valid_y_max + 1):
  410. for x in range(valid_x_min, valid_x_max + 1):
  411. region = grow_region(x, y, z)
  412. if region:
  413. regions.append(region)
  414. # Collect centroids using intensity-weighted average
  415. centroids = []
  416. for region in regions:
  417. points = np.array([matrix.MultiplyPoint([*point, 1])[:3] for point in region])
  418. intensities = np.array([image_data.GetScalarComponentAsDouble(*point, 0) for point in region])
  419. if intensities.sum() > 0:
  420. weighted_centroid = np.average(points, axis=0, weights=intensities)
  421. max_intensity = intensities.max()
  422. centroids.append((np.round(weighted_centroid, 2), max_intensity))
  423. unique_centroids = []
  424. for centroid, intensity in centroids:
  425. if not any(np.linalg.norm(centroid - existing_centroid) < centroid_merge_threshold for existing_centroid, _ in unique_centroids):
  426. unique_centroids.append((centroid, intensity))
  427. if create_marker:
  428. markups_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"Markers_{volume_name}")
  429. for centroid, intensity in unique_centroids:
  430. markups_node.AddControlPoint(*centroid)
  431. markups_node.SetDisplayVisibility(False)
  432. #print(f"Detected Centroid (RAS): {centroid}, Max Intensity: {intensity}")
  433. return unique_centroids
  434. def find_table_top_z(ct_volume_name, writefilecheck, makemarkerscheck, yesCbct):
  435. """
  436. Najde višino zgornjega roba mize v CT/CBCT volumnu in (opcija) doda marker ter shrani podatke.
  437. """
  438. # Pridobi volumen in osnovne lastnosti
  439. ct_volume_node = slicer.util.getNode(ct_volume_name)
  440. image_data = ct_volume_node.GetImageData()
  441. spacing = ct_volume_node.GetSpacing()
  442. dims = image_data.GetDimensions()
  443. np_array = slicer.util.arrayFromVolume(ct_volume_node)
  444. # Izračun sredinskih koordinat
  445. mid_ijk = [max(0, min(dims[i] - 1, dims[i] // 2)) for i in range(3)]
  446. mid_z_voxel = mid_ijk[2]
  447. mid_x_voxel = max(0, mid_ijk[0] - 15) # levo od središča
  448. # Izvoz slice in stolpca intenzitet
  449. slice_data = np_array[mid_z_voxel, :, :]
  450. column_values = slice_data[:, mid_x_voxel]
  451. # Pridobi transformacijo IJK → RAS
  452. ijkToRasMatrix = vtk.vtkMatrix4x4()
  453. ct_volume_node.GetIJKToRASMatrix(ijkToRasMatrix)
  454. # Iskanje roba mize
  455. threshold = -300 if yesCbct else -100
  456. min_jump = 100 if yesCbct else 50
  457. previous_value = -1000
  458. edge_count = 0
  459. table_top_y = None
  460. for y in range(len(column_values) - 1, -1, -1):
  461. intensity = column_values[y]
  462. if (intensity - previous_value) > min_jump and intensity > threshold:
  463. if yesCbct:
  464. if column_values[y-1] > -300:
  465. table_top_y = y
  466. break
  467. if edge_count == 0 or (edge_count == 1 and previous_value < -200):
  468. edge_count += 1
  469. if edge_count == 2:
  470. table_top_y = y
  471. break
  472. previous_value = intensity
  473. if table_top_y is None:
  474. print("❌ Zgornji rob mize ni bil najden!")
  475. return None
  476. # Pretvorba v RAS prostor
  477. table_ijk = [mid_x_voxel, table_top_y, mid_z_voxel]
  478. table_ras = np.array(ijkToRasMatrix.MultiplyPoint([*table_ijk, 1]))[:3]
  479. z_value = table_ras[2] # shranimo Z komponento
  480. # Dodaj marker, če zahtevano
  481. if makemarkerscheck:
  482. table_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"VišinaMize_{ct_volume_name}")
  483. table_node.AddControlPoint(table_ras)
  484. table_node.SetDisplayVisibility(False)
  485. # 📝 Shrani osnovni rezultat v heightdata.csv
  486. if writefilecheck:
  487. height_file = os.path.join(os.path.dirname(__file__), "heightdata.csv")
  488. with open(height_file, mode='a', newline='') as file:
  489. writer = csv.writer(file)
  490. modality = "CBCT " if yesCbct else "CT "
  491. writer.writerow([modality, ct_volume_name, f" Upper part of table detected at Z = {table_ras[1]:.2f} mm"])
  492. # 📁 Shrani razširjene informacije v table_z_debug.csv
  493. if writefilecheck:
  494. debug_file = os.path.join(os.path.dirname(__file__), "table_z_debug.csv")
  495. neighborhood = 30
  496. start = max(0, table_top_y - neighborhood)
  497. end = min(len(column_values), table_top_y + 2*neighborhood + 1)
  498. values_around = column_values[start:end]
  499. modality = "CBCT" if yesCbct else "CT"
  500. table_z_values[modality] = z_value # shrani trenutno vrednost
  501. dz_string = ""
  502. if "CT" in table_z_values and "CBCT" in table_z_values:
  503. dz = abs(table_z_values["CBCT"] - table_z_values["CT"])
  504. dz_string = f"{dz:.2f} mm"
  505. else:
  506. dz_string = "—"
  507. with open(debug_file, mode='a', newline='') as file:
  508. writer = csv.writer(file)
  509. writer.writerow([
  510. modality,
  511. ct_volume_name,
  512. f"{z_value:.2f}", # RAS-Z vrednost
  513. f"slice={table_top_y}",
  514. f"dz={dz_string}"
  515. ] + list(values_around))
  516. return table_ras[1], table_top_y # vrni samo Z komponento
  517. def align_cbct_to_ct(volumeNode, scan_type, offset, CT_offset=None, CT_spacing=None):
  518. """
  519. Aligns CBCT volume to CT volume based on height offset.
  520. Args:
  521. volumeNode (vtkMRMLScalarVolumeNode): The volume node to be aligned.
  522. scan_type (str): The type of scan ("CT" or "CBCT").
  523. offset (float): The height offset of the current volume from the center in mm.
  524. CT_offset (float, optional): The height offset of the CT volume from the center. Required for CBCT alignment.
  525. CT_spacing (float, optional): The voxel spacing of the CT volume in mm (for scaling the offset).
  526. Returns:
  527. float: The alignment offset applied to the CBCT volume (if applicable).
  528. """
  529. if scan_type == "CT":
  530. CT_offset = offset
  531. CT_spacing = volumeNode.GetSpacing()[1]
  532. #print(f"CT offset set to: {CT_offset}, CT spacing: {CT_spacing} mm/voxel")
  533. return CT_offset, CT_spacing
  534. else:
  535. if CT_offset is None or CT_spacing is None:
  536. raise ValueError("CT_offset and CT_spacing must be provided to align CBCT to CT.")
  537. CBCT_offset = offset
  538. # Razlika v mm brez skaliranja na CBCT_spacing
  539. alignment_offset_mm = CT_offset - CBCT_offset
  540. #print(f"CT offset: {CT_offset}, CBCT offset: {CBCT_offset}")
  541. #print(f"CT spacing: {CT_spacing} mm/voxel, CBCT spacing: {volumeNode.GetSpacing()[1]} mm/voxel")
  542. #print(f"Aligning CBCT with CT. Offset in mm: {alignment_offset_mm}")
  543. # Uporabi transformacijo
  544. transform = vtk.vtkTransform()
  545. transform.Translate(0, alignment_offset_mm, 0)
  546. transformNode = slicer.vtkMRMLTransformNode()
  547. slicer.mrmlScene.AddNode(transformNode)
  548. transformNode.SetAndObserveTransformToParent(transform)
  549. volumeNode.SetAndObserveTransformNodeID(transformNode.GetID())
  550. slicer.vtkSlicerTransformLogic().hardenTransform(volumeNode)
  551. slicer.mrmlScene.RemoveNode(transformNode)
  552. # Poskusi najti ustrezen marker in ga premakniti
  553. marker_name = f"VišinaMize_{volumeNode.GetName()}"
  554. # Robustno iskanje markerja po imenu
  555. table_node = None
  556. for node in slicer.util.getNodesByClass("vtkMRMLMarkupsFiducialNode"):
  557. if node.GetName() == marker_name:
  558. table_node = node
  559. break
  560. if table_node is not None:
  561. current_point = [0, 0, 0]
  562. table_node.GetNthControlPointPosition(0, current_point)
  563. moved_point = [
  564. current_point[0],
  565. current_point[1] + alignment_offset_mm,
  566. current_point[2]
  567. ]
  568. table_node.SetNthControlPointPosition(0, *moved_point)
  569. return alignment_offset_mm
  570. def print_orientation(volume_name):
  571. node = slicer.util.getNode(volume_name)
  572. matrix = vtk.vtkMatrix4x4()
  573. node.GetIJKToRASMatrix(matrix)
  574. print(f"{volume_name} IJK→RAS:")
  575. for i in range(3):
  576. print([matrix.GetElement(i, j) for j in range(3)])
  577. def prealign_by_centroid(cbct_points, ct_points):
  578. """
  579. Predporavna CBCT markerje na CT markerje glede na centrične točke.
  580. Args:
  581. cbct_points: List ali ndarray točk iz CBCT.
  582. ct_points: List ali ndarray točk iz CT.
  583. Returns:
  584. List: CBCT točke premaknjene tako, da so centrične točke usklajene.
  585. """
  586. cbct_points = np.array(cbct_points)
  587. ct_points = np.array(ct_points)
  588. cbct_centroid = np.mean(cbct_points, axis=0)
  589. ct_centroid = np.mean(ct_points, axis=0)
  590. translation_vector = ct_centroid - cbct_centroid
  591. aligned_cbct = cbct_points + translation_vector
  592. return aligned_cbct
  593. def choose_best_translation(cbct_points, ct_points, rotation_matrix):
  594. """
  595. Izbere boljšo translacijo: centroidno ali povprečno po rotaciji (retranslation).
  596. Args:
  597. cbct_points (array-like): Točke iz CBCT (še ne rotirane).
  598. ct_points (array-like): Ciljne CT točke.
  599. rotation_matrix (ndarray): Rotacijska matrika.
  600. Returns:
  601. tuple: (best_translation_vector, transformed_cbct_points, used_method)
  602. """
  603. cbct_points = np.array(cbct_points)
  604. ct_points = np.array(ct_points)
  605. # 1. Rotiraj CBCT točke
  606. rotated_cbct = np.dot(cbct_points, rotation_matrix.T)
  607. # 2. Centroid translacija
  608. centroid_moving = np.mean(cbct_points, axis=0)
  609. centroid_fixed = np.mean(ct_points, axis=0)
  610. translation_centroid = centroid_fixed - np.dot(centroid_moving, rotation_matrix)
  611. transformed_centroid = rotated_cbct + translation_centroid
  612. error_centroid = np.mean(np.linalg.norm(transformed_centroid - ct_points, axis=1))
  613. # 3. Retranslacija (srednja razlika)
  614. translation_recomputed = np.mean(ct_points - rotated_cbct, axis=0)
  615. transformed_recomputed = rotated_cbct + translation_recomputed
  616. error_recomputed = np.mean(np.linalg.norm(transformed_recomputed - ct_points, axis=1))
  617. # 4. Izberi boljšo
  618. if error_recomputed < error_centroid:
  619. print(f"✅ Using retranslation (error: {error_recomputed:.2f} mm)")
  620. return translation_recomputed, transformed_recomputed, "retranslation"
  621. else:
  622. print(f"✅ Using centroid-based translation (error: {error_centroid:.2f} mm)")
  623. return translation_centroid, transformed_centroid, "centroid"
  624. def rescale_points_to_match_spacing(points, source_spacing, target_spacing):
  625. scale_factors = np.array(target_spacing) / np.array(source_spacing)
  626. return np.array(points) * scale_factors
  627. def visualize_point_matches_in_slicer(cbct_points, ct_points, study_name="MatchVisualization"):
  628. assert len(cbct_points) == len(ct_points), "Mora biti enako število točk!"
  629. # Ustvari markups za CBCT
  630. cbct_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"{study_name}_CBCT")
  631. cbct_node.GetDisplayNode().SetSelectedColor(0, 0, 1) # modra
  632. # Ustvari markups za CT
  633. ct_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"{study_name}_CT")
  634. ct_node.GetDisplayNode().SetSelectedColor(1, 0, 0) # rdeča
  635. # Dodaj točke
  636. for i, (cbct, ct) in enumerate(zip(cbct_points, ct_points)):
  637. cbct_node.AddControlPoint(*cbct, f"CBCT_{i}")
  638. ct_node.AddControlPoint(*ct, f"CT_{i}")
  639. # Ustvari model z linijami med pari
  640. points = vtk.vtkPoints()
  641. lines = vtk.vtkCellArray()
  642. for i, (p1, p2) in enumerate(zip(cbct_points, ct_points)):
  643. id1 = points.InsertNextPoint(p1)
  644. id2 = points.InsertNextPoint(p2)
  645. line = vtk.vtkLine()
  646. line.GetPointIds().SetId(0, id1)
  647. line.GetPointIds().SetId(1, id2)
  648. lines.InsertNextCell(line)
  649. polyData = vtk.vtkPolyData()
  650. polyData.SetPoints(points)
  651. polyData.SetLines(lines)
  652. # Model node
  653. modelNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode", f"{study_name}_Connections")
  654. modelNode.SetAndObservePolyData(polyData)
  655. modelDisplay = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelDisplayNode")
  656. modelDisplay.SetColor(0, 0, 0) # črna
  657. modelDisplay.SetLineWidth(2)
  658. modelDisplay.SetVisibility(True)
  659. modelNode.SetAndObserveDisplayNodeID(modelDisplay.GetID())
  660. modelNode.SetAndObservePolyData(polyData)
  661. print(f"✅ Vizualizacija dodana za {study_name} (točke + povezave)")
  662. def remove_lowest_marker(points, axis=1):
  663. """
  664. Odstrani točko, ki je najnižja po dani osi (default Y-os v RAS prostoru).
  665. """
  666. if len(points) <= 3:
  667. return points # Ni potrebe po brisanju
  668. arr = np.array(points)
  669. lowest_index = np.argmin(arr[:, axis])
  670. removed = points.pop(lowest_index)
  671. print(f"⚠️ Odstranjena najnižja točka (os {axis}): {removed}")
  672. return points
  673. # Globalni seznami za končno statistiko
  674. prostate_size_est = []
  675. ctcbct_distance = []
  676. table_z_values = {}
  677. # Pridobimo SubjectHierarchyNode
  678. shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
  679. studyItems = vtk.vtkIdList()
  680. shNode.GetItemChildren(shNode.GetSceneItemID(), studyItems)
  681. for i in range(studyItems.GetNumberOfIds()):
  682. studyItem = studyItems.GetId(i)
  683. studyName = shNode.GetItemName(studyItem)
  684. print(f"\nProcessing study: {studyName}")
  685. # **LOKALNI** seznami, resetirajo se pri vsakem study-ju
  686. cbct_list = []
  687. ct_list = []
  688. volume_points_dict = {}
  689. CT_offset = 0
  690. # Get child items of the study item
  691. volumeItems = vtk.vtkIdList()
  692. shNode.GetItemChildren(studyItem, volumeItems)
  693. # Iteracija čez vse volumne v posameznem studyju
  694. for j in range(volumeItems.GetNumberOfIds()):
  695. intermediateItem = volumeItems.GetId(j)
  696. finalVolumeItems = vtk.vtkIdList()
  697. shNode.GetItemChildren(intermediateItem, finalVolumeItems) # Išči globlje!
  698. for k in range(finalVolumeItems.GetNumberOfIds()):
  699. volumeItem = finalVolumeItems.GetId(k)
  700. volumeNode = shNode.GetItemDataNode(volumeItem)
  701. try:
  702. dicomUIDs = volumeNode.GetAttribute("DICOM.instanceUIDs")
  703. except AttributeError:
  704. print(f"⚠️ Volume node '{volumeNode}' ni bil najden ali nima atributa 'DICOM.instanceUIDs'. Preskakujem.")
  705. dicomUIDs = None
  706. continue # Preskoči, če ni veljaven volume
  707. if not dicomUIDs:
  708. print("❌ This is an NRRD volume!")
  709. continue # Preskoči, če ni DICOM volume
  710. volumeName = volumeNode.GetName()
  711. imageItem = shNode.GetItemByDataNode(volumeNode)
  712. modality = shNode.GetItemAttribute(imageItem, "DICOM.Modality") #deluje!
  713. #dimensions = volumeNode.GetImageData().GetDimensions()
  714. #spacing = volumeNode.GetSpacing()
  715. #print(f"Volume {volumeNode.GetName()} - Dimenzije: {dimensions}, Spacing: {spacing}")
  716. if modality != "CT":
  717. print("Not a CT")
  718. continue # Preskoči, če ni CT
  719. # Preveri, če volume obstaja v sceni
  720. if not slicer.mrmlScene.IsNodePresent(volumeNode):
  721. print(f"Volume {volumeName} not present in the scene.")
  722. continue
  723. # Preverimo proizvajalca (DICOM metapodatki)
  724. manufacturer = shNode.GetItemAttribute(imageItem, 'DICOM.Manufacturer')
  725. #manufacturer = volumeNode.GetAttribute("DICOM.Manufacturer")
  726. #manufacturer = slicer.dicomDatabase.fileValue(uid, "0008,0070")
  727. #print(manufacturer)
  728. # Določimo, ali gre za CBCT ali CT
  729. if "varian" in manufacturer.lower() or "elekta" in manufacturer.lower():
  730. cbct_list.append(volumeName)
  731. scan_type = "CBCT"
  732. yesCbct = True
  733. else: # Siemens ali Philips
  734. ct_list.append(volumeName)
  735. scan_type = "CT"
  736. yesCbct = False
  737. if volumeNode and volumeNode.IsA("vtkMRMLScalarVolumeNode"):
  738. print(f"✔️ {scan_type} {volumeNode.GetName()} (ID: {volumeItem})")
  739. if not volumeNode or not volumeNode.IsA("vtkMRMLScalarVolumeNode"):
  740. print("Can't find volumeNode")
  741. #continue # Preskoči, če ni veljaven volume
  742. # Detekcija točk v volumnu
  743. ustvari_marker = not yesCbct # Ustvari markerje pred poravnavo na mizo
  744. grouped_points = detect_points_region_growing(volumeName, yesCbct, ustvari_marker, intensity_threshold=3000)
  745. #print(f"Populating volume_points_dict with key ('{scan_type}', '{volumeName}')")
  746. volume_points_dict[(scan_type, volumeName)] = grouped_points
  747. #print(volume_points_dict) # Check if the key is correctly added
  748. # Če imamo oba tipa volumna (CBCT in CT) **znotraj istega studyja**
  749. if cbct_list and ct_list:
  750. ct_volume_name = ct_list[0] # Uporabi prvi CT kot referenco
  751. ct_volume_Node = slicer.util.getNode(ct_volume_name)
  752. print(f"\nProcessing CT: {ct_volume_name}")
  753. yesCbct = False
  754. makemarkerscheck = False
  755. mm_offset, pixel_offset = find_table_top_z(ct_volume_name, writefilecheck, makemarkerscheck, yesCbct)
  756. CT_offset, CT_spacing = align_cbct_to_ct(ct_volume_Node, "CT", mm_offset)
  757. ct_points = [centroid for centroid, _ in volume_points_dict[("CT", ct_volume_name)]]
  758. ct_points = remove_lowest_marker(ct_points) #odstrani marker v riti, če obstaja
  759. print(f"CT points: {ct_points}")
  760. if len(ct_points) < 3:
  761. print(f"CT volume {ct_volume_name} doesn't have enough points for registration. Points: {len(ct_points)}")
  762. continue
  763. else:
  764. for cbct_volume_name in cbct_list:
  765. print(f"\nProcessing CBCT Volume: {cbct_volume_name}")
  766. yesCbct = True
  767. latest_transform_matrix = None
  768. scan_type = "CBCT" #redundant but here we are
  769. cbct_volume_node = slicer.util.getNode(cbct_volume_name)
  770. makemarkerscheck = False # Ustvari markerje pred poravnavo
  771. mm_offset, pixel_offset = find_table_top_z(cbct_volume_name, writefilecheck, makemarkerscheck, yesCbct)
  772. cbct_points = [centroid for centroid, _ in volume_points_dict[("CBCT", cbct_volume_name)]] #zastareli podatki
  773. cbct_points_array = np.array(cbct_points) # Pretvorba v numpy array
  774. # print_orientation(ct_volume_name)
  775. # print_orientation(cbct_volume_name)
  776. #for i, (cb, ct) in enumerate(zip(cbct_points, ct_points)):
  777. # print(f"Pair {i}: CBCT {cb}, CT {ct}, diff: {np.linalg.norm(cb - ct):.2f}")
  778. align_cbct_to_ct(cbct_volume_node, scan_type, mm_offset, CT_offset, CT_spacing)
  779. ustvari_marker = False # Ustvari markerje
  780. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
  781. #cbct_points = detect_points_region_growing(cbct_volume_name, yesCbct, intensity_threshold=3000)
  782. #cbct_points = [centroid for centroid, _ in volume_points_dict[("CBCT", cbct_volume_name)]] #zastareli podatki
  783. if len(cbct_points) < 3:
  784. print(f"CBCT Volume '{cbct_volume_name}' doesn't have enough points for registration. Points: {len(cbct_points)}")
  785. continue
  786. cbct_spacing = cbct_volume_node.GetSpacing()
  787. ct_spacing = ct_volume_Node.GetSpacing()
  788. cbct_points = rescale_points_to_match_spacing(cbct_points, cbct_spacing, ct_spacing)
  789. #Sortiramo točke po X/Y/Z da se izognemo težavam pri poravnavi
  790. cbct_points = match_points(cbct_points, ct_points)
  791. #visualize_point_matches_in_slicer(cbct_points, ct_points, studyName) #poveže pare markerjev
  792. if writefilecheck:
  793. # Shranjevanje razdalj
  794. distances_ct_cbct = []
  795. distances_internal = {"A-B": [], "B-C": [], "C-A": []}
  796. cbct_volume_node = slicer.util.getNode(cbct_volume_name)
  797. # Sortiramo točke po Z-koordinati (ali X/Y, če raje uporabljaš drugo os)
  798. cbct_points_sorted = cbct_points_array[np.argsort(cbct_points_array[:, 2])]
  799. # Razdalje med CT in CBCT (SORTIRANE točke!)
  800. d_ct_cbct = np.linalg.norm(cbct_points_sorted - ct_points, axis=1)
  801. distances_ct_cbct.append(d_ct_cbct)
  802. # Razdalje med točkami znotraj SORTIRANIH cbct_points
  803. d_ab = np.linalg.norm(cbct_points_sorted[0] - cbct_points_sorted[1])
  804. d_bc = np.linalg.norm(cbct_points_sorted[1] - cbct_points_sorted[2])
  805. d_ca = np.linalg.norm(cbct_points_sorted[2] - cbct_points_sorted[0])
  806. # Sortiramo razdalje po velikosti, da so vedno v enakem vrstnem redu
  807. sorted_distances = sorted([d_ab, d_bc, d_ca])
  808. distances_internal["A-B"].append(sorted_distances[0])
  809. distances_internal["B-C"].append(sorted_distances[1])
  810. distances_internal["C-A"].append(sorted_distances[2])
  811. # Dodamo ime študije za v statistiko
  812. studyName = shNode.GetItemName(studyItem)
  813. # **Shrani razdalje v globalne sezname**
  814. prostate_size_est.append({"Study": studyName, "Distances": sorted_distances})
  815. ctcbct_distance.append({"Study": studyName, "Distances": list(distances_ct_cbct[-1])}) # Pretvorimo v seznam
  816. # Izberi metodo glede na uporabnikov izbor
  817. chosen_rotation_matrix = np.eye(3)
  818. chosen_translation_vector = np.zeros(3)
  819. print("Markerji pred transformacijo:", cbct_points, ct_points)
  820. if applyScaling:
  821. scaling_factors = compute_optimal_scaling_per_axis(cbct_points, ct_points)
  822. #print("Scaling factors: ", scaling_factors)
  823. cbct_points = compute_scaling(cbct_points, scaling_factors)
  824. initial_error = np.mean(np.linalg.norm(np.array(cbct_points) - np.array(ct_points), axis=1))
  825. if initial_error > 30:
  826. print("⚠️ Initial distance too large, applying centroid prealignment.")
  827. cbct_points = prealign_by_centroid(cbct_points, ct_points)
  828. if applyRotation:
  829. if selectedMethod == "Kabsch":
  830. chosen_rotation_matrix = compute_Kabsch_rotation(cbct_points, ct_points)
  831. elif selectedMethod == "Horn":
  832. chosen_rotation_matrix = compute_Horn_rotation(cbct_points, ct_points)
  833. elif selectedMethod == "Iterative Closest Point (Kabsch)":
  834. _, chosen_rotation_matrix, _ = icp_algorithm(cbct_points, ct_points)
  835. #print("Rotation Matrix:\n", chosen_rotation_matrix)
  836. fine_shift = np.zeros(3) # Inicializiraj fine premike
  837. if applyTranslation:
  838. chosen_translation_vector, cbct_points_transformed, method_used = choose_best_translation(
  839. cbct_points, ct_points, chosen_rotation_matrix)
  840. # Sistematična razlika (signed shift)
  841. rotated_cbct = np.dot(cbct_points, chosen_rotation_matrix.T)
  842. translated_cbct = rotated_cbct + chosen_translation_vector
  843. delta_y_list = [ct[1] - cbct[1] for ct, cbct in zip(ct_points, translated_cbct)]
  844. mean_delta_y = np.mean(delta_y_list)
  845. # Uporabi sistematični shift za dodatno poravnavo v y-osi
  846. fine_shift = np.array([0.0, mean_delta_y, 0.0]) # samo Y-os
  847. cbct_points_transformed += fine_shift
  848. print(f"🔧 Fine correction shifts: ΔX={fine_shift[0]:.2f} mm, ΔY={fine_shift[1]:.2f} mm, ΔZ={fine_shift[2]:.2f} mm")
  849. # ✅ Kombinirana transformacija
  850. total_translation = chosen_translation_vector + fine_shift
  851. chosen_translation_vector = total_translation
  852. vtk_transform = create_vtk_transform(chosen_rotation_matrix, chosen_translation_vector, studyName, cbct_volume_name) #Tukaj se tudi izpiše transformacijska matrika
  853. # 🔄 Pripni transformacijo
  854. imeTransformNoda = cbct_volume_name + " Transform"
  855. transform_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTransformNode", imeTransformNoda)
  856. transform_node.SetAndObserveTransformToParent(vtk_transform)
  857. cbct_volume_node = slicer.util.getNode(cbct_volume_name)
  858. cbct_volume_node.SetAndObserveTransformNodeID(transform_node.GetID())
  859. # 🔨 Uporabi (ali shrani transformacijo kasneje)
  860. slicer.vtkSlicerTransformLogic().hardenTransform(cbct_volume_node)
  861. slicer.mrmlScene.RemoveNode(transform_node)
  862. # 📍 Detekcija markerjev po transformaciji
  863. ustvari_marker = False
  864. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
  865. print("Markerji po transformaciji:\n", cbct_points, ct_points)
  866. #popravek v x osi
  867. delta_x_list = [ct[0] - cbct[0] for ct, cbct in zip(ct_points, cbct_points)]
  868. mean_delta_x = np.mean(delta_x_list)
  869. #popravek v y osi
  870. delta_y_list = [ct[1] - cbct[1] for ct, cbct in zip(ct_points, cbct_points)]
  871. mean_delta_y = np.mean(delta_y_list)
  872. #popravek v z osi
  873. delta_z_list = [ct[2] - cbct[2] for ct, cbct in zip(ct_points, cbct_points)]
  874. mean_delta_z = np.mean(delta_z_list)
  875. # Uporabi sistematični shift za dodatno poravnavo
  876. fine_shift = np.array([mean_delta_x, mean_delta_y, mean_delta_z])
  877. #cbct_points_transformed += fine_shift
  878. chosen_rotation_matrix = np.eye(3) #tokrat brez rotacije
  879. vtk_transform = create_vtk_transform(chosen_rotation_matrix, fine_shift, studyName, cbct_volume_name) #Tukaj se tudi izpiše transformacijska matrika
  880. # 🔄 Pripni transformacijo
  881. imeTransformNoda = cbct_volume_name + " Transform"
  882. transform_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTransformNode", imeTransformNoda)
  883. transform_node.SetAndObserveTransformToParent(vtk_transform)
  884. cbct_volume_node = slicer.util.getNode(cbct_volume_name)
  885. cbct_volume_node.SetAndObserveTransformNodeID(transform_node.GetID())
  886. # 🔨 Uporabi (ali shrani transformacijo kasneje)
  887. slicer.vtkSlicerTransformLogic().hardenTransform(cbct_volume_node)
  888. slicer.mrmlScene.RemoveNode(transform_node)
  889. ustvari_marker = True
  890. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
  891. print(f"🔧 Fine correction shifts: ΔX={fine_shift[0]:.2f} mm, ΔY={fine_shift[1]:.2f} mm, ΔZ={fine_shift[2]:.2f} mm")
  892. # 📏 Izračun napake
  893. errors = [np.linalg.norm(cbct - ct) for cbct, ct in zip(cbct_points, ct_points)]
  894. mean_error = np.mean(errors)
  895. print("Individualne napake:", errors)
  896. print("📏 Povprečna napaka poravnave: {:.2f} mm".format(mean_error))
  897. for i, (cbct, ct) in enumerate(zip(cbct_points, ct_points)):
  898. diff = np.array(cbct) - np.array(ct)
  899. print(f"Točka {i+1}: ΔX={diff[0]:.2f} mm, ΔY={diff[1]:.2f} mm, ΔZ={diff[2]:.2f} mm")
  900. else:
  901. print(f"Study {studyItem} doesn't have any appropriate CT or CBCT volumes.")
  902. # Izpis globalne statistike
  903. if writefilecheck:
  904. #print("Distances between CT & CBCT markers: ", ctcbct_distance)
  905. #print("Distances between pairs of markers for each volume: ", prostate_size_est)
  906. # Define file paths
  907. prostate_size_file = os.path.join(os.path.dirname(__file__), "prostate_size.csv")
  908. ctcbct_distance_file = os.path.join(os.path.dirname(__file__), "ct_cbct_distance.csv")
  909. # Write prostate size data
  910. with open(prostate_size_file, mode='w', newline='') as file:
  911. writer = csv.writer(file)
  912. writer.writerow(["Prostate Size"])
  913. for size in prostate_size_est:
  914. writer.writerow([size])
  915. #print("Prostate size file written at ", prostate_size_file)
  916. # Write CT-CBCT distance data
  917. with open(ctcbct_distance_file, mode='w', newline='') as file:
  918. writer = csv.writer(file)
  919. writer.writerow(["CT-CBCT Distance"])
  920. for distance in ctcbct_distance:
  921. writer.writerow([distance])
  922. #print("CT-CBCT distance file written at ", ctcbct_distance_file)
  923. end_time = time.time()
  924. # Calculate and print elapsed time
  925. elapsed_time = end_time - start_time
  926. # Convert to minutes and seconds
  927. minutes = int(elapsed_time // 60)
  928. seconds = elapsed_time % 60
  929. print(f"Execution time: {minutes} minutes and {seconds:.6f} seconds")