SeekTransformModule.py 63 KB

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