Explorar o código

Ena končnih verzij, vključuje opcijo izvoz v DICOM datoteko in predporavnavo glede na višino CT slike. Dodal preverjanje podatkov in opozorila. Segmentacije izvožene iz Eclipsa je potrebno ročno pretvoriti v modele.

Luka hai 2 semanas
pai
achega
f7096ca748
Modificáronse 1 ficheiros con 576 adicións e 103 borrados
  1. 576 103
      SeekTransformModule/SeekTransformModule.py

+ 576 - 103
SeekTransformModule/SeekTransformModule.py

@@ -5,8 +5,11 @@ import re
 from scipy.spatial.distance import cdist
 from scipy.spatial.transform import Rotation as R
 import slicer
+import slicer.util
 import itertools
+import DICOMLib
 from DICOMLib import DICOMUtils
+import DicomRtImportExportPlugin
 from collections import deque, Counter
 import vtk
 from slicer.ScriptedLoadableModule import *
@@ -14,6 +17,11 @@ import qt
 import matplotlib.pyplot as plt
 import csv
 import time
+import logging
+import matplotlib
+matplotlib.use('Agg')  # << to dodaš ZGORAJ, da omogoči PNG zapis brez GUI
+from mpl_toolkits.mplot3d import Axes3D  
+
 
 #exec(open("C:/Users/lkomar/Documents/Prostata/FirstTryRegister.py").read())
 
@@ -57,12 +65,21 @@ class SeekTransformModuleWidget(ScriptedLoadableModuleWidget):
         self.layout.addWidget(self.translationCheckBox)
 
         self.markersCheckBox = qt.QCheckBox("Place control points for detected markers")
-        self.markersCheckBox.setChecked(True)
+        self.markersCheckBox.setChecked(False)
         self.layout.addWidget(self.markersCheckBox)
         
-        self.writefileCheckBox = qt.QCheckBox("Write distances to csv file")
+        self.writefileCheckBox = qt.QCheckBox("Write data to csv file")
         self.writefileCheckBox.setChecked(True)
         self.layout.addWidget(self.writefileCheckBox)
+        
+        self.tableCheckBox = qt.QCheckBox("Find top of the table and match height")
+        self.tableCheckBox.setChecked(True)
+        self.layout.addWidget(self.tableCheckBox)
+        
+        self.DicomCheckBox = qt.QCheckBox("Save transformed DICOM")
+        self.DicomCheckBox.setChecked(True)
+        self.layout.addWidget(self.DicomCheckBox)
+
 
         # Load button
         self.applyButton = qt.QPushButton("Find markers and transform")
@@ -76,6 +93,15 @@ class SeekTransformModuleWidget(ScriptedLoadableModuleWidget):
         self.layout.addStretch(1)
 
     def onApplyButton(self):
+                # Nastavi globalni logger
+        log_file_path = os.path.join("C:/Users/lkomar/Documents/Prostata", "seektransform_log.txt")
+        logging.basicConfig(filename=log_file_path, level=logging.INFO, format='%(asctime)s - %(message)s')
+        
+        try:
+            logging.info("▶️ onApplyButton pressed.")
+        except Exception as e:
+            print("❌ Logging setup failed:", e)
+        
         logic = MyTransformModuleLogic()
         selectedMethod = self.rotationMethodComboBox.currentText  # izberi metodo izračuna rotacije
 
@@ -85,9 +111,11 @@ class SeekTransformModuleWidget(ScriptedLoadableModuleWidget):
         applyScaling = self.scalingCheckBox.isChecked()
         applyMarkers = self.markersCheckBox.isChecked()
         writefilecheck = self.writefileCheckBox.isChecked()
+        tablefind = self.tableCheckBox.isChecked()
+        saveasdicom = self.DicomCheckBox.isChecked()
 
         # Pokliči logiko z izbranimi nastavitvami
-        logic.run(selectedMethod, applyRotation, applyTranslation, applyScaling, applyMarkers, writefilecheck)
+        logic.run(selectedMethod, applyRotation, applyTranslation, applyScaling, applyMarkers, writefilecheck, tablefind, saveasdicom)
 
 
 class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
