SeekTransformModule.py 117 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377
  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. from datetime import datetime
  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. import matplotlib.pyplot as plt
  24. from mpl_toolkits.mplot3d import Axes3D
  25. #exec(open("C:/Users/lkomar/Documents/Prostata/FirstTryRegister.py").read())
  26. cumulative_matrices = {}
  27. class SeekTransformModule(ScriptedLoadableModule):
  28. """
  29. Module description shown in the module panel.
  30. """
  31. def __init__(self, parent):
  32. ScriptedLoadableModule.__init__(self, parent)
  33. self.parent.title = "Seek Transform module"
  34. self.parent.categories = ["Image Processing"]
  35. self.parent.contributors = ["Luka Komar (Onkološki Inštitut Ljubljana, Fakulteta za Matematiko in Fiziko Ljubljana)"]
  36. self.parent.helpText = "This module applies rigid transformations to CBCT volumes based on reference CT volumes."
  37. self.parent.acknowledgementText = "Supported by doc. Primož Peterlin & prof. Andrej Studen"
  38. class SeekTransformModuleWidget(ScriptedLoadableModuleWidget):
  39. """
  40. GUI of the module.
  41. """
  42. def setup(self):
  43. ScriptedLoadableModuleWidget.setup(self)
  44. # Dropdown menu za izbiro metode
  45. self.rotationMethodComboBox = qt.QComboBox()
  46. self.rotationMethodComboBox.addItems(["Kabsch", "Horn", "Iterative Closest Point (Horn)"])
  47. self.layout.addWidget(self.rotationMethodComboBox)
  48. # Checkboxi za transformacije
  49. self.scalingCheckBox = qt.QCheckBox("Scaling")
  50. self.scalingCheckBox.setChecked(False)
  51. self.layout.addWidget(self.scalingCheckBox)
  52. self.rotationCheckBox = qt.QCheckBox("Rotation")
  53. self.rotationCheckBox.setChecked(True)
  54. self.layout.addWidget(self.rotationCheckBox)
  55. self.translationCheckBox = qt.QCheckBox("Translation")
  56. self.translationCheckBox.setChecked(True)
  57. self.layout.addWidget(self.translationCheckBox)
  58. self.markersCheckBox = qt.QCheckBox("Place control points for detected markers")
  59. self.markersCheckBox.setChecked(False)
  60. self.layout.addWidget(self.markersCheckBox)
  61. self.writefileCheckBox = qt.QCheckBox("Write data to csv file")
  62. self.writefileCheckBox.setChecked(True)
  63. self.layout.addWidget(self.writefileCheckBox)
  64. self.tableCheckBox = qt.QCheckBox("Find top of the table and match height")
  65. self.tableCheckBox.setChecked(True)
  66. self.layout.addWidget(self.tableCheckBox)
  67. self.FineShiftCheckBox = qt.QCheckBox("Use extra fine shift correction")
  68. self.FineShiftCheckBox.setChecked(True)
  69. self.layout.addWidget(self.FineShiftCheckBox)
  70. self.save_as_dicomCheckBox = qt.QCheckBox("Save transformed CT and segmentations as DICOM files")
  71. self.save_as_dicomCheckBox.setChecked(False)
  72. self.layout.addWidget(self.save_as_dicomCheckBox)
  73. # Load button
  74. self.applyButton = qt.QPushButton("Find markers and transform")
  75. self.applyButton.toolTip = "Finds markers, computes optimal rigid transform and applies it to CBCT volumes."
  76. self.applyButton.enabled = True
  77. self.layout.addWidget(self.applyButton)
  78. # Connect button to logic
  79. self.applyButton.connect('clicked(bool)', self.onApplyButton)
  80. self.layout.addStretch(1)
  81. def onApplyButton(self):
  82. # Nastavi globalni logger
  83. log_file_path = os.path.join("C:/Users/lkomar/Documents/Prostata", "seektransform_log.txt")
  84. logging.basicConfig(filename=log_file_path, level=logging.INFO, format='%(asctime)s - %(message)s')
  85. try:
  86. logging.info("▶️ onApplyButton pressed.")
  87. except Exception as e:
  88. print("❌ Logging setup failed:", e)
  89. logic = MyTransformModuleLogic()
  90. selectedMethod = self.rotationMethodComboBox.currentText # izberi metodo izračuna rotacije
  91. # Preberi stanje checkboxov
  92. applyRotation = self.rotationCheckBox.isChecked()
  93. applyTranslation = self.translationCheckBox.isChecked()
  94. applyScaling = self.scalingCheckBox.isChecked()
  95. applyMarkers = self.markersCheckBox.isChecked()
  96. writefilecheck = self.writefileCheckBox.isChecked()
  97. tablefind = self.tableCheckBox.isChecked()
  98. use_fine_shift = self.FineShiftCheckBox.isChecked()
  99. save_as_dicom = self.save_as_dicomCheckBox.isChecked()
  100. # Pokliči logiko z izbranimi nastavitvami
  101. logic.run(selectedMethod, applyRotation, applyTranslation, applyScaling, applyMarkers, writefilecheck, tablefind, use_fine_shift, save_as_dicom)
  102. class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
  103. """
  104. Core logic of the module.
  105. """
  106. def run(self, selectedMethod, applyRotation, applyTranslation, applyScaling, applymarkers, writefilecheck, tablefind, use_fine_shift, save_as_dicom):
  107. start_time = time.time()
  108. print("Calculating...")
  109. #slicer.util.delayDisplay(f"Starting", 1000)
  110. def group_points(points, threshold):
  111. # Function to group points that are close to each other
  112. grouped_points = []
  113. while points:
  114. point = points.pop() # Take one point from the list
  115. group = [point] # Start a new group
  116. # Find all points close to this one
  117. distances = cdist([point], points) # Calculate distances from this point to others
  118. close_points = [i for i, dist in enumerate(distances[0]) if dist < threshold]
  119. # Add the close points to the group
  120. group.extend([points[i] for i in close_points])
  121. # Remove the grouped points from the list
  122. points = [point for i, point in enumerate(points) if i not in close_points]
  123. # Add the group to the result
  124. grouped_points.append(group)
  125. return grouped_points
  126. def region_growing(image_data, seed, intensity_threshold, max_distance):
  127. dimensions = image_data.GetDimensions()
  128. visited = set()
  129. region = []
  130. queue = deque([seed])
  131. while queue:
  132. x, y, z = queue.popleft()
  133. if (x, y, z) in visited:
  134. continue
  135. visited.add((x, y, z))
  136. voxel_value = image_data.GetScalarComponentAsDouble(x, y, z, 0)
  137. if voxel_value >= intensity_threshold:
  138. region.append((x, y, z))
  139. # Add neighbors within bounds
  140. for dx, dy, dz in [(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)]:
  141. nx, ny, nz = x + dx, y + dy, z + dz
  142. if 0 <= nx < dimensions[0] and 0 <= ny < dimensions[1] and 0 <= nz < dimensions[2]:
  143. if (nx, ny, nz) not in visited:
  144. queue.append((nx, ny, nz))
  145. return region
  146. def compute_scaling_stddev(moving_points, fixed_points):
  147. moving = np.array(moving_points)
  148. fixed = np.array(fixed_points)
  149. # Standard deviation around centroid, po osi
  150. scaling_factors = np.std(fixed, axis=0) / np.std(moving, axis=0)
  151. return tuple(scaling_factors)
  152. def compute_scaling(cbct_points, scaling_factors):
  153. """Applies non-uniform scaling to CBCT points.
  154. Args:
  155. cbct_points (list of lists): List of (x, y, z) points.
  156. scaling_factors (tuple): Scaling factors (sx, sy, sz) for each axis.
  157. Returns:
  158. np.ndarray: Scaled CBCT points.
  159. """
  160. sx, sy, sz = scaling_factors # Extract scaling factors
  161. scaling_matrix = np.diag([sx, sy, sz]) # Create diagonal scaling matrix
  162. cbct_points_np = np.array(cbct_points) # Convert to numpy array
  163. scaled_points = cbct_points_np @ scaling_matrix.T # Apply scaling
  164. scaling_4x4 = np.eye(4)
  165. scaling_4x4[0, 0] = sx
  166. scaling_4x4[1, 1] = sy
  167. scaling_4x4[2, 2] = sz
  168. return scaled_points.tolist() # Convert back to list
  169. def compute_Kabsch_rotation(moving_points, fixed_points):
  170. """
  171. Computes the optimal rotation matrix to align moving_points to fixed_points.
  172. Parameters:
  173. moving_points (list or ndarray): List of points to be rotated CBCT
  174. fixed_points (list or ndarray): List of reference points CT
  175. Returns:
  176. ndarray: Optimal rotation matrix.
  177. """
  178. assert len(moving_points) == len(fixed_points), "Point lists must be the same length."
  179. # Convert to numpy arrays
  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. # Compute covariance matrix
  189. H = np.dot(moving_centered.T, fixed_centered)
  190. # SVD decomposition
  191. U, _, Vt = np.linalg.svd(H)
  192. Rotate_optimal = np.dot(Vt.T, U.T)
  193. # Correct improper rotation (reflection)
  194. if np.linalg.det(Rotate_optimal) < 0:
  195. Vt[-1, :] *= -1
  196. Rotate_optimal = np.dot(Vt.T, U.T)
  197. return Rotate_optimal
  198. def compute_Horn_rotation(moving_points, fixed_points):
  199. """
  200. Computes the optimal rotation matrix using quaternions.
  201. Parameters:
  202. moving_points (list or ndarray): List of points to be rotated.
  203. fixed_points (list or ndarray): List of reference points.
  204. Returns:
  205. ndarray: Optimal rotation matrix.
  206. """
  207. assert len(moving_points) == len(fixed_points), "Point lists must be the same length."
  208. moving = np.array(moving_points)
  209. fixed = np.array(fixed_points)
  210. # Compute centroids
  211. centroid_moving = np.mean(moving, axis=0)
  212. centroid_fixed = np.mean(fixed, axis=0)
  213. # Center the points
  214. moving_centered = moving - centroid_moving
  215. fixed_centered = fixed - centroid_fixed
  216. # Construct the cross-dispersion matrix
  217. M = np.dot(moving_centered.T, fixed_centered)
  218. # Construct the N matrix for quaternion solution
  219. A = M - M.T
  220. delta = np.array([A[1, 2], A[2, 0], A[0, 1]])
  221. trace = np.trace(M)
  222. N = np.zeros((4, 4))
  223. N[0, 0] = trace
  224. N[1:, 0] = delta
  225. N[0, 1:] = delta
  226. N[1:, 1:] = M + M.T - np.eye(3) * trace
  227. # Compute the eigenvector corresponding to the maximum eigenvalue
  228. eigvals, eigvecs = np.linalg.eigh(N)
  229. q_optimal = eigvecs[:, np.argmax(eigvals)] # Optimal quaternion
  230. # Convert quaternion to rotation matrix
  231. w, x, y, z = q_optimal
  232. R = np.array([
  233. [1 - 2*(y**2 + z**2), 2*(x*y - z*w), 2*(x*z + y*w)],
  234. [2*(x*y + z*w), 1 - 2*(x**2 + z**2), 2*(y*z - x*w)],
  235. [2*(x*z - y*w), 2*(y*z + x*w), 1 - 2*(x**2 + y**2)]
  236. ])
  237. return R
  238. def icp_algorithm(moving_points, fixed_points, max_iterations=100, tolerance=1e-5):
  239. """
  240. Iterative Closest Point (ICP) algorithm to align moving_points to fixed_points.
  241. Parameters:
  242. moving_points (list or ndarray): List of points to be aligned.
  243. fixed_points (list or ndarray): List of reference points.
  244. max_iterations (int): Maximum number of iterations.
  245. tolerance (float): Convergence tolerance.
  246. Returns:
  247. ndarray: Transformed moving points.
  248. ndarray: Optimal rotation matrix.
  249. ndarray: Optimal translation vector.
  250. """
  251. # Convert to numpy arrays
  252. moving = np.array(moving_points)
  253. fixed = np.array(fixed_points)
  254. # Initialize transformation
  255. R = np.eye(3) # Identity matrix for rotation
  256. t = np.zeros(3) # Zero vector for translation
  257. prev_error = np.inf # Initialize previous error to a large value
  258. for iteration in range(max_iterations):
  259. # Step 1: Find the nearest neighbors (correspondences)
  260. distances = np.linalg.norm(moving[:, np.newaxis] - fixed, axis=2)
  261. nearest_indices = np.argmin(distances, axis=1)
  262. nearest_points = fixed[nearest_indices]
  263. # Step 2: Compute the optimal rotation and translation
  264. R_new = compute_Horn_rotation(moving, nearest_points)
  265. centroid_moving = np.mean(moving, axis=0)
  266. centroid_fixed = np.mean(nearest_points, axis=0)
  267. t_new = centroid_fixed - np.dot(R_new, centroid_moving)
  268. # Step 3: Apply the transformation
  269. moving = np.dot(moving, R_new.T) + t_new
  270. # Update the cumulative transformation
  271. R = np.dot(R_new, R)
  272. t = np.dot(R_new, t) + t_new
  273. # Step 4: Check for convergence
  274. mean_error = np.mean(np.linalg.norm(moving - nearest_points, axis=1))
  275. if np.abs(prev_error - mean_error) < tolerance:
  276. print(f"ICP converged after {iteration + 1} iterations.")
  277. break
  278. prev_error = mean_error
  279. else:
  280. print(f"ICP reached maximum iterations ({max_iterations}).")
  281. return moving, R, t
  282. 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):
  283. def side_lengths(points):
  284. lengths = [
  285. np.linalg.norm(points[0] - points[1]),
  286. np.linalg.norm(points[1] - points[2]),
  287. np.linalg.norm(points[2] - points[0])
  288. ]
  289. return lengths
  290. def triangle_angles(points):
  291. a = np.linalg.norm(points[1] - points[2])
  292. b = np.linalg.norm(points[0] - points[2])
  293. c = np.linalg.norm(points[0] - points[1])
  294. angle_A = np.arccos(np.clip((b**2 + c**2 - a**2) / (2 * b * c), -1.0, 1.0))
  295. angle_B = np.arccos(np.clip((a**2 + c**2 - b**2) / (2 * a * c), -1.0, 1.0))
  296. angle_C = np.pi - angle_A - angle_B
  297. return [angle_A, angle_B, angle_C]
  298. def normalize(vec):
  299. norm = np.linalg.norm(vec)
  300. return [v / norm for v in vec] if norm > 0 else vec
  301. def permutation_score(perm, ct_lengths, ct_angles, w_len, w_ang, penalty_angle_thresh=np.deg2rad(10)):
  302. perm_lengths = side_lengths(perm)
  303. perm_angles = triangle_angles(perm)
  304. # Filter za minimum razdalje
  305. if min(perm_lengths) < min_distance:
  306. return float('inf')
  307. lengths_1 = normalize(perm_lengths) if normalize_lengths else perm_lengths
  308. lengths_2 = normalize(ct_lengths) if normalize_lengths else ct_lengths
  309. angles_1 = normalize(perm_angles) if normalize_angles else perm_angles
  310. angles_2 = normalize(ct_angles) if normalize_angles else ct_angles
  311. score_len = sum(abs(a - b) for a, b in zip(lengths_1, lengths_2))
  312. score_ang = sum(abs(a - b) for a, b in zip(angles_1, angles_2))
  313. order_penalty = order_mismatch_penalty(ct_points, perm, axis='z')
  314. return w_len * score_len + w_ang * score_ang + order_penalty + w_order * order_penalty
  315. def smart_sort_cbct_points(cbct_points, z_threshold=5.0):
  316. """
  317. Sortira točke tako, da poskusi najprej po Z. Če so razlike po Z manjše
  318. od praga, sortira po (Y, X), sicer sortira po Z.
  319. """
  320. z_values = [pt[2] for pt in cbct_points]
  321. z_range = max(z_values) - min(z_values)
  322. if z_range < z_threshold:
  323. # Sortiraj po Y, nato X (če so točke v isti ravnini po Z)
  324. return sorted(cbct_points, key=lambda pt: (pt[1], pt[0]))
  325. else:
  326. # Sortiraj po Z, nato Y, nato X
  327. return sorted(cbct_points, key=lambda pt: (pt[2], pt[1], pt[0]))
  328. def order_mismatch_penalty(ct_points, perm, axis='z'):
  329. axis_idx = {'x': 0, 'y': 1, 'z': 2}[axis]
  330. ct_sorted = np.argsort([pt[axis_idx] for pt in ct_points])
  331. perm_sorted = np.argsort([pt[axis_idx] for pt in perm])
  332. return sum(1 for a, b in zip(ct_sorted, perm_sorted) if a != b)
  333. cbct_points = list(cbct_points)
  334. print("CBCT points:", cbct_points)
  335. ct_lengths = side_lengths(np.array(ct_points))
  336. ct_angles = triangle_angles(np.array(ct_points))
  337. if auto_weights:
  338. var_len = np.var(ct_lengths)
  339. var_ang = np.var(ct_angles)
  340. total_var = var_len + var_ang + 1e-6
  341. weight_length = (1 - var_len / total_var)
  342. weight_angle = (1 - var_ang / total_var)
  343. else:
  344. weight_length = 0.8
  345. weight_angle = 0.2
  346. cbct_sorted = smart_sort_cbct_points(cbct_points)
  347. original_score = permutation_score(np.array(cbct_sorted), ct_lengths, ct_angles, weight_length, weight_angle)
  348. # Če je ta rezultat dovolj dober, uporabi
  349. best_score = float('inf')
  350. best_perm = None
  351. if original_score < float('inf'): # lahko dodaš prag če želiš
  352. best_score = original_score
  353. best_perm = np.array(cbct_sorted)
  354. # Nato preveri vse permutacije (vključno s prvotnim vrstnim redom, če fallback_if_worse=True)
  355. for perm in itertools.permutations(cbct_points):
  356. perm = np.array(perm)
  357. score = permutation_score(perm, ct_lengths, ct_angles, weight_length, weight_angle)
  358. if score < best_score:
  359. best_score = score
  360. best_perm = perm
  361. print(f"New best permutation found with perm: {perm}")
  362. #print("CT centroid:", np.mean(ct_points, axis=0))
  363. #print("CBCT centroid (best perm):", np.mean(best_perm, axis=0))
  364. if fallback_if_worse:
  365. #original_score = permutation_score(np.array(cbct_points), ct_lengths, ct_angles, weight_length, weight_angle)
  366. print("Original score: ", original_score)
  367. if original_score <= best_score:
  368. print("Fallback to original points due to worse score of the permutation.")
  369. return list(cbct_points)
  370. return list(best_perm)
  371. def compute_translation(moving_points, fixed_points, rotation_matrix):
  372. """
  373. Computes the translation vector to align moving_points to fixed_points given a rotation matrix.
  374. Parameters:
  375. moving_points (list or ndarray): List of points to be translated.
  376. fixed_points (list or ndarray): List of reference points.
  377. rotation_matrix (ndarray): Rotation matrix.
  378. Returns:
  379. ndarray: Translation vector.
  380. """
  381. # Convert to numpy arrays
  382. moving = np.array(moving_points)
  383. fixed = np.array(fixed_points)
  384. # Compute centroids
  385. centroid_moving = np.mean(moving, axis=0)
  386. centroid_fixed = np.mean(fixed, axis=0)
  387. # Compute translation
  388. translation = centroid_fixed - np.dot(centroid_moving, rotation_matrix)
  389. return translation
  390. def create_vtk_transform(rotation_matrix, translation_vector, tablefound, study_name=None, cbct_volume_name=None, scaling_factors=None):
  391. """
  392. Creates a vtkTransform from scaling, rotation, and translation.
  393. Shrani tudi kumulativno matriko v globalni slovar cumulative_matrices.
  394. """
  395. # ----- Inicializacija -----
  396. global cumulative_matrices
  397. transform = vtk.vtkTransform()
  398. # ----- 1. Skaliranje -----
  399. if scaling_factors is not None:
  400. sx, sy, sz = scaling_factors
  401. transform.Scale(sx, sy, sz)
  402. # ----- 2. Rotacija -----
  403. # Rotacijsko matriko in translacijo pretvori v homogeno matriko
  404. affine_matrix = np.eye(4)
  405. affine_matrix[:3, :3] = rotation_matrix
  406. affine_matrix[:3, 3] = translation_vector
  407. # Vstavi v vtkMatrix4x4
  408. vtk_matrix = vtk.vtkMatrix4x4()
  409. for i in range(4):
  410. for j in range(4):
  411. vtk_matrix.SetElement(i, j, affine_matrix[i, j])
  412. transform.Concatenate(vtk_matrix)
  413. # # ----- 3. Debug izpis -----
  414. # print("Transform matrix:")
  415. # for i in range(4):
  416. # print(" ".join(f"{vtk_matrix.GetElement(i, j):.6f}" for j in range(4)))
  417. # ----- 4. Shrani v kumulativni matriki -----
  418. if study_name and cbct_volume_name:
  419. key = (study_name, cbct_volume_name)
  420. if key not in cumulative_matrices:
  421. cumulative_matrices[key] = np.eye(4)
  422. cumulative_matrices[key] = np.dot(cumulative_matrices[key], affine_matrix)
  423. return transform
  424. def save_transform_matrix(matrix, study_name, cbct_volume_name):
  425. """
  426. Appends the given 4x4 matrix to a text file under the given study folder.
  427. """
  428. base_folder = os.path.join(os.path.dirname(__file__), "Transformacijske matrike")
  429. study_folder = os.path.join(base_folder, study_name)
  430. os.makedirs(study_folder, exist_ok=True) # Create folders if they don't exist
  431. safe_cbct_name = re.sub(r'[<>:"/\\|?*]', '_', cbct_volume_name)
  432. # Preveri ali je CT miza najdena
  433. filename = os.path.join(study_folder, f"{safe_cbct_name}.txt")
  434. with open(filename, "w") as f:
  435. #f.write("Transformacija:\n")
  436. for row in matrix:
  437. f.write(" ".join(f"{elem:.6f}" for elem in row) + "\n")
  438. print(" ".join(f"{elem:.6f}" for elem in row) + "\n")
  439. f.write("\n") # Dodaj prazen vrstico med transformacijami
  440. #print(f"Transform matrix saved to {filename}")
  441. def detect_points_region_growing(volume_name, yesCbct, create_marker, intensity_threshold=3000, x_min=90, x_max=380, y_min=200, y_max=380, z_min=25, z_max=140, max_distance=9, centroid_merge_threshold=5):
  442. volume_node = find_volume_node_by_partial_name(volume_name)
  443. if not volume_node:
  444. raise RuntimeError(f"Volume {volume_name} not found.")
  445. image_data = volume_node.GetImageData()
  446. matrix = vtk.vtkMatrix4x4()
  447. volume_node.GetIJKToRASMatrix(matrix)
  448. dimensions = image_data.GetDimensions()
  449. #detected_regions = []
  450. if yesCbct: #je cbct ali ct?
  451. valid_x_min, valid_x_max = 0, dimensions[0] - 1
  452. valid_y_min, valid_y_max = 0, dimensions[1] - 1
  453. valid_z_min, valid_z_max = 0, dimensions[2] - 1
  454. else:
  455. valid_x_min, valid_x_max = max(x_min, 0), min(x_max, dimensions[0] - 1)
  456. valid_y_min, valid_y_max = max(y_min, 0), min(y_max, dimensions[1] - 1)
  457. valid_z_min, valid_z_max = max(z_min, 0), min(z_max, dimensions[2] - 1)
  458. visited = set()
  459. def grow_region(x, y, z):
  460. if (x, y, z) in visited:
  461. return None
  462. voxel_value = image_data.GetScalarComponentAsDouble(x, y, z, 0)
  463. if voxel_value < intensity_threshold:
  464. return None
  465. region = region_growing(image_data, (x, y, z), intensity_threshold, max_distance=max_distance)
  466. if region:
  467. for point in region:
  468. visited.add(tuple(point))
  469. return region
  470. return None
  471. regions = []
  472. for z in range(valid_z_min, valid_z_max + 1):
  473. for y in range(valid_y_min, valid_y_max + 1):
  474. for x in range(valid_x_min, valid_x_max + 1):
  475. region = grow_region(x, y, z)
  476. if region:
  477. regions.append(region)
  478. # Collect centroids using intensity-weighted average
  479. centroids = []
  480. for region in regions:
  481. points = np.array([matrix.MultiplyPoint([*point, 1])[:3] for point in region])
  482. intensities = np.array([image_data.GetScalarComponentAsDouble(*point, 0) for point in region])
  483. if intensities.sum() > 0:
  484. weighted_centroid = np.average(points, axis=0, weights=intensities)
  485. max_intensity = intensities.max()
  486. centroids.append((np.round(weighted_centroid, 2), max_intensity))
  487. unique_centroids = []
  488. for centroid, intensity in centroids:
  489. if not any(np.linalg.norm(centroid - existing_centroid) < centroid_merge_threshold for existing_centroid, _ in unique_centroids):
  490. unique_centroids.append((centroid, intensity))
  491. if create_marker:
  492. markups_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"Markers_{volume_name}")
  493. for centroid, intensity in unique_centroids:
  494. markups_node.AddControlPoint(*centroid)
  495. markups_node.SetDisplayVisibility(False)
  496. #print(f"Detected Centroid (RAS): {centroid}, Max Intensity: {intensity}")
  497. return unique_centroids
  498. def find_table_top_z(volume_name, writefilecheck, makemarkerscheck, yesCbct):
  499. """
  500. Najde višino zgornjega roba mize v CT/CBCT volumnu in po želji doda marker v sceno.
  501. Args:
  502. ct_volume_name (str): Ime volumna.
  503. writefilecheck (bool): Ali naj se rezultat shrani v .csv.
  504. makemarkerscheck (bool): Ali naj se doda marker v 3D Slicer.
  505. yesCbct (bool): True, če je CBCT; False, če je CT.
  506. Returns:
  507. (float, int): Z komponenta v RAS prostoru, in Y indeks v slicerjevem volumnu.
  508. """
  509. # --- Pridobi volume node ---
  510. volume_node = find_volume_node_by_partial_name(volume_name)
  511. np_array = slicer.util.arrayFromVolume(volume_node) # (Z, Y, X)
  512. ijkToRasMatrix = vtk.vtkMatrix4x4()
  513. volume_node.GetIJKToRASMatrix(ijkToRasMatrix)
  514. # --- Določimo lokacijo stolpca ---
  515. z_index = np_array.shape[0] // 2 # srednji slice
  516. y_size = np_array.shape[1]
  517. # x_index = int(np_array.shape[2] * 0.15)
  518. # x_index = max(0, min(x_index, np_array.shape[2] - 1))
  519. # --- Izračun spodnje tretjine (spodnji del slike) ---
  520. y_start = int(y_size * 2 / 3)
  521. slice_data = np_array[z_index, :, :] # (Y, X)
  522. y_end = y_size # Do dna slike
  523. #column_values = slice_data[y_start:y_end, x_index] # (Y)
  524. # --- Parametri za rob ---
  525. threshold_high = -300 if yesCbct else -100
  526. threshold_low = -700 if yesCbct else -350
  527. min_jump = 100 if yesCbct else 100
  528. window_size = 4 # število voxelov nad/pod
  529. #previous_value = column_values[-1]
  530. table_top_y = None
  531. # --- Več stolpcev okoli x_index ---
  532. x_center = np_array.shape[2] // 2
  533. x_offset = 30 # 30 levo od sredine
  534. x_index_base = max(0, x_center - x_offset)
  535. candidate_y_values = []
  536. search_range = range(-5, 6) # od -5 do +5 stolpcev
  537. for dx in search_range:
  538. x_index = x_index_base + dx
  539. if x_index < 0 or x_index >= np_array.shape[2]:
  540. continue
  541. column_values = slice_data[y_start:y_end, x_index]
  542. for i in range(window_size, len(column_values) - window_size):
  543. curr = column_values[i]
  544. above_avg = np.mean(column_values[i - window_size:i])
  545. below_avg = np.mean(column_values[i + 1:i + 1 + window_size])
  546. if (threshold_low < curr < threshold_high
  547. and (above_avg - below_avg) > min_jump
  548. and below_avg < -400
  549. and above_avg > -300):
  550. y_found = y_start + i
  551. candidate_y_values.append(y_found)
  552. break # samo prvi zadetek v stolpcu
  553. if candidate_y_values:
  554. most_common_y, _ = Counter(candidate_y_values).most_common(1)[0]
  555. table_top_y = most_common_y
  556. print(f"candidate_y_values: {candidate_y_values}")
  557. print(f"✅ Rob mize (najpogostejši Y): {table_top_y}, pojavitev: {candidate_y_values.count(table_top_y)}/11")
  558. """ # --- Poišči skok navzdol pod prag (od spodaj navzgor) ---
  559. for i in range(len(column_values) - 2, -1, -1): # od spodaj proti vrhu
  560. intensity = column_values[i]
  561. if (intensity - previous_value) > min_jump and intensity < thresholdhigh and intensity > thresholdlow:
  562. table_top_y = y_start + i - 1
  563. print(f"✅ Rob mize najden pri Y = {table_top_y}, intenziteta = {intensity}")
  564. print("Column values (partial):", column_values.tolist())
  565. break
  566. previous_value = intensity """
  567. if table_top_y is None:
  568. print(f"⚠️ Rob mize ni bil najden (X = {x_index})")
  569. print("Column values (partial):", column_values.tolist())
  570. return None
  571. # --- Pretvorba v RAS koordinato ---
  572. table_ijk = [x_index, table_top_y, z_index]
  573. table_ras = np.array(ijkToRasMatrix.MultiplyPoint([*table_ijk, 1]))[:3]
  574. # --- Marker ---
  575. if makemarkerscheck:
  576. table_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"VišinaMize_{ct_volume_name}")
  577. table_node.AddControlPoint(table_ras)
  578. table_node.SetDisplayVisibility(False)
  579. # --- Shrani v CSV ---
  580. if writefilecheck:
  581. height_file = os.path.join(os.path.dirname(__file__), "heightdata.csv")
  582. with open(height_file, mode='a', newline='') as file:
  583. writer = csv.writer(file)
  584. modality = "CBCT" if yesCbct else "CT"
  585. writer.writerow([modality, ct_volume_name, f"Upper table edge at Z = {table_ras[1]:.2f} mm"])
  586. return table_ras[1], table_top_y
  587. def align_cbct_to_ct(volumeNode, scan_type, offset, CT_offset=None, CT_spacing=None):
  588. """
  589. Aligns CBCT volume to CT volume based on height offset.
  590. Args:
  591. volumeNode (vtkMRMLScalarVolumeNode): The volume node to be aligned.
  592. scan_type (str): The type of scan ("CT" or "CBCT").
  593. offset (float): The height offset of the current volume from the center in mm.
  594. CT_offset (float, optional): The height offset of the CT volume from the center. Required for CBCT alignment.
  595. CT_spacing (float, optional): The voxel spacing of the CT volume in mm (for scaling the offset).
  596. Returns:
  597. float: The alignment offset applied to the CBCT volume (if applicable).
  598. """
  599. if scan_type == "CT":
  600. CT_offset = offset
  601. CT_spacing = volumeNode.GetSpacing()[1]
  602. #print(f"CT offset set to: {CT_offset}, CT spacing: {CT_spacing} mm/voxel")
  603. return CT_offset, CT_spacing
  604. else:
  605. if CT_offset is None or CT_spacing is None:
  606. raise ValueError("CT_offset and CT_spacing must be provided to align CBCT to CT.")
  607. CBCT_offset = offset
  608. # Razlika v mm brez skaliranja na CBCT_spacing
  609. alignment_offset_mm = CT_offset - CBCT_offset
  610. #print(f"CT offset: {CT_offset}, CBCT offset: {CBCT_offset}")
  611. #print(f"CT spacing: {CT_spacing} mm/voxel, CBCT spacing: {volumeNode.GetSpacing()[1]} mm/voxel")
  612. #print(f"Aligning CBCT with CT. Offset in mm: {alignment_offset_mm}")
  613. # Uporabi transformacijo
  614. transform = vtk.vtkTransform()
  615. transform.Translate(0, alignment_offset_mm, 0)
  616. transformNode = slicer.vtkMRMLTransformNode()
  617. slicer.mrmlScene.AddNode(transformNode)
  618. transformNode.SetAndObserveTransformToParent(transform)
  619. volumeNode.SetAndObserveTransformNodeID(transformNode.GetID())
  620. slicer.vtkSlicerTransformLogic().hardenTransform(volumeNode)
  621. slicer.mrmlScene.RemoveNode(transformNode)
  622. # Poskusi najti ustrezen marker in ga premakniti
  623. marker_name = f"VišinaMize_{volumeNode.GetName()}"
  624. # Robustno iskanje markerja po imenu
  625. table_node = None
  626. for node in slicer.util.getNodesByClass("vtkMRMLMarkupsFiducialNode"):
  627. if node.GetName() == marker_name:
  628. table_node = node
  629. break
  630. if table_node is not None:
  631. current_point = [0, 0, 0]
  632. table_node.GetNthControlPointPosition(0, current_point)
  633. moved_point = [
  634. current_point[0],
  635. current_point[1] + alignment_offset_mm,
  636. current_point[2]
  637. ]
  638. table_node.SetNthControlPointPosition(0, *moved_point)
  639. return alignment_offset_mm
  640. def print_orientation(volume_name):
  641. node = find_volume_node_by_partial_name(volume_name)
  642. matrix = vtk.vtkMatrix4x4()
  643. node.GetIJKToRASMatrix(matrix)
  644. print(f"{volume_name} IJK→RAS:")
  645. for i in range(3):
  646. print([matrix.GetElement(i, j) for j in range(3)])
  647. def prealign_by_centroid(cbct_points, ct_points):
  648. """
  649. Predporavna CBCT markerje na CT markerje glede na centrične točke.
  650. Args:
  651. cbct_points: List ali ndarray točk iz CBCT.
  652. ct_points: List ali ndarray točk iz CT.
  653. Returns:
  654. List: CBCT točke premaknjene tako, da so centrične točke usklajene.
  655. """
  656. cbct_points = np.array(cbct_points)
  657. ct_points = np.array(ct_points)
  658. cbct_centroid = np.mean(cbct_points, axis=0)
  659. ct_centroid = np.mean(ct_points, axis=0)
  660. translation_vector = ct_centroid - cbct_centroid
  661. aligned_cbct = cbct_points + translation_vector
  662. return aligned_cbct, translation_vector
  663. def choose_best_translation(cbct_points, ct_points, rotation_matrix):
  664. """
  665. Izbere boljšo translacijo: centroidno ali povprečno po rotaciji (retranslation).
  666. Args:
  667. cbct_points (array-like): Točke iz CBCT (še ne rotirane).
  668. ct_points (array-like): Ciljne CT točke.
  669. rotation_matrix (ndarray): Rotacijska matrika.
  670. Returns:
  671. tuple: (best_translation_vector, transformed_cbct_points, used_method)
  672. """
  673. cbct_points = np.array(cbct_points)
  674. ct_points = np.array(ct_points)
  675. # 1. Rotiraj CBCT točke
  676. rotated_cbct = np.dot(cbct_points, rotation_matrix.T)
  677. # 2. Centroid translacija
  678. centroid_moving = np.mean(cbct_points, axis=0)
  679. centroid_fixed = np.mean(ct_points, axis=0)
  680. translation_centroid = centroid_fixed - np.dot(centroid_moving, rotation_matrix)
  681. transformed_centroid = rotated_cbct + translation_centroid
  682. error_centroid = np.mean(np.linalg.norm(transformed_centroid - ct_points, axis=1))
  683. # 3. Retranslacija (srednja razlika)
  684. translation_recomputed = np.mean(ct_points - rotated_cbct, axis=0)
  685. transformed_recomputed = rotated_cbct + translation_recomputed
  686. error_recomputed = np.mean(np.linalg.norm(transformed_recomputed - ct_points, axis=1))
  687. # 4. Izberi boljšo
  688. if error_recomputed < error_centroid:
  689. #print(f"✅ Using retranslation (error: {error_recomputed:.2f} mm)")
  690. return translation_recomputed, transformed_recomputed, "retranslation"
  691. else:
  692. #print(f"✅ Using centroid-based translation (error: {error_centroid:.2f} mm)")
  693. return translation_centroid, transformed_centroid, "centroid"
  694. def rescale_points_to_match_spacing(points, source_spacing, target_spacing):
  695. scale_factors = np.array(target_spacing) / np.array(source_spacing)
  696. return np.array(points) * scale_factors
  697. def visualize_point_matches_in_slicer(cbct_points, ct_points, study_name="MatchVisualization"):
  698. assert len(cbct_points) == len(ct_points), "Mora biti enako število točk!"
  699. # Ustvari markups za CBCT
  700. cbct_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"{study_name}_CBCT")
  701. cbct_node.GetDisplayNode().SetSelectedColor(0, 0, 1) # modra
  702. # Ustvari markups za CT
  703. ct_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode", f"{study_name}_CT")
  704. ct_node.GetDisplayNode().SetSelectedColor(1, 0, 0) # rdeča
  705. # Dodaj točke
  706. for i, (cbct, ct) in enumerate(zip(cbct_points, ct_points)):
  707. cbct_node.AddControlPoint(*cbct, f"CBCT_{i}")
  708. ct_node.AddControlPoint(*ct, f"CT_{i}")
  709. # Ustvari model z linijami med pari
  710. points = vtk.vtkPoints()
  711. lines = vtk.vtkCellArray()
  712. for i, (p1, p2) in enumerate(zip(cbct_points, ct_points)):
  713. id1 = points.InsertNextPoint(p1)
  714. id2 = points.InsertNextPoint(p2)
  715. line = vtk.vtkLine()
  716. line.GetPointIds().SetId(0, id1)
  717. line.GetPointIds().SetId(1, id2)
  718. lines.InsertNextCell(line)
  719. polyData = vtk.vtkPolyData()
  720. polyData.SetPoints(points)
  721. polyData.SetLines(lines)
  722. # Model node
  723. modelNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode", f"{study_name}_Connections")
  724. modelNode.SetAndObservePolyData(polyData)
  725. modelDisplay = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelDisplayNode")
  726. modelDisplay.SetColor(0, 0, 0) # črna
  727. modelDisplay.SetLineWidth(2)
  728. modelDisplay.SetVisibility(True)
  729. modelNode.SetAndObserveDisplayNodeID(modelDisplay.GetID())
  730. modelNode.SetAndObservePolyData(polyData)
  731. print(f"✅ Vizualizacija dodana za {study_name} (točke + povezave)")
  732. def remove_lowest_marker(points, axis=1):
  733. """
  734. Odstrani outlier: točko z največjo 3D razdaljo do robustnega centroida (median).
  735. Združljivo: 'points' je lahko [XYZ] ali [(XYZ, intensity)].
  736. :param points: list[np.array([x,y,z])] ali list[(np.array([x,y,z]), meta)]
  737. :return: isti list, z odstranjenim outlierjem
  738. """
  739. import numpy as np
  740. if not points or len(points) <= 3:
  741. # nič ne odstrani, potrebujemo vsaj 4 kandidate (npr. 3 markerji + 1 koža)
  742. return points
  743. # Normaliziraj vhod v Nx3 matriko koordinat + obdrži indeksno preslikavo
  744. coords = []
  745. for p in points:
  746. if isinstance(p, (list, tuple)) and len(p) == 2 and hasattr(p[0], "__len__"):
  747. xyz = np.asarray(p[0], dtype=float) # (centroid, intensity)
  748. else:
  749. xyz = np.asarray(p, dtype=float) # samo centroid
  750. coords.append(xyz[:3])
  751. A = np.vstack(coords) # (N,3)
  752. # Robustni centroid
  753. center = np.median(A, axis=0)
  754. # Evklidske razdalje (kvadrati zadoščajo za max)
  755. d2 = np.sum((A - center) ** 2, axis=1)
  756. idx_out = int(np.argmax(d2))
  757. removed_point = A[idx_out]
  758. removed = points.pop(idx_out)
  759. print(
  760. f"⚠️ Odstranjen outlier (najbolj oddaljen od median-centroida): "
  761. f"{removed_point.round(2)} | d={float(np.sqrt(d2[idx_out])):.2f} mm | "
  762. f"centroid≈{center.round(2)}"
  763. )
  764. return points
  765. def update_timing_csv(timing_data, study_name):
  766. file_path = os.path.join(os.path.dirname(__file__), "timing_summary.csv")
  767. file_exists = os.path.isfile(file_path)
  768. with open(file_path, mode='a', newline='') as csvfile:
  769. fieldnames = ["Study", "IO", "Fixing", "Table", "Scaling", "CentroidAlign", "Rotation", "Translation", "Transform", "FileSave", "Total"]
  770. writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
  771. if not file_exists:
  772. writer.writeheader()
  773. row = {"Study": study_name}
  774. row.update(timing_data)
  775. writer.writerow(row)
  776. def find_volume_node_by_partial_name(partial_name):
  777. for node in slicer.util.getNodesByClass("vtkMRMLScalarVolumeNode"):
  778. if partial_name in node.GetName():
  779. return node
  780. raise RuntimeError(f"❌ Volume with name containing '{partial_name}' not found.")
  781. def convert_rtstruct_to_segmentation_nodes(ct_volume_node):
  782. shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
  783. segmentation_nodes = []
  784. # ✅ Najdi vse SegmentationNode-e, ki imajo segmente
  785. for seg_node in slicer.util.getNodesByClass("vtkMRMLSegmentationNode"):
  786. num_segments = seg_node.GetSegmentation().GetNumberOfSegments()
  787. if num_segments == 0:
  788. continue
  789. # Če še nima referenceVolume, jo nastavimo
  790. if not seg_node.GetNodeReferenceID("referenceVolume"):
  791. seg_node.SetReferenceImageGeometryParameterFromVolumeNode(ct_volume_node)
  792. seg_node.SetNodeReferenceID("referenceVolume", ct_volume_node.GetID())
  793. print(f"🔗 Nastavljena referenca na CT za: {seg_node.GetName()}")
  794. segmentation_nodes.append(seg_node)
  795. print(f"📎 Najdena segmentacija z vsebino: {seg_node.GetName()}, segmentov: {num_segments}")
  796. if segmentation_nodes:
  797. return segmentation_nodes
  798. for item_id in range(shNode.GetNumberOfItems()):
  799. modality = shNode.GetItemAttribute(item_id, "DICOM.Modality")
  800. if modality != "RTSTRUCT":
  801. continue
  802. rtstruct_node = shNode.GetItemDataNode(item_id)
  803. if not rtstruct_node:
  804. continue
  805. print(f"📎 Najden RTSTRUCT: {rtstruct_node.GetName()}")
  806. # Ustvari nov SegmentationNode
  807. seg_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", f"Seg_{rtstruct_node.GetName()}")
  808. seg_node.SetReferenceImageGeometryParameterFromVolumeNode(ct_volume_node)
  809. seg_node.SetNodeReferenceID("referenceVolume", ct_volume_node.GetID())
  810. seg_node.SetAttribute("DICOM.StructureSetLabel", f"Transformiran_{cbct_date}")
  811. seg_node.SetAttribute("DICOM.SeriesDescription", f"Transformiran {studyName.replace('^', ' ')} ({cbct_date})")
  812. seg_node.SetAttribute("DICOM.ReferencedSeriesInstanceUID", ct_volume_node.GetAttribute("DICOM.SeriesInstanceUID"))
  813. seg_node.SetAttribute("DICOM.ReferencedStudyInstanceUID", ct_volume_node.GetAttribute("DICOM.StudyInstanceUID"))
  814. # Najdi child elemente v SubjectHierarchy (strukture)
  815. rtstruct_item_id = shNode.GetItemByDataNode(rtstruct_node)
  816. structure_ids = vtk.vtkIdList()
  817. shNode.GetItemChildren(rtstruct_item_id, structure_ids)
  818. segment_count = 0
  819. for i in range(structure_ids.GetNumberOfIds()):
  820. structure_item_id = structure_ids.GetId(i)
  821. name = shNode.GetItemName(structure_item_id)
  822. associated_node = shNode.GetItemDataNode(structure_item_id)
  823. if associated_node and associated_node.IsA("vtkMRMLModelNode"):
  824. print(f" ➕ Dodajam strukturo: {name}")
  825. slicer.modules.segmentations.logic().ImportModelToSegmentationNode(associated_node, seg_node)
  826. seg_node.GetSegmentation().GetSegment(seg_node.GetSegmentation().GetNumberOfSegments() - 1).SetName(name)
  827. segment_count += 1
  828. # Če ni bilo nič dodano iz SH: poskusi uvoziti modele iz scene
  829. if segment_count == 0:
  830. print("⚠️ RTSTRUCT nima struktur v SubjectHierarchy – poskus uvoza vseh modelov iz scene.")
  831. for model_node in slicer.util.getNodesByClass("vtkMRMLModelNode"):
  832. if "RTSTRUCT" in model_node.GetName().upper() or model_node.GetName().startswith("Model"):
  833. print(f" ➕ [fallback] Uvoz modela: {model_node.GetName()}")
  834. slicer.modules.segmentations.logic().ImportModelToSegmentationNode(model_node, seg_node)
  835. seg_node.GetSegmentation().GetSegment(seg_node.GetSegmentation().GetNumberOfSegments() - 1).SetName(model_node.GetName())
  836. print(f"📊 Segmentov v {seg_node.GetName()}: {seg_node.GetSegmentation().GetNumberOfSegments()}")
  837. segmentation_nodes.append(seg_node)
  838. return segmentation_nodes
  839. def apply_cumulative_transform_to_segmentation(segmentation_node, matrix):
  840. transform = vtk.vtkTransform()
  841. vtk_matrix = vtk.vtkMatrix4x4()
  842. for i in range(4):
  843. for j in range(4):
  844. vtk_matrix.SetElement(i, j, matrix[i, j])
  845. transform.SetMatrix(vtk_matrix)
  846. transform_node = slicer.vtkMRMLTransformNode()
  847. slicer.mrmlScene.AddNode(transform_node)
  848. transform_node.SetAndObserveTransformToParent(transform)
  849. segmentation_node.SetAndObserveTransformNodeID(transform_node.GetID())
  850. slicer.vtkSlicerTransformLogic().hardenTransform(segmentation_node)
  851. slicer.mrmlScene.RemoveNode(transform_node)
  852. def convert_all_models_to_segmentation(reference_volume_name: str, prefix: str = "Imported_"):
  853. """
  854. Pretvori vse modele (ModelNode) v sceni v enoten vtkMRMLSegmentationNode.
  855. 📥 VHODI:
  856. ----------
  857. reference_volume_name : str
  858. Ime obstoječega CT volumna (npr. "CT_1"), ki določa geometrijo za segmentacijo.
  859. Ta volumen mora biti že naložen v sceni.
  860. prefix : str
  861. Predpona za ime novega segmentation noda (npr. "Imported_").
  862. Ime novega noda bo nekaj kot: "Imported_Segmentation".
  863. 📤 IZHOD:
  864. ----------
  865. segmentation_node : vtkMRMLSegmentationNode
  866. Nov nod, ki vsebuje en segment za vsak najden model v sceni.
  867. Ta segmentacijski nod je pripravljen za transformacijo in DICOM export (vsebuje BinaryLabelmap).
  868. """
  869. import slicer
  870. # Pridobi referenčni volumen (CT)
  871. reference_volume = slicer.util.getNode(reference_volume_name)
  872. # Ustvari nov segmentacijski nod
  873. segmentation_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", prefix + "Segmentation")
  874. segmentation_node.SetReferenceImageGeometryParameterFromVolumeNode(reference_volume)
  875. segmentation_node.SetNodeReferenceID("referenceVolume", reference_volume.GetID())
  876. # Najdi vse modele
  877. model_nodes = slicer.util.getNodesByClass("vtkMRMLModelNode")
  878. #print(f"📦 Najdenih modelov: {len(model_nodes)}")
  879. for model_node in model_nodes:
  880. name = model_node.GetName()
  881. #print(f"🔍 Model: {name}")
  882. skip = (
  883. name.lower().startswith("segmentation")
  884. or name.lower().startswith("surface")
  885. or name.lower() in ["red volume slice", "green volume slice", "yellow volume slice"]
  886. or "rtstruct" in name.lower()
  887. )
  888. if skip:
  889. continue
  890. # Uvozi model kot segment
  891. success = slicer.modules.segmentations.logic().ImportModelToSegmentationNode(model_node, segmentation_node)
  892. if not success:
  893. #print(f"✅ Model '{name}' uvožen kot segment.")
  894. print(f"❌ Napaka pri uvozu modela: {name}")
  895. # Ustvari BinaryLabelmap reprezentacijo (nujno za DICOM export)
  896. created = segmentation_node.GetSegmentation().CreateRepresentation("BinaryLabelmap")
  897. if created:
  898. print("✅ BinaryLabelmap reprezentacija uspešno ustvarjena.")
  899. segmentation_node.GetSegmentation().SetMasterRepresentationName("BinaryLabelmap")
  900. #else:
  901. #print("❌ Pretvorba v BinaryLabelmap ni uspela.")
  902. return segmentation_node
  903. def apply_cumulative_transform_to_volume(volume_node, matrix):
  904. transform = vtk.vtkTransform()
  905. vtk_matrix = vtk.vtkMatrix4x4()
  906. for i in range(4):
  907. for j in range(4):
  908. vtk_matrix.SetElement(i, j, matrix[i, j])
  909. transform.SetMatrix(vtk_matrix)
  910. transform_node = slicer.vtkMRMLTransformNode()
  911. slicer.mrmlScene.AddNode(transform_node)
  912. transform_node.SetAndObserveTransformToParent(transform)
  913. volume_node.SetAndObserveTransformNodeID(transform_node.GetID())
  914. slicer.vtkSlicerTransformLogic().hardenTransform(volume_node)
  915. slicer.mrmlScene.RemoveNode(transform_node)
  916. def dicom_timestamp_from_volume(volume_node):
  917. """Vrne npr. '20250112_093045' iz DICOM metapodatkov ali None, če jih ni."""
  918. uids_attr = volume_node.GetAttribute('DICOM.instanceUIDs')
  919. if not uids_attr:
  920. return None
  921. uid0 = uids_attr.split()[0]
  922. db = slicer.dicomDatabase
  923. fpath = db.fileForInstance(uid0)
  924. def tag(tagstr):
  925. v = db.fileValue(fpath, tagstr)
  926. return v if v not in (None, "", "Unknown") else None
  927. # Datumi: Acquisition (0008,0022) → Series (0008,0021) → Study (0008,0020)
  928. date = tag("0008,0022") or tag("0008,0021") or tag("0008,0020")
  929. # Časi: Acquisition (0008,0032) → Series (0008,0031) → Study (0008,0030)
  930. time = tag("0008,0032") or tag("0008,0031") or tag("0008,0030")
  931. if not date:
  932. return None
  933. # Normaliziraj: YYYYMMDD in HHMMSS
  934. date = date.replace("-", "").replace(".", "").strip()
  935. date = (date + "00000000")[:8] # zaščita, če je prekratko
  936. if time:
  937. time = time.split(".")[0] # odreži frakcije
  938. time = (time + "000000")[:6]
  939. return f"{date}_{time}"
  940. return date
  941. def triangle_similarity(p1, p2):
  942. """
  943. Primerja dva trikotnika (vsak definiran s 3 točkami v 3D) na podlagi:
  944. - razmerij dolžin
  945. - razlik med koti
  946. Če je podanih 4 točk, avtomatsko odstrani eno (ujemajočo), ki najmanj vpliva na podobnost.
  947. :param p1: seznam (np.array) treh ali štirih točk v 3D (npr. ct_points)
  948. :param p2: seznam treh ali štirih točk v 3D (npr. cbct_points)
  949. :return: dict z razliko dolžin, razliko kotov, povprečno napako
  950. """
  951. def side_lengths(pts):
  952. a = np.linalg.norm(pts[1] - pts[0])
  953. b = np.linalg.norm(pts[2] - pts[1])
  954. c = np.linalg.norm(pts[0] - pts[2])
  955. return np.array([a, b, c])
  956. def angles(pts):
  957. a, b, c = side_lengths(pts)
  958. angle_A = np.arccos((b**2 + c**2 - a**2) / (2 * b * c))
  959. angle_B = np.arccos((a**2 + c**2 - b**2) / (2 * a * c))
  960. angle_C = np.pi - angle_A - angle_B
  961. return np.degrees([angle_A, angle_B, angle_C])
  962. # Če imamo 4 točke v vsaki množici, iščemo najboljše 3 ujemajoče
  963. if len(p1) == 4 and len(p2) == 4:
  964. best_score = np.inf
  965. best_idx = None
  966. for i in range(4):
  967. tri1 = np.delete(p1, i, axis=0)
  968. tri2 = np.delete(p2, i, axis=0)
  969. l1 = side_lengths(tri1)
  970. l2 = side_lengths(tri2)
  971. a1 = angles(tri1)
  972. a2 = angles(tri2)
  973. score = np.mean(np.abs(l1 - l2)) + np.mean(np.abs(a1 - a2)) / 10
  974. if score < best_score:
  975. best_score = score
  976. best_idx = i
  977. print(f"📐 Samodejno odstranjena točka {best_idx} za trikotniško primerjavo.")
  978. p1 = np.delete(p1, best_idx, axis=0)
  979. p2 = np.delete(p2, best_idx, axis=0)
  980. if len(p1) != 3 or len(p2) != 3:
  981. raise ValueError("Obe množici točk morata vsebovati 3 (ali 4) točke.")
  982. l1 = side_lengths(p1)
  983. l2 = side_lengths(p2)
  984. a1 = angles(p1)
  985. a2 = angles(p2)
  986. length_diff = np.abs(l1 - l2)
  987. angle_diff = np.abs(a1 - a2)
  988. return {
  989. "side_length_diff_mm": length_diff,
  990. "angle_diff_deg": angle_diff,
  991. "mean_length_error_mm": np.mean(length_diff),
  992. "mean_angle_error_deg": np.mean(angle_diff),
  993. "TSR": 1 / (1 + np.mean(length_diff) + np.mean(angle_diff) / 10)
  994. }
  995. def _pick_dose_node():
  996. """Izbere RTDOSE; če obstaja 'Accumulated_*', jo vzame prednostno."""
  997. dose_nodes = [n for n in slicer.util.getNodesByClass("vtkMRMLScalarVolumeNode")
  998. if "DOSE" in (n.GetName() or "").upper()]
  999. if not dose_nodes:
  1000. return None
  1001. acc = [n for n in dose_nodes if "ACCUMULATED" in n.GetName().upper()]
  1002. return acc[0] if acc else dose_nodes[0]
  1003. def _pick_segmentation_node():
  1004. """Vzemi RTSTRUCT segmentacijo (ali prvo smiselno segmentacijo)."""
  1005. cand = slicer.util.getNodesByClass("vtkMRMLSegmentationNode")
  1006. if not cand:
  1007. return None
  1008. # prednostno RTSTRUCT*
  1009. for n in cand:
  1010. if "RTSTRUCT" in n.GetName().upper():
  1011. return n
  1012. return cand[0]
  1013. def _segment_ids(seg_node, name_filters=None):
  1014. """Vrne [(segmentId, name), ...] (filtrirano po imenih, če je podano)."""
  1015. seg = seg_node.GetSegmentation()
  1016. seg.CreateRepresentation("BinaryLabelmap")
  1017. all_ids = vtk.vtkStringArray()
  1018. seg.GetSegmentIDs(all_ids)
  1019. out = []
  1020. all_names = []
  1021. for i in range(all_ids.GetNumberOfValues()):
  1022. sid = all_ids.GetValue(i)
  1023. name = seg.GetSegment(sid).GetName() or sid
  1024. all_names.append(name)
  1025. if (not name_filters) or any(f.lower() in name.lower() for f in name_filters):
  1026. out.append((sid, name))
  1027. #print(f"[DVH/DEBUG] _segment_ids: total={all_ids.GetNumberOfValues()} names={all_names}")
  1028. #if name_filters:
  1029. #print(f"[DVH/DEBUG] _segment_ids: filters={name_filters} -> kept={[n for _,n in out]}")
  1030. return out
  1031. def compute_dvh_numpy(dose_node, seg_node, name_filters=None, out_csv_path=None):
  1032. """
  1033. Izračuna DVH z notraj-voxel histogramiranjem (numpy).
  1034. Segmentacijo sproti preslika v geometrijo doze (ExportSegmentsToLabelmapNode),
  1035. zato ni več težav z 'no overlap' ali geometrijo.
  1036. Vrne: list [ [Name,Dmin,Dmax,Dmean,D98,D95,D50,D2], ... ]
  1037. """
  1038. logic = slicer.modules.segmentations.logic()
  1039. results = []
  1040. #print("[DVH/DEBUG] compute_dvh_numpy: dose =", dose_node.GetName(), "shape=",
  1041. # slicer.util.arrayFromVolume(dose_node).shape)
  1042. #print("[DVH/DEBUG] compute_dvh_numpy: seg =", seg_node.GetName())
  1043. try:
  1044. src_rep = seg_node.GetSegmentation().GetSourceRepresentationName()
  1045. except Exception:
  1046. src_rep = "?"
  1047. #print("[DVH/DEBUG] compute_dvh_numpy: seg =", seg_node.GetName(), "sourceRep=", src_rep)
  1048. seg_list = _segment_ids(seg_node, name_filters)
  1049. if not seg_list:
  1050. print("[DVH] Ni segmentov za obdelavo.")
  1051. return results
  1052. dose_arr = slicer.util.arrayFromVolume(dose_node).astype(np.float64)
  1053. for sid, sname in seg_list:
  1054. #print(f"[DVH/DEBUG] Processing segment: '{sname}' (id={sid})")
  1055. # en segment → labelmap v geometriji doze
  1056. lm = _labelmap_of_segment_on_dose(seg_node, dose_node, sid)
  1057. if lm is None:
  1058. print(f"[DVH] Prazna maska za '{sname}'.")
  1059. continue
  1060. arrLM = slicer.util.arrayFromVolume(lm)
  1061. #print("[DVH/DEBUG] labelmap shape:", None if arrLM is None else arrLM.shape,
  1062. #" nonzero:", None if arrLM is None else int((arrLM>0).sum()))
  1063. seg_api = seg_node.GetSegmentation()
  1064. #print("[DVH/DEBUG] Export using sourceRep:", seg_api.GetSourceRepresentationName(), " -> dose:", dose_node.GetName())
  1065. mask = arrLM > 0 if arrLM is not None else None
  1066. slicer.mrmlScene.RemoveNode(lm)
  1067. if mask is None or not mask.any():
  1068. print(f"[DVH] Prazna maska za '{sname}'.")
  1069. continue
  1070. vox = dose_arr[mask]
  1071. #print(f"[DVH/DEBUG] voxels in mask: {vox.size}, Dmean={vox.mean():.2f} Gy")
  1072. # osnovne metrike (Gy)
  1073. Dmin = float(np.min(vox))
  1074. Dmax = float(np.max(vox))
  1075. Dmean = float(np.mean(vox))
  1076. D98 = float(np.percentile(vox, 2))
  1077. D95 = float(np.percentile(vox, 5))
  1078. D50 = float(np.percentile(vox, 50))
  1079. D2 = float(np.percentile(vox, 98))
  1080. results.append([sname, Dmin, Dmax, Dmean, D98, D95, D50, D2])
  1081. if out_csv_path and results:
  1082. os.makedirs(os.path.dirname(out_csv_path), exist_ok=True)
  1083. with open(out_csv_path, "w", newline="") as f:
  1084. w = csv.writer(f)
  1085. w.writerow(["Segment","Dmin(Gy)","Dmax(Gy)","Dmean(Gy)","D98(Gy)","D95(Gy)","D50(Gy)","D2(Gy)"])
  1086. w.writerows(results)
  1087. return results
  1088. def _for_uid(volume_node):
  1089. """Vrne DICOM FrameOfReferenceUID (0020,0052) ali None."""
  1090. # 1) neposredno z MRML atributa (če je prisoten)
  1091. direct = volume_node.GetAttribute('DICOM.FrameOfReferenceUID')
  1092. if direct:
  1093. return direct
  1094. # 2) iz DICOM DB prek prve instance
  1095. uids_attr = volume_node.GetAttribute('DICOM.instanceUIDs')
  1096. if not uids_attr:
  1097. return None
  1098. db = slicer.dicomDatabase
  1099. f = db.fileForInstance(uids_attr.split()[0])
  1100. return db.fileValue(f, "0020,0052") or None
  1101. def _pick_matching_dose_node(ct_node):
  1102. """Vrne RTDOSE z istim FORUID kot CT (če obstaja), sicer None/first fallback."""
  1103. ct_for = _for_uid(ct_node) if ct_node else None
  1104. dose_nodes = [n for n in slicer.util.getNodesByClass("vtkMRMLScalarVolumeNode")
  1105. if "DOSE" in (n.GetName() or "").upper()]
  1106. if not dose_nodes:
  1107. return None
  1108. if ct_for:
  1109. for dn in dose_nodes:
  1110. if _for_uid(dn) == ct_for:
  1111. return dn
  1112. # fallback: prva doza (bolje kot nič, a lahko ne prekriva)
  1113. return dose_nodes[0]
  1114. def _ensure_overlap_by_resample(dose_node, ct_node):
  1115. """Če doza in CT nimata istega FORUID, dozo resampla na CT mrežo in vrne novo dozo."""
  1116. if not dose_node or not ct_node:
  1117. return dose_node
  1118. if _for_uid(dose_node) == _for_uid(ct_node):
  1119. return dose_node # že matcha
  1120. import slicer
  1121. newDose = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode",
  1122. dose_node.GetName() + "_onCT")
  1123. params = {
  1124. "inputVolume": dose_node.GetID(),
  1125. "referenceVolume": ct_node.GetID(),
  1126. "outputVolume": newDose.GetID(),
  1127. "interpolationType": "linear",
  1128. }
  1129. slicer.cli.runSync(slicer.modules.resamplescalarvectordwivolume, None, params)
  1130. print("[DVH] Fallback: resampled dose to CT grid ->", newDose.GetName())
  1131. return newDose
  1132. def _labelmap_of_segment_on_dose(seg_node, dose_node, segment_id):
  1133. """Vrne TMP labelmap node za en segment v geometriji doze (ali None ob neuspehu)."""
  1134. logic = slicer.modules.segmentations.logic()
  1135. # 0) vedno referenciraj geometrijo na DOZO
  1136. seg_node.SetReferenceImageGeometryParameterFromVolumeNode(dose_node)
  1137. # 1) poskusi direktni export
  1138. sid = vtk.vtkStringArray(); sid.InsertNextValue(segment_id)
  1139. lm = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode", f"tmpLM_{segment_id}")
  1140. ok = logic.ExportSegmentsToLabelmapNode(seg_node, sid, lm, dose_node)
  1141. if ok:
  1142. arr = slicer.util.arrayFromVolume(lm)
  1143. if arr is not None and (arr > 0).any():
  1144. return lm
  1145. slicer.mrmlScene.RemoveNode(lm)
  1146. # 2) FALLBACK: prisili pot PlanarContour -> Closed surface -> BinaryLabelmap na mreži doze
  1147. seg = seg_node.GetSegmentation()
  1148. seg.CreateRepresentation("Closed surface")
  1149. seg.SetMasterRepresentationName("Closed surface")
  1150. # kopiraj samo ta segment v novo, "čisto" segmentacijo na mreži doze
  1151. seg_tmp = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", "Seg_onDose_TMP")
  1152. seg_tmp.SetReferenceImageGeometryParameterFromVolumeNode(dose_node)
  1153. seg_tmp.GetSegmentation().CopySegmentFromSegmentation(seg, segment_id, segment_id)
  1154. seg_tmp.GetSegmentation().CreateRepresentation("BinaryLabelmap")
  1155. lm2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLabelMapVolumeNode", f"tmpLM2_{segment_id}")
  1156. ok2 = logic.ExportSegmentsToLabelmapNode(seg_tmp, sid, lm2, dose_node)
  1157. slicer.mrmlScene.RemoveNode(seg_tmp)
  1158. if not ok2:
  1159. slicer.mrmlScene.RemoveNode(lm2)
  1160. return None
  1161. arr2 = slicer.util.arrayFromVolume(lm2)
  1162. if arr2 is None or not (arr2 > 0).any():
  1163. slicer.mrmlScene.RemoveNode(lm2)
  1164. return None
  1165. return lm2
  1166. def save_triangle_visualization(ct_pts, cbct_pts, outpath):
  1167. fig = plt.figure(figsize=(10, 8))
  1168. ax = fig.add_subplot(111, projection='3d')
  1169. ct_closed = np.vstack([ct_pts, ct_pts[0]])
  1170. cbct_closed = np.vstack([cbct_pts, cbct_pts[0]])
  1171. ax.plot(ct_closed[:, 0], ct_closed[:, 1], ct_closed[:, 2], 'b-', label='CT trikotnik')
  1172. ax.scatter(ct_pts[:, 0], ct_pts[:, 1], ct_pts[:, 2], c='blue')
  1173. ax.plot(cbct_closed[:, 0], cbct_closed[:, 1], cbct_closed[:, 2], 'r--', label='CBCT trikotnik')
  1174. ax.scatter(cbct_pts[:, 0], cbct_pts[:, 1], cbct_pts[:, 2], c='red')
  1175. ct_centroid = np.mean(ct_pts, axis=0)
  1176. cbct_centroid = np.mean(cbct_pts, axis=0)
  1177. ax.scatter(*ct_centroid, c='blue', marker='x', s=60)
  1178. ax.scatter(*cbct_centroid, c='red', marker='x', s=60)
  1179. ax.set_title("Trikotnik markerjev: CT (modro) vs CBCT (rdeče)")
  1180. ax.set_xlabel('X (mm)')
  1181. ax.set_ylabel('Y (mm)')
  1182. ax.set_zlabel('Z (mm)')
  1183. ax.legend()
  1184. ax.view_init(elev=20, azim=30)
  1185. plt.tight_layout()
  1186. try:
  1187. fig.savefig(outpath)
  1188. #print(f"🖼 Triangle visualization saved to: {outpath}")
  1189. except Exception as e:
  1190. print(f"❌ Failed to save triangle visualization: {e}")
  1191. finally:
  1192. plt.close(fig)
  1193. def resample_scalar_to_reference(input_node, reference_node, interpolator="linear"):
  1194. """Resampla input_node na geometrijo reference_node in PREPIŠE image data + IJK↔RAS + origin/spacing."""
  1195. out = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode", input_node.GetName() + "_tmp")
  1196. params = {
  1197. "inputVolume": input_node.GetID(),
  1198. "referenceVolume": reference_node.GetID(),
  1199. "outputVolume": out.GetID(),
  1200. "interpolationType": "linear" if interpolator=="linear" else "nn",
  1201. }
  1202. slicer.cli.runSync(slicer.modules.resamplescalarvectordwivolume, None, params)
  1203. # --- PREPIŠI ImageData ---
  1204. input_node.SetAndObserveImageData(out.GetImageData())
  1205. # --- PREPIŠI GEOMETRIJO (IJK↔RAS) ---
  1206. ijkToRas = vtk.vtkMatrix4x4(); out.GetIJKToRASMatrix(ijkToRas)
  1207. rasToIjk = vtk.vtkMatrix4x4(); out.GetRASToIJKMatrix(rasToIjk)
  1208. input_node.SetIJKToRASMatrix(ijkToRas)
  1209. input_node.SetRASToIJKMatrix(rasToIjk)
  1210. # --- PREPIŠI origin/spacing (za vsak slučaj, čeprav IJK↔RAS običajno zadošča) ---
  1211. input_node.SetOrigin(out.GetOrigin())
  1212. input_node.SetSpacing(out.GetSpacing())
  1213. # Po želji: prenesi še DisplayNode lastnosti ipd.
  1214. slicer.mrmlScene.RemoveNode(out)
  1215. def export_seg_or_rtstruct(ct_volume_Node, seg_node, export_dir):
  1216. """
  1217. Poskusi izvoziti RTSTRUCT (Planar contour). Če ne uspe, izvozi DICOM SEG.
  1218. Zahteve:
  1219. - seg_node je že PORAVNAN na CT (apliciran transform)
  1220. - reference image geometry in UID-ji so nastavljeni
  1221. """
  1222. import slicer
  1223. from DICOMPlugins import DicomRtImportExportPlugin
  1224. from DICOMExportSegmentations import DICOMSegmentationExporter
  1225. # 0) Referenčna geometrija in ujemanje v SubjectHierarchy
  1226. seg_node.SetReferenceImageGeometryParameterFromVolumeNode(ct_volume_Node)
  1227. sh = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
  1228. ct_item = sh.GetItemByDataNode(ct_volume_Node)
  1229. seg_item = sh.GetItemByDataNode(seg_node)
  1230. # poskrbi, da sta CT in SEG pod ISTIM Study (DICOM exporter to uporablja)
  1231. study_ct = sh.GetItemParent(ct_item)
  1232. if study_ct and study_ct != sh.GetItemParent(seg_item):
  1233. sh.SetItemParent(seg_item, study_ct)
  1234. # 1) Vnesi ključne DICOM atribute iz CT na SEG (Reference Series in FORUID)
  1235. ct_series_uid = ct_volume_Node.GetAttribute("DICOM.SeriesInstanceUID") or ""
  1236. ct_study_uid = ct_volume_Node.GetAttribute("DICOM.StudyInstanceUID") or ""
  1237. ct_for_uid = ct_volume_Node.GetAttribute("DICOM.FrameOfReferenceUID") or ""
  1238. if ct_series_uid:
  1239. sh.SetItemAttribute(seg_item, "DICOM.ReferencedInstanceUIDs", ct_series_uid)
  1240. if ct_for_uid:
  1241. seg_node.SetAttribute("DICOM.FrameOfReferenceUID", ct_for_uid)
  1242. # 2) Poskrbi za reprezentance in nastavi "Source" (nova API namesto "Master")
  1243. seg = seg_node.GetSegmentation()
  1244. # Closed surface naj obstaja
  1245. if not seg.ContainsRepresentation("Closed surface"):
  1246. seg.CreateRepresentation("Closed surface")
  1247. # Poskusi ustvariti Planar contour (zahteva veljavno referenčno geometrijo)
  1248. try:
  1249. if not seg.ContainsRepresentation("Planar contour"):
  1250. seg.CreateRepresentation("Planar contour")
  1251. except Exception as e:
  1252. print("[RTSTRUCT] CreateRepresentation('Planar contour') failed:", e)
  1253. # Najprej probamo s Planar contour kot "Source" (nov API)
  1254. if seg.ContainsRepresentation("Planar contour"):
  1255. seg.SetSourceRepresentationName("Planar contour")
  1256. source_repr = "Planar contour"
  1257. else:
  1258. seg.SetSourceRepresentationName("Closed surface")
  1259. source_repr = "Closed surface"
  1260. print(f"[RTSTRUCT] Source: {seg.GetSourceRepresentationName()}")
  1261. print(f"[RTSTRUCT] Has Planar: {seg.ContainsRepresentation('Planar contour')}")
  1262. print(f"[RTSTRUCT] Has Closed: {seg.ContainsRepresentation('Closed surface')}")
  1263. print("Ref Series UID:", sh.GetItemAttribute(seg_item, "DICOM.ReferencedInstanceUIDs"))
  1264. print("FOR UID:", seg_node.GetAttribute("DICOM.FrameOfReferenceUID"))
  1265. # 3) Pripravi exportables in filtriraj RTSTRUCT kandidat(e)
  1266. plugin = DicomRtImportExportPlugin.DicomRtImportExportPluginClass()
  1267. def fill_tags(exp):
  1268. pn = ct_volume_Node.GetAttribute("DICOM.PatientName")
  1269. pid = ct_volume_Node.GetAttribute("DICOM.PatientID")
  1270. study_uid = ct_volume_Node.GetAttribute("DICOM.StudyInstanceUID")
  1271. if pn: exp.setTag("0010,0010", pn)
  1272. if pid: exp.setTag("0010,0020", pid)
  1273. if study_uid: exp.setTag("0020,000D", study_uid)
  1274. exp.directory = export_dir
  1275. exp_all = []
  1276. exp_all += plugin.examineForExport(sh.GetItemByDataNode(ct_volume_Node))
  1277. exp_all += plugin.examineForExport(sh.GetItemByDataNode(seg_node))
  1278. def pick_rtstruct(exportables):
  1279. cand = []
  1280. for e in exportables:
  1281. name = getattr(e, "name", "")
  1282. etype = getattr(e, "exportType", "")
  1283. sop = getattr(e, "SOPClassUID", "")
  1284. if etype == "RTSTRUCT" or "RTSTRUCT" in (name or "") or sop == "1.2.840.10008.5.1.4.1.1.481.3":
  1285. cand.append(e)
  1286. return cand
  1287. cand_rt = pick_rtstruct(exp_all)
  1288. # Če RTSTRUCT ni našel in trenutno source ni Closed surface, poskusi še enkrat
  1289. if not cand_rt and source_repr != "Closed surface":
  1290. seg.SetSourceRepresentationName("Closed surface")
  1291. print("[RTSTRUCT] Retry with source=Closed surface")
  1292. exp_all = []
  1293. exp_all += plugin.examineForExport(sh.GetItemByDataNode(ct_volume_Node))
  1294. exp_all += plugin.examineForExport(sh.GetItemByDataNode(seg_node))
  1295. cand_rt = pick_rtstruct(exp_all)
  1296. # 4) Izvoz
  1297. if cand_rt:
  1298. for e in cand_rt:
  1299. fill_tags(e)
  1300. ok = plugin.export(cand_rt)
  1301. print(f"✅ RTSTRUCT export status: {ok} → {export_dir}")
  1302. return "RTSTRUCT"
  1303. else:
  1304. # Fallback: DICOM SEG (OpenTPS 2.x ima odlično podporo in DVH deluje)
  1305. options = DICOMSegmentationExporter.ExportOptions()
  1306. options.outputDirectory = export_dir
  1307. options.segmentationNode = seg_node
  1308. options.referencedVolumeNode = ct_volume_Node
  1309. DICOMSegmentationExporter.export(options)
  1310. print(f"✅ DICOM SEG exported → {export_dir} (DVH/CCC v OpenTPS 2.x deluje tudi iz SEG)")
  1311. return "SEG"
  1312. # Globalni seznami za končno statistiko
  1313. prostate_size_est = []
  1314. ctcbct_distance = []
  1315. table_z_values = {}
  1316. # Pridobimo SubjectHierarchyNode
  1317. shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
  1318. studyItems = vtk.vtkIdList()
  1319. shNode.GetItemChildren(shNode.GetSceneItemID(), studyItems)
  1320. #slicer.util.delayDisplay(f"[DEBUG] Starting the loops", 1000)
  1321. for i in range(studyItems.GetNumberOfIds()):
  1322. study_start_time = time.time()
  1323. start_io = time.time()
  1324. studyItem = studyItems.GetId(i)
  1325. studyName = shNode.GetItemName(studyItem)
  1326. print(f"\nProcessing study: {studyName}")
  1327. # **LOKALNI** seznami, resetirajo se pri vsakem study-ju
  1328. cbct_list = []
  1329. ct_list = []
  1330. volume_points_dict = {}
  1331. CT_offset = 0
  1332. # Get child items of the study item
  1333. volumeItems = vtk.vtkIdList()
  1334. shNode.GetItemChildren(studyItem, volumeItems)
  1335. # Iteracija čez vse volumne v posameznem studyju
  1336. for j in range(volumeItems.GetNumberOfIds()):
  1337. intermediateItem = volumeItems.GetId(j)
  1338. finalVolumeItems = vtk.vtkIdList()
  1339. shNode.GetItemChildren(intermediateItem, finalVolumeItems) # Išči globlje!
  1340. for k in range(finalVolumeItems.GetNumberOfIds()):
  1341. volumeItem = finalVolumeItems.GetId(k)
  1342. volumeNode = shNode.GetItemDataNode(volumeItem)
  1343. try:
  1344. dicomUIDs = volumeNode.GetAttribute("DICOM.instanceUIDs")
  1345. except AttributeError:
  1346. print(f"⚠️ Volume node '{volumeNode}' not found or no attribute 'DICOM.instanceUIDs'. Skip.")
  1347. dicomUIDs = None
  1348. continue # Preskoči, če ni veljaven volume
  1349. if not dicomUIDs:
  1350. print("❌ This is an NRRD volume!")
  1351. continue # Preskoči, če ni DICOM volume
  1352. volumeName = volumeNode.GetName()
  1353. imageItem = shNode.GetItemByDataNode(volumeNode)
  1354. modality = shNode.GetItemAttribute(imageItem, "DICOM.Modality") #deluje!
  1355. #dimensions = volumeNode.GetImageData().GetDimensions()
  1356. #spacing = volumeNode.GetSpacing()
  1357. #print(f"Volume {volumeNode.GetName()} - Dimenzije: {dimensions}, Spacing: {spacing}")
  1358. if modality != "CT":
  1359. print("Not a CT")
  1360. continue # Preskoči, če ni CT
  1361. # Preveri, če volume obstaja v sceni
  1362. if not slicer.mrmlScene.IsNodePresent(volumeNode):
  1363. print(f"Volume {volumeName} not present in the scene.")
  1364. continue
  1365. # Preverimo proizvajalca (DICOM metapodatki)
  1366. manufacturer = shNode.GetItemAttribute(imageItem, 'DICOM.Manufacturer')
  1367. #manufacturer = volumeNode.GetAttribute("DICOM.Manufacturer")
  1368. #manufacturer = slicer.dicomDatabase.fileValue(uid, "0008,0070")
  1369. #print(manufacturer)
  1370. # Določimo, ali gre za CBCT ali CT
  1371. if "varian" in manufacturer.lower() or "elekta" in manufacturer.lower():
  1372. cbct_list.append(volumeName)
  1373. scan_type = "CBCT"
  1374. yesCbct = True
  1375. else: # Siemens ali Philips
  1376. ct_list.append(volumeName)
  1377. scan_type = "CT"
  1378. yesCbct = False
  1379. if volumeNode and volumeNode.IsA("vtkMRMLScalarVolumeNode"):
  1380. print(f"✔️ {scan_type} {volumeNode.GetName()} (ID: {volumeItem})")
  1381. if not volumeNode or not volumeNode.IsA("vtkMRMLScalarVolumeNode"):
  1382. print("Can't find volumeNode")
  1383. #continue # Preskoči, če ni veljaven volume
  1384. # Detekcija točk v volumnu
  1385. ustvari_marker = not yesCbct # Ustvari markerje pred poravnavo na mizo
  1386. grouped_points = detect_points_region_growing(volumeName, yesCbct, ustvari_marker, intensity_threshold=3000)
  1387. if grouped_points is None or len(grouped_points) < 3:
  1388. print(f"⚠️ Volume {volumeName} doesn't have enough points for registration. Points: {len(grouped_points)}")
  1389. continue
  1390. if not yesCbct:
  1391. # loči koordinate in intenzitete
  1392. coords_only = [pt for pt, _ in grouped_points]
  1393. intensities = [intensity for _, intensity in grouped_points]
  1394. # permutiraj koordinate (npr. zaradi boljšega ujemanja)
  1395. coords_sorted = match_points(coords_only, coords_only)
  1396. # ponovno sestavi pare (točka, intenziteta)
  1397. grouped_points = list(zip(coords_sorted, intensities))
  1398. #print(f"Populating volume_points_dict with key ('{scan_type}', '{volumeName}')")
  1399. volume_points_dict[(scan_type, volumeName)] = grouped_points
  1400. #print(volume_points_dict) # Check if the key is correctly added
  1401. # Če imamo oba tipa volumna (CBCT in CT) **znotraj istega studyja**
  1402. end_io = time.time()
  1403. if cbct_list and ct_list:
  1404. fixing = fixing_end = 0
  1405. table1_time = table1end_time = 0
  1406. table2_time = table2end_time = 0
  1407. start_scaling = end_scaling = 0
  1408. start_align = end_align = 0
  1409. start_rotation = end_rotation = 0
  1410. start_translation = end_translation = 0
  1411. start_transform = end_transform = 0
  1412. study_start_time = study_end_time = 0
  1413. ct_volume_name = ct_list[0] # Uporabi prvi CT kot referenco
  1414. ct_volume_Node = find_volume_node_by_partial_name(ct_volume_name)
  1415. print(f"\nProcessing CT: {ct_volume_name}")
  1416. ct_points = [centroid for centroid, _ in volume_points_dict[("CT", ct_volume_name)]]
  1417. print(f"CT points: {ct_points}")
  1418. if len(ct_points) < 3:
  1419. print(f"CT volume {ct_volume_name} doesn't have enough points for registration. Points: {len(ct_points)}")
  1420. continue
  1421. else:
  1422. # if len(ct_points) == 4:
  1423. # ct_points = remove_lowest_marker(ct_points) #odstrani marker v riti, če obstaja
  1424. for cbct_volume_name in cbct_list:
  1425. print(f"\nProcessing CBCT Volume: {cbct_volume_name}")
  1426. tablefound = False
  1427. ct_table_found = False
  1428. cbct_table_found = False
  1429. CT_offset = None
  1430. CT_spacing = None
  1431. yesCbct = True
  1432. scan_type = "CBCT" #redundant but here we are
  1433. cbct_volume_node = find_volume_node_by_partial_name(cbct_volume_name)
  1434. key = (studyName, cbct_volume_name)
  1435. if key not in cumulative_matrices:
  1436. cumulative_matrices[key] = np.eye(4)
  1437. table_shift_matrix = np.eye(4)
  1438. fixing = time.time()
  1439. if tablefind:
  1440. # --- CT table reference ---
  1441. yesCbct = False
  1442. table1_time = time.time()
  1443. makemarkerscheck = True
  1444. resCT = find_table_top_z(ct_volume_name, writefilecheck, makemarkerscheck, yesCbct)
  1445. if resCT is not None:
  1446. ct_edge_mm, _ = resCT
  1447. ct_table_found = True
  1448. # zapišemo referenco CT (ne premikamo nič)
  1449. CT_offset, CT_spacing = align_cbct_to_ct(ct_volume_Node, "CT", ct_edge_mm)
  1450. else:
  1451. print("⚠️ CT table top not found – skip table alignment.")
  1452. ct_table_found = False
  1453. table1end_time = time.time()
  1454. # --- CBCT table measure & align ---
  1455. yesCbct = True
  1456. table2_time = time.time()
  1457. makemarkerscheck = False
  1458. if ct_table_found:
  1459. resCBCT = find_table_top_z(cbct_volume_name, writefilecheck, makemarkerscheck, yesCbct)
  1460. #skupnires = resCBCT - resCT if resCBCT is not None else None
  1461. if resCBCT is not None:
  1462. cbct_edge_mm, _ = resCBCT
  1463. # Poravnava CBCT -> CT (harden transform na CBCT)
  1464. align_cbct_to_ct(cbct_volume_node, "CBCT", cbct_edge_mm, CT_offset, CT_spacing)
  1465. tablefound = True
  1466. # ⛔️ NE dodajaj table_shift_matrix v cumulative_matrices (hardeno je že na volumnu)
  1467. else:
  1468. print("⚠️ CBCT table top not found – skip table alignment.")
  1469. tablefound = False
  1470. table2end_time = time.time()
  1471. else:
  1472. tablefound = False
  1473. resample_scalar_to_reference(cbct_volume_node, ct_volume_Node, interpolator="nn") # Resampla CBCT na CT mrežo
  1474. cbct_points = [centroid for centroid, _ in volume_points_dict[("CBCT", cbct_volume_name)]] #zastareli podatki
  1475. cbct_points_array = np.array(cbct_points) # Pretvorba v numpy array
  1476. if len(cbct_points) != len(ct_points):
  1477. if(len(cbct_points) + 1 == len(ct_points)):
  1478. ct_points = remove_lowest_marker(ct_points) #odstrani marker v riti, če obstaja
  1479. else:
  1480. print(f"Neujemajoče število točk! CBCT: {len(cbct_points)}, CT: {len(ct_points)}")
  1481. continue
  1482. # print_orientation(ct_volume_name)
  1483. # print_orientation(cbct_volume_name)
  1484. #for i, (cb, ct) in enumerate(zip(cbct_points, ct_points)):
  1485. # print(f"Pair {i}: CBCT {cb}, CT {ct}, diff: {np.linalg.norm(cb - ct):.2f}")
  1486. ustvari_marker = False # Ustvari markerje
  1487. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
  1488. #cbct_points = detect_points_region_growing(cbct_volume_name, yesCbct, intensity_threshold=3000)
  1489. #cbct_points = [centroid for centroid, _ in volume_points_dict[("CBCT", cbct_volume_name)]] #zastareli podatki
  1490. if len(cbct_points) < 3:
  1491. print(f"CBCT Volume '{cbct_volume_name}' doesn't have enough points for registration. Points: {len(cbct_points)}")
  1492. continue
  1493. cbct_spacing = cbct_volume_node.GetSpacing()
  1494. ct_spacing = ct_volume_Node.GetSpacing()
  1495. if not np.allclose(cbct_spacing, ct_spacing, atol=1e-6):
  1496. cbct_points = rescale_points_to_match_spacing(cbct_points, cbct_spacing, ct_spacing)
  1497. # sicer ne reskaliramo, ker je grid že ujemajoč
  1498. #Sortiramo točke po X/Y/Z da se izognemo težavam pri poravnavi
  1499. cbct_points = match_points(cbct_points, ct_points)
  1500. fixing_end = time.time()
  1501. #visualize_point_matches_in_slicer(cbct_points, ct_points, studyName) #poveže pare markerjev
  1502. if writefilecheck:
  1503. # Shranjevanje razdalj
  1504. distances_ct_cbct = []
  1505. distances_internal = {"A-B": [], "B-C": [], "C-A": []}
  1506. cbct_volume_node = find_volume_node_by_partial_name(cbct_volume_name)
  1507. # Sortiramo točke po Z-koordinati (ali X/Y, če raje uporabljaš drugo os)
  1508. cbct_points_sorted = cbct_points_array[np.argsort(cbct_points_array[:, 2])]
  1509. # Razdalje med CT in CBCT (SORTIRANE točke!)
  1510. if cbct_points_sorted.shape[0] != len(ct_points):
  1511. print(f"⚠️ Število točk CBCT ({cbct_points_sorted.shape[0]}) != CT ({len(ct_points)}), preskakujem izračun.")
  1512. else:
  1513. d_ct_cbct = np.linalg.norm(cbct_points_sorted - ct_points, axis=1)
  1514. distances_ct_cbct.append(d_ct_cbct)
  1515. # Razdalje med točkami znotraj SORTIRANIH cbct_points
  1516. d_ab = np.linalg.norm(cbct_points_sorted[0] - cbct_points_sorted[1])
  1517. d_bc = np.linalg.norm(cbct_points_sorted[1] - cbct_points_sorted[2])
  1518. d_ca = np.linalg.norm(cbct_points_sorted[2] - cbct_points_sorted[0])
  1519. # Sortiramo razdalje po velikosti, da so vedno v enakem vrstnem redu
  1520. sorted_distances = sorted([d_ab, d_bc, d_ca])
  1521. distances_internal["A-B"].append(sorted_distances[0])
  1522. distances_internal["B-C"].append(sorted_distances[1])
  1523. distances_internal["C-A"].append(sorted_distances[2])
  1524. # Dodamo ime študije za v statistiko
  1525. studyName = shNode.GetItemName(studyItem)
  1526. # **Shrani razdalje v globalne sezname**
  1527. prostate_size_est.append({"Study": studyName, "Distances": sorted_distances})
  1528. ctcbct_distance.append({"Study": studyName, "Distances": list(distances_ct_cbct[-1])}) # Pretvorimo v seznam
  1529. # Izberi metodo glede na uporabnikov izbor
  1530. chosen_rotation_matrix = np.eye(3)
  1531. chosen_translation_vector = np.zeros(3)
  1532. #print("Markerji pred transformacijo:", cbct_points, ct_points)
  1533. start_scaling = time.time()
  1534. scaling_factors = None
  1535. if applyScaling:
  1536. scaling_factors = compute_scaling_stddev(cbct_points, ct_points)
  1537. #print("Scaling factors: ", scaling_factors)
  1538. cbct_points = compute_scaling(cbct_points, scaling_factors)
  1539. end_scaling = time.time()
  1540. start_align = time.time()
  1541. initial_error = np.mean(np.linalg.norm(np.array(cbct_points) - np.array(ct_points), axis=1))
  1542. if initial_error > 30:
  1543. #print("⚠️ Initial distance too large, applying centroid prealignment.")
  1544. cbct_points, transvector = prealign_by_centroid(cbct_points, ct_points)
  1545. else:
  1546. transvector = np.zeros(3)
  1547. end_align = time.time()
  1548. start_rotation = time.time()
  1549. if applyRotation:
  1550. if selectedMethod == "Kabsch":
  1551. chosen_rotation_matrix = compute_Kabsch_rotation(cbct_points, ct_points)
  1552. elif selectedMethod == "Horn":
  1553. chosen_rotation_matrix = compute_Horn_rotation(cbct_points, ct_points)
  1554. elif selectedMethod == "Iterative Closest Point (Horn)":
  1555. _, chosen_rotation_matrix, _ = icp_algorithm(cbct_points, ct_points)
  1556. #print("Rotation Matrix:\n", chosen_rotation_matrix)
  1557. end_rotation = time.time()
  1558. start_translation = time.time()
  1559. fine_shift = np.zeros(3) # Inicializiraj fine premike
  1560. if applyTranslation:
  1561. chosen_translation_vector, cbct_points_transformed, method_used = choose_best_translation(
  1562. cbct_points, ct_points, chosen_rotation_matrix)
  1563. # Sistematična razlika (signed shift)
  1564. rotated_cbct = np.dot(cbct_points, chosen_rotation_matrix.T)
  1565. translated_cbct = rotated_cbct + chosen_translation_vector
  1566. delta_y_list = [ct[1] - cbct[1] for ct, cbct in zip(ct_points, translated_cbct)]
  1567. mean_delta_y = np.mean(delta_y_list)
  1568. # Uporabi sistematični shift za dodatno poravnavo v y-osi
  1569. dy = mean_delta_y
  1570. if tablefind and tablefound:
  1571. dy = 0.0 # meja: ali np.clip(dy, -3.0, 3.0)
  1572. fine_shift = np.array([0.0, dy, 0.0])
  1573. cbct_points_transformed += fine_shift
  1574. end_translation = time.time()
  1575. start_transform = time.time()
  1576. # ✅ Kombinirana transformacija
  1577. total_translation = chosen_translation_vector + fine_shift
  1578. chosen_translation_vector = total_translation
  1579. vtk_transform = create_vtk_transform(chosen_rotation_matrix, chosen_translation_vector, tablefound, studyName, cbct_volume_name, scaling_factors)
  1580. combined_matrix = np.eye(4)
  1581. # 1. Rotacija
  1582. if chosen_rotation_matrix is not None:
  1583. combined_matrix[:3, :3] = chosen_rotation_matrix
  1584. # 2. Skaliranje
  1585. if scaling_factors is not None:
  1586. # Pomnoži rotacijo s skalirnim faktorjem po vsaki osi
  1587. scaled_rotation = combined_matrix[:3, :3] * scaling_factors # broadcasting vsake vrstice
  1588. combined_matrix[:3, :3] = scaled_rotation
  1589. # 3. Translacija
  1590. if chosen_translation_vector is not None:
  1591. combined_matrix[:3, 3] = chosen_translation_vector + transvector # združena translacija
  1592. cumulative_matrices[(studyName, cbct_volume_name)] = np.dot(cumulative_matrices[(studyName, cbct_volume_name)], combined_matrix)
  1593. # 🔄 Pripni transformacijo
  1594. imeTransformNoda = cbct_volume_name + " Transform"
  1595. transform_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTransformNode", imeTransformNoda)
  1596. transform_node.SetAndObserveTransformToParent(vtk_transform)
  1597. cbct_volume_node = find_volume_node_by_partial_name(cbct_volume_name)
  1598. cbct_volume_node.SetAndObserveTransformNodeID(transform_node.GetID())
  1599. # 🔨 Uporabi (ali shrani transformacijo kasneje)
  1600. slicer.vtkSlicerTransformLogic().hardenTransform(cbct_volume_node)
  1601. slicer.mrmlScene.RemoveNode(transform_node)
  1602. end_transform = time.time()
  1603. # 📍 Detekcija markerjev po transformaciji
  1604. ustvari_marker = False
  1605. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
  1606. #print("Markerji po transformaciji:\n", cbct_points, ct_points)
  1607. cbct_points = match_points(cbct_points, ct_points)
  1608. #popravek v x osi
  1609. delta_x_list = [ct[0] - cbct[0] for ct, cbct in zip(ct_points, cbct_points)]
  1610. mean_delta_x = np.mean(delta_x_list)
  1611. #popravek v y osi
  1612. delta_y_list = [ct[1] - cbct[1] for ct, cbct in zip(ct_points, cbct_points)]
  1613. mean_delta_y = np.mean(delta_y_list)
  1614. #popravek v z osi
  1615. delta_z_list = [ct[2] - cbct[2] for ct, cbct in zip(ct_points, cbct_points)]
  1616. mean_delta_z = np.mean(delta_z_list)
  1617. pre_fine_matrices = {}
  1618. pre_fine_matrices[(studyName, cbct_volume_name)] = cumulative_matrices[(studyName, cbct_volume_name)].copy()
  1619. fine_shift = np.zeros(3)
  1620. if(use_fine_shift):
  1621. # Uporabi sistematični shift za dodatno poravnavo
  1622. fine_shift = np.array([mean_delta_x, mean_delta_y, mean_delta_z])
  1623. #cbct_points_transformed += fine_shift
  1624. if fine_shift is not None:
  1625. shift_matrix = np.eye(4)
  1626. shift_matrix[:3, 3] = fine_shift
  1627. cumulative_matrices[(studyName, cbct_volume_name)] = np.dot(cumulative_matrices[(studyName, cbct_volume_name)], shift_matrix)
  1628. print(f"Fine correction shifts: ΔX={fine_shift[0]:.2f} mm, ΔY={fine_shift[1]:.2f} mm, ΔZ={fine_shift[2]:.2f} mm")
  1629. chosen_rotation_matrix = np.eye(3) #tokrat brez rotacije
  1630. #### TEST ROTACIJA ########
  1631. # angle_deg = 0
  1632. # angle_rad = np.deg2rad(angle_deg)
  1633. # chosen_rotation_matrix = np.array([
  1634. # [np.cos(angle_rad), -np.sin(angle_rad), 0],
  1635. # [np.sin(angle_rad), np.cos(angle_rad), 0],
  1636. # [0, 0, 1]
  1637. # ])
  1638. ###KONEC TESTA###
  1639. vtk_transform = create_vtk_transform(chosen_rotation_matrix, fine_shift, tablefound, studyName, cbct_volume_name) #Tukaj se tudi izpiše transformacijska matrika
  1640. # 🔄 Pripni transformacijo
  1641. imeTransformNoda = cbct_volume_name + " Transform"
  1642. transform_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLTransformNode", imeTransformNoda)
  1643. transform_node.SetAndObserveTransformToParent(vtk_transform)
  1644. cbct_volume_node = find_volume_node_by_partial_name(cbct_volume_name)
  1645. cbct_volume_node.SetAndObserveTransformNodeID(transform_node.GetID())
  1646. # 🔨 Uporabi (ali shrani transformacijo kasneje)
  1647. slicer.vtkSlicerTransformLogic().hardenTransform(cbct_volume_node)
  1648. slicer.mrmlScene.RemoveNode(transform_node)
  1649. # table_shift_matrix_ct = np.eye(4)
  1650. # table_shift_matrix_ct[1, 3] = # ali skupni_offset, če je potreben simetričen premik
  1651. # cumulative_matrices[(studyName, cbct_volume_name)] = np.dot(
  1652. # table_shift_matrix,
  1653. # cumulative_matrices[(studyName, cbct_volume_name)]
  1654. # )
  1655. #ustvari_marker = True
  1656. cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, applymarkers, intensity_threshold=3000)]
  1657. #preverjanje determinante
  1658. rot_part = combined_matrix[:3, :3]
  1659. det = np.linalg.det(rot_part)
  1660. if not np.isclose(det, 1.0, atol=0.01):
  1661. print(f"⚠️ Neortogonalna rotacija! Determinanta: {det}")
  1662. M_cbct2ct_RAS = cumulative_matrices[(studyName, cbct_volume_name)].copy()
  1663. # RAS<->LPS pretvornik
  1664. R_lps = np.diag([-1.0, -1.0, 1.0, 1.0])
  1665. # CBCT->CT v LPS
  1666. M_cbct2ct_LPS = R_lps @ M_cbct2ct_RAS @ R_lps
  1667. # Kar želiš aplicirati na CT v Ariji/Eclipse (CT->CBCT), rigid v mm, LPS:
  1668. M_ct2cbct_LPS = np.linalg.inv(M_cbct2ct_LPS)
  1669. if (resCBCT is not None) and (resCT is not None):
  1670. deltaY_RAS = float(resCBCT[0]) - float(resCT[0]) # samo mm komponenta (RAS)
  1671. else:
  1672. deltaY_RAS = 0.0
  1673. # RAS -> LPS: X in Y zamenjata predznak, Z ostane
  1674. deltaY_LPS = -deltaY_RAS
  1675. M_ct2cbct_LPS = M_ct2cbct_LPS.copy()
  1676. M_ct2cbct_LPS[1,3] += deltaY_LPS
  1677. #shrani transformacijsko matriko v datoteko
  1678. save_transform_matrix(M_ct2cbct_LPS, studyName, cbct_volume_name)
  1679. Tx, Ty, Tz = float(M_ct2cbct_LPS[0,3]), float(M_ct2cbct_LPS[1,3]), float(M_ct2cbct_LPS[2,3])
  1680. R = M_ct2cbct_LPS[:3,:3]
  1681. ry = np.degrees(np.arcsin(np.clip(-R[2,0], -1.0, 1.0)))
  1682. rx = np.degrees(np.arctan2(R[2,1], R[2,2]))
  1683. rz = np.degrees(np.arctan2(R[1,0], R[0,0]))
  1684. print(f"Eclipse 6DoF (LPS): Tx={Tx:.2f} mm, Ty={Ty:.2f} mm, Tz={Tz:.2f} mm | Rx={rx:.3f}°, Ry={ry:.3f}°, Rz={rz:.3f}°")
  1685. # M_ct2cbct_LPS že imaš
  1686. R = np.diag([-1.0, -1.0, 1.0, 1.0])
  1687. M_ct2cbct_RAS = R @ M_ct2cbct_LPS @ R # pretvorba LPS->RAS
  1688. # 1) ustvari transform node in nastavi 4x4
  1689. tNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLLinearTransformNode", "CTmove_to_CBCT")
  1690. m = vtk.vtkMatrix4x4()
  1691. for r in range(4):
  1692. for c in range(4):
  1693. m.SetElement(r, c, float(M_ct2cbct_RAS[r, c]))
  1694. tNode.SetMatrixTransformToParent(m)
  1695. # 2) apliciraj na CT in (po želji) na strukture/segmente
  1696. #ctNode = find_volume_node_by_partial_name(ct_volume_name) # ime CT-ja, ki ga že uporabljaš
  1697. #ctNode.SetAndObserveTransformNodeID(tNode.GetID())
  1698. #slicer.vtkSlicerTransformLogic().hardenTransform(ctNode)
  1699. # če imaš segmentation/RTSTRUCT kot segmentation node:
  1700. # segNode = slicer.util.getNode("Segmentation") # prilagodi ime
  1701. # segNode.SetAndObserveTransformNodeID(tNode.GetID()); slicer.vtkSlicerTransformLogic().hardenTransform(segNode)
  1702. #print("✅ CT je bil transformiran (harden) v RAS-mm po M_ct2cbct.")
  1703. #Pridobi podatke o isocentru
  1704. # iso_coords = extract_isocenter_from_loaded_rtplan()
  1705. # if iso_coords is not None:
  1706. # matrix = cumulative_matrices.get((studyName, cbct_volume_name))
  1707. # if matrix is not None:
  1708. # homogeneous_iso = np.append(iso_coords, 1.0) # Doda homogeni člen
  1709. # transformed_iso = np.dot(matrix, homogeneous_iso)
  1710. # print(f"🧭 Transformirani izocenter: {transformed_iso[:3]} mm")
  1711. # print(f"Razlika med izocentroma: ΔX={transformed_iso[0]-iso_coords[0]:.2f} mm, ΔY={transformed_iso[1]-iso_coords[1]:.2f} mm, ΔZ={transformed_iso[2]-iso_coords[2]:.2f} mm")
  1712. results_base = os.path.join(os.path.dirname(__file__), "Rezultati")
  1713. study_folder = studyName.replace('^', '_')
  1714. study_dir = os.path.join(results_base, study_folder)
  1715. os.makedirs(study_dir, exist_ok=True)
  1716. cbct_date = dicom_timestamp_from_volume(cbct_volume_node) or datetime.now().strftime("%Y%m%d_%H%M%S")
  1717. export_dir = os.path.join(study_dir, f"{cbct_date}_DICOM")
  1718. os.makedirs(export_dir, exist_ok=True)
  1719. #save_as_dicom = False
  1720. if save_as_dicom:
  1721. # Apply transform to CT
  1722. logging.info(f"[{studyName}] Start applying transform to CT volume.")
  1723. slicer.util.delayDisplay(f"[DEBUG] Start transform on CT", 1000)
  1724. apply_cumulative_transform_to_volume(ct_volume_Node, cumulative_matrices[(studyName, cbct_volume_name)])
  1725. # Convert RTSTRUCT to SegmentationNode if needed
  1726. logging.info(f"[{studyName}] Getting segmentation nodes from RTSTRUCT.")
  1727. #slicer.util.delayDisplay(f"[DEBUG] Start segmentation conversion", 1000)
  1728. #seg_nodes = convert_rtstruct_to_segmentation_nodes(ct_volume_Node)
  1729. seg_node = convert_all_models_to_segmentation(ct_volume_name, prefix="Imported_")
  1730. #new_seg_node = slicer.util.getNode(new_seg_name)
  1731. apply_cumulative_transform_to_segmentation(seg_node, cumulative_matrices[(studyName, cbct_volume_name)])
  1732. plugin = DicomRtImportExportPlugin.DicomRtImportExportPluginClass()
  1733. shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
  1734. ct_itemID = shNode.GetItemByDataNode(ct_volume_Node)
  1735. seg_itemID = shNode.GetItemByDataNode(seg_node)
  1736. cbct_item = shNode.GetItemByDataNode(cbct_volume_node)
  1737. cbct_date = shNode.GetItemAttribute(cbct_item, "DICOM.SeriesDate") or "unknownDate"
  1738. #cbct_date_acq = cbct_volume_node.GetAttribute("DICOM.AcquisitionDate") or "unknownDate"
  1739. #print(f"CBCT date (SeriesDate): {cbct_date}, AcquisitionDate: {cbct_date_acq}")
  1740. exportables = []
  1741. exportables += plugin.examineForExport(ct_itemID)
  1742. exportables += plugin.examineForExport(seg_itemID)
  1743. for exp in exportables:
  1744. # Kopiraj podatke iz volumetričnega noda v exportable
  1745. if ct_volume_Node.GetAttribute("DICOM.PatientName"):
  1746. exp.setTag("0010,0010", ct_volume_Node.GetAttribute("DICOM.PatientName")) # PatientName
  1747. if ct_volume_Node.GetAttribute("DICOM.PatientID"):
  1748. exp.setTag("0010,0020", ct_volume_Node.GetAttribute("DICOM.PatientID")) # PatientID
  1749. if ct_volume_Node.GetAttribute("DICOM.StudyInstanceUID"):
  1750. exp.setTag("0020,000D", ct_volume_Node.GetAttribute("DICOM.StudyInstanceUID")) # StudyInstanceUID
  1751. exp.directory = export_dir
  1752. print(f"[{studyName}] ✅ Export successful in [{export_dir}]")
  1753. plugin.export(exportables)
  1754. # def export_seg_as_dicom_seg(seg_node, ref_ct_node, out_dir, series_desc="AutoExport SEG"):
  1755. # # 0) Referenčna geometrija in hierarhija
  1756. # seg_node.SetReferenceImageGeometryParameterFromVolumeNode(ref_ct_node)
  1757. # sh = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
  1758. # ct_item = sh.GetItemByDataNode(ref_ct_node)
  1759. # seg_item = sh.GetItemByDataNode(seg_node)
  1760. # study_ct = sh.GetItemParent(ct_item)
  1761. # if study_ct and study_ct != sh.GetItemParent(seg_item):
  1762. # sh.SetItemParent(seg_item, study_ct)
  1763. # # 1) Poskrbi za binarni labelmap kot source (DICOM SEG to pričakuje)
  1764. # seg = seg_node.GetSegmentation()
  1765. # if not seg.ContainsRepresentation("Binary labelmap"):
  1766. # seg.CreateRepresentation("Binary labelmap")
  1767. # seg.SetSourceRepresentationName("Binary labelmap")
  1768. # # 2) Eksplicitno nastavi DICOM reference (pomoč, če manjka v SH atributih)
  1769. # ct_series_uid = ref_ct_node.GetAttribute("DICOM.SeriesInstanceUID") or ""
  1770. # ct_for_uid = ref_ct_node.GetAttribute("DICOM.FrameOfReferenceUID") or ""
  1771. # if ct_series_uid:
  1772. # sh.SetItemAttribute(seg_item, "DICOM.ReferencedInstanceUIDs", ct_series_uid)
  1773. # if ct_for_uid:
  1774. # seg_node.SetAttribute("DICOM.FrameOfReferenceUID", ct_for_uid)
  1775. # # 3) IZVOZ (brez dodatnih modulov)
  1776. # logic = slicer.modules.segmentations.logic()
  1777. # ok = logic.ExportAllSegmentsToDICOMSegmentation(seg_node, ref_ct_node, out_dir, series_desc)
  1778. # print(f"✅ DICOM SEG export status: {ok} → {out_dir}")
  1779. # return ok
  1780. #export_seg_as_dicom_seg(seg_node, ct_volume_Node, export_dir)
  1781. # 📏 Izračun napake
  1782. errors = [np.linalg.norm(cbct - ct) for cbct, ct in zip(cbct_points, ct_points)]
  1783. mean_error = np.mean(errors)
  1784. ct_pts = np.array(ct_points)
  1785. cbct_pts = np.array(cbct_points)
  1786. ct_centroid = ct_pts.mean(axis=0)
  1787. cbct_centroid = cbct_pts.mean(axis=0)
  1788. centroid_delta = cbct_centroid - ct_centroid
  1789. centroid_norm = float(np.linalg.norm(centroid_delta))
  1790. print("Total Individual errors:", errors)
  1791. print("Average error: {:.2f} mm".format(mean_error))
  1792. for i, (cbct, ct) in enumerate(zip(cbct_points, ct_points)):
  1793. diff = np.array(cbct) - np.array(ct)
  1794. print(f"Specific marker errors {i+1}: ΔX={diff[0]:.2f} mm, ΔY={diff[1]:.2f} mm, ΔZ={diff[2]:.2f} mm")
  1795. if len(ct_pts) == 3 and len(cbct_pts) == 3 or len(ct_pts) == 4 and len(cbct_pts) == 4:
  1796. sim = triangle_similarity(ct_pts, cbct_pts)
  1797. print("\n Triangle similarity analysis:")
  1798. print(f"Centroid Δ: ΔX={centroid_delta[0]:.2f} mm, ΔY={centroid_delta[1]:.2f} mm, ΔZ={centroid_delta[2]:.2f} mm | |Δ|={centroid_norm:.2f} mm")
  1799. print(f"Side length differences (mm): {sim['side_length_diff_mm']}")
  1800. print(f"Angle differences (deg): {sim['angle_diff_deg']}")
  1801. print(f"Mean length error: {sim['mean_length_error_mm']:.2f} mm")
  1802. print(f"Mean angle error: {sim['mean_angle_error_deg']:.2f}°")
  1803. print(f"Triangle Similarity Ratio (TSR): {sim['TSR']:.3f}")
  1804. else:
  1805. print("⚠️ Za primerjavo trikotnikov potrebne točno 3 točke.")
  1806. if writefilecheck:
  1807. errorsfile = os.path.join(study_dir, f"{cbct_date}_errors.csv")
  1808. with open(errorsfile, mode='a', newline='') as file:
  1809. writer = csv.writer(file)
  1810. # Glava za študijo
  1811. writer.writerow(["Study", studyName])
  1812. writer.writerow(["Total Individual Errors (mm)"])
  1813. for error in errors:
  1814. writer.writerow(["", f"{error:.2f}"])
  1815. writer.writerow(["Average Error (mm)", f"{mean_error:.2f}"])
  1816. # Specifične napake markerjev
  1817. writer.writerow(["Specific Marker Errors"])
  1818. writer.writerow(["Marker", "ΔX (mm)", "ΔY (mm)", "ΔZ (mm)"])
  1819. for i, (cbct, ct) in enumerate(zip(cbct_points, ct_points)):
  1820. diff = np.array(cbct) - np.array(ct)
  1821. writer.writerow([f"Marker {i+1}", f"{diff[0]:.2f}", f"{diff[1]:.2f}", f"{diff[2]:.2f}"])
  1822. if len(cbct_points) == 3 and len(ct_points) == 3:
  1823. writer.writerow([])
  1824. writer.writerow(["Triangle Similarity"])
  1825. writer.writerow(["Side Length Diff (mm)", *[f"{v:.2f}" for v in sim["side_length_diff_mm"]]])
  1826. writer.writerow(["Angle Diff (deg)", *[f"{v:.2f}" for v in sim["angle_diff_deg"]]])
  1827. writer.writerow(["Mean Length Error (mm)", f"{sim['mean_length_error_mm']:.2f}"])
  1828. writer.writerow(["Mean Angle Error (deg)", f"{sim['mean_angle_error_deg']:.2f}"])
  1829. writer.writerow(["Triangle Similarity Ratio (TSR)", f"{sim['TSR']:.3f}"])
  1830. writer.writerow(["Centroid (CT) X/Y/Z (mm)", f"{ct_centroid[0]:.2f}", f"{ct_centroid[1]:.2f}", f"{ct_centroid[2]:.2f}"])
  1831. writer.writerow(["Centroid (CBCT) X/Y/Z (mm)", f"{cbct_centroid[0]:.2f}", f"{cbct_centroid[1]:.2f}", f"{cbct_centroid[2]:.2f}"])
  1832. writer.writerow(["Centroid Δ X/Y/Z (mm) | |Δ|", f"{centroid_delta[0]:.2f}", f"{centroid_delta[1]:.2f}", f"{centroid_delta[2]:.2f}", f"{centroid_norm:.2f}"])
  1833. writer.writerow([])
  1834. writer.writerow([])
  1835. #writer.writerow(["Isocenter at: ", transformed_iso[:3]])
  1836. graphs_dir = os.path.join(results_base, study_folder)
  1837. os.makedirs(graphs_dir, exist_ok=True)
  1838. pngfile = os.path.join(graphs_dir, f"{cbct_date}_trikotnika.png")
  1839. save_triangle_visualization(np.array(ct_points), np.array(cbct_points), pngfile)
  1840. else:
  1841. print(f"Study {studyItem} doesn't have any appropriate CT or CBCT volumes.")
  1842. continue
  1843. study_end_time = time.time()
  1844. timing_data = {
  1845. "IO": end_io - start_io,
  1846. "Fixing": fixing_end - fixing,
  1847. "Table": ((table1end_time - table1_time)+(table2end_time - table2_time)) if tablefind else 0,
  1848. "Scaling": end_scaling - start_scaling,
  1849. "CentroidAlign": end_align - start_align,
  1850. "Rotation": end_rotation - start_rotation,
  1851. "Translation": end_translation - start_translation,
  1852. "Transform": end_transform - start_transform,
  1853. "Total": study_end_time - study_start_time}
  1854. update_timing_csv(timing_data, studyName)
  1855. print(f"Timing data for pacient: {timing_data}")
  1856. # Izpis globalne statistike
  1857. start_save = time.time()
  1858. if writefilecheck:
  1859. #print("Distances between CT & CBCT markers: ", ctcbct_distance)
  1860. #print("Distances between pairs of markers for each volume: ", prostate_size_est)
  1861. # Define file paths
  1862. prostate_size_file = os.path.join(os.path.dirname(__file__), "prostate_size.csv")
  1863. ctcbct_distance_file = os.path.join(os.path.dirname(__file__), "ct_cbct_distance.csv")
  1864. # Write prostate size data
  1865. with open(prostate_size_file, mode='w', newline='') as file:
  1866. writer = csv.writer(file)
  1867. writer.writerow(["Prostate Size"])
  1868. for size in prostate_size_est:
  1869. writer.writerow([size])
  1870. #print("Prostate size file written at ", prostate_size_file)
  1871. # Write CT-CBCT distance data
  1872. with open(ctcbct_distance_file, mode='w', newline='') as file:
  1873. writer = csv.writer(file)
  1874. writer.writerow(["CT-CBCT Distance"])
  1875. for distance in ctcbct_distance:
  1876. writer.writerow([distance])
  1877. #print("CT-CBCT distance file written at ", ctcbct_distance_file)
  1878. end_save = time.time()
  1879. print(f"Saving time: {end_save - start_save:.2f} seconds")
  1880. end_time = time.time()
  1881. # Calculate and print elapsed time
  1882. elapsed_time = end_time - start_time
  1883. # Convert to minutes and seconds
  1884. minutes = int(elapsed_time // 60)
  1885. seconds = elapsed_time % 60
  1886. print(f"Execution time: {minutes} minutes and {seconds:.6f} seconds")