SeekTransformModule.py 88 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813
  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 slicer.util
  9. import itertools
  10. import DICOMLib
  11. from DICOMLib import DICOMUtils
  12. import DicomRtImportExportPlugin
  13. from collections import deque, Counter
  14. import vtk
  15. from slicer.ScriptedLoadableModule import *
  16. import qt
  17. import matplotlib.pyplot as plt
  18. import csv
  19. import time
  20. import logging
  21. import matplotlib
  22. matplotlib.use('Agg') # << to dodaš ZGORAJ, da omogoči PNG zapis brez GUI
  23. from mpl_toolkits.mplot3d import Axes3D
  24. #exec(open("C:/Users/lkomar/Documents/Prostata/FirstTryRegister.py").read())
  25. cumulative_matrices = {}
  26. class SeekTransformModule(ScriptedLoadableModule):
  27. """
  28. Module description shown in the module panel.
  29. """
  30. def __init__(self, parent):
  31. ScriptedLoadableModule.__init__(self, parent)
  32. self.parent.title = "Seek Transform module"
  33. self.parent.categories = ["Image Processing"]
  34. self.parent.contributors = ["Luka Komar (Onkološki Inštitut Ljubljana, Fakulteta za Matematiko in Fiziko Ljubljana)"]
  35. self.parent.helpText = "This module applies rigid transformations to CBCT volumes based on reference CT volumes."
  36. self.parent.acknowledgementText = "Supported by doc. Primož Peterlin & prof. Andrej Studen"
  37. class SeekTransformModuleWidget(ScriptedLoadableModuleWidget):
  38. """
  39. GUI of the module.
  40. """
  41. def setup(self):
  42. ScriptedLoadableModuleWidget.setup(self)
  43. # Dropdown menu za izbiro metode
  44. self.rotationMethodComboBox = qt.QComboBox()
  45. self.rotationMethodComboBox.addItems(["Kabsch", "Horn", "Iterative Closest Point (Horn)"])
  46. self.layout.addWidget(self.rotationMethodComboBox)
  47. # Checkboxi za transformacije
  48. self.scalingCheckBox = qt.QCheckBox("Scaling")
  49. self.scalingCheckBox.setChecked(True)
  50. self.layout.addWidget(self.scalingCheckBox)
  51. self.rotationCheckBox = qt.QCheckBox("Rotation")
  52. self.rotationCheckBox.setChecked(True)
  53. self.layout.addWidget(self.rotationCheckBox)
  54. self.translationCheckBox = qt.QCheckBox("Translation")
  55. self.translationCheckBox.setChecked(True)
  56. self.layout.addWidget(self.translationCheckBox)
  57. self.markersCheckBox = qt.QCheckBox("Place control points for detected markers")
  58. self.markersCheckBox.setChecked(False)
  59. self.layout.addWidget(self.markersCheckBox)
  60. self.writefileCheckBox = qt.QCheckBox("Write data to csv file")
  61. self.writefileCheckBox.setChecked(True)
  62. self.layout.addWidget(self.writefileCheckBox)
  63. self.tableCheckBox = qt.QCheckBox("Find top of the table and match height")
  64. self.tableCheckBox.setChecked(True)
  65. self.layout.addWidget(self.tableCheckBox)
  66. self.DicomCheckBox = qt.QCheckBox("Save transformed DICOM")
  67. self.DicomCheckBox.setChecked(True)
  68. self.layout.addWidget(self.DicomCheckBox)
  69. # Load button
  70. self.applyButton = qt.QPushButton("Find markers and transform")
  71. self.applyButton.toolTip = "Finds markers, computes optimal rigid transform and applies it to CBCT volumes."
  72. self.applyButton.enabled = True
  73. self.layout.addWidget(self.applyButton)
  74. # Connect button to logic
  75. self.applyButton.connect('clicked(bool)', self.onApplyButton)
  76. self.layout.addStretch(1)
  77. def onApplyButton(self):
  78. # Nastavi globalni logger
  79. log_file_path = os.path.join("C:/Users/lkomar/Documents/Prostata", "seektransform_log.txt")
  80. logging.basicConfig(filename=log_file_path, level=logging.INFO, format='%(asctime)s - %(message)s')
  81. try:
  82. logging.info("▶️ onApplyButton pressed.")
  83. except Exception as e:
  84. print("❌ Logging setup failed:", e)
  85. logic = MyTransformModuleLogic()
  86. selectedMethod = self.rotationMethodComboBox.currentText # izberi metodo izračuna rotacije
  87. # Preberi stanje checkboxov
  88. applyRotation = self.rotationCheckBox.isChecked()
  89. applyTranslation = self.translationCheckBox.isChecked()
  90. applyScaling = self.scalingCheckBox.isChecked()
  91. applyMarkers = self.markersCheckBox.isChecked()
  92. writefilecheck = self.writefileCheckBox.isChecked()
  93. tablefind = self.tableCheckBox.isChecked()
  94. saveasdicom = self.DicomCheckBox.isChecked()
  95. # Pokliči logiko z izbranimi nastavitvami
  96. logic.run(selectedMethod, applyRotation, applyTranslation, applyScaling, applyMarkers, writefilecheck, tablefind, saveasdicom)
  97. class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
  98. """
  99. Core logic of the module.
  100. """
  101. def run(self, selectedMethod, applyRotation, applyTranslation, applyScaling, applymarkers, writefilecheck, tablefind, save_as_dicom):
  102. start_time = time.time()
  103. print("Calculating...")
  104. #slicer.util.delayDisplay(f"Starting", 1000)
  105. def group_points(points, threshold):
  106. # Function to group points that are close to each other
  107. grouped_points = []
  108. while points:
  109. point = points.pop() # Take one point from the list
  110. group = [point] # Start a new group
  111. # Find all points close to this one
  112. distances = cdist([point], points) # Calculate distances from this point to others
  113. close_points = [i for i, dist in enumerate(distances[0]) if dist < threshold]
  114. # Add the close points to the group
  115. group.extend([points[i] for i in close_points])
  116. # Remove the grouped points from the list
  117. points = [point for i, point in enumerate(points) if i not in close_points]
  118. # Add the group to the result
  119. grouped_points.append(group)
  120. return grouped_points
  121. def region_growing(image_data, seed, intensity_threshold, max_distance):
  122. dimensions = image_data.GetDimensions()
  123. visited = set()
  124. region = []
  125. queue = deque([seed])
  126. while queue:
  127. x, y, z = queue.popleft()
  128. if (x, y, z) in visited:
  129. continue
  130. visited.add((x, y, z))
  131. voxel_value = image_data.GetScalarComponentAsDouble(x, y, z, 0)
  132. if voxel_value >= intensity_threshold:
  133. region.append((x, y, z))
  134. # Add neighbors within bounds
  135. for dx, dy, dz in [(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)]:
  136. nx, ny, nz = x + dx, y + dy, z + dz
  137. if 0 <= nx < dimensions[0] and 0 <= ny < dimensions[1] and 0 <= nz < dimensions[2]:
  138. if (nx, ny, nz) not in visited:
  139. queue.append((nx, ny, nz))
  140. return region
  141. def compute_scaling_stddev(moving_points, fixed_points):
  142. moving = np.array(moving_points)
  143. fixed = np.array(fixed_points)
  144. # Standard deviation around centroid, po osi
  145. scaling_factors = np.std(fixed, axis=0) / np.std(moving, axis=0)
  146. return tuple(scaling_factors)
  147. def compute_scaling(cbct_points, scaling_factors):
  148. """Applies non-uniform scaling to CBCT points.
  149. Args:
  150. cbct_points (list of lists): List of (x, y, z) points.
  151. scaling_factors (tuple): Scaling factors (sx, sy, sz) for each axis.
  152. Returns:
  153. np.ndarray: Scaled CBCT points.
  154. """
  155. sx, sy, sz = scaling_factors # Extract scaling factors
  156. scaling_matrix = np.diag([sx, sy, sz]) # Create diagonal scaling matrix
  157. cbct_points_np = np.array(cbct_points) # Convert to numpy array
  158. scaled_points = cbct_points_np @ scaling_matrix.T # Apply scaling
  159. scaling_4x4 = np.eye(4)
  160. scaling_4x4[0, 0] = sx
  161. scaling_4x4[1, 1] = sy
  162. scaling_4x4[2, 2] = sz
  163. return scaled_points.tolist() # Convert back to list
  164. def compute_Kabsch_rotation(moving_points, fixed_points):
  165. """
  166. Computes the optimal rotation matrix to align moving_points to fixed_points.
  167. Parameters:
  168. moving_points (list or ndarray): List of points to be rotated CBCT
  169. fixed_points (list or ndarray): List of reference points CT
  170. Returns:
  171. ndarray: Optimal rotation matrix.
  172. """
  173. assert len(moving_points) == len(fixed_points), "Point lists must be the same length."
  174. # Convert to numpy arrays
  175. moving = np.array(moving_points)
  176. fixed = np.array(fixed_points)
  177. # Compute centroids
  178. centroid_moving = np.mean(moving, axis=0)
  179. centroid_fixed = np.mean(fixed, axis=0)
  180. # Center the points
  181. moving_centered = moving - centroid_moving
  182. fixed_centered = fixed - centroid_fixed
  183. # Compute covariance matrix
  184. H = np.dot(moving_centered.T, fixed_centered)
  185. # SVD decomposition
  186. U, _, Vt = np.linalg.svd(H)
  187. Rotate_optimal = np.dot(Vt.T, U.T)
  188. # Correct improper rotation (reflection)
  189. if np.linalg.det(Rotate_optimal) < 0:
  190. Vt[-1, :] *= -1
  191. Rotate_optimal = np.dot(Vt.T, U.T)
  192. return Rotate_optimal
  193. def compute_Horn_rotation(moving_points, fixed_points):
  194. """
  195. Computes the optimal rotation matrix using quaternions.
  196. Parameters:
  197. moving_points (list or ndarray): List of points to be rotated.
  198. fixed_points (list or ndarray): List of reference points.
  199. Returns:
  200. ndarray: Optimal rotation matrix.
  201. """
  202. assert len(moving_points) == len(fixed_points), "Point lists must be the same length."
  203. moving = np.array(moving_points)
  204. fixed = np.array(fixed_points)
  205. # Compute centroids
  206. centroid_moving = np.mean(moving, axis=0)
  207. centroid_fixed = np.mean(fixed, axis=0)
  208. # Center the points
  209. moving_centered = moving - centroid_moving
  210. fixed_centered = fixed - centroid_fixed
  211. # Construct the cross-dispersion matrix
  212. M = np.dot(moving_centered.T, fixed_centered)
  213. # Construct the N matrix for quaternion solution
  214. A = M - M.T
  215. delta = np.array([A[1, 2], A[2, 0], A[0, 1]])
  216. trace = np.trace(M)
  217. N = np.zeros((4, 4))
  218. N[0, 0] = trace
  219. N[1:, 0] = delta
  220. N[0, 1:] = delta
  221. N[1:, 1:] = M + M.T - np.eye(3) * trace
  222. # Compute the eigenvector corresponding to the maximum eigenvalue
  223. eigvals, eigvecs = np.linalg.eigh(N)
  224. q_optimal = eigvecs[:, np.argmax(eigvals)] # Optimal quaternion
  225. # Convert quaternion to rotation matrix
  226. w, x, y, z = q_optimal
  227. R = np.array([
  228. [1 - 2*(y**2 + z**2), 2*(x*y - z*w), 2*(x*z + y*w)],
  229. [2*(x*y + z*w), 1 - 2*(x**2 + z**2), 2*(y*z - x*w)],
  230. [2*(x*z - y*w), 2*(y*z + x*w), 1 - 2*(x**2 + y**2)]
  231. ])
  232. return R
  233. def icp_algorithm(moving_points, fixed_points, max_iterations=100, tolerance=1e-5):
  234. """
  235. Iterative Closest Point (ICP) algorithm to align moving_points to fixed_points.
  236. Parameters:
  237. moving_points (list or ndarray): List of points to be aligned.
  238. fixed_points (list or ndarray): List of reference points.
  239. max_iterations (int): Maximum number of iterations.
  240. tolerance (float): Convergence tolerance.
  241. Returns:
  242. ndarray: Transformed moving points.
  243. ndarray: Optimal rotation matrix.
  244. ndarray: Optimal translation vector.
  245. """
  246. # Convert to numpy arrays
  247. moving = np.array(moving_points)
  248. fixed = np.array(fixed_points)
  249. # Initialize transformation
  250. R = np.eye(3) # Identity matrix for rotation
  251. t = np.zeros(3) # Zero vector for translation
  252. prev_error = np.inf # Initialize previous error to a large value
  253. for iteration in range(max_iterations):
  254. # Step 1: Find the nearest neighbors (correspondences)
  255. distances = np.linalg.norm(moving[:, np.newaxis] - fixed, axis=2)
  256. nearest_indices = np.argmin(distances, axis=1)
  257. nearest_points = fixed[nearest_indices]
  258. # Step 2: Compute the optimal rotation and translation
  259. R_new = compute_Horn_rotation(moving, nearest_points)
  260. centroid_moving = np.mean(moving, axis=0)
  261. centroid_fixed = np.mean(nearest_points, axis=0)
  262. t_new = centroid_fixed - np.dot(R_new, centroid_moving)
  263. # Step 3: Apply the transformation
  264. moving = np.dot(moving, R_new.T) + t_new
  265. # Update the cumulative transformation
  266. R = np.dot(R_new, R)
  267. t = np.dot(R_new, t) + t_new
  268. # Step 4: Check for convergence
  269. mean_error = np.mean(np.linalg.norm(moving - nearest_points, axis=1))
  270. if np.abs(prev_error - mean_error) < tolerance:
  271. print(f"ICP converged after {iteration + 1} iterations.")
  272. break
  273. prev_error = mean_error
  274. else:
  275. print(f"ICP reached maximum iterations ({max_iterations}).")
  276. return moving, R, t
  277. def match_points(cbct_points, ct_points, auto_weights=False, fallback_if_worse=False, normalize_lengths=True, normalize_angles=False, min_distance=5, w_order=1.0):
  278. def side_lengths(points):
  279. lengths = [
  280. np.linalg.norm(points[0] - points[1]),
  281. np.linalg.norm(points[1] - points[2]),
  282. np.linalg.norm(points[2] - points[0])
  283. ]
  284. return lengths
  285. def triangle_angles(points):
  286. a = np.linalg.norm(points[1] - points[2])
  287. b = np.linalg.norm(points[0] - points[2])
  288. c = np.linalg.norm(points[0] - points[1])
  289. angle_A = np.arccos(np.clip((b**2 + c**2 - a**2) / (2 * b * c), -1.0, 1.0))
  290. angle_B = np.arccos(np.clip((a**2 + c**2 - b**2) / (2 * a * c), -1.0, 1.0))
  291. angle_C = np.pi - angle_A - angle_B
  292. return [angle_A, angle_B, angle_C]
  293. def normalize(vec):
  294. norm = np.linalg.norm(vec)
  295. return [v / norm for v in vec] if norm > 0 else vec
  296. def permutation_score(perm, ct_lengths, ct_angles, w_len, w_ang, penalty_angle_thresh=np.deg2rad(10)):
  297. perm_lengths = side_lengths(perm)
  298. perm_angles = triangle_angles(perm)
  299. # Filter za minimum razdalje
  300. if min(perm_lengths) < min_distance:
  301. return float('inf')
  302. lengths_1 = normalize(perm_lengths) if normalize_lengths else perm_lengths
  303. lengths_2 = normalize(ct_lengths) if normalize_lengths else ct_lengths
  304. angles_1 = normalize(perm_angles) if normalize_angles else perm_angles
  305. angles_2 = normalize(ct_angles) if normalize_angles else ct_angles
  306. score_len = sum(abs(a - b) for a, b in zip(lengths_1, lengths_2))
  307. score_ang = sum(abs(a - b) for a, b in zip(angles_1, angles_2))
  308. order_penalty = order_mismatch_penalty(ct_points, perm, axis='z')
  309. return w_len * score_len + w_ang * score_ang + order_penalty + w_order * order_penalty
  310. def smart_sort_cbct_points(cbct_points, z_threshold=5.0):
  311. """
  312. Sortira točke tako, da poskusi najprej po Z. Če so razlike po Z manjše
  313. od praga, sortira po (Y, X), sicer sortira po Z.
  314. """
  315. z_values = [pt[2] for pt in cbct_points]
  316. z_range = max(z_values) - min(z_values)
  317. if z_range < z_threshold:
  318. # Sortiraj po Y, nato X (če so točke v isti ravnini po Z)
  319. return sorted(cbct_points, key=lambda pt: (pt[1], pt[0]))
  320. else:
  321. # Sortiraj po Z, nato Y, nato X
  322. return sorted(cbct_points, key=lambda pt: (pt[2], pt[1], pt[0]))
  323. def order_mismatch_penalty(ct_points, perm, axis='z'):
  324. axis_idx = {'x': 0, 'y': 1, 'z': 2}[axis]
  325. ct_sorted = np.argsort([pt[axis_idx] for pt in ct_points])
  326. perm_sorted = np.argsort([pt[axis_idx] for pt in perm])
  327. return sum(1 for a, b in zip(ct_sorted, perm_sorted) if a != b)
  328. cbct_points = list(cbct_points)
  329. print("CBCT points:", cbct_points)
  330. ct_lengths = side_lengths(np.array(ct_points))
  331. ct_angles = triangle_angles(np.array(ct_points))
  332. if auto_weights:
  333. var_len = np.var(ct_lengths)
  334. var_ang = np.var(ct_angles)
  335. total_var = var_len + var_ang + 1e-6
  336. weight_length = (1 - var_len / total_var)
  337. weight_angle = (1 - var_ang / total_var)
  338. else:
  339. weight_length = 0.8
  340. weight_angle = 0.2
  341. cbct_sorted = smart_sort_cbct_points(cbct_points)
  342. original_score = permutation_score(np.array(cbct_sorted), ct_lengths, ct_angles, weight_length, weight_angle)
  343. # Če je ta rezultat dovolj dober, uporabi
  344. best_score = float('inf')
  345. best_perm = None
  346. if original_score < float('inf'): # lahko dodaš prag če želiš
  347. best_score = original_score
  348. best_perm = np.array(cbct_sorted)
  349. # Nato preveri vse permutacije (vključno s prvotnim vrstnim redom, če fallback_if_worse=True)
  350. for perm in itertools.permutations(cbct_points):
  351. perm = np.array(perm)
  352. score = permutation_score(perm, ct_lengths, ct_angles, weight_length, weight_angle)
  353. if score < best_score:
  354. best_score = score
  355. best_perm = perm
  356. print(f"New best permutation found with perm: {perm}")
  357. #print("CT centroid:", np.mean(ct_points, axis=0))
  358. #print("CBCT centroid (best perm):", np.mean(best_perm, axis=0))
  359. if fallback_if_worse:
  360. #original_score = permutation_score(np.array(cbct_points), ct_lengths, ct_angles, weight_length, weight_angle)
  361. print("Original score: ", original_score)
  362. if original_score <= best_score:
  363. print("Fallback to original points due to worse score of the permutation.")
  364. return list(cbct_points)
  365. return list(best_perm)
  366. def compute_translation(moving_points, fixed_points, rotation_matrix):
  367. """
  368. Computes the translation vector to align moving_points to fixed_points given a rotation matrix.
  369. Parameters:
  370. moving_points (list or ndarray): List of points to be translated.
  371. fixed_points (list or ndarray): List of reference points.
  372. rotation_matrix (ndarray): Rotation matrix.
  373. Returns:
  374. ndarray: Translation vector.
  375. """
  376. # Convert to numpy arrays
  377. moving = np.array(moving_points)
  378. fixed = np.array(fixed_points)
  379. # Compute centroids
  380. centroid_moving = np.mean(moving, axis=0)
  381. centroid_fixed = np.mean(fixed, axis=0)
  382. # Compute translation
  383. translation = centroid_fixed - np.dot(centroid_moving, rotation_matrix)
  384. return translation
  385. def create_vtk_transform(rotation_matrix, translation_vector, tablefound, study_name=None, cbct_volume_name=None, scaling_factors=None):
  386. """
  387. Creates a vtkTransform from scaling, rotation, and translation.
  388. Shrani tudi kumulativno matriko v globalni slovar cumulative_matrices.
  389. """
  390. # ----- Inicializacija -----
  391. global cumulative_matrices
  392. transform = vtk.vtkTransform()
  393. # ----- 1. Skaliranje -----
  394. if scaling_factors is not None:
  395. sx, sy, sz = scaling_factors
  396. transform.Scale(sx, sy, sz)
  397. # ----- 2. Rotacija -----
  398. # Rotacijsko matriko in translacijo pretvori v homogeno matriko
  399. affine_matrix = np.eye(4)
  400. affine_matrix[:3, :3] = rotation_matrix
  401. affine_matrix[:3, 3] = translation_vector
  402. # Vstavi v vtkMatrix4x4
  403. vtk_matrix = vtk.vtkMatrix4x4()
  404. for i in range(4):
  405. for j in range(4):
  406. vtk_matrix.SetElement(i, j, affine_matrix[i, j])
  407. transform.Concatenate(vtk_matrix)
  408. # ----- 3. Debug izpis -----
  409. print("Transform matrix:")
  410. for i in range(4):
  411. print(" ".join(f"{vtk_matrix.GetElement(i, j):.6f}" for j in range(4)))
  412. # ----- 4. Shrani v kumulativni matriki -----
  413. if study_name and cbct_volume_name:
  414. key = (study_name, cbct_volume_name)
  415. if key not in cumulative_matrices:
  416. cumulative_matrices[key] = np.eye(4)
  417. cumulative_matrices[key] = np.dot(cumulative_matrices[key], affine_matrix)
  418. return transform
  419. def save_transform_matrix(matrix, study_name, cbct_volume_name):
  420. """
  421. Appends the given 4x4 matrix to a text file under the given study folder.
  422. """
  423. base_folder = os.path.join(os.path.dirname(__file__), "Transformacijske matrike")
  424. study_folder = os.path.join(base_folder, study_name)
  425. os.makedirs(study_folder, exist_ok=True) # Create folders if they don't exist
  426. safe_cbct_name = re.sub(r'[<>:"/\\|?*]', '_', cbct_volume_name)
  427. # Preveri ali je CT miza najdena
  428. filename = os.path.join(study_folder, f"{safe_cbct_name}.txt")
  429. with open(filename, "w") as f:
  430. #f.write("Transformacija:\n")
  431. for row in matrix:
  432. f.write(" ".join(f"{elem:.6f}" for elem in row) + "\n")
  433. f.write("\n") # Dodaj prazen vrstico med transformacijami
  434. #print(f"Transform matrix saved to {filename}")
  435. 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):
  436. volume_node = find_volume_node_by_partial_name(volume_name)
  437. if not volume_node:
  438. raise RuntimeError(f"Volume {volume_name} not found.")
  439. image_data = volume_node.GetImageData()
  440. matrix = vtk.vtkMatrix4x4()
  441. volume_node.GetIJKToRASMatrix(matrix)
  442. dimensions = image_data.GetDimensions()
  443. #detected_regions = []
  444. if yesCbct: #je cbct ali ct?
  445. valid_x_min, valid_x_max = 0, dimensions[0] - 1
  446. valid_y_min, valid_y_max = 0, dimensions[1] - 1
  447. valid_z_min, valid_z_max = 0, dimensions[2] - 1
  448. else:
  449. valid_x_min, valid_x_max = max(x_min, 0), min(x_max, dimensions[0] - 1)
  450. valid_y_min, valid_y_max = max(y_min, 0), min(y_max, dimensions[1] - 1)
  451. valid_z_min, valid_z_max = max(z_min, 0), min(z_max, dimensions[2] - 1)
  452. visited = set()
  453. def grow_region(x, y, z):
  454. if (x, y, z) in visited:
  455. return None
  456. voxel_value = image_data.GetScalarComponentAsDouble(x, y, z, 0)
  457. if voxel_value < intensity_threshold:
  458. return None
  459. region = region_growing(image_data, (x, y, z), intensity_threshold, max_distance=max_distance)
  460. if region:
  461. for point in region:
  462. visited.add(tuple(point))
  463. return region
  464. return None
  465. regions = []
  466. for z in range(valid_z_min, valid_z_max + 1):
  467. for y in range(valid_y_min, valid_y_max + 1):
  468. for x in range(valid_x_min, valid_x_max + 1):
  469. region = grow_region(x, y, z)
  470. if region:
  471. regions.append(region)
  472. # Collect centroids using intensity-weighted average
  473. centroids = []
  474. for region in regions:
  475. points = np.array([matrix.MultiplyPoint([*point, 1])[:3] for point in region])
  476. intensities = np.array([image_data.GetScalarComponentAsDouble(*point, 0) for point in region])
  477. if intensities.sum() > 0:
  478. weighted_centroid = np.average(points, axis=0, weights=intensities)
  479. max_intensity = intensities.max()
  480. centroids.append((np.round(weighted_centroid, 2), max_intensity))
  481. unique_centroids = []
  482. for centroid, intensity in centroids:
  483. if not any(np.linalg.norm(centroid - existing_centroid) < centroid_merge_threshold for existing_centroid, _ in unique_centroids):
  484. unique_centroids.append((centroid, intensity))
  485. if create_marker:
  486. markups_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"Markers_{volume_name}")
  487. for centroid, intensity in unique_centroids:
  488. markups_node.AddControlPoint(*centroid)
  489. markups_node.SetDisplayVisibility(False)
  490. #print(f"Detected Centroid (RAS): {centroid}, Max Intensity: {intensity}")
  491. return unique_centroids
  492. def find_table_top_z(ct_volume_name, writefilecheck, makemarkerscheck, yesCbct):
  493. """
  494. Najde višino zgornjega roba mize v CT/CBCT volumnu in po želji doda marker v sceno.
  495. Args:
  496. ct_volume_name (str): Ime volumna.
  497. writefilecheck (bool): Ali naj se rezultat shrani v .csv.
  498. makemarkerscheck (bool): Ali naj se doda marker v 3D Slicer.
  499. yesCbct (bool): True, če je CBCT; False, če je CT.
  500. Returns:
  501. (float, int): Z komponenta v RAS prostoru, in Y indeks v slicerjevem volumnu.
  502. """
  503. # --- Pridobi volume node ---
  504. ct_volume_node = find_volume_node_by_partial_name(ct_volume_name)
  505. np_array = slicer.util.arrayFromVolume(ct_volume_node) # (Z, Y, X)
  506. ijkToRasMatrix = vtk.vtkMatrix4x4()
  507. ct_volume_node.GetIJKToRASMatrix(ijkToRasMatrix)
  508. # --- Določimo lokacijo stolpca ---
  509. z_index = np_array.shape[0] // 2 # srednji slice
  510. y_size = np_array.shape[1]
  511. # x_index = int(np_array.shape[2] * 0.15)
  512. # x_index = max(0, min(x_index, np_array.shape[2] - 1))
  513. # --- Izračun spodnje tretjine (spodnji del slike) ---
  514. y_start = int(y_size * 2 / 3)
  515. slice_data = np_array[z_index, :, :] # (Y, X)
  516. y_end = y_size # Do dna slike
  517. #column_values = slice_data[y_start:y_end, x_index] # (Y)
  518. # --- Parametri za rob ---
  519. threshold_high = -300 if yesCbct else -100
  520. threshold_low = -700 if yesCbct else -350
  521. min_jump = 100 if yesCbct else 100
  522. window_size = 4 # število voxelov nad/pod
  523. #previous_value = column_values[-1]
  524. table_top_y = None
  525. # --- Več stolpcev okoli x_index ---
  526. x_center = np_array.shape[2] // 2
  527. x_offset = 30 # 30 levo od sredine
  528. x_index_base = max(0, x_center - x_offset)
  529. candidate_y_values = []
  530. search_range = range(-5, 6) # od -5 do +5 stolpcev
  531. for dx in search_range:
  532. x_index = x_index_base + dx
  533. if x_index < 0 or x_index >= np_array.shape[2]:
  534. continue
  535. column_values = slice_data[y_start:y_end, x_index]
  536. for i in range(window_size, len(column_values) - window_size):
  537. curr = column_values[i]
  538. above_avg = np.mean(column_values[i - window_size:i])
  539. below_avg = np.mean(column_values[i + 1:i + 1 + window_size])
  540. if (threshold_low < curr < threshold_high
  541. and (above_avg - below_avg) > min_jump
  542. and below_avg < -400
  543. and above_avg > -300):
  544. y_found = y_start + i
  545. candidate_y_values.append(y_found)
  546. break # samo prvi zadetek v stolpcu
  547. if candidate_y_values:
  548. most_common_y, _ = Counter(candidate_y_values).most_common(1)[0]
  549. table_top_y = most_common_y
  550. print(f"✅ Rob mize (najpogostejši Y): {table_top_y}, pojavitev: {candidate_y_values.count(table_top_y)}/11")
  551. """ # --- Poišči skok navzdol pod prag (od spodaj navzgor) ---
  552. for i in range(len(column_values) - 2, -1, -1): # od spodaj proti vrhu
  553. intensity = column_values[i]
  554. if (intensity - previous_value) > min_jump and intensity < thresholdhigh and intensity > thresholdlow:
  555. table_top_y = y_start + i - 1
  556. print(f"✅ Rob mize najden pri Y = {table_top_y}, intenziteta = {intensity}")
  557. print("Column values (partial):", column_values.tolist())
  558. break
  559. previous_value = intensity """
  560. if table_top_y is None:
  561. print(f"⚠️ Rob mize ni bil najden (X = {x_index})")
  562. print("Column values (partial):", column_values.tolist())
  563. return None
  564. # --- Pretvorba v RAS koordinato ---
  565. table_ijk = [x_index, table_top_y, z_index]
  566. table_ras = np.array(ijkToRasMatrix.MultiplyPoint([*table_ijk, 1]))[:3]
  567. # --- Marker ---
  568. if makemarkerscheck:
  569. table_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"VišinaMize_{ct_volume_name}")
  570. table_node.AddControlPoint(table_ras)
  571. table_node.SetDisplayVisibility(False)
  572. # --- Shrani v CSV ---
  573. if writefilecheck:
  574. height_file = os.path.join(os.path.dirname(__file__), "heightdata.csv")
  575. with open(height_file, mode='a', newline='') as file:
  576. writer = csv.writer(file)
  577. modality = "CBCT" if yesCbct else "CT"
  578. writer.writerow([modality, ct_volume_name, f"Upper table edge at Z = {table_ras[1]:.2f} mm"])
  579. return table_ras[1], table_top_y
  580. def align_cbct_to_ct(volumeNode, scan_type, offset, CT_offset=None, CT_spacing=None):
  581. """
  582. Aligns CBCT volume to CT volume based on height offset.
  583. Args:
  584. volumeNode (vtkMRMLScalarVolumeNode): The volume node to be aligned.
  585. scan_type (str): The type of scan ("CT" or "CBCT").
  586. offset (float): The height offset of the current volume from the center in mm.
  587. CT_offset (float, optional): The height offset of the CT volume from the center. Required for CBCT alignment.
  588. CT_spacing (float, optional): The voxel spacing of the CT volume in mm (for scaling the offset).
  589. Returns:
  590. float: The alignment offset applied to the CBCT volume (if applicable).
  591. """
  592. if scan_type == "CT":
  593. CT_offset = offset
  594. CT_spacing = volumeNode.GetSpacing()[1]
  595. #print(f"CT offset set to: {CT_offset}, CT spacing: {CT_spacing} mm/voxel")
  596. return CT_offset, CT_spacing
  597. else:
  598. if CT_offset is None or CT_spacing is None:
  599. raise ValueError("CT_offset and CT_spacing must be provided to align CBCT to CT.")
  600. CBCT_offset = offset
  601. # Razlika v mm brez skaliranja na CBCT_spacing
  602. alignment_offset_mm = CT_offset
  603. #print(f"CT offset: {CT_offset}, CBCT offset: {CBCT_offset}")
  604. #print(f"CT spacing: {CT_spacing} mm/voxel, CBCT spacing: {volumeNode.GetSpacing()[1]} mm/voxel")
  605. #print(f"Aligning CBCT with CT. Offset in mm: {alignment_offset_mm}")
  606. # Uporabi transformacijo
  607. transform = vtk.vtkTransform()
  608. transform.Translate(0, alignment_offset_mm, 0)
  609. transformNode = slicer.vtkMRMLTransformNode()
  610. slicer.mrmlScene.AddNode(transformNode)
  611. transformNode.SetAndObserveTransformToParent(transform)
  612. volumeNode.SetAndObserveTransformNodeID(transformNode.GetID())
  613. slicer.vtkSlicerTransformLogic().hardenTransform(volumeNode)
  614. slicer.mrmlScene.RemoveNode(transformNode)
  615. # Poskusi najti ustrezen marker in ga premakniti
  616. marker_name = f"VišinaMize_{volumeNode.GetName()}"
  617. # Robustno iskanje markerja po imenu
  618. table_node = None
  619. for node in slicer.util.getNodesByClass("vtkMRMLMarkupsFiducialNode"):
  620. if node.GetName() == marker_name:
  621. table_node = node
  622. break
  623. if table_node is not None:
  624. current_point = [0, 0, 0]
  625. table_node.GetNthControlPointPosition(0, current_point)
  626. moved_point = [
  627. current_point[0],
  628. current_point[1] + alignment_offset_mm,
  629. current_point[2]
  630. ]
  631. table_node.SetNthControlPointPosition(0, *moved_point)
  632. return alignment_offset_mm
  633. def print_orientation(volume_name):
  634. node = find_volume_node_by_partial_name(volume_name)
  635. matrix = vtk.vtkMatrix4x4()
  636. node.GetIJKToRASMatrix(matrix)
  637. print(f"{volume_name} IJK→RAS:")
  638. for i in range(3):
  639. print([matrix.GetElement(i, j) for j in range(3)])
  640. def prealign_by_centroid(cbct_points, ct_points):
  641. """
  642. Predporavna CBCT markerje na CT markerje glede na centrične točke.
  643. Args:
  644. cbct_points: List ali ndarray točk iz CBCT.
  645. ct_points: List ali ndarray točk iz CT.
  646. Returns:
  647. List: CBCT točke premaknjene tako, da so centrične točke usklajene.
  648. """
  649. cbct_points = np.array(cbct_points)
  650. ct_points = np.array(ct_points)
  651. cbct_centroid = np.mean(cbct_points, axis=0)
  652. ct_centroid = np.mean(ct_points, axis=0)
  653. translation_vector = ct_centroid - cbct_centroid
  654. aligned_cbct = cbct_points + translation_vector
  655. return aligned_cbct, translation_vector
  656. def choose_best_translation(cbct_points, ct_points, rotation_matrix):
  657. """
  658. Izbere boljšo translacijo: centroidno ali povprečno po rotaciji (retranslation).
  659. Args:
  660. cbct_points (array-like): Točke iz CBCT (še ne rotirane).
  661. ct_points (array-like): Ciljne CT točke.
  662. rotation_matrix (ndarray): Rotacijska matrika.
  663. Returns:
  664. tuple: (best_translation_vector, transformed_cbct_points, used_method)
  665. """
  666. cbct_points = np.array(cbct_points)
  667. ct_points = np.array(ct_points)
  668. # 1. Rotiraj CBCT točke
  669. rotated_cbct = np.dot(cbct_points, rotation_matrix.T)
  670. # 2. Centroid translacija
  671. centroid_moving = np.mean(cbct_points, axis=0)
  672. centroid_fixed = np.mean(ct_points, axis=0)
  673. translation_centroid = centroid_fixed - np.dot(centroid_moving, rotation_matrix)
  674. transformed_centroid = rotated_cbct + translation_centroid
  675. error_centroid = np.mean(np.linalg.norm(transformed_centroid - ct_points, axis=1))
  676. # 3. Retranslacija (srednja razlika)
  677. translation_recomputed = np.mean(ct_points - rotated_cbct, axis=0)
  678. transformed_recomputed = rotated_cbct + translation_recomputed
  679. error_recomputed = np.mean(np.linalg.norm(transformed_recomputed - ct_points, axis=1))
  680. # 4. Izberi boljšo
  681. if error_recomputed < error_centroid:
  682. #print(f"✅ Using retranslation (error: {error_recomputed:.2f} mm)")
  683. return translation_recomputed, transformed_recomputed, "retranslation"
  684. else:
  685. #print(f"✅ Using centroid-based translation (error: {error_centroid:.2f} mm)")
  686. return translation_centroid, transformed_centroid, "centroid"
  687. def rescale_points_to_match_spacing(points, source_spacing, target_spacing):
  688. scale_factors = np.array(target_spacing) / np.array(source_spacing)
  689. return np.array(points) * scale_factors
  690. def visualize_point_matches_in_slicer(cbct_points, ct_points, study_name="MatchVisualization"):
  691. assert len(cbct_points) == len(ct_points), "Mora biti enako število točk!"
  692. # Ustvari markups za CBCT
  693. cbct_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"{study_name}_CBCT")
  694. cbct_node.GetDisplayNode().SetSelectedColor(0, 0, 1) # modra
  695. # Ustvari markups za CT
  696. ct_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"{study_name}_CT")
  697. ct_node.GetDisplayNode().SetSelectedColor(1, 0, 0) # rdeča
  698. # Dodaj točke
  699. for i, (cbct, ct) in enumerate(zip(cbct_points, ct_points)):
  700. cbct_node.AddControlPoint(*cbct, f"CBCT_{i}")
  701. ct_node.AddControlPoint(*ct, f"CT_{i}")
  702. # Ustvari model z linijami med pari
  703. points = vtk.vtkPoints()
  704. lines = vtk.vtkCellArray()
  705. for i, (p1, p2) in enumerate(zip(cbct_points, ct_points)):
  706. id1 = points.InsertNextPoint(p1)
  707. id2 = points.InsertNextPoint(p2)
  708. line = vtk.vtkLine()
  709. line.GetPointIds().SetId(0, id1)
  710. line.GetPointIds().SetId(1, id2)
  711. lines.InsertNextCell(line)
  712. polyData = vtk.vtkPolyData()
  713. polyData.SetPoints(points)
  714. polyData.SetLines(lines)
  715. # Model node
  716. modelNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode", f"{study_name}_Connections")
  717. modelNode.SetAndObservePolyData(polyData)
  718. modelDisplay = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelDisplayNode")
  719. modelDisplay.SetColor(0, 0, 0) # črna
  720. modelDisplay.SetLineWidth(2)
  721. modelDisplay.SetVisibility(True)
  722. modelNode.SetAndObserveDisplayNodeID(modelDisplay.GetID())
  723. modelNode.SetAndObservePolyData(polyData)
  724. print(f"✅ Vizualizacija dodana za {study_name} (točke + povezave)")
  725. def remove_lowest_marker(points, axis=1):
  726. """
  727. Odstrani točko, ki je najnižja po dani osi (default Y-os v RAS prostoru).
  728. """
  729. arr = np.array(points)
  730. lowest_index = np.argmin(arr[:, axis])
  731. removed = points.pop(lowest_index)
  732. print(f"⚠️ Odstranjena najnižja točka (os {axis}): {removed}")
  733. return points
  734. def update_timing_csv(timing_data, study_name):
  735. file_path = os.path.join(os.path.dirname(__file__), "timing_summary.csv")
  736. file_exists = os.path.isfile(file_path)
  737. with open(file_path, mode='a', newline='') as csvfile:
  738. fieldnames = ["Study", "IO", "Fixing", "Table", "Scaling", "CentroidAlign", "Rotation", "Translation", "Transform", "FileSave", "Total"]
  739. writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
  740. if not file_exists:
  741. writer.writeheader()
  742. row = {"Study": study_name}
  743. row.update(timing_data)
  744. writer.writerow(row)
  745. def find_volume_node_by_partial_name(partial_name):
  746. for node in slicer.util.getNodesByClass("vtkMRMLScalarVolumeNode"):
  747. if partial_name in node.GetName():
  748. return node
  749. raise RuntimeError(f"❌ Volume with name containing '{partial_name}' not found.")
  750. def convert_rtstruct_to_segmentation_nodes(ct_volume_node):
  751. shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
  752. segmentation_nodes = []
  753. # ✅ Najdi vse SegmentationNode-e, ki imajo segmente
  754. for seg_node in slicer.util.getNodesByClass("vtkMRMLSegmentationNode"):
  755. num_segments = seg_node.GetSegmentation().GetNumberOfSegments()
  756. if num_segments == 0:
  757. continue
  758. # Če še nima referenceVolume, jo nastavimo
  759. if not seg_node.GetNodeReferenceID("referenceVolume"):
  760. seg_node.SetReferenceImageGeometryParameterFromVolumeNode(ct_volume_node)
  761. seg_node.SetNodeReferenceID("referenceVolume", ct_volume_node.GetID())
  762. print(f"🔗 Nastavljena referenca na CT za: {seg_node.GetName()}")
  763. segmentation_nodes.append(seg_node)
  764. print(f"📎 Najdena segmentacija z vsebino: {seg_node.GetName()}, segmentov: {num_segments}")
  765. if segmentation_nodes:
  766. return segmentation_nodes
  767. for item_id in range(shNode.GetNumberOfItems()):
  768. modality = shNode.GetItemAttribute(item_id, "DICOM.Modality")
  769. if modality != "RTSTRUCT":
  770. continue
  771. rtstruct_node = shNode.GetItemDataNode(item_id)
  772. if not rtstruct_node:
  773. continue
  774. print(f"📎 Najden RTSTRUCT: {rtstruct_node.GetName()}")
  775. # Ustvari nov SegmentationNode
  776. seg_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", f"Seg_{rtstruct_node.GetName()}")
  777. seg_node.SetReferenceImageGeometryParameterFromVolumeNode(ct_volume_node)
  778. seg_node.SetNodeReferenceID("referenceVolume", ct_volume_node.GetID())
  779. # Najdi child elemente v SubjectHierarchy (strukture)
  780. rtstruct_item_id = shNode.GetItemByDataNode(rtstruct_node)
  781. structure_ids = vtk.vtkIdList()
  782. shNode.GetItemChildren(rtstruct_item_id, structure_ids)
  783. segment_count = 0
  784. for i in range(structure_ids.GetNumberOfIds()):
  785. structure_item_id = structure_ids.GetId(i)
  786. name = shNode.GetItemName(structure_item_id)
  787. associated_node = shNode.GetItemDataNode(structure_item_id)
  788. if associated_node and associated_node.IsA("vtkMRMLModelNode"):
  789. print(f" ➕ Dodajam strukturo: {name}")
  790. slicer.modules.segmentations.logic().ImportModelToSegmentationNode(associated_node, seg_node)
  791. seg_node.GetSegmentation().GetSegment(seg_node.GetSegmentation().GetNumberOfSegments() - 1).SetName(name)
  792. segment_count += 1
  793. # Če ni bilo nič dodano iz SH: poskusi uvoziti modele iz scene
  794. if segment_count == 0:
  795. print("⚠️ RTSTRUCT nima struktur v SubjectHierarchy – poskus uvoza vseh modelov iz scene.")
  796. for model_node in slicer.util.getNodesByClass("vtkMRMLModelNode"):
  797. if "RTSTRUCT" in model_node.GetName().upper() or model_node.GetName().startswith("Model"):
  798. print(f" ➕ [fallback] Uvoz modela: {model_node.GetName()}")
  799. slicer.modules.segmentations.logic().ImportModelToSegmentationNode(model_node, seg_node)
  800. seg_node.GetSegmentation().GetSegment(seg_node.GetSegmentation().GetNumberOfSegments() - 1).SetName(model_node.GetName())
  801. print(f"📊 Segmentov v {seg_node.GetName()}: {seg_node.GetSegmentation().GetNumberOfSegments()}")
  802. segmentation_nodes.append(seg_node)
  803. return segmentation_nodes
  804. def apply_cumulative_transform_to_segmentation(segmentation_node, matrix):
  805. transform = vtk.vtkTransform()
  806. vtk_matrix = vtk.vtkMatrix4x4()
  807. for i in range(4):
  808. for j in range(4):
  809. vtk_matrix.SetElement(i, j, matrix[i, j])
  810. transform.SetMatrix(vtk_matrix)
  811. transform_node = slicer.vtkMRMLTransformNode()
  812. slicer.mrmlScene.AddNode(transform_node)
  813. transform_node.SetAndObserveTransformToParent(transform)
  814. segmentation_node.SetAndObserveTransformNodeID(transform_node.GetID())
  815. slicer.vtkSlicerTransformLogic().hardenTransform(segmentation_node)
  816. slicer.mrmlScene.RemoveNode(transform_node)
  817. def convert_all_models_to_segmentation(reference_volume_name: str, prefix: str = "Imported_"):
  818. """
  819. Pretvori vse modele (ModelNode) v sceni v enoten vtkMRMLSegmentationNode.
  820. 📥 VHODI:
  821. ----------
  822. reference_volume_name : str
  823. Ime obstoječega CT volumna (npr. "CT_1"), ki določa geometrijo za segmentacijo.
  824. Ta volumen mora biti že naložen v sceni.
  825. prefix : str
  826. Predpona za ime novega segmentation noda (npr. "Imported_").
  827. Ime novega noda bo nekaj kot: "Imported_Segmentation".
  828. 📤 IZHOD:
  829. ----------
  830. segmentation_node : vtkMRMLSegmentationNode
  831. Nov nod, ki vsebuje en segment za vsak najden model v sceni.
  832. Ta segmentacijski nod je pripravljen za transformacijo in DICOM export (vsebuje BinaryLabelmap).
  833. """
  834. import slicer
  835. # Pridobi referenčni volumen (CT)
  836. reference_volume = slicer.util.getNode(reference_volume_name)
  837. # Ustvari nov segmentacijski nod
  838. segmentation_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", prefix + "Segmentation")
  839. segmentation_node.SetReferenceImageGeometryParameterFromVolumeNode(reference_volume)
  840. segmentation_node.SetNodeReferenceID("referenceVolume", reference_volume.GetID())
  841. # Najdi vse modele
  842. model_nodes = slicer.util.getNodesByClass("vtkMRMLModelNode")
  843. #print(f"📦 Najdenih modelov: {len(model_nodes)}")
  844. for model_node in model_nodes:
  845. name = model_node.GetName()
  846. #print(f"🔍 Model: {name}")
  847. skip = (
  848. name.lower().startswith("segmentation")
  849. or name.lower().startswith("surface")
  850. or name.lower() in ["red volume slice", "green volume slice", "yellow volume slice"]
  851. or "rtstruct" in name.lower()
  852. )
  853. if skip:
  854. continue
  855. # Uvozi model kot segment
  856. success = slicer.modules.segmentations.logic().ImportModelToSegmentationNode(model_node, segmentation_node)
  857. if not success:
  858. #print(f"✅ Model '{name}' uvožen kot segment.")
  859. print(f"❌ Napaka pri uvozu modela: {name}")
  860. # Ustvari BinaryLabelmap reprezentacijo (nujno za DICOM export)
  861. created = segmentation_node.GetSegmentation().CreateRepresentation("BinaryLabelmap")
  862. if created:
  863. print("✅ BinaryLabelmap reprezentacija uspešno ustvarjena.")
  864. segmentation_node.GetSegmentation().SetMasterRepresentationName("BinaryLabelmap")
  865. #else:
  866. #print("❌ Pretvorba v BinaryLabelmap ni uspela.")
  867. return segmentation_node
  868. def apply_cumulative_transform_to_volume(volume_node, matrix):
  869. transform = vtk.vtkTransform()
  870. vtk_matrix = vtk.vtkMatrix4x4()
  871. for i in range(4):
  872. for j in range(4):
  873. vtk_matrix.SetElement(i, j, matrix[i, j])
  874. transform.SetMatrix(vtk_matrix)
  875. transform_node = slicer.vtkMRMLTransformNode()
  876. slicer.mrmlScene.AddNode(transform_node)
  877. transform_node.SetAndObserveTransformToParent(transform)
  878. volume_node.SetAndObserveTransformNodeID(transform_node.GetID())
  879. slicer.vtkSlicerTransformLogic().hardenTransform(volume_node)
  880. slicer.mrmlScene.RemoveNode(transform_node)
  881. def triangle_similarity(p1, p2):
  882. """
  883. Primerja dva trikotnika (vsak definiran s 3 točkami v 3D) na podlagi:
  884. - razmerij dolžin
  885. - razlik med koti
  886. :param p1: seznam treh točk (npr. ct_points)
  887. :param p2: seznam treh točk (npr. cbct_points)
  888. :return: dict z razliko dolžin, razliko kotov, povprečno napako
  889. """
  890. def side_lengths(pts):
  891. a = np.linalg.norm(pts[1] - pts[0])
  892. b = np.linalg.norm(pts[2] - pts[1])
  893. c = np.linalg.norm(pts[0] - pts[2])
  894. return np.array([a, b, c])
  895. def angles(pts):
  896. # uporabimo kosinusni izrek
  897. a, b, c = side_lengths(pts)
  898. angle_A = np.arccos((b**2 + c**2 - a**2) / (2*b*c))
  899. angle_B = np.arccos((a**2 + c**2 - b**2) / (2*a*c))
  900. angle_C = np.pi - angle_A - angle_B
  901. return np.degrees([angle_A, angle_B, angle_C])
  902. l1 = side_lengths(p1)
  903. l2 = side_lengths(p2)
  904. a1 = angles(p1)
  905. a2 = angles(p2)
  906. length_diff = np.abs(l1 - l2)
  907. angle_diff = np.abs(a1 - a2)
  908. return {
  909. "side_length_diff_mm": length_diff,
  910. "angle_diff_deg": angle_diff,
  911. "mean_length_error_mm": np.mean(length_diff),
  912. "mean_angle_error_deg": np.mean(angle_diff),
  913. "TSR": 1 / (1 + np.mean(length_diff) + np.mean(angle_diff) / 10)
  914. }
  915. def save_triangle_visualization(ct_pts, cbct_pts, outpath):
  916. fig = plt.figure(figsize=(10, 8))
  917. ax = fig.add_subplot(111, projection='3d')
  918. ct_closed = np.vstack([ct_pts, ct_pts[0]])
  919. cbct_closed = np.vstack([cbct_pts, cbct_pts[0]])
  920. ax.plot(ct_closed[:, 0], ct_closed[:, 1], ct_closed[:, 2], 'b-', label='CT trikotnik')
  921. ax.scatter(ct_pts[:, 0], ct_pts[:, 1], ct_pts[:, 2], c='blue')
  922. ax.plot(cbct_closed[:, 0], cbct_closed[:, 1], cbct_closed[:, 2], 'r--', label='CBCT trikotnik')
  923. ax.scatter(cbct_pts[:, 0], cbct_pts[:, 1], cbct_pts[:, 2], c='red')
  924. ct_centroid = np.mean(ct_pts, axis=0)
  925. cbct_centroid = np.mean(cbct_pts, axis=0)
  926. ax.scatter(*ct_centroid, c='blue', marker='x', s=60)
  927. ax.scatter(*cbct_centroid, c='red', marker='x', s=60)
  928. ax.set_title("Trikotnik markerjev: CT (modro) vs CBCT (rdeče)")
  929. ax.set_xlabel('X (mm)')
  930. ax.set_ylabel('Y (mm)')
  931. ax.set_zlabel('Z (mm)')
  932. ax.legend()
  933. ax.view_init(elev=20, azim=30)
  934. plt.tight_layout()
  935. try:
  936. fig.savefig(outpath)
  937. print(f"🖼 Triangle visualization saved to: {outpath}")
  938. except Exception as e:
  939. print(f"❌ Failed to save triangle visualization: {e}")
  940. finally:
  941. plt.close(fig)
  942. # Globalni seznami za končno statistiko
  943. prostate_size_est = []
  944. ctcbct_distance = []
  945. table_z_values = {}
  946. # Pridobimo SubjectHierarchyNode
  947. shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
  948. studyItems = vtk.vtkIdList()
  949. shNode.GetItemChildren(shNode.GetSceneItemID(), studyItems)
  950. #slicer.util.delayDisplay(f"[DEBUG] Starting the loops", 1000)
  951. for i in range(studyItems.GetNumberOfIds()):
  952. study_start_time = time.time()
  953. start_io = time.time()
  954. studyItem = studyItems.GetId(i)
  955. studyName = shNode.GetItemName(studyItem)
  956. print(f"\nProcessing study: {studyName}")
  957. # **LOKALNI** seznami, resetirajo se pri vsakem study-ju
  958. cbct_list = []
  959. ct_list = []
  960. volume_points_dict = {}
  961. CT_offset = 0
  962. # Get child items of the study item
  963. volumeItems = vtk.vtkIdList()
  964. shNode.GetItemChildren(studyItem, volumeItems)
  965. # Iteracija čez vse volumne v posameznem studyju
  966. for j in range(volumeItems.GetNumberOfIds()):
  967. intermediateItem = volumeItems.GetId(j)
  968. finalVolumeItems = vtk.vtkIdList()
  969. shNode.GetItemChildren(intermediateItem, finalVolumeItems) # Išči globlje!
  970. for k in range(finalVolumeItems.GetNumberOfIds()):
  971. volumeItem = finalVolumeItems.GetId(k)
  972. volumeNode = shNode.GetItemDataNode(volumeItem)
  973. try:
  974. dicomUIDs = volumeNode.GetAttribute("DICOM.instanceUIDs")
  975. except AttributeError:
  976. print(f"⚠️ Volume node '{volumeNode}' not found or no attribute 'DICOM.instanceUIDs'. Skip.")
  977. dicomUIDs = None
  978. continue # Preskoči, če ni veljaven volume
  979. if not dicomUIDs:
  980. print("❌ This is an NRRD volume!")
  981. continue # Preskoči, če ni DICOM volume
  982. volumeName = volumeNode.GetName()
  983. imageItem = shNode.GetItemByDataNode(volumeNode)
  984. modality = shNode.GetItemAttribute(imageItem, "DICOM.Modality") #deluje!
  985. #dimensions = volumeNode.GetImageData().GetDimensions()
  986. #spacing = volumeNode.GetSpacing()
  987. #print(f"Volume {volumeNode.GetName()} - Dimenzije: {dimensions}, Spacing: {spacing}")
  988. if modality != "CT":
  989. print("Not a CT")
  990. continue # Preskoči, če ni CT
  991. # Preveri, če volume obstaja v sceni
  992. if not slicer.mrmlScene.IsNodePresent(volumeNode):
  993. print(f"Volume {volumeName} not present in the scene.")
  994. continue
  995. # Preverimo proizvajalca (DICOM metapodatki)
  996. manufacturer = shNode.GetItemAttribute(imageItem, 'DICOM.Manufacturer')
  997. #manufacturer = volumeNode.GetAttribute("DICOM.Manufacturer")
  998. #manufacturer = slicer.dicomDatabase.fileValue(uid, "0008,0070")
  999. #print(manufacturer)
  1000. # Določimo, ali gre za CBCT ali CT
  1001. if "varian" in manufacturer.lower() or "elekta" in manufacturer.lower():
  1002. cbct_list.append(volumeName)
  1003. scan_type = "CBCT"
  1004. yesCbct = True
  1005. else: # Siemens ali Philips
  1006. ct_list.append(volumeName)
  1007. scan_type = "CT"
  1008. yesCbct = False
  1009. if volumeNode and volumeNode.IsA("vtkMRMLScalarVolumeNode"):
  1010. print(f"✔️ {scan_type} {volumeNode.GetName()} (ID: {volumeItem})")
  1011. if not volumeNode or not volumeNode.IsA("vtkMRMLScalarVolumeNode"):
  1012. print("Can't find volumeNode")
  1013. #continue # Preskoči, če ni veljaven volume
  1014. # Detekcija točk v volumnu
  1015. ustvari_marker = not yesCbct # Ustvari markerje pred poravnavo na mizo
  1016. grouped_points = detect_points_region_growing(volumeName, yesCbct, ustvari_marker, intensity_threshold=3000)
  1017. if grouped_points is None or len(grouped_points) < 3:
  1018. print(f"⚠️ Volume {volumeName} doesn't have enough points for registration. Points: {len(grouped_points)}")
  1019. continue
  1020. if not yesCbct:
  1021. # loči koordinate in intenzitete
  1022. coords_only = [pt for pt, _ in grouped_points]
  1023. intensities = [intensity for _, intensity in grouped_points]
  1024. # permutiraj koordinate (npr. zaradi boljšega ujemanja)
  1025. coords_sorted = match_points(coords_only, coords_only)
  1026. # ponovno sestavi pare (točka, intenziteta)
  1027. grouped_points = list(zip(coords_sorted, intensities))
  1028. #print(f"Populating volume_points_dict with key ('{scan_type}', '{volumeName}')")
  1029. volume_points_dict[(scan_type, volumeName)] = grouped_points
  1030. #print(volume_points_dict) # Check if the key is correctly added
  1031. # Če imamo oba tipa volumna (CBCT in CT) **znotraj istega studyja**
  1032. end_io = time.time()
  1033. if cbct_list and ct_list:
  1034. fixing = fixing_end = 0
  1035. table1_time = table1end_time = 0
  1036. table2_time = table2end_time = 0
  1037. start_scaling = end_scaling = 0
  1038. start_align = end_align = 0
  1039. start_rotation = end_rotation = 0
  1040. start_translation = end_translation = 0
  1041. start_transform = end_transform = 0
  1042. study_start_time = study_end_time = 0
  1043. ct_volume_name = ct_list[0] # Uporabi prvi CT kot referenco
  1044. ct_volume_Node = find_volume_node_by_partial_name(ct_volume_name)
  1045. print(f"\nProcessing CT: {ct_volume_name}")
  1046. yesCbct = False
  1047. if(tablefind):
  1048. table1_time = time.time()
  1049. makemarkerscheck = True
  1050. result = find_table_top_z(ct_volume_name, writefilecheck, makemarkerscheck, yesCbct)
  1051. if result is not None:
  1052. mm_offset, pixel_offset = result
  1053. ct_table_found = True
  1054. #print(f"✔️ Poravnava z višino mize: ΔY = {mm_offset:.2f} mm")
  1055. # Dodaj ΔY k translaciji ali transformaciji po potrebi
  1056. else:
  1057. print("⚠️ Table top not found – continue without correction on Y axis.")
  1058. mm_offset = 0.0 # ali None, če želiš eksplicitno ignorirati
  1059. ct_table_found = False
  1060. CT_offset, CT_spacing = align_cbct_to_ct(ct_volume_Node, "CT", mm_offset)
  1061. table1end_time = time.time()
  1062. ct_points = [centroid for centroid, _ in volume_points_dict[("CT", ct_volume_name)]]
  1063. print(f"CT points: {ct_points}")
  1064. if len(ct_points) < 3:
  1065. print(f"CT volume {ct_volume_name} doesn't have enough points for registration. Points: {len(ct_points)}")
  1066. continue
  1067. else:
  1068. # if len(ct_points) == 4:
  1069. # ct_points = remove_lowest_marker(ct_points) #odstrani marker v riti, če obstaja
  1070. for cbct_volume_name in cbct_list:
  1071. print(f"\nProcessing CBCT Volume: {cbct_volume_name}")
  1072. yesCbct = True
  1073. scan_type = "CBCT" #redundant but here we are
  1074. cbct_volume_node = find_volume_node_by_partial_name(cbct_volume_name)
  1075. key = (studyName, cbct_volume_name)
  1076. if key not in cumulative_matrices:
  1077. cumulative_matrices[key] = np.eye(4)
  1078. fixing = time.time()
  1079. if(tablefind):
  1080. table2_time = time.time()
  1081. makemarkerscheck = False # Ustvari CBCT miza markerje pred poravnavo
  1082. if(ct_table_found):
  1083. result = find_table_top_z(cbct_volume_name, writefilecheck, makemarkerscheck, yesCbct) #!!!!!!!!!!!!!???????????? ct_volume_name
  1084. if result is not None:
  1085. mm_offset, pixel_offset = result
  1086. print(f"CT offset: {CT_offset}, cbct offset: {mm_offset}")
  1087. #ne rabimo več CT_offset, le še cbct offset.
  1088. skupni_offset = align_cbct_to_ct(cbct_volume_node, scan_type, mm_offset, CT_offset, CT_spacing) #poravna CBCT in sporoči skupni offset
  1089. table_shift_matrix = np.eye(4)
  1090. table_shift_matrix[1, 3] = skupni_offset # Premik po Y
  1091. # Pomnoži obstoječo kumulativno matriko
  1092. key = (studyName, cbct_volume_name)
  1093. if key not in cumulative_matrices:
  1094. cumulative_matrices[key] = np.eye(4)
  1095. cumulative_matrices[key] = np.dot(cumulative_matrices[key], table_shift_matrix)
  1096. else:
  1097. print("⚠️ Table top not found – continue without correction on Y axis.")
  1098. mm_offset = 0.0
  1099. table2end_time = time.time()
  1100. cbct_points = [centroid for centroid, _ in volume_points_dict[("CBCT", cbct_volume_name)]] #zastareli podatki
  1101. cbct_points_array = np.array(cbct_points) # Pretvorba v numpy array
  1102. if len(cbct_points) != len(ct_points):
  1103. if(len(cbct_points) + 1 == len(ct_points)):
  1104. ct_points = remove_lowest_marker(ct_points) #odstrani marker v riti, če obstaja
  1105. else:
  1106. print(f"Neujemajoče število točk! CBCT: {len(cbct_points)}, CT: {len(ct_points)}")
  1107. continue
  1108. # print_orientation(ct_volume_name)
  1109. # print_orientation(cbct_volume_name)
  1110. #for i, (cb, ct) in enumerate(zip(cbct_points, ct_points)):
  1111. # print(f"Pair {i}: CBCT {cb}, CT {ct}, diff: {np.linalg.norm(cb - ct):.2f}")
  1112. ustvari_marker = False # Ustvari markerje
  1113. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
  1114. #cbct_points = detect_points_region_growing(cbct_volume_name, yesCbct, intensity_threshold=3000)
  1115. #cbct_points = [centroid for centroid, _ in volume_points_dict[("CBCT", cbct_volume_name)]] #zastareli podatki
  1116. if len(cbct_points) < 3:
  1117. print(f"CBCT Volume '{cbct_volume_name}' doesn't have enough points for registration. Points: {len(cbct_points)}")
  1118. continue
  1119. cbct_spacing = cbct_volume_node.GetSpacing()
  1120. ct_spacing = ct_volume_Node.GetSpacing()
  1121. cbct_points = rescale_points_to_match_spacing(cbct_points, cbct_spacing, ct_spacing)
  1122. #Sortiramo točke po X/Y/Z da se izognemo težavam pri poravnavi
  1123. cbct_points = match_points(cbct_points, ct_points)
  1124. fixing_end = time.time()
  1125. #visualize_point_matches_in_slicer(cbct_points, ct_points, studyName) #poveže pare markerjev
  1126. if writefilecheck:
  1127. # Shranjevanje razdalj
  1128. distances_ct_cbct = []
  1129. distances_internal = {"A-B": [], "B-C": [], "C-A": []}
  1130. cbct_volume_node = find_volume_node_by_partial_name(cbct_volume_name)
  1131. # Sortiramo točke po Z-koordinati (ali X/Y, če raje uporabljaš drugo os)
  1132. cbct_points_sorted = cbct_points_array[np.argsort(cbct_points_array[:, 2])]
  1133. # Razdalje med CT in CBCT (SORTIRANE točke!)
  1134. if cbct_points_sorted.shape[0] != len(ct_points):
  1135. print(f"⚠️ Število točk CBCT ({cbct_points_sorted.shape[0]}) != CT ({len(ct_points)}), preskakujem izračun.")
  1136. else:
  1137. d_ct_cbct = np.linalg.norm(cbct_points_sorted - ct_points, axis=1)
  1138. distances_ct_cbct.append(d_ct_cbct)
  1139. # Razdalje med točkami znotraj SORTIRANIH cbct_points
  1140. d_ab = np.linalg.norm(cbct_points_sorted[0] - cbct_points_sorted[1])
  1141. d_bc = np.linalg.norm(cbct_points_sorted[1] - cbct_points_sorted[2])
  1142. d_ca = np.linalg.norm(cbct_points_sorted[2] - cbct_points_sorted[0])
  1143. # Sortiramo razdalje po velikosti, da so vedno v enakem vrstnem redu
  1144. sorted_distances = sorted([d_ab, d_bc, d_ca])
  1145. distances_internal["A-B"].append(sorted_distances[0])
  1146. distances_internal["B-C"].append(sorted_distances[1])
  1147. distances_internal["C-A"].append(sorted_distances[2])
  1148. # Dodamo ime študije za v statistiko
  1149. studyName = shNode.GetItemName(studyItem)
  1150. # **Shrani razdalje v globalne sezname**
  1151. prostate_size_est.append({"Study": studyName, "Distances": sorted_distances})
  1152. ctcbct_distance.append({"Study": studyName, "Distances": list(distances_ct_cbct[-1])}) # Pretvorimo v seznam
  1153. # Izberi metodo glede na uporabnikov izbor
  1154. chosen_rotation_matrix = np.eye(3)
  1155. chosen_translation_vector = np.zeros(3)
  1156. #print("Markerji pred transformacijo:", cbct_points, ct_points)
  1157. start_scaling = time.time()
  1158. scaling_factors = None
  1159. if applyScaling:
  1160. scaling_factors = compute_scaling_stddev(cbct_points, ct_points)
  1161. #print("Scaling factors: ", scaling_factors)
  1162. cbct_points = compute_scaling(cbct_points, scaling_factors)
  1163. end_scaling = time.time()
  1164. start_align = time.time()
  1165. initial_error = np.mean(np.linalg.norm(np.array(cbct_points) - np.array(ct_points), axis=1))
  1166. if initial_error > 30:
  1167. #print("⚠️ Initial distance too large, applying centroid prealignment.")
  1168. cbct_points, transvector = prealign_by_centroid(cbct_points, ct_points)
  1169. else:
  1170. transvector = np.zeros(3)
  1171. end_align = time.time()
  1172. start_rotation = time.time()
  1173. if applyRotation:
  1174. if selectedMethod == "Kabsch":
  1175. chosen_rotation_matrix = compute_Kabsch_rotation(cbct_points, ct_points)
  1176. elif selectedMethod == "Horn":
  1177. chosen_rotation_matrix = compute_Horn_rotation(cbct_points, ct_points)
  1178. elif selectedMethod == "Iterative Closest Point (Kabsch)":
  1179. _, chosen_rotation_matrix, _ = icp_algorithm(cbct_points, ct_points)
  1180. #print("Rotation Matrix:\n", chosen_rotation_matrix)
  1181. end_rotation = time.time()
  1182. start_translation = time.time()
  1183. fine_shift = np.zeros(3) # Inicializiraj fine premike
  1184. if applyTranslation:
  1185. chosen_translation_vector, cbct_points_transformed, method_used = choose_best_translation(
  1186. cbct_points, ct_points, chosen_rotation_matrix)
  1187. # Sistematična razlika (signed shift)
  1188. rotated_cbct = np.dot(cbct_points, chosen_rotation_matrix.T)
  1189. translated_cbct = rotated_cbct + chosen_translation_vector
  1190. delta_y_list = [ct[1] - cbct[1] for ct, cbct in zip(ct_points, translated_cbct)]
  1191. mean_delta_y = np.mean(delta_y_list)
  1192. # Uporabi sistematični shift za dodatno poravnavo v y-osi
  1193. fine_shift = np.array([0.0, mean_delta_y, 0.0]) # samo Y-os
  1194. cbct_points_transformed += fine_shift
  1195. end_translation = time.time()
  1196. start_transform = time.time()
  1197. # ✅ Kombinirana transformacija
  1198. total_translation = chosen_translation_vector + fine_shift
  1199. chosen_translation_vector = total_translation
  1200. vtk_transform = create_vtk_transform(chosen_rotation_matrix, chosen_translation_vector, studyName, cbct_volume_name, scaling_factors)
  1201. combined_matrix = np.eye(4)
  1202. # 1. Rotacija
  1203. if chosen_rotation_matrix is not None:
  1204. combined_matrix[:3, :3] = chosen_rotation_matrix
  1205. # 2. Skaliranje
  1206. if scaling_factors is not None:
  1207. # Pomnoži rotacijo s skalirnim faktorjem po vsaki osi
  1208. scaled_rotation = combined_matrix[:3, :3] * scaling_factors # broadcasting vsake vrstice
  1209. combined_matrix[:3, :3] = scaled_rotation
  1210. # 3. Translacija
  1211. if chosen_translation_vector is not None:
  1212. combined_matrix[:3, 3] = chosen_translation_vector + transvector # združena translacija
  1213. #preverjanje determinante
  1214. rot_part = combined_matrix[:3, :3]
  1215. det = np.linalg.det(rot_part)
  1216. if not np.isclose(det, 1.0, atol=0.01):
  1217. print(f"⚠️ Neortogonalna rotacija! Determinanta: {det}")
  1218. cumulative_matrices[(studyName, cbct_volume_name)] = np.dot(cumulative_matrices[(studyName, cbct_volume_name)], combined_matrix)
  1219. # 🔄 Pripni transformacijo
  1220. imeTransformNoda = cbct_volume_name + " Transform"
  1221. transform_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTransformNode", imeTransformNoda)
  1222. transform_node.SetAndObserveTransformToParent(vtk_transform)
  1223. cbct_volume_node = find_volume_node_by_partial_name(cbct_volume_name)
  1224. cbct_volume_node.SetAndObserveTransformNodeID(transform_node.GetID())
  1225. # 🔨 Uporabi (ali shrani transformacijo kasneje)
  1226. slicer.vtkSlicerTransformLogic().hardenTransform(cbct_volume_node)
  1227. slicer.mrmlScene.RemoveNode(transform_node)
  1228. end_transform = time.time()
  1229. # 📍 Detekcija markerjev po transformaciji
  1230. ustvari_marker = False
  1231. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
  1232. #print("Markerji po transformaciji:\n", cbct_points, ct_points)
  1233. cbct_points = match_points(cbct_points, ct_points)
  1234. #popravek v x osi
  1235. delta_x_list = [ct[0] - cbct[0] for ct, cbct in zip(ct_points, cbct_points)]
  1236. mean_delta_x = np.mean(delta_x_list)
  1237. #popravek v y osi
  1238. delta_y_list = [ct[1] - cbct[1] for ct, cbct in zip(ct_points, cbct_points)]
  1239. mean_delta_y = np.mean(delta_y_list)
  1240. #popravek v z osi
  1241. delta_z_list = [ct[2] - cbct[2] for ct, cbct in zip(ct_points, cbct_points)]
  1242. mean_delta_z = np.mean(delta_z_list)
  1243. # Uporabi sistematični shift za dodatno poravnavo
  1244. fine_shift = np.array([mean_delta_x, mean_delta_y, mean_delta_z])
  1245. #cbct_points_transformed += fine_shift
  1246. if fine_shift is not None:
  1247. shift_matrix = np.eye(4)
  1248. shift_matrix[:3, 3] = fine_shift
  1249. cumulative_matrices[(studyName, cbct_volume_name)] = np.dot(cumulative_matrices[(studyName, cbct_volume_name)], shift_matrix)
  1250. chosen_rotation_matrix = np.eye(3) #tokrat brez rotacije
  1251. #### TEST ROTACIJA ########
  1252. angle_deg = 0
  1253. angle_rad = np.deg2rad(angle_deg)
  1254. chosen_rotation_matrix = np.array([
  1255. [np.cos(angle_rad), -np.sin(angle_rad), 0],
  1256. [np.sin(angle_rad), np.cos(angle_rad), 0],
  1257. [0, 0, 1]
  1258. ])
  1259. ###KONEC TESTA###
  1260. vtk_transform = create_vtk_transform(chosen_rotation_matrix, fine_shift, studyName, cbct_volume_name) #Tukaj se tudi izpiše transformacijska matrika
  1261. # 🔄 Pripni transformacijo
  1262. imeTransformNoda = cbct_volume_name + " Transform"
  1263. transform_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTransformNode", imeTransformNoda)
  1264. transform_node.SetAndObserveTransformToParent(vtk_transform)
  1265. cbct_volume_node = find_volume_node_by_partial_name(cbct_volume_name)
  1266. cbct_volume_node.SetAndObserveTransformNodeID(transform_node.GetID())
  1267. # 🔨 Uporabi (ali shrani transformacijo kasneje)
  1268. slicer.vtkSlicerTransformLogic().hardenTransform(cbct_volume_node)
  1269. slicer.mrmlScene.RemoveNode(transform_node)
  1270. table_shift_matrix_ct = np.eye(4)
  1271. table_shift_matrix_ct[1, 3] = CT_offset # ali skupni_offset, če je potreben simetričen premik
  1272. cumulative_matrices[(studyName, cbct_volume_name)] = np.dot(
  1273. table_shift_matrix,
  1274. cumulative_matrices[(studyName, cbct_volume_name)]
  1275. )
  1276. #ustvari_marker = True
  1277. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, applymarkers, intensity_threshold=3000)]
  1278. print(f"Fine correction shifts: ΔX={fine_shift[0]:.2f} mm, ΔY={fine_shift[1]:.2f} mm, ΔZ={fine_shift[2]:.2f} mm")
  1279. #shrani transformacijsko matriko v datoteko
  1280. save_transform_matrix(cumulative_matrices[(studyName, cbct_volume_name)], studyName, cbct_volume_name)
  1281. if save_as_dicom:
  1282. # Apply transform to CT
  1283. logging.info(f"[{studyName}] Start applying transform to CT volume.")
  1284. slicer.util.delayDisplay(f"[DEBUG] Start transform on CT", 1000)
  1285. apply_cumulative_transform_to_volume(ct_volume_Node, cumulative_matrices[(studyName, cbct_volume_name)])
  1286. # Convert RTSTRUCT to SegmentationNode if needed
  1287. logging.info(f"[{studyName}] Getting segmentation nodes from RTSTRUCT.")
  1288. #slicer.util.delayDisplay(f"[DEBUG] Start segmentation conversion", 1000)
  1289. #seg_nodes = convert_rtstruct_to_segmentation_nodes(ct_volume_Node)
  1290. seg_node = convert_all_models_to_segmentation(ct_volume_name, prefix="Imported_")
  1291. #new_seg_node = slicer.util.getNode(new_seg_name)
  1292. apply_cumulative_transform_to_segmentation(seg_node, cumulative_matrices[(studyName, cbct_volume_name)])
  1293. plugin = DicomRtImportExportPlugin.DicomRtImportExportPluginClass()
  1294. shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
  1295. ct_itemID = shNode.GetItemByDataNode(ct_volume_Node)
  1296. seg_itemID = shNode.GetItemByDataNode(seg_node)
  1297. cbct_item = shNode.GetItemByDataNode(cbct_volume_node)
  1298. cbct_date = shNode.GetItemAttribute(cbct_item, "DICOM.SeriesDate") or "unknownDate"
  1299. #cbct_date = cbct_volume_node.GetAttribute("DICOM.AcquisitionDate") or "unknownDate"
  1300. results_base = os.path.join(os.path.dirname(__file__), "Rezultati")
  1301. study_folder = studyName.replace('^', '_')
  1302. study_dir = os.path.join(results_base, study_folder)
  1303. os.makedirs(study_dir, exist_ok=True)
  1304. export_dir = os.path.join(study_dir, f"{cbct_date}_DICOM")
  1305. os.makedirs(export_dir, exist_ok=True)
  1306. exportables = []
  1307. exportables += plugin.examineForExport(ct_itemID)
  1308. exportables += plugin.examineForExport(seg_itemID)
  1309. for exp in exportables:
  1310. # Kopiraj podatke iz volumetričnega noda v exportable
  1311. if ct_volume_Node.GetAttribute("DICOM.PatientName"):
  1312. exp.setTag("0010,0010", ct_volume_Node.GetAttribute("DICOM.PatientName")) # PatientName
  1313. if ct_volume_Node.GetAttribute("DICOM.PatientID"):
  1314. exp.setTag("0010,0020", ct_volume_Node.GetAttribute("DICOM.PatientID")) # PatientID
  1315. if ct_volume_Node.GetAttribute("DICOM.StudyInstanceUID"):
  1316. exp.setTag("0020,000D", ct_volume_Node.GetAttribute("DICOM.StudyInstanceUID")) # StudyInstanceUID
  1317. exp.directory = export_dir
  1318. print(f"[{studyName}] ✅ Export successful")
  1319. plugin.export(exportables)
  1320. # 📏 Izračun napake
  1321. errors = [np.linalg.norm(cbct - ct) for cbct, ct in zip(cbct_points, ct_points)]
  1322. mean_error = np.mean(errors)
  1323. print("Total Individual errors:", errors)
  1324. print("Average error: {:.2f} mm".format(mean_error))
  1325. for i, (cbct, ct) in enumerate(zip(cbct_points, ct_points)):
  1326. diff = np.array(cbct) - np.array(ct)
  1327. print(f"Specific marker errors {i+1}: ΔX={diff[0]:.2f} mm, ΔY={diff[1]:.2f} mm, ΔZ={diff[2]:.2f} mm")
  1328. ct_pts = np.array(ct_points)
  1329. cbct_pts = np.array(cbct_points)
  1330. if len(ct_pts) == 3 and len(cbct_pts) == 3:
  1331. sim = triangle_similarity(ct_pts, cbct_pts)
  1332. print("\n📐 Triangle similarity analysis:")
  1333. print(f"Side length differences (mm): {sim['side_length_diff_mm']}")
  1334. print(f"Angle differences (deg): {sim['angle_diff_deg']}")
  1335. print(f"Mean length error: {sim['mean_length_error_mm']:.2f} mm")
  1336. print(f"Mean angle error: {sim['mean_angle_error_deg']:.2f}°")
  1337. print(f"Triangle Similarity Ratio (TSR): {sim['TSR']:.3f}")
  1338. else:
  1339. print("⚠️ Za primerjavo trikotnikov potrebne točno 3 točke.")
  1340. if writefilecheck:
  1341. errorsfile = os.path.join(study_dir, f"{cbct_date}_errors.csv")
  1342. with open(errorsfile, mode='a', newline='') as file:
  1343. writer = csv.writer(file)
  1344. # Glava za študijo
  1345. writer.writerow(["Study", studyName])
  1346. writer.writerow(["Total Individual Errors (mm)"])
  1347. for error in errors:
  1348. writer.writerow(["", f"{error:.2f}"])
  1349. writer.writerow(["Average Error (mm)", f"{mean_error:.2f}"])
  1350. # Specifične napake markerjev
  1351. writer.writerow(["Specific Marker Errors"])
  1352. writer.writerow(["Marker", "ΔX (mm)", "ΔY (mm)", "ΔZ (mm)"])
  1353. for i, (cbct, ct) in enumerate(zip(cbct_points, ct_points)):
  1354. diff = np.array(cbct) - np.array(ct)
  1355. writer.writerow([f"Marker {i+1}", f"{diff[0]:.2f}", f"{diff[1]:.2f}", f"{diff[2]:.2f}"])
  1356. if len(cbct_points) == 3 and len(ct_points) == 3:
  1357. writer.writerow([])
  1358. writer.writerow(["Triangle Similarity"])
  1359. writer.writerow(["Side Length Diff (mm)", *[f"{v:.2f}" for v in sim["side_length_diff_mm"]]])
  1360. writer.writerow(["Angle Diff (deg)", *[f"{v:.2f}" for v in sim["angle_diff_deg"]]])
  1361. writer.writerow(["Mean Length Error (mm)", f"{sim['mean_length_error_mm']:.2f}"])
  1362. writer.writerow(["Mean Angle Error (deg)", f"{sim['mean_angle_error_deg']:.2f}"])
  1363. writer.writerow(["Triangle Similarity Ratio (TSR)", f"{sim['TSR']:.3f}"])
  1364. writer.writerow([])
  1365. graphs_dir = os.path.join(results_base, study_folder)
  1366. os.makedirs(graphs_dir, exist_ok=True)
  1367. pngfile = os.path.join(graphs_dir, f"{cbct_date}_trikotnika.png")
  1368. save_triangle_visualization(np.array(ct_points), np.array(cbct_points), pngfile)
  1369. #print("Prostate size file written at ", prostate_size_file)
  1370. else:
  1371. print(f"Study {studyItem} doesn't have any appropriate CT or CBCT volumes.")
  1372. continue
  1373. study_end_time = time.time()
  1374. timing_data = {
  1375. "IO": end_io - start_io,
  1376. "Fixing": fixing_end - fixing,
  1377. "Table": ((table1end_time - table1_time)+(table2end_time - table2_time)) if tablefind else 0,
  1378. "Scaling": end_scaling - start_scaling,
  1379. "CentroidAlign": end_align - start_align,
  1380. "Rotation": end_rotation - start_rotation,
  1381. "Translation": end_translation - start_translation,
  1382. "Transform": end_transform - start_transform,
  1383. "Total": study_end_time - study_start_time}
  1384. update_timing_csv(timing_data, studyName)
  1385. print(f"Timing data for {studyName}: {timing_data}")
  1386. # Izpis globalne statistike
  1387. start_save = time.time()
  1388. if writefilecheck:
  1389. #print("Distances between CT & CBCT markers: ", ctcbct_distance)
  1390. #print("Distances between pairs of markers for each volume: ", prostate_size_est)
  1391. # Define file paths
  1392. prostate_size_file = os.path.join(os.path.dirname(__file__), "prostate_size.csv")
  1393. ctcbct_distance_file = os.path.join(os.path.dirname(__file__), "ct_cbct_distance.csv")
  1394. # Write prostate size data
  1395. with open(prostate_size_file, mode='w', newline='') as file:
  1396. writer = csv.writer(file)
  1397. writer.writerow(["Prostate Size"])
  1398. for size in prostate_size_est:
  1399. writer.writerow([size])
  1400. #print("Prostate size file written at ", prostate_size_file)
  1401. # Write CT-CBCT distance data
  1402. with open(ctcbct_distance_file, mode='w', newline='') as file:
  1403. writer = csv.writer(file)
  1404. writer.writerow(["CT-CBCT Distance"])
  1405. for distance in ctcbct_distance:
  1406. writer.writerow([distance])
  1407. #print("CT-CBCT distance file written at ", ctcbct_distance_file)
  1408. end_save = time.time()
  1409. print(f"Saving time: {end_save - start_save:.2f} seconds")
  1410. end_time = time.time()
  1411. # Calculate and print elapsed time
  1412. elapsed_time = end_time - start_time
  1413. # Convert to minutes and seconds
  1414. minutes = int(elapsed_time // 60)
  1415. seconds = elapsed_time % 60
  1416. print(f"Execution time: {minutes} minutes and {seconds:.6f} seconds")