@@ -96,9 +124,12 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
     """
     
     
-    def run(self, selectedMethod, applyRotation, applyTranslation, applyScaling, applymarkers, writefilecheck):
+    def run(self, selectedMethod, applyRotation, applyTranslation, applyScaling, applymarkers, writefilecheck, tablefind, save_as_dicom):
         start_time = time.time()
         print("Calculating...")
+        #slicer.util.delayDisplay(f"Starting", 1000)
+
+        
         
         def group_points(points, threshold):
             # Function to group points that are close to each other
@@ -326,7 +357,7 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
 
             return moving, R, t
 
-        def match_points(cbct_points, ct_points, auto_weights=True, fallback_if_worse=True, normalize_lengths=True, normalize_angles=False, min_distance=5):
+        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):
 
             def side_lengths(points):
                 lengths = [
@@ -349,7 +380,7 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                 norm = np.linalg.norm(vec)
                 return [v / norm for v in vec] if norm > 0 else vec
 
-            def permutation_score(perm, ct_lengths, ct_angles, w_len, w_ang):
+            def permutation_score(perm, ct_lengths, ct_angles, w_len, w_ang, penalty_angle_thresh=np.deg2rad(10)):
                 perm_lengths = side_lengths(perm)
                 perm_angles = triangle_angles(perm)
 
@@ -365,10 +396,34 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
 
                 score_len = sum(abs(a - b) for a, b in zip(lengths_1, lengths_2))
                 score_ang = sum(abs(a - b) for a, b in zip(angles_1, angles_2))
-
-                return w_len * score_len + w_ang * score_ang
-
+                
+                order_penalty = order_mismatch_penalty(ct_points, perm, axis='z')
+                return w_len * score_len + w_ang * score_ang + order_penalty + w_order * order_penalty
+                
+            def smart_sort_cbct_points(cbct_points, z_threshold=5.0):
+                """
+                Sortira točke tako, da poskusi najprej po Z. Če so razlike po Z manjše
+                od praga, sortira po (Y, X), sicer sortira po Z.
+                """
+                z_values = [pt[2] for pt in cbct_points]
+                z_range = max(z_values) - min(z_values)
+
+                if z_range < z_threshold:
+                    # Sortiraj po Y, nato X (če so točke v isti ravnini po Z)
+                    return sorted(cbct_points, key=lambda pt: (pt[1], pt[0]))
+                else:
+                    # Sortiraj po Z, nato Y, nato X
+                    return sorted(cbct_points, key=lambda pt: (pt[2], pt[1], pt[0]))
+            
+            def order_mismatch_penalty(ct_points, perm, axis='z'):
+                axis_idx = {'x': 0, 'y': 1, 'z': 2}[axis]
+                ct_sorted = np.argsort([pt[axis_idx] for pt in ct_points])  
+                perm_sorted = np.argsort([pt[axis_idx] for pt in perm])
+                return sum(1 for a, b in zip(ct_sorted, perm_sorted) if a != b)
+            
+            
             cbct_points = list(cbct_points)
+            print("CBCT points:", cbct_points)
             ct_lengths = side_lengths(np.array(ct_points))
             ct_angles = triangle_angles(np.array(ct_points))
 
@@ -379,30 +434,37 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                 weight_length = (1 - var_len / total_var)
                 weight_angle = (1 - var_ang / total_var)
             else:
-                weight_length = 0.5
-                weight_angle = 0.5
+                weight_length = 0.8
+                weight_angle = 0.2
 
-            best_perm = None
+            cbct_sorted = smart_sort_cbct_points(cbct_points)
+            original_score = permutation_score(np.array(cbct_sorted), ct_lengths, ct_angles, weight_length, weight_angle)
+
+            # Če je ta rezultat dovolj dober, uporabi
             best_score = float('inf')
-            #print(f"CT points: {ct_points}")
+            best_perm = None
+
+            if original_score < float('inf'):  # lahko dodaš prag če želiš
+                best_score = original_score
+                best_perm = np.array(cbct_sorted)
+
+            # Nato preveri vse permutacije (vključno s prvotnim vrstnim redom, če fallback_if_worse=True)
             for perm in itertools.permutations(cbct_points):
                 perm = np.array(perm)
                 score = permutation_score(perm, ct_lengths, ct_angles, weight_length, weight_angle)
-
-                #print(f"Perm: {perm.tolist()} -> Score: {score:.4f}")
-
                 if score < best_score:
                     best_score = score
                     best_perm = perm
+                    print(f"New best permutation found with perm: {perm}")
 
             #print("CT centroid:", np.mean(ct_points, axis=0))
             #print("CBCT centroid (best perm):", np.mean(best_perm, axis=0))
             
             if fallback_if_worse:
-                original_score = permutation_score(np.array(cbct_points), ct_lengths, ct_angles, weight_length, weight_angle)
-                #print("Original score: ", original_score)
+                #original_score = permutation_score(np.array(cbct_points), ct_lengths, ct_angles, weight_length, weight_angle)
+                print("Original score: ", original_score)
                 if original_score <= best_score:
-                    #print("Fallback to original points due to worse score of the permutation.")
+                    print("Fallback to original points due to worse score of the permutation.")
                     return list(cbct_points)
 
             return list(best_perm)
@@ -475,20 +537,8 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                 cumulative_matrices[key] = np.dot(cumulative_matrices[key], affine_matrix)
 
             return transform
-        
-        def matrix_to_array(vtk_transform):
-            """
-            Converts a vtkTransform to a 4x4 numpy array.
-            """
-            vtk_matrix = vtk.vtkMatrix4x4()
-            vtk_transform.GetMatrix(vtk_matrix)
-            np_matrix = np.zeros((4, 4))
-            for i in range(4):
-                for j in range(4):
-                    np_matrix[i, j] = vtk_matrix.GetElement(i, j)
-            return np_matrix
-        
-        def save_transform_matrix(matrix, study_name, cbct_volume_name, ct_table_found):
+
+        def save_transform_matrix(matrix, study_name, cbct_volume_name):
             """
             Appends the given 4x4 matrix to a text file under the given study folder.
             """
@@ -497,17 +547,13 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
             os.makedirs(study_folder, exist_ok=True)  # Create folders if they don't exist
             safe_cbct_name = re.sub(r'[<>:"/\\|?*]', '_', cbct_volume_name)
             # Preveri ali je CT miza najdena
-            if ct_table_found:
-                safe_cbct_name += "_MizaPoravnava"
-            else:
-                safe_cbct_name += "_NIMizaPoravnana"
             filename = os.path.join(study_folder, f"{safe_cbct_name}.txt")
-            with open(filename, "a") as f:
+            with open(filename, "w") as f:
                 #f.write("Transformacija:\n")
                 for row in matrix:
                     f.write(" ".join(f"{elem:.6f}" for elem in row) + "\n")
                 f.write("\n")  # Dodaj prazen vrstico med transformacijami
-            print(f"Transform matrix saved to {filename}")
+            #print(f"Transform matrix saved to {filename}")
                 
         def detect_points_region_growing(volume_name, yesCbct, create_marker, intensity_threshold=3000, x_min=90, x_max=380, y_min=190, y_max=380, z_min=50, z_max=140, max_distance=9, centroid_merge_threshold=5):
             volume_node = find_volume_node_by_partial_name(volume_name)
@@ -650,7 +696,7 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
             if candidate_y_values:
                 most_common_y, _ = Counter(candidate_y_values).most_common(1)[0]
                 table_top_y = most_common_y
-                print(f"✅ Rob mize (najpogostejši Y): {table_top_y}, pojavitev: {candidate_y_values.count(table_top_y)}×")
+                print(f"✅ Rob mize (najpogostejši Y): {table_top_y}, pojavitev: {candidate_y_values.count(table_top_y)}/11")
 
             
             """ # --- Poišči skok navzdol pod prag (od spodaj navzgor) ---
@@ -688,7 +734,6 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
 
             return table_ras[1], table_top_y
 
