SeekTransformModule.py 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895
  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 (Kabsch)"])
  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_Kabsch_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):
  258. """
  259. Vrne cbct_points permutirane tako, da so najbolje poravnani s ct_points
  260. glede na vsoto evklidskih razdalj.
  261. """
  262. best_perm = None
  263. min_total_dist = float('inf')
  264. for perm in itertools.permutations(cbct_points):
  265. total_dist = sum(np.linalg.norm(np.array(p1) - np.array(p2)) for p1, p2 in zip(perm, ct_points))
  266. if total_dist < min_total_dist:
  267. min_total_dist = total_dist
  268. best_perm = perm
  269. return list(best_perm)
  270. def compute_translation(moving_points, fixed_points, rotation_matrix):
  271. """
  272. Computes the translation vector to align moving_points to fixed_points given a rotation matrix.
  273. Parameters:
  274. moving_points (list or ndarray): List of points to be translated.
  275. fixed_points (list or ndarray): List of reference points.
  276. rotation_matrix (ndarray): Rotation matrix.
  277. Returns:
  278. ndarray: Translation vector.
  279. """
  280. # Convert to numpy arrays
  281. moving = np.array(moving_points)
  282. fixed = np.array(fixed_points)
  283. # Compute centroids
  284. centroid_moving = np.mean(moving, axis=0)
  285. centroid_fixed = np.mean(fixed, axis=0)
  286. # Compute translation
  287. translation = centroid_fixed - np.dot(centroid_moving, rotation_matrix)
  288. return translation
  289. def create_vtk_transform(rotation_matrix, translation_vector):
  290. """
  291. Creates a vtkTransform from a rotation matrix and a translation vector.
  292. """
  293. # Create a 4x4 transformation matrix
  294. transform_matrix = np.eye(4) # Start with an identity matrix
  295. transform_matrix[:3, :3] = rotation_matrix # Set rotation part
  296. transform_matrix[:3, 3] = translation_vector # Set translation part
  297. # Convert to vtkMatrix4x4
  298. vtk_matrix = vtk.vtkMatrix4x4()
  299. for i in range(4):
  300. for j in range(4):
  301. vtk_matrix.SetElement(i, j, transform_matrix[i, j])
  302. print("Transform matrix:")
  303. for i in range(4):
  304. print(" ".join(f"{vtk_matrix.GetElement(i, j):.6f}" for j in range(4)))
  305. # Create vtkTransform and set the matrix
  306. transform = vtk.vtkTransform()
  307. transform.SetMatrix(vtk_matrix)
  308. return transform
  309. 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=80, z_max=140, max_distance=9, centroid_merge_threshold=5):
  310. volume_node = slicer.util.getNode(volume_name)
  311. if not volume_node:
  312. raise RuntimeError(f"Volume {volume_name} not found.")
  313. image_data = volume_node.GetImageData()
  314. matrix = vtk.vtkMatrix4x4()
  315. volume_node.GetIJKToRASMatrix(matrix)
  316. dimensions = image_data.GetDimensions()
  317. #detected_regions = []
  318. if yesCbct: #je cbct ali ct?
  319. valid_x_min, valid_x_max = 0, dimensions[0] - 1
  320. valid_y_min, valid_y_max = 0, dimensions[1] - 1
  321. valid_z_min, valid_z_max = 0, dimensions[2] - 1
  322. else:
  323. valid_x_min, valid_x_max = max(x_min, 0), min(x_max, dimensions[0] - 1)
  324. valid_y_min, valid_y_max = max(y_min, 0), min(y_max, dimensions[1] - 1)
  325. valid_z_min, valid_z_max = max(z_min, 0), min(z_max, dimensions[2] - 1)
  326. visited = set()
  327. def grow_region(x, y, z):
  328. if (x, y, z) in visited:
  329. return None
  330. voxel_value = image_data.GetScalarComponentAsDouble(x, y, z, 0)
  331. if voxel_value < intensity_threshold:
  332. return None
  333. region = region_growing(image_data, (x, y, z), intensity_threshold, max_distance=max_distance)
  334. if region:
  335. for point in region:
  336. visited.add(tuple(point))
  337. return region
  338. return None
  339. regions = []
  340. for z in range(valid_z_min, valid_z_max + 1):
  341. for y in range(valid_y_min, valid_y_max + 1):
  342. for x in range(valid_x_min, valid_x_max + 1):
  343. region = grow_region(x, y, z)
  344. if region:
  345. regions.append(region)
  346. # Collect centroids using intensity-weighted average
  347. centroids = []
  348. for region in regions:
  349. points = np.array([matrix.MultiplyPoint([*point, 1])[:3] for point in region])
  350. intensities = np.array([image_data.GetScalarComponentAsDouble(*point, 0) for point in region])
  351. if intensities.sum() > 0:
  352. weighted_centroid = np.average(points, axis=0, weights=intensities)
  353. max_intensity = intensities.max()
  354. centroids.append((np.round(weighted_centroid, 2), max_intensity))
  355. unique_centroids = []
  356. for centroid, intensity in centroids:
  357. if not any(np.linalg.norm(centroid - existing_centroid) < centroid_merge_threshold for existing_centroid, _ in unique_centroids):
  358. unique_centroids.append((centroid, intensity))
  359. if create_marker:
  360. markups_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"Markers_{volume_name}")
  361. for centroid, intensity in unique_centroids:
  362. markups_node.AddControlPoint(*centroid)
  363. markups_node.SetDisplayVisibility(False)
  364. #print(f"Detected Centroid (RAS): {centroid}, Max Intensity: {intensity}")
  365. return unique_centroids
  366. def find_table_top_z(ct_volume_name, writefilecheck, yesCbct):
  367. """
  368. Najde višino zgornjega roba mize v CT/CBCT volumnu in doda markerje.
  369. :param ct_volume_name: Ime volumna v slicerju
  370. :param writefilecheck: Če je True, zapiše rezultat v CSV
  371. :param yesCbct: Če je True, uporabi CBCT thresholde
  372. :return: Višina zgornjega roba mize v mm
  373. """
  374. # Pridobi volumen
  375. ct_volume_node = slicer.util.getNode(ct_volume_name)
  376. image_data = ct_volume_node.GetImageData()
  377. spacing = ct_volume_node.GetSpacing()
  378. dims = image_data.GetDimensions()
  379. #origin = ct_volume_node.GetOrigin()
  380. # Pretvori volumen v numpy array
  381. np_array = slicer.util.arrayFromVolume(ct_volume_node)
  382. # Izračunaj sredinske IJK koordinate
  383. mid_ijk = [dims[0] // 2, dims[1] // 2, dims[2] // 2]
  384. # Preveri, da so IJK indeksi v mejah volumna
  385. mid_ijk = [max(0, min(dims[i] - 1, mid_ijk[i])) for i in range(3)]
  386. # Pretvorba IJK → RAS
  387. ijkToRasMatrix = vtk.vtkMatrix4x4()
  388. ct_volume_node.GetIJKToRASMatrix(ijkToRasMatrix)
  389. #mid_ras = np.array(ijkToRasMatrix.MultiplyPoint([*mid_ijk, 1]))[:3]
  390. # Sredinski Z slice
  391. mid_z_voxel = mid_ijk[2]
  392. slice_data = np_array[mid_z_voxel, :, :] # (Y, X)
  393. # Sredinski stolpec
  394. mid_x_voxel = mid_ijk[0] - 15 # 15 pikslov levo od sredine da ne merimo pri hrbtenici
  395. column_values = slice_data[:, mid_x_voxel] # Y smer
  396. # Doda marker v RAS koordinatah
  397. #mid_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"Sredina_{ct_volume_name}")
  398. #mid_node.AddControlPoint(mid_ras)
  399. # Določi threshold glede na CBCT ali CT
  400. threshold = -300 if yesCbct else -100 # če je cbct iščemo vrednost -300, sicer pri CT 0
  401. # Poišči rob mize (drugi dvig)
  402. previous_value = -1000
  403. edge_count = 0
  404. table_top_y = None
  405. min_jump = 100 if yesCbct else 50 # Minimalni skok za CBCT in CT
  406. for y in range(len(column_values) - 1, -1, -1): # Od spodaj navzgor
  407. intensity = column_values[y]
  408. #if column_values[y] > threshold and previous_value <= threshold:
  409. if (intensity - previous_value) > min_jump and intensity > threshold: # Namesto primerjave s threshold
  410. if yesCbct:
  411. table_top_y = y + 1 #Da nismo že v trupu
  412. #print(f"Zgornji rob mize najden pri Y = {table_top_y}") # CBCT
  413. break
  414. if edge_count == 0 or (edge_count == 1 and previous_value < -200): # Check if intensity is back lower than -400
  415. edge_count += 1
  416. #print(f"Zaznan rob mize pri X, Y, Z = {mid_x_voxel},{y}, {mid_z_voxel}")
  417. if edge_count == 2: # Drugi dvig = zgornji rob mize
  418. table_top_y = y + 1
  419. #print(f"Zgornji rob mize najden pri Y = {table_top_y}")
  420. break
  421. previous_value = column_values[y]
  422. if table_top_y is None:
  423. print("❌ Zgornji rob mize ni bil najden!")
  424. return None
  425. # Pretvorba Y IJK → RAS
  426. table_ijk = [mid_x_voxel, table_top_y, mid_z_voxel]
  427. table_ras = np.array(ijkToRasMatrix.MultiplyPoint([*table_ijk, 1]))[:3]
  428. # Doda marker za višino mize
  429. #table_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"VišinaMize_{ct_volume_name}")
  430. #table_node.AddControlPoint(table_ras)
  431. #table_node.SetDisplayVisibility(False)
  432. # Izračun višine v mm
  433. #image_center_y = dims[1] // 2
  434. pixel_offset = table_top_y
  435. #mm_offset = pixel_offset * spacing[1]
  436. #print(f"📏 Miza je {abs(mm_offset):.2f} mm {'nižja' if mm_offset > 0 else 'višja'} od središča.")
  437. #print(f"📏 Miza je {abs(pixel_offset)} pixlov {'nižja' if pixel_offset > 0 else 'višja'} od središča.")
  438. #print(f"📏 CBCT table_top_y: {table_top_y}, image_center_y: {image_center_y}, pixel_offset: {pixel_offset}, mm_offset: {mm_offset}")
  439. #print(f"🛏 Absolutna višina mize (RAS Y): {table_ras[1]} mm")
  440. # Shrani v CSV
  441. if writefilecheck:
  442. file_path = os.path.join(os.path.dirname(__file__), "heightdata.csv")
  443. with open(file_path, mode='a', newline='') as file:
  444. writer = csv.writer(file)
  445. modality = "CBCT " if yesCbct else "CT "
  446. writer.writerow([modality, ct_volume_name, f" Upper part of table detected at Z = {table_ras[1]:.2f} mm"])
  447. return table_ras[1], pixel_offset
  448. def align_cbct_to_ct(volumeNode, scan_type, offset, CT_offset=None, CT_spacing=None):
  449. """
  450. Aligns CBCT volume to CT volume based on height offset.
  451. Args:
  452. volumeNode (vtkMRMLScalarVolumeNode): The volume node to be aligned.
  453. scan_type (str): The type of scan ("CT" or "CBCT").
  454. offset (float): The height offset of the current volume from the center in mm.
  455. CT_offset (float, optional): The height offset of the CT volume from the center. Required for CBCT alignment.
  456. CT_spacing (float, optional): The voxel spacing of the CT volume in mm (for scaling the offset).
  457. Returns:
  458. float: The alignment offset applied to the CBCT volume (if applicable).
  459. """
  460. if scan_type == "CT":
  461. CT_offset = offset
  462. CT_spacing = volumeNode.GetSpacing()[1]
  463. #print(f"CT offset set to: {CT_offset}, CT spacing: {CT_spacing} mm/voxel")
  464. return CT_offset, CT_spacing
  465. else:
  466. if CT_offset is None or CT_spacing is None:
  467. raise ValueError("CT_offset and CT_spacing must be provided to align CBCT to CT.")
  468. CBCT_offset = offset
  469. # Razlika v mm brez skaliranja na CBCT_spacing
  470. alignment_offset_mm = CT_offset - CBCT_offset
  471. #print(f"CT offset: {CT_offset}, CBCT offset: {CBCT_offset}")
  472. #print(f"CT spacing: {CT_spacing} mm/voxel, CBCT spacing: {volumeNode.GetSpacing()[1]} mm/voxel")
  473. #print(f"Aligning CBCT with CT. Offset in mm: {alignment_offset_mm}")
  474. # Uporabi transformacijo
  475. transform = vtk.vtkTransform()
  476. transform.Translate(0, alignment_offset_mm, 0)
  477. transformNode = slicer.vtkMRMLTransformNode()
  478. slicer.mrmlScene.AddNode(transformNode)
  479. transformNode.SetAndObserveTransformToParent(transform)
  480. volumeNode.SetAndObserveTransformNodeID(transformNode.GetID())
  481. slicer.vtkSlicerTransformLogic().hardenTransform(volumeNode)
  482. slicer.mrmlScene.RemoveNode(transformNode)
  483. return alignment_offset_mm
  484. def print_orientation(volume_name):
  485. node = slicer.util.getNode(volume_name)
  486. matrix = vtk.vtkMatrix4x4()
  487. node.GetIJKToRASMatrix(matrix)
  488. print(f"{volume_name} IJK→RAS:")
  489. for i in range(3):
  490. print([matrix.GetElement(i, j) for j in range(3)])
  491. # Globalni seznami za končno statistiko
  492. prostate_size_est = []
  493. ctcbct_distance = []
  494. # Pridobimo SubjectHierarchyNode
  495. shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
  496. studyItems = vtk.vtkIdList()
  497. shNode.GetItemChildren(shNode.GetSceneItemID(), studyItems)
  498. for i in range(studyItems.GetNumberOfIds()):
  499. studyItem = studyItems.GetId(i)
  500. studyName = shNode.GetItemName(studyItem)
  501. print(f"\nProcessing study: {studyName}")
  502. # **LOKALNI** seznami, resetirajo se pri vsakem study-ju
  503. cbct_list = []
  504. ct_list = []
  505. volume_points_dict = {}
  506. CT_offset = 0
  507. # Get child items of the study item
  508. volumeItems = vtk.vtkIdList()
  509. shNode.GetItemChildren(studyItem, volumeItems)
  510. # Iteracija čez vse volumne v posameznem studyju
  511. for j in range(volumeItems.GetNumberOfIds()):
  512. intermediateItem = volumeItems.GetId(j)
  513. finalVolumeItems = vtk.vtkIdList()
  514. shNode.GetItemChildren(intermediateItem, finalVolumeItems) # Išči globlje!
  515. for k in range(finalVolumeItems.GetNumberOfIds()):
  516. volumeItem = finalVolumeItems.GetId(k)
  517. volumeNode = shNode.GetItemDataNode(volumeItem)
  518. dicomUIDs = volumeNode.GetAttribute("DICOM.instanceUIDs")
  519. if not dicomUIDs:
  520. print("❌ This is an NRRD volume!")
  521. continue # Preskoči, če ni DICOM volume
  522. volumeName = volumeNode.GetName()
  523. imageItem = shNode.GetItemByDataNode(volumeNode)
  524. modality = shNode.GetItemAttribute(imageItem, "DICOM.Modality") #deluje!
  525. #dimensions = volumeNode.GetImageData().GetDimensions()
  526. #spacing = volumeNode.GetSpacing()
  527. #print(f"Volume {volumeNode.GetName()} - Dimenzije: {dimensions}, Spacing: {spacing}")
  528. if modality != "CT":
  529. print("Not a CT")
  530. continue # Preskoči, če ni CT
  531. # Preveri, če volume obstaja v sceni
  532. if not slicer.mrmlScene.IsNodePresent(volumeNode):
  533. print(f"Volume {volumeName} not present in the scene.")
  534. continue
  535. # Preverimo proizvajalca (DICOM metapodatki)
  536. manufacturer = shNode.GetItemAttribute(imageItem, 'DICOM.Manufacturer')
  537. #manufacturer = volumeNode.GetAttribute("DICOM.Manufacturer")
  538. #manufacturer = slicer.dicomDatabase.fileValue(uid, "0008,0070")
  539. #print(manufacturer)
  540. # Določimo, ali gre za CBCT ali CT
  541. if "varian" in manufacturer.lower() or "elekta" in manufacturer.lower():
  542. cbct_list.append(volumeName)
  543. scan_type = "CBCT"
  544. yesCbct = True
  545. else: # Siemens ali Philips
  546. ct_list.append(volumeName)
  547. scan_type = "CT"
  548. yesCbct = False
  549. if volumeNode and volumeNode.IsA("vtkMRMLScalarVolumeNode"):
  550. print(f"✔️ {scan_type} {volumeNode.GetName()} (ID: {volumeItem})")
  551. if not volumeNode or not volumeNode.IsA("vtkMRMLScalarVolumeNode"):
  552. print("Can't find volumeNode")
  553. #continue # Preskoči, če ni veljaven volume
  554. # Detekcija točk v volumnu
  555. ustvari_marker = False # Ustvari markerje
  556. grouped_points = detect_points_region_growing(volumeName, yesCbct, ustvari_marker, intensity_threshold=3000)
  557. #print(f"Populating volume_points_dict with key ('{scan_type}', '{volumeName}')")
  558. volume_points_dict[(scan_type, volumeName)] = grouped_points
  559. #print(volume_points_dict) # Check if the key is correctly added
  560. # Če imamo oba tipa volumna (CBCT in CT) **znotraj istega studyja**
  561. if cbct_list and ct_list:
  562. ct_volume_name = ct_list[0] # Uporabi prvi CT kot referenco
  563. print(f"\nProcessing CT: {ct_volume_name}")
  564. yesCbct = False
  565. mm_offset, pixel_offset = find_table_top_z(ct_volume_name, writefilecheck, yesCbct)
  566. CT_offset, CT_spacing = align_cbct_to_ct(slicer.util.getNode(ct_volume_name), "CT", mm_offset)
  567. ct_points = [centroid for centroid, _ in volume_points_dict[("CT", ct_volume_name)]]
  568. print(f"CT points: {ct_points}")
  569. if len(ct_points) < 3:
  570. print(f"CT volume {ct_volume_name} doesn't have enough points for registration. Points: {len(ct_points)}")
  571. continue
  572. else:
  573. for cbct_volume_name in cbct_list:
  574. print(f"\nProcessing CBCT Volume: {cbct_volume_name}")
  575. yesCbct = True
  576. scan_type = "CBCT" #redundant but here we are
  577. cbct_volume_node = slicer.util.getNode(cbct_volume_name)
  578. mm_offset, pixel_offset = find_table_top_z(cbct_volume_name, writefilecheck, yesCbct)
  579. cbct_points = [centroid for centroid, _ in volume_points_dict[("CBCT", cbct_volume_name)]] #zastareli podatki
  580. cbct_points_array = np.array(cbct_points) # Pretvorba v numpy array
  581. #print_orientation(ct_volume_name)
  582. #print_orientation(cbct_volume_name)
  583. #for i, (cb, ct) in enumerate(zip(cbct_points, ct_points)):
  584. # print(f"Pair {i}: CBCT {cb}, CT {ct}, diff: {np.linalg.norm(cb - ct):.2f}")
  585. align_cbct_to_ct(cbct_volume_node, scan_type, mm_offset, CT_offset, CT_spacing)
  586. #cbct_points = [centroid for centroid, _ in volume_points_dict[("CBCT", cbct_volume_name)]] #zastareli podatki
  587. # for i in range(len(cbct_points)):
  588. # x, y, z = cbct_points[i]
  589. # y_new = y + mm_offset # Upoštevaj premik zaradi poravnave mize
  590. # cbct_points[i] = (x, y_new, z) # Posodobi marker s premaknjeno višino
  591. ustvari_marker = False # Ustvari markerje
  592. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
  593. #cbct_points = detect_points_region_growing(cbct_volume_name, yesCbct, intensity_threshold=3000)
  594. #cbct_points = [centroid for centroid, _ in volume_points_dict[("CBCT", cbct_volume_name)]] #zastareli podatki
  595. if len(cbct_points) < 3:
  596. print(f"CBCT Volume '{cbct_volume_name}' doesn't have enough points for registration. Points: {len(cbct_points)}")
  597. continue
  598. #Sortiramo točke po X/Y/Z da se izognemo težavam pri poravnavi
  599. cbct_points = match_points(cbct_points, ct_points)
  600. #for i, (cb, ct) in enumerate(zip(cbct_points, ct_points)):
  601. # print(f"Pair {i}: CBCT {cb}, CT {ct}, diff: {np.linalg.norm(cb - ct):.2f}")
  602. # Shranjevanje razdalj
  603. distances_ct_cbct = []
  604. distances_internal = {"A-B": [], "B-C": [], "C-A": []}
  605. cbct_volume_node = slicer.util.getNode(cbct_volume_name)
  606. # Sortiramo točke po Z-koordinati (ali X/Y, če raje uporabljaš drugo os)
  607. cbct_points_sorted = cbct_points_array[np.argsort(cbct_points_array[:, 2])]
  608. # Razdalje med CT in CBCT (SORTIRANE točke!)
  609. d_ct_cbct = np.linalg.norm(cbct_points_sorted - ct_points, axis=1)
  610. distances_ct_cbct.append(d_ct_cbct)
  611. # Razdalje med točkami znotraj SORTIRANIH cbct_points
  612. d_ab = np.linalg.norm(cbct_points_sorted[0] - cbct_points_sorted[1])
  613. d_bc = np.linalg.norm(cbct_points_sorted[1] - cbct_points_sorted[2])
  614. d_ca = np.linalg.norm(cbct_points_sorted[2] - cbct_points_sorted[0])
  615. # Sortiramo razdalje po velikosti, da so vedno v enakem vrstnem redu
  616. sorted_distances = sorted([d_ab, d_bc, d_ca])
  617. distances_internal["A-B"].append(sorted_distances[0])
  618. distances_internal["B-C"].append(sorted_distances[1])
  619. distances_internal["C-A"].append(sorted_distances[2])
  620. # Dodamo ime študije za v statistiko
  621. studyName = shNode.GetItemName(studyItem)
  622. # **Shrani razdalje v globalne sezname**
  623. prostate_size_est.append({"Study": studyName, "Distances": sorted_distances})
  624. ctcbct_distance.append({"Study": studyName, "Distances": list(distances_ct_cbct[-1])}) # Pretvorimo v seznam
  625. # Izberi metodo glede na uporabnikov izbor
  626. chosen_rotation_matrix = np.eye(3)
  627. chosen_translation_vector = np.zeros(3)
  628. print("Markerji pred transformacijo:", cbct_points, ct_points)
  629. if applyScaling:
  630. scaling_factors = compute_optimal_scaling_per_axis(cbct_points, ct_points)
  631. #print("Scaling factors: ", scaling_factors)
  632. cbct_points = compute_scaling(cbct_points, scaling_factors)
  633. if applyRotation:
  634. if selectedMethod == "Kabsch":
  635. chosen_rotation_matrix = compute_Kabsch_rotation(cbct_points, ct_points)
  636. elif selectedMethod == "Horn":
  637. chosen_rotation_matrix = compute_Horn_rotation(cbct_points, ct_points)
  638. elif selectedMethod == "Iterative Closest Point (Kabsch)":
  639. _, chosen_rotation_matrix, _ = icp_algorithm(cbct_points, ct_points)
  640. #print("Rotation Matrix:\n", chosen_rotation_matrix)
  641. if applyTranslation:
  642. chosen_translation_vector = compute_translation(cbct_points, ct_points, chosen_rotation_matrix)
  643. #print("Translation Vector:\n", chosen_translation_vector)
  644. # Ustvari vtkTransformNode in ga poveži z CBCT volumenom
  645. imeTransformNoda = cbct_volume_name + " Transform"
  646. transform_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTransformNode", imeTransformNoda)
  647. # Kreiraj transformacijo in jo uporabi
  648. vtk_transform = create_vtk_transform(chosen_rotation_matrix, chosen_translation_vector)
  649. transform_node.SetAndObserveTransformToParent(vtk_transform)
  650. # Pridobi CBCT volumen in aplikacijo transformacije
  651. cbct_volume_node = slicer.util.getNode(cbct_volume_name)
  652. cbct_volume_node.SetAndObserveTransformNodeID(transform_node.GetID())
  653. # Uporabi transformacijo na volumnu (fizična aplikacija)
  654. slicer.vtkSlicerTransformLogic().hardenTransform(cbct_volume_node) #aplicira transformacijo na volumnu
  655. slicer.mrmlScene.RemoveNode(transform_node) # Odstrani transformacijo iz scene
  656. #print("Transform successful on ", cbct_volume_name)
  657. ustvari_marker = True # Ustvari markerje
  658. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
  659. print("Markerji po transformaciji:\n", cbct_points, ct_points)
  660. # Izračun razdalj
  661. errors = [np.linalg.norm(cbct - ct) for cbct, ct in zip(cbct_points, ct_points)]
  662. mean_error = np.mean(errors)
  663. print("Individualne napake:", errors)
  664. print("📏 Povprečna napaka poravnave: {:.2f} mm".format(mean_error))
  665. else:
  666. print(f"Study {studyItem} doesn't have any appropriate CT or CBCT volumes.")
  667. # Izpis globalne statistike
  668. if writefilecheck:
  669. print("Distances between CT & CBCT markers: ", ctcbct_distance)
  670. print("Distances between pairs of markers for each volume: ", prostate_size_est)
  671. # Define file paths
  672. prostate_size_file = os.path.join(os.path.dirname(__file__), "prostate_size.csv")
  673. ctcbct_distance_file = os.path.join(os.path.dirname(__file__), "ct_cbct_distance.csv")
  674. # Write prostate size data
  675. with open(prostate_size_file, mode='w', newline='') as file:
  676. writer = csv.writer(file)
  677. writer.writerow(["Prostate Size"])
  678. for size in prostate_size_est:
  679. writer.writerow([size])
  680. print("Prostate size file written at ", prostate_size_file)
  681. # Write CT-CBCT distance data
  682. with open(ctcbct_distance_file, mode='w', newline='') as file:
  683. writer = csv.writer(file)
  684. writer.writerow(["CT-CBCT Distance"])
  685. for distance in ctcbct_distance:
  686. writer.writerow([distance])
  687. print("CT-CBCT distance file written at ", ctcbct_distance_file)
  688. end_time = time.time()
  689. # Calculate and print elapsed time
  690. elapsed_time = end_time - start_time
  691. # Convert to minutes and seconds
  692. minutes = int(elapsed_time // 60)
  693. seconds = elapsed_time % 60
  694. print(f"Execution time: {minutes} minutes and {seconds:.6f} seconds")