-        
         def align_cbct_to_ct(volumeNode, scan_type, offset, CT_offset=None, CT_spacing=None):
             """
             Aligns CBCT volume to CT volume based on height offset.
@@ -715,7 +760,7 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                 CBCT_offset = offset
 
                 # Razlika v mm brez skaliranja na CBCT_spacing
-                alignment_offset_mm = CT_offset - CBCT_offset
+                alignment_offset_mm = CT_offset
 
                 #print(f"CT offset: {CT_offset}, CBCT offset: {CBCT_offset}")
                 #print(f"CT spacing: {CT_spacing} mm/voxel, CBCT spacing: {volumeNode.GetSpacing()[1]} mm/voxel")
@@ -876,9 +921,6 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
             """
             Odstrani točko, ki je najnižja po dani osi (default Y-os v RAS prostoru).
             """
-            if len(points) <= 3:
-                return points  # Ni potrebe po brisanju
-
             arr = np.array(points)
             lowest_index = np.argmin(arr[:, axis])
             removed = points.pop(lowest_index)
@@ -890,7 +932,7 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
             file_exists = os.path.isfile(file_path)
 
             with open(file_path, mode='a', newline='') as csvfile:
-                fieldnames = ["Study", "IO", "Fixing", "Scaling", "CentroidAlign", "Rotation", "Translation", "Transform", "FileSave", "Total"]
+                fieldnames = ["Study", "IO", "Fixing", "Table", "Scaling", "CentroidAlign", "Rotation", "Translation", "Transform", "FileSave", "Total"]
                 writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
 
                 if not file_exists:
@@ -906,6 +948,248 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                     return node
             raise RuntimeError(f"❌ Volume with name containing '{partial_name}' not found.")
                 
+        def convert_rtstruct_to_segmentation_nodes(ct_volume_node):
+            shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+            segmentation_nodes = []
+
+            # ✅ Najdi vse SegmentationNode-e, ki imajo segmente
+            for seg_node in slicer.util.getNodesByClass("vtkMRMLSegmentationNode"):
+                num_segments = seg_node.GetSegmentation().GetNumberOfSegments()
+                if num_segments == 0:
+                    continue
+
+                # Če še nima referenceVolume, jo nastavimo
+                if not seg_node.GetNodeReferenceID("referenceVolume"):
+                    seg_node.SetReferenceImageGeometryParameterFromVolumeNode(ct_volume_node)
+                    seg_node.SetNodeReferenceID("referenceVolume", ct_volume_node.GetID())
+                    print(f"🔗 Nastavljena referenca na CT za: {seg_node.GetName()}")
+
+                segmentation_nodes.append(seg_node)
+                print(f"📎 Najdena segmentacija z vsebino: {seg_node.GetName()}, segmentov: {num_segments}")
+
+            if segmentation_nodes:
+                return segmentation_nodes
+
+            for item_id in range(shNode.GetNumberOfItems()):
+                modality = shNode.GetItemAttribute(item_id, "DICOM.Modality")
+                if modality != "RTSTRUCT":
+                    continue
+
+                rtstruct_node = shNode.GetItemDataNode(item_id)
+                if not rtstruct_node:
+                    continue
+
+                print(f"📎 Najden RTSTRUCT: {rtstruct_node.GetName()}")
+
+                # Ustvari nov SegmentationNode
+                seg_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", f"Seg_{rtstruct_node.GetName()}")
+                seg_node.SetReferenceImageGeometryParameterFromVolumeNode(ct_volume_node)
+                seg_node.SetNodeReferenceID("referenceVolume", ct_volume_node.GetID())
+
+
+
+                # Najdi child elemente v SubjectHierarchy (strukture)
+                rtstruct_item_id = shNode.GetItemByDataNode(rtstruct_node)
+                structure_ids = vtk.vtkIdList()
+                shNode.GetItemChildren(rtstruct_item_id, structure_ids)
+
+                segment_count = 0
+
+                for i in range(structure_ids.GetNumberOfIds()):
+                    structure_item_id = structure_ids.GetId(i)
+                    name = shNode.GetItemName(structure_item_id)
+                    associated_node = shNode.GetItemDataNode(structure_item_id)
+                    if associated_node and associated_node.IsA("vtkMRMLModelNode"):
+                        print(f" ➕ Dodajam strukturo: {name}")
+                        slicer.modules.segmentations.logic().ImportModelToSegmentationNode(associated_node, seg_node)
+                        seg_node.GetSegmentation().GetSegment(seg_node.GetSegmentation().GetNumberOfSegments() - 1).SetName(name)
+                        segment_count += 1
+
+                # Če ni bilo nič dodano iz SH: poskusi uvoziti modele iz scene
+                if segment_count == 0:
+                    print("⚠️ RTSTRUCT nima struktur v SubjectHierarchy – poskus uvoza vseh modelov iz scene.")
+                    for model_node in slicer.util.getNodesByClass("vtkMRMLModelNode"):
+                        if "RTSTRUCT" in model_node.GetName().upper() or model_node.GetName().startswith("Model"):
+                            print(f" ➕ [fallback] Uvoz modela: {model_node.GetName()}")
+                            slicer.modules.segmentations.logic().ImportModelToSegmentationNode(model_node, seg_node)
+                            seg_node.GetSegmentation().GetSegment(seg_node.GetSegmentation().GetNumberOfSegments() - 1).SetName(model_node.GetName())
+                
+                print(f"📊 Segmentov v {seg_node.GetName()}: {seg_node.GetSegmentation().GetNumberOfSegments()}")
+                segmentation_nodes.append(seg_node)
+
+            return segmentation_nodes
+
+        def apply_cumulative_transform_to_segmentation(segmentation_node, matrix):
+            transform = vtk.vtkTransform()
+            vtk_matrix = vtk.vtkMatrix4x4()
+            for i in range(4):
+                for j in range(4):
+                    vtk_matrix.SetElement(i, j, matrix[i, j])
+            transform.SetMatrix(vtk_matrix)
+
+            transform_node = slicer.vtkMRMLTransformNode()
+            slicer.mrmlScene.AddNode(transform_node)
+            transform_node.SetAndObserveTransformToParent(transform)
+            segmentation_node.SetAndObserveTransformNodeID(transform_node.GetID())
+
+            slicer.vtkSlicerTransformLogic().hardenTransform(segmentation_node)
+            slicer.mrmlScene.RemoveNode(transform_node)
+
+        def convert_all_models_to_segmentation(reference_volume_name: str, prefix: str = "Imported_"):
+            """
+            Pretvori vse modele (ModelNode) v sceni v enoten vtkMRMLSegmentationNode.
+
+            📥 VHODI:
+            ----------
+            reference_volume_name : str
+                Ime obstoječega CT volumna (npr. "CT_1"), ki določa geometrijo za segmentacijo.
+                Ta volumen mora biti že naložen v sceni.
+
+            prefix : str
+                Predpona za ime novega segmentation noda (npr. "Imported_").
+                Ime novega noda bo nekaj kot: "Imported_Segmentation".
+
+            📤 IZHOD:
+            ----------
+            segmentation_node : vtkMRMLSegmentationNode
+                Nov nod, ki vsebuje en segment za vsak najden model v sceni.
+                Ta segmentacijski nod je pripravljen za transformacijo in DICOM export (vsebuje BinaryLabelmap).
+            """
+            import slicer
+
+            # Pridobi referenčni volumen (CT)
+            reference_volume = slicer.util.getNode(reference_volume_name)
+
+            # Ustvari nov segmentacijski nod
+            segmentation_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode", prefix + "Segmentation")
+            segmentation_node.SetReferenceImageGeometryParameterFromVolumeNode(reference_volume)
+            segmentation_node.SetNodeReferenceID("referenceVolume", reference_volume.GetID())
+
+            # Najdi vse modele
+            model_nodes = slicer.util.getNodesByClass("vtkMRMLModelNode")
+            #print(f"📦 Najdenih modelov: {len(model_nodes)}")
+            
+            for model_node in model_nodes:
+                name = model_node.GetName()
+                #print(f"🔍 Model: {name}")
+                skip = (
+                    name.lower().startswith("segmentation")
+                    or name.lower().startswith("surface")
+                    or name.lower() in ["red volume slice", "green volume slice", "yellow volume slice"]
+                    or "rtstruct" in name.lower()
+                )
+                if skip:
+                    continue
+
+
+                # Uvozi model kot segment
+                success = slicer.modules.segmentations.logic().ImportModelToSegmentationNode(model_node, segmentation_node)
+                if not success:
+                    #print(f"✅ Model '{name}' uvožen kot segment.")
+                    print(f"❌ Napaka pri uvozu modela: {name}")
+                    
+
+            # Ustvari BinaryLabelmap reprezentacijo (nujno za DICOM export)
+            created = segmentation_node.GetSegmentation().CreateRepresentation("BinaryLabelmap")
+            if created:
+                print("✅ BinaryLabelmap reprezentacija uspešno ustvarjena.")
+                segmentation_node.GetSegmentation().SetMasterRepresentationName("BinaryLabelmap")
+            #else:
+                #print("❌ Pretvorba v BinaryLabelmap ni uspela.")
+
+            return segmentation_node
+
+        def apply_cumulative_transform_to_volume(volume_node, matrix):
+            transform = vtk.vtkTransform()
+            vtk_matrix = vtk.vtkMatrix4x4()
+            for i in range(4):
+                for j in range(4):
+                    vtk_matrix.SetElement(i, j, matrix[i, j])
+            transform.SetMatrix(vtk_matrix)
+
+            transform_node = slicer.vtkMRMLTransformNode()
+            slicer.mrmlScene.AddNode(transform_node)
+            transform_node.SetAndObserveTransformToParent(transform)
+            volume_node.SetAndObserveTransformNodeID(transform_node.GetID())
+
+            slicer.vtkSlicerTransformLogic().hardenTransform(volume_node)
+            slicer.mrmlScene.RemoveNode(transform_node)
+
+        def triangle_similarity(p1, p2):
+            """
+            Primerja dva trikotnika (vsak definiran s 3 točkami v 3D) na podlagi:
+            - razmerij dolžin
+            - razlik med koti
+
+            :param p1: seznam treh točk (npr. ct_points)
+            :param p2: seznam treh točk (npr. cbct_points)
+            :return: dict z razliko dolžin, razliko kotov, povprečno napako
+            """
+            def side_lengths(pts):
+                a = np.linalg.norm(pts[1] - pts[0])
+                b = np.linalg.norm(pts[2] - pts[1])
+                c = np.linalg.norm(pts[0] - pts[2])
+                return np.array([a, b, c])
+
+            def angles(pts):
+                # uporabimo kosinusni izrek
+                a, b, c = side_lengths(pts)
+                angle_A = np.arccos((b**2 + c**2 - a**2) / (2*b*c))
+                angle_B = np.arccos((a**2 + c**2 - b**2) / (2*a*c))
+                angle_C = np.pi - angle_A - angle_B
+                return np.degrees([angle_A, angle_B, angle_C])
+
+            l1 = side_lengths(p1)
+            l2 = side_lengths(p2)
+            a1 = angles(p1)
+            a2 = angles(p2)
+
+            length_diff = np.abs(l1 - l2)
+            angle_diff = np.abs(a1 - a2)
+
+            return {
+                "side_length_diff_mm": length_diff,
+                "angle_diff_deg": angle_diff,
+                "mean_length_error_mm": np.mean(length_diff),
+                "mean_angle_error_deg": np.mean(angle_diff),
+                "TSR": 1 / (1 + np.mean(length_diff) + np.mean(angle_diff) / 10)
+            }
+
+        def save_triangle_visualization(ct_pts, cbct_pts, outpath):
+            fig = plt.figure(figsize=(10, 8))
+            ax = fig.add_subplot(111, projection='3d')
+
+            ct_closed = np.vstack([ct_pts, ct_pts[0]])
+            cbct_closed = np.vstack([cbct_pts, cbct_pts[0]])
+
+            ax.plot(ct_closed[:, 0], ct_closed[:, 1], ct_closed[:, 2], 'b-', label='CT trikotnik')
+            ax.scatter(ct_pts[:, 0], ct_pts[:, 1], ct_pts[:, 2], c='blue')
+
+            ax.plot(cbct_closed[:, 0], cbct_closed[:, 1], cbct_closed[:, 2], 'r--', label='CBCT trikotnik')
+            ax.scatter(cbct_pts[:, 0], cbct_pts[:, 1], cbct_pts[:, 2], c='red')
+
+            ct_centroid = np.mean(ct_pts, axis=0)
+            cbct_centroid = np.mean(cbct_pts, axis=0)
+            ax.scatter(*ct_centroid, c='blue', marker='x', s=60)
+            ax.scatter(*cbct_centroid, c='red', marker='x', s=60)
+
+            
+            ax.set_title("Trikotnik markerjev: CT (modro) vs CBCT (rdeče)")
+            ax.set_xlabel('X (mm)')
+            ax.set_ylabel('Y (mm)')
+            ax.set_zlabel('Z (mm)')
+            ax.legend()
+            ax.view_init(elev=20, azim=30)
+            plt.tight_layout()
+
+            try:
+                fig.savefig(outpath)
+                print(f"🖼 Triangle visualization saved to: {outpath}")
+            except Exception as e:
+                print(f"❌ Failed to save triangle visualization: {e}")
+            finally:
+                plt.close(fig)
+            
         # Globalni seznami za končno statistiko
         prostate_size_est = []
         ctcbct_distance = []
@@ -917,6 +1201,9 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
         studyItems = vtk.vtkIdList()
         shNode.GetItemChildren(shNode.GetSceneItemID(), studyItems)
         
+        #slicer.util.delayDisplay(f"[DEBUG] Starting the loops", 1000)
+
+        
         for i in range(studyItems.GetNumberOfIds()):
             study_start_time = time.time()
             start_io = time.time()
@@ -998,6 +1285,21 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                     # Detekcija točk v volumnu
                     ustvari_marker = not yesCbct  # Ustvari markerje pred poravnavo na mizo
                     grouped_points = detect_points_region_growing(volumeName, yesCbct, ustvari_marker, intensity_threshold=3000)
+                    if grouped_points is None or len(grouped_points) < 3:
+                        print(f"⚠️ Volume {volumeName} doesn't have enough points for registration. Points: {len(grouped_points)}")
+                        continue
+                    if not yesCbct:
+                        
+                        # loči koordinate in intenzitete
+                        coords_only = [pt for pt, _ in grouped_points]
+                        intensities = [intensity for _, intensity in grouped_points]
+
+                        # permutiraj koordinate (npr. zaradi boljšega ujemanja)
+                        coords_sorted = match_points(coords_only, coords_only)
+
+                        # ponovno sestavi pare (točka, intenziteta)
+                        grouped_points = list(zip(coords_sorted, intensities))
+                        
                     #print(f"Populating volume_points_dict with key ('{scan_type}', '{volumeName}')")
                     volume_points_dict[(scan_type, volumeName)] = grouped_points
                     #print(volume_points_dict)  # Check if the key is correctly added
@@ -1005,31 +1307,47 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
             # Če imamo oba tipa volumna (CBCT in CT) **znotraj istega studyja**
             end_io = time.time()
             if cbct_list and ct_list:
+                fixing = fixing_end = 0
+                table1_time = table1end_time = 0
+                table2_time = table2end_time = 0
+                start_scaling = end_scaling = 0
+                start_align = end_align = 0
+                start_rotation = end_rotation = 0
+                start_translation = end_translation = 0
+                start_transform = end_transform = 0
+                study_start_time = study_end_time = 0
+                
                 ct_volume_name = ct_list[0]  # Uporabi prvi CT kot referenco
                 ct_volume_Node = find_volume_node_by_partial_name(ct_volume_name)
                 print(f"\nProcessing CT: {ct_volume_name}")
                 yesCbct = False
-                makemarkerscheck = True                               
-                result = find_table_top_z(ct_volume_name, writefilecheck, makemarkerscheck, yesCbct)
-                if result is not None:
-                    mm_offset, pixel_offset = result
-                    ct_table_found = True
-                    #print(f"✔️ Poravnava z višino mize: ΔY = {mm_offset:.2f} mm")
-                    # Dodaj ΔY k translaciji ali transformaciji po potrebi
-                else:
-                    print("⚠️ Table top not found – continue without correction on Y axis.")
-                    mm_offset = 0.0  # ali None, če želiš eksplicitno ignorirati
-                    ct_table_found = False
-                CT_offset, CT_spacing = align_cbct_to_ct(ct_volume_Node, "CT", mm_offset)                                    
+                if(tablefind):
+                    table1_time = time.time()
+                    makemarkerscheck = True                               
+                    result = find_table_top_z(ct_volume_name, writefilecheck, makemarkerscheck, yesCbct)
+                    if result is not None:
+                        mm_offset, pixel_offset = result
+                        ct_table_found = True
+                        #print(f"✔️ Poravnava z višino mize: ΔY = {mm_offset:.2f} mm")
+                        # Dodaj ΔY k translaciji ali transformaciji po potrebi
+                    else:
+                        print("⚠️ Table top not found – continue without correction on Y axis.")
+                        mm_offset = 0.0  # ali None, če želiš eksplicitno ignorirati
+                        ct_table_found = False
+                    CT_offset, CT_spacing = align_cbct_to_ct(ct_volume_Node, "CT", mm_offset)
+                    table1end_time = time.time()                                    
                      
                 
                 ct_points = [centroid for centroid, _ in volume_points_dict[("CT", ct_volume_name)]]
-                ct_points = remove_lowest_marker(ct_points) #odstrani marker v riti, če obstaja
                 print(f"CT points: {ct_points}")                
                 if len(ct_points) < 3:
                     print(f"CT volume {ct_volume_name} doesn't have enough points for registration. Points: {len(ct_points)}")
                     continue
                 else:
+                    # if len(ct_points) == 4:
+                    #     ct_points = remove_lowest_marker(ct_points) #odstrani marker v riti, če obstaja
+                    
+                    
                     for cbct_volume_name in cbct_list:
                         print(f"\nProcessing CBCT Volume: {cbct_volume_name}")                        
                         yesCbct = True
@@ -1041,31 +1359,43 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                             cumulative_matrices[key] = np.eye(4)
                         
                         fixing = time.time()
-                        makemarkerscheck = False  # Ustvari CBCT miza markerje pred poravnavo
-                        if(ct_table_found):
-                            result = find_table_top_z(cbct_volume_name, writefilecheck, makemarkerscheck, yesCbct) #!!!!!!!!!!!!!???????????? ct_volume_name
-                            if result is not None:
-                                mm_offset, pixel_offset = result
-                                #print(f"✔️ Poravnava z višino mize: ΔY = {mm_offset:.2f} mm")
-                                skupni_offset = align_cbct_to_ct(cbct_volume_node, scan_type, mm_offset, CT_offset, CT_spacing) #poravna CBCT in sporoči skupni offset                                
-                                
-                                table_shift_matrix = np.eye(4)
-                                table_shift_matrix[1, 3] = skupni_offset  # Premik po Y
-
-                                # Pomnoži obstoječo kumulativno matriko
-                                key = (studyName, cbct_volume_name)
-                                if key not in cumulative_matrices:
-                                    cumulative_matrices[key] = np.eye(4)
-
-                                cumulative_matrices[key] = np.dot(cumulative_matrices[key], table_shift_matrix)
-                                
-                            else:
-                                print("⚠️ Table top not found – continue without correction on Y axis.")
-                                mm_offset = 0.0             
+                        if(tablefind):
+                            table2_time = time.time()
+                            makemarkerscheck = False  # Ustvari CBCT miza markerje pred poravnavo
+                            if(ct_table_found):
+                                result = find_table_top_z(cbct_volume_name, writefilecheck, makemarkerscheck, yesCbct) #!!!!!!!!!!!!!???????????? ct_volume_name
+                                if result is not None:
+                                    mm_offset, pixel_offset = result
+                                    print(f"CT offset: {CT_offset}, cbct offset: {mm_offset}")
+                                    
+                                    #ne rabimo več CT_offset, le še cbct offset. 
+                                    skupni_offset = align_cbct_to_ct(cbct_volume_node, scan_type, mm_offset, CT_offset, CT_spacing) #poravna CBCT in sporoči skupni offset                                
+                                    
+                                    table_shift_matrix = np.eye(4)
+                                    table_shift_matrix[1, 3] = skupni_offset  # Premik po Y
+
+                                    # Pomnoži obstoječo kumulativno matriko
+                                    key = (studyName, cbct_volume_name)
+                                    if key not in cumulative_matrices:
+                                        cumulative_matrices[key] = np.eye(4)
+
+                                    cumulative_matrices[key] = np.dot(cumulative_matrices[key], table_shift_matrix)
+                                    
+                                else:
+                                    print("⚠️ Table top not found – continue without correction on Y axis.")
+                                    mm_offset = 0.0
+                            table2end_time = time.time()            
                                             
                         cbct_points = [centroid for centroid, _ in volume_points_dict[("CBCT", cbct_volume_name)]] #zastareli podatki
                         cbct_points_array = np.array(cbct_points)  # Pretvorba v numpy array
                         
+                        if len(cbct_points) != len(ct_points):
+                            if(len(cbct_points) + 1 == len(ct_points)):
+                                ct_points = remove_lowest_marker(ct_points) #odstrani marker v riti, če obstaja
+                            else:
+                                print(f"Neujemajoče število točk! CBCT: {len(cbct_points)}, CT: {len(ct_points)}")
+                                continue  
+                        
                         # print_orientation(ct_volume_name)
                         # print_orientation(cbct_volume_name)
                         
@@ -1102,10 +1432,14 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                             
                             # Sortiramo točke po Z-koordinati (ali X/Y, če raje uporabljaš drugo os)
                             cbct_points_sorted = cbct_points_array[np.argsort(cbct_points_array[:, 2])]
-
-                            # Razdalje med CT in CBCT (SORTIRANE točke!)
-                            d_ct_cbct = np.linalg.norm(cbct_points_sorted - ct_points, axis=1)
-                            distances_ct_cbct.append(d_ct_cbct)
+                            
+                            # Razdalje med CT in CBCT (SORTIRANE točke!) 
+                            if cbct_points_sorted.shape[0] != len(ct_points):
+                                print(f"⚠️ Število točk CBCT ({cbct_points_sorted.shape[0]}) != CT ({len(ct_points)}), preskakujem izračun.")
+                            else:
+                                d_ct_cbct = np.linalg.norm(cbct_points_sorted - ct_points, axis=1)
+                                distances_ct_cbct.append(d_ct_cbct)
+                            
 
                             # Razdalje med točkami znotraj SORTIRANIH cbct_points
                             d_ab = np.linalg.norm(cbct_points_sorted[0] - cbct_points_sorted[1])
@@ -1136,11 +1470,7 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                             scaling_factors = compute_scaling_stddev(cbct_points, ct_points)
                             #print("Scaling factors: ", scaling_factors)
                             cbct_points = compute_scaling(cbct_points, scaling_factors)
-                            
-                        
-                        
-                            
-                            
+    
                         end_scaling = time.time()
                         start_align = time.time()
                         initial_error = np.mean(np.linalg.norm(np.array(cbct_points) - np.array(ct_points), axis=1))
@@ -1193,16 +1523,27 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                         
                         
                         combined_matrix = np.eye(4)
-                        
-                        if scaling_factors is not None:
-                            scaling_matrix = np.diag([scaling_factors[0], scaling_factors[1], scaling_factors[2], 1.0])
-                            combined_matrix = np.dot(scaling_matrix, combined_matrix)
-                        
-                        if chosen_translation_vector is not None:
-                            combined_matrix[:3, 3] = chosen_translation_vector + transvector # glavna poravnava, sistematični y shift in groba poravnava
+
+                        # 1. Rotacija
                         if chosen_rotation_matrix is not None:
                             combined_matrix[:3, :3] = chosen_rotation_matrix
+
+                        # 2. Skaliranje
+                        if scaling_factors is not None:
+                            # Pomnoži rotacijo s skalirnim faktorjem po vsaki osi
+                            scaled_rotation = combined_matrix[:3, :3] * scaling_factors  # broadcasting vsake vrstice
+                            combined_matrix[:3, :3] = scaled_rotation
+
+                        # 3. Translacija
+                        if chosen_translation_vector is not None:
+                            combined_matrix[:3, 3] = chosen_translation_vector + transvector  # združena translacija
+
                         
+                        #preverjanje determinante
+                        rot_part = combined_matrix[:3, :3]
+                        det = np.linalg.det(rot_part)
+                        if not np.isclose(det, 1.0, atol=0.01):
+                            print(f"⚠️ Neortogonalna rotacija! Determinanta: {det}")
                         
                         cumulative_matrices[(studyName, cbct_volume_name)] = np.dot(cumulative_matrices[(studyName, cbct_volume_name)], combined_matrix)
 
@@ -1226,7 +1567,7 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                         ustvari_marker = False
                         cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
                         #print("Markerji po transformaciji:\n", cbct_points, ct_points)
-                        
+                        cbct_points = match_points(cbct_points, ct_points)
                         #popravek v x osi
                         delta_x_list = [ct[0] - cbct[0] for ct, cbct in zip(ct_points, cbct_points)]
                         mean_delta_x = np.mean(delta_x_list)
@@ -1248,6 +1589,18 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                  
                     
                         chosen_rotation_matrix = np.eye(3) #tokrat brez rotacije
+                        
+                        #### TEST ROTACIJA ########
+                        angle_deg = 0
+                        angle_rad = np.deg2rad(angle_deg)
+
+                        chosen_rotation_matrix = np.array([
+                            [np.cos(angle_rad), -np.sin(angle_rad), 0],
+                            [np.sin(angle_rad),  np.cos(angle_rad), 0],
+                            [0,                 0,                 1]
+                        ])
+                        ###KONEC TESTA###
+                        
                         vtk_transform = create_vtk_transform(chosen_rotation_matrix, fine_shift, studyName, cbct_volume_name) #Tukaj se tudi izpiše transformacijska matrika
 
                         # 🔄 Pripni transformacijo
@@ -1263,14 +1616,70 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                         slicer.mrmlScene.RemoveNode(transform_node)
 
                         
-                        ustvari_marker = True
-                        cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, ustvari_marker, intensity_threshold=3000)]
+                        table_shift_matrix_ct = np.eye(4)
+                        table_shift_matrix_ct[1, 3] = CT_offset  # ali skupni_offset, če je potreben simetričen premik
+                        
+                        cumulative_matrices[(studyName, cbct_volume_name)] = np.dot(
+                            table_shift_matrix,
+                            cumulative_matrices[(studyName, cbct_volume_name)]
+                        )
+                        
+                        #ustvari_marker = True
+                        cbct_points = [centroid for centroid, _ in detect_points_region_growing(cbct_volume_name, yesCbct, applymarkers, intensity_threshold=3000)]
                         print(f"Fine correction shifts: ΔX={fine_shift[0]:.2f} mm, ΔY={fine_shift[1]:.2f} mm, ΔZ={fine_shift[2]:.2f} mm")
 
                         #shrani transformacijsko matriko v datoteko
-                        save_transform_matrix(cumulative_matrices[(studyName, cbct_volume_name)], studyName, cbct_volume_name, ct_table_found)
-                        
+                        save_transform_matrix(cumulative_matrices[(studyName, cbct_volume_name)], studyName, cbct_volume_name)
                         
+                        if save_as_dicom:
+                            # Apply transform to CT
+                            logging.info(f"[{studyName}] Start applying transform to CT volume.")
+                            slicer.util.delayDisplay(f"[DEBUG] Start transform on CT", 1000)
+                            apply_cumulative_transform_to_volume(ct_volume_Node, cumulative_matrices[(studyName, cbct_volume_name)])
+                            
+                            # Convert RTSTRUCT to SegmentationNode if needed
+                            logging.info(f"[{studyName}] Getting segmentation nodes from RTSTRUCT.")
+                            #slicer.util.delayDisplay(f"[DEBUG] Start segmentation conversion", 1000)
+                            #seg_nodes = convert_rtstruct_to_segmentation_nodes(ct_volume_Node)
+                            seg_node = convert_all_models_to_segmentation(ct_volume_name, prefix="Imported_")
+                            #new_seg_node = slicer.util.getNode(new_seg_name)
+                            apply_cumulative_transform_to_segmentation(seg_node, cumulative_matrices[(studyName, cbct_volume_name)])
+
+                            plugin = DicomRtImportExportPlugin.DicomRtImportExportPluginClass()
+                            shNode = slicer.vtkMRMLSubjectHierarchyNode.GetSubjectHierarchyNode(slicer.mrmlScene)
+                            ct_itemID = shNode.GetItemByDataNode(ct_volume_Node)
+                            seg_itemID = shNode.GetItemByDataNode(seg_node)
+
+                            
+                            
+                            cbct_item = shNode.GetItemByDataNode(cbct_volume_node)
+                            cbct_date = shNode.GetItemAttribute(cbct_item, "DICOM.SeriesDate") or "unknownDate"
+
+                            #cbct_date = cbct_volume_node.GetAttribute("DICOM.AcquisitionDate") or "unknownDate"
+                            results_base = os.path.join(os.path.dirname(__file__), "Rezultati")
+                            study_folder = studyName.replace('^', '_')
+                            study_dir = os.path.join(results_base, study_folder)
+                            os.makedirs(study_dir, exist_ok=True)  
+
+                            export_dir = os.path.join(study_dir, f"{cbct_date}_DICOM")
+                            os.makedirs(export_dir, exist_ok=True)
+                            
+                            exportables = []
+                            exportables += plugin.examineForExport(ct_itemID)
+                            exportables += plugin.examineForExport(seg_itemID)
+                            for exp in exportables:
+                                 # Kopiraj podatke iz volumetričnega noda v exportable
+                                if ct_volume_Node.GetAttribute("DICOM.PatientName"):
+                                    exp.setTag("0010,0010", ct_volume_Node.GetAttribute("DICOM.PatientName"))  # PatientName
+                                if ct_volume_Node.GetAttribute("DICOM.PatientID"):
+                                    exp.setTag("0010,0020", ct_volume_Node.GetAttribute("DICOM.PatientID"))    # PatientID
+                                if ct_volume_Node.GetAttribute("DICOM.StudyInstanceUID"):
+                                    exp.setTag("0020,000D", ct_volume_Node.GetAttribute("DICOM.StudyInstanceUID"))  # StudyInstanceUID
+                                exp.directory = export_dir
+                                print(f"[{studyName}] ✅ Export successful")
+                            plugin.export(exportables)
+
+                                            
                         # 📏 Izračun napake
                         errors = [np.linalg.norm(cbct - ct) for cbct, ct in zip(cbct_points, ct_points)]
                         mean_error = np.mean(errors)
@@ -1282,6 +1691,66 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                             diff = np.array(cbct) - np.array(ct)
                             print(f"Specific marker errors {i+1}: ΔX={diff[0]:.2f} mm, ΔY={diff[1]:.2f} mm, ΔZ={diff[2]:.2f} mm")
                         
+                        ct_pts = np.array(ct_points)
+                        cbct_pts = np.array(cbct_points)
+
+                        if len(ct_pts) == 3 and len(cbct_pts) == 3:
+                            sim = triangle_similarity(ct_pts, cbct_pts)
+                            print("\n📐 Triangle similarity analysis:")
+                            print(f"Side length differences (mm): {sim['side_length_diff_mm']}")
+                            print(f"Angle differences (deg): {sim['angle_diff_deg']}")
+                            print(f"Mean length error: {sim['mean_length_error_mm']:.2f} mm")
+                            print(f"Mean angle error: {sim['mean_angle_error_deg']:.2f}°")
+                            print(f"Triangle Similarity Ratio (TSR): {sim['TSR']:.3f}")
+                        else:
+                            print("⚠️ Za primerjavo trikotnikov potrebne točno 3 točke.")
+                        
+                        if writefilecheck:
+
+ 
+                            errorsfile = os.path.join(study_dir, f"{cbct_date}_errors.csv")
+
+                            with open(errorsfile, mode='a', newline='') as file:
+                                writer = csv.writer(file)
+
+                                # Glava za študijo
+                                writer.writerow(["Study", studyName])
+                                writer.writerow(["Total Individual Errors (mm)"])
+                                
+                                for error in errors:
+                                    writer.writerow(["", f"{error:.2f}"])
+
+                                writer.writerow(["Average Error (mm)", f"{mean_error:.2f}"])
+
+                                # Specifične napake markerjev
+                                writer.writerow(["Specific Marker Errors"])
+                                writer.writerow(["Marker", "ΔX (mm)", "ΔY (mm)", "ΔZ (mm)"])
+
+                                for i, (cbct, ct) in enumerate(zip(cbct_points, ct_points)):
+                                    diff = np.array(cbct) - np.array(ct)
+                                    writer.writerow([f"Marker {i+1}", f"{diff[0]:.2f}", f"{diff[1]:.2f}", f"{diff[2]:.2f}"])
+
+                                if len(cbct_points) == 3 and len(ct_points) == 3:
+                                    writer.writerow([])
+                                    writer.writerow(["Triangle Similarity"])
+                                    writer.writerow(["Side Length Diff (mm)", *[f"{v:.2f}" for v in sim["side_length_diff_mm"]]])
+                                    writer.writerow(["Angle Diff (deg)", *[f"{v:.2f}" for v in sim["angle_diff_deg"]]])
+                                    writer.writerow(["Mean Length Error (mm)", f"{sim['mean_length_error_mm']:.2f}"])
+                                    writer.writerow(["Mean Angle Error (deg)", f"{sim['mean_angle_error_deg']:.2f}"])
+                                    writer.writerow(["Triangle Similarity Ratio (TSR)", f"{sim['TSR']:.3f}"])
+                                    writer.writerow([])
+                                 
+                            
+                            graphs_dir = os.path.join(results_base, study_folder)
+                            os.makedirs(graphs_dir, exist_ok=True)
+
+                            pngfile = os.path.join(graphs_dir, f"{cbct_date}_trikotnika.png")
+                            save_triangle_visualization(np.array(ct_points), np.array(cbct_points), pngfile)
+                                
+
+                            #print("Prostate size file written at ", prostate_size_file)
+                        
+                        
                         
                 
             else:
@@ -1292,6 +1761,7 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
             timing_data = {
                 "IO": end_io - start_io,
                 "Fixing": fixing_end - fixing,
+                "Table": ((table1end_time - table1_time)+(table2end_time - table2_time)) if tablefind else 0,
                 "Scaling": end_scaling - start_scaling,
                 "CentroidAlign": end_align - start_align,
                 "Rotation": end_rotation - start_rotation,
@@ -1300,6 +1770,10 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
                 "Total": study_end_time - study_start_time}
             update_timing_csv(timing_data, studyName)
             print(f"Timing data for {studyName}: {timing_data}")
+    
+
+
+
 
         # Izpis globalne statistike
         
@@ -1336,5 +1810,4 @@ class MyTransformModuleLogic(ScriptedLoadableModuleLogic):
         minutes = int(elapsed_time // 60)
         seconds = elapsed_time % 60
 
-        print(f"Execution time: {minutes} minutes and {seconds:.6f} seconds")
-        
+        print(f"Execution time: {minutes} minutes and {seconds:.6f} seconds")