"""
Intraoral X-ray Radiation Shielding Calculator
Compliant with Ontario Ministry of Health Standards
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import fitz  # PyMuPDF for PDF handling
from PIL import Image, ImageTk
import math
import numpy as np
import os
import platform
import gc

# Try importing OpenCV for image preprocessing
try:
    import cv2
    OPENCV_AVAILABLE = True
    print("✓ OpenCV available for image preprocessing")
except ImportError:
    OPENCV_AVAILABLE = False
    print("⚠️ OpenCV not installed - advanced OCR preprocessing disabled")

# Try importing EasyOCR for text detection
try:
    import easyocr
    EASYOCR_AVAILABLE = True
    print("✓ EasyOCR available for text filtering")
except ImportError:
    EASYOCR_AVAILABLE = False
    print("⚠️ EasyOCR not installed - EasyOCR text filtering disabled")


# Try to import CuPy for GPU acceleration
try:
    import cupy as cp
    GPU_AVAILABLE = True
    print("🚀 GPU acceleration available (CuPy detected)")
except ImportError:
    cp = None
    GPU_AVAILABLE = False
    print("⚠️  GPU acceleration not available - using CPU (install 'cupy' for GPU support)")

class XrayShieldingCalculator:
    def __init__(self, root):
        self.root = root
        self.root.title("Intraoral X-ray Radiation Shielding Calculator")
        self.root.geometry("1600x1000")  # Increased window size
        self.root.state('zoomed')  # Maximize window on startup
        
        # Variables
        self.pdf_document = None
        self.current_page = 0
        self.floor_plan_image = None
        self.canvas_image = None
        self.display_scale = 1.0  # Display scale factor (0.25 = 25% of original for display)
        self.scale_factor = 1.0  # pixels per meter
        self.calibration_points = []
        self.calibration_distance = tk.DoubleVar(value=1.0)  # meters
        
        # Equipment parameters
        self.tube_head_position = None
        self.primary_beam_direction = tk.IntVar(value=0)  # 0 degrees (straight ahead)
        self.operator_position = None
        
        # Three beam directions with workload distribution
        # 0° = 20% workload, 90° = 40% workload, -90° = 40% workload
        self.beam_angles = [0, 90, -90]  # degrees from primary direction
        self.beam_workload_factors = [0.20, 0.40, 0.40]  # percentage of total workload
        
        # Lead barrier parameters
        self.lead_08mm_color = None  # RGB color for 0.8mm lead
        self.lead_016mm_color = None  # RGB color for 0.16mm lead
        self.wall_spacing = None  # Pixel spacing between parallel wall lines
        self.wall_point1 = None  # First point for wall detection
        self.wall_point2 = None  # Second point for wall detection
        self.color_tolerance = 30  # Color matching tolerance
        self.lead_barriers = []  # List of detected barriers
        self.wall_barriers = []  # List of detected wall barriers (parallel lines)
        
        # Wall construction parameters
        self.wall_thickness = tk.DoubleVar(value=0.15)  # meters (6 inches typical)
        self.include_drywall = tk.BooleanVar(value=True)
        self.drywall_layers = tk.IntVar(value=2)
        
        # EasyOCR configuration options
        self.easyocr_languages = tk.StringVar(value="en")  # Languages (comma separated)
        self.easyocr_gpu = tk.BooleanVar(value=True)  # Use GPU if available
        self.easyocr_detect_red = tk.BooleanVar(value=True)  # Enable red text detection
        self.easyocr_min_confidence = tk.IntVar(value=30)  # Minimum confidence (0-100)
        self.easyocr_whitelist = tk.StringVar(value="")  # Character whitelist
        self.easyocr_paragraph = tk.BooleanVar(value=False)  # Paragraph mode
        self.easyocr_contrast = tk.IntVar(value=1)  # Contrast adjustment (0-2, 1=normal)
        self.easyocr_padding = tk.IntVar(value=10)  # Bounding box padding (pixels)
        self.easyocr_rotate_detection = tk.BooleanVar(value=True)  # Run OCR on rotated image (90°)

        # Initialize EasyOCR reader (lazy loading - only when needed)
        self.ocr_reader = None
        self.ocr_text_regions = []  # Store detected text regions for visualization
        
        # Global OCR preprocessing toggle
        self.use_opencv_preprocessing = tk.BooleanVar(value=True)  # Default: ON

        # Preprocessing filter toggles
        self.preproc_use_grayscale = tk.BooleanVar(value=True)
        self.preproc_use_threshold = tk.BooleanVar(value=True)
        self.preproc_use_denoise = tk.BooleanVar(value=True)
        self.preproc_use_morphology = tk.BooleanVar(value=True)
        self.preproc_use_red_isolation = tk.BooleanVar(value=True)  # Convert red text to binary mask

        # Preprocessing parameters
        self.preproc_threshold_blocksize = tk.IntVar(value=11)  # Adaptive threshold block size
        self.preproc_threshold_c = tk.IntVar(value=2)  # Adaptive threshold C constant
        self.preproc_denoise_h = tk.IntVar(value=10)  # Denoising filter strength
        self.preproc_morph_kernel = tk.IntVar(value=2)  # Morphology kernel size

        # Red text detection parameters
        self.preproc_red_r_min = tk.IntVar(value=200)  # Red channel minimum
        self.preproc_red_gb_max = tk.IntVar(value=50)  # Green/Blue channel maximum

        # X-ray machine parameters (typical intraoral values)
        self.kvp = tk.DoubleVar(value=70)  # kVp
        self.ma = tk.DoubleVar(value=7)    # mA
        self.exposure_time = tk.DoubleVar(value=0.2)  # seconds
        self.workload = tk.DoubleVar(value=200)  # exposures per week
        self.use_factor = tk.DoubleVar(value=1.0)  # 1.0 for primary beam direction
        
        # Ontario standards
        self.weekly_dose_limit = 0.02  # mSv/week for public (operator considered controlled area)
        self.controlled_area_limit = 0.1  # mSv/week for controlled areas
        
        # Modes
        self.mode = "view"  # view, calibrate, place_tubehead, set_direction, place_operator, select_lead_08, select_lead_016, select_drywall
        self.show_heatmap = False  # Toggle for radiation heat map
        
        self.setup_ui()
        
    def setup_ui(self):
        # Main container
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        
        # Left panel - Controls with scrollbar
        left_container = ttk.Frame(main_frame, width=350)
        left_container.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5))
        left_container.pack_propagate(False)
        
        # Create canvas for scrolling
        canvas_left = tk.Canvas(left_container, highlightthickness=0, width=330)
        scrollbar_left = ttk.Scrollbar(left_container, orient="vertical", command=canvas_left.yview)
        self.scrollable_frame = ttk.Frame(canvas_left)
        
        self.scrollable_frame.bind(
            "<Configure>",
            lambda e: canvas_left.configure(scrollregion=canvas_left.bbox("all"))
        )
        
        canvas_left.create_window((0, 0), window=self.scrollable_frame, anchor="nw", width=310)
        canvas_left.configure(yscrollcommand=scrollbar_left.set)
        
        # Bind mouse wheel to scroll
        def _on_mousewheel(event):
            canvas_left.yview_scroll(int(-1*(event.delta/120)), "units")
        
        canvas_left.bind_all("<MouseWheel>", _on_mousewheel)
        
        canvas_left.pack(side="left", fill="both", expand=True)
        scrollbar_left.pack(side="right", fill="y")
        
        # Right panel - Canvas
        right_panel = ttk.Frame(main_frame)
        right_panel.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        self.setup_controls(self.scrollable_frame)
        self.setup_canvas(right_panel)
        
    def setup_controls(self, parent):
        # File loading section
        file_frame = ttk.LabelFrame(parent, text="Floor Plan", padding=5)
        file_frame.pack(fill=tk.X, pady=5)
        
        ttk.Button(file_frame, text="Open PDF Floor Plan", 
                   command=self.load_pdf).pack(fill=tk.X, pady=2, padx=5)
        
        # Calibration section
        cal_frame = ttk.LabelFrame(parent, text="Calibration", padding=5)
        cal_frame.pack(fill=tk.X, pady=5)
        
        ttk.Label(cal_frame, text="Known Distance (m):", wraplength=280).pack()
        ttk.Entry(cal_frame, textvariable=self.calibration_distance).pack(fill=tk.X, pady=2, padx=5)
        
        ttk.Button(cal_frame, text="Calibrate Distance", 
                   command=self.start_calibration).pack(fill=tk.X, pady=2, padx=5)
        
        self.cal_status = ttk.Label(cal_frame, text="Not calibrated", foreground="red")
        self.cal_status.pack(pady=5)
        
        # Equipment placement section
        equip_frame = ttk.LabelFrame(parent, text="Equipment Placement", padding=5)
        equip_frame.pack(fill=tk.X, pady=5)
        
        ttk.Button(equip_frame, text="Place Tube Head & Patient", 
                   command=self.start_place_tubehead).pack(fill=tk.X, pady=2, padx=5)
        
        ttk.Label(equip_frame, text="Primary Beam Direction:", font=("Arial", 9, "bold")).pack(pady=(10,2))
        directions = [
            ("E (0°) →", 0), 
            ("SE (135°) ↘", 135), 
            ("S (180°) ↓", 180),
            ("SW (225°) ↙", 225),
            ("W (270°) ←", 270),
            ("NW (315°) ↖", 315),
            ("N (360°) ↑", 0)
        ]
        for text, value in directions:
            ttk.Radiobutton(equip_frame, text=text, variable=self.primary_beam_direction,
                           value=value, command=self.update_display).pack(anchor=tk.W)
        
        ttk.Button(equip_frame, text="Place Operator Position", 
                   command=self.start_place_operator).pack(fill=tk.X, pady=(10,2), padx=5)
        
        # Lead barrier detection section
        lead_frame = ttk.LabelFrame(parent, text="Lead Barrier Detection", padding=5)
        lead_frame.pack(fill=tk.X, pady=5)
        
        ttk.Label(lead_frame, text="Click on floor plan colors to identify lead barriers:",
                 wraplength=330).pack(pady=5)
        
        # Color tolerance adjustment
        tolerance_frame = ttk.Frame(lead_frame)
        tolerance_frame.pack(fill=tk.X, pady=5)
        ttk.Label(tolerance_frame, text="Color Match Tolerance:").pack(side=tk.LEFT)
        self.tolerance_var = tk.IntVar(value=30)
        tolerance_spinbox = ttk.Spinbox(tolerance_frame, from_=5, to=100, 
                                       textvariable=self.tolerance_var, width=8,
                                       command=self.update_tolerance)
        tolerance_spinbox.pack(side=tk.LEFT, padx=5)
        ttk.Label(tolerance_frame, text="(±RGB units)").pack(side=tk.LEFT)
        
        # 0.8mm lead
        frame_08 = ttk.Frame(lead_frame)
        frame_08.pack(fill=tk.X, pady=2)
        
        ttk.Button(frame_08, text="Select 0.8mm Lead Barrier", 
                   command=self.start_select_lead_08).pack(fill=tk.X, padx=5)
        
        color_frame_08 = ttk.Frame(frame_08)
        color_frame_08.pack(fill=tk.X, pady=2)
        
        self.color_08_display = tk.Canvas(color_frame_08, width=30, height=20, bg="white", relief=tk.SUNKEN, bd=1)
        self.color_08_display.pack(side=tk.LEFT, padx=2)
        
        self.label_08_status = ttk.Label(color_frame_08, text="Not set", foreground="gray")
        self.label_08_status.pack(side=tk.LEFT, padx=2)
        
        # 0.16mm lead
        frame_016 = ttk.Frame(lead_frame)
        frame_016.pack(fill=tk.X, pady=2)
        
        ttk.Button(frame_016, text="Select 0.16mm Lead Barrier", 
                   command=self.start_select_lead_016).pack(fill=tk.X, padx=5)
        
        color_frame_016 = ttk.Frame(frame_016)
        color_frame_016.pack(fill=tk.X, pady=2)
        
        self.color_016_display = tk.Canvas(color_frame_016, width=30, height=20, bg="white", relief=tk.SUNKEN, bd=1)
        self.color_016_display.pack(side=tk.LEFT, padx=2)
        
        self.label_016_status = ttk.Label(color_frame_016, text="Not set", foreground="gray")
        self.label_016_status.pack(side=tk.LEFT, padx=2)
        
        # Wall detection (parallel black lines)
        frame_wall = ttk.Frame(lead_frame)
        frame_wall.pack(fill=tk.X, pady=2)
        
        ttk.Button(frame_wall, text="Select Wall (2 Parallel Lines)", 
                   command=self.start_select_wall).pack(fill=tk.X, padx=5)
        
        color_frame_wall = ttk.Frame(frame_wall)
        color_frame_wall.pack(fill=tk.X, pady=2)
        
        self.wall_display = tk.Canvas(color_frame_wall, width=30, height=20, bg="white", relief=tk.SUNKEN, bd=1)
        self.wall_display.pack(side=tk.LEFT, padx=2)
        
        self.label_wall_status = ttk.Label(color_frame_wall, text="Not detected", foreground="gray")
        self.label_wall_status.pack(side=tk.LEFT, padx=2)
        
        # Detection buttons
        detect_btn_frame = ttk.Frame(lead_frame)
        detect_btn_frame.pack(fill=tk.X, pady=5)
        
        ttk.Button(detect_btn_frame, text="🔍 Detect Barriers", 
                   command=self.detect_lead_barriers).pack(fill=tk.X, padx=5, pady=2)
        
        # Global preprocessing toggle
        preprocess_frame = ttk.Frame(detect_btn_frame)
        preprocess_frame.pack(fill=tk.X, padx=5, pady=2)
        ttk.Checkbutton(preprocess_frame, text="Use OpenCV Preprocessing", 
                       variable=self.use_opencv_preprocessing).pack(side=tk.LEFT)
        
        ttk.Button(detect_btn_frame, text="📝 Run OCR - EasyOCR",
                   command=self.run_ocr_detection).pack(fill=tk.X, padx=5, pady=2)

        # EasyOCR Configuration (collapsible)
        easyocr_config_frame = ttk.LabelFrame(lead_frame, text="⚙️ EasyOCR Settings", padding=5)
        easyocr_config_frame.pack(fill=tk.X, padx=5, pady=5)

        # Languages
        lang_frame = ttk.Frame(easyocr_config_frame)
        lang_frame.pack(fill=tk.X, pady=2)
        ttk.Label(lang_frame, text="Languages:", width=14).pack(side=tk.LEFT)
        easyocr_lang_entry = ttk.Entry(lang_frame, textvariable=self.easyocr_languages, width=15)
        easyocr_lang_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)

        ttk.Label(easyocr_config_frame, text="Comma separated (e.g., en, fr, de)",
                 foreground="gray", font=("Arial", 7)).pack(fill=tk.X)

        # Quick language presets
        lang_preset_frame = ttk.Frame(easyocr_config_frame)
        lang_preset_frame.pack(fill=tk.X, pady=2)
        ttk.Button(lang_preset_frame, text="EN", width=4,
                  command=lambda: self.easyocr_languages.set("en")).pack(side=tk.LEFT, padx=1)
        ttk.Button(lang_preset_frame, text="EN+FR", width=6,
                  command=lambda: self.easyocr_languages.set("en,fr")).pack(side=tk.LEFT, padx=1)
        ttk.Button(lang_preset_frame, text="Multi", width=5,
                  command=lambda: self.easyocr_languages.set("en,fr,de,es")).pack(side=tk.LEFT, padx=1)

        # GPU checkbox
        ttk.Checkbutton(easyocr_config_frame, text="Use GPU acceleration",
                       variable=self.easyocr_gpu).pack(fill=tk.X, pady=2)

        # Minimum confidence slider
        easyocr_conf_frame = ttk.Frame(easyocr_config_frame)
        easyocr_conf_frame.pack(fill=tk.X, pady=2)
        ttk.Label(easyocr_conf_frame, text="Min Conf:", width=14).pack(side=tk.LEFT)

        easyocr_conf_slider = ttk.Scale(easyocr_conf_frame, from_=0, to=100, orient=tk.HORIZONTAL,
                                        variable=self.easyocr_min_confidence, command=lambda v: self.update_easyocr_conf_label())
        easyocr_conf_slider.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)

        self.easyocr_conf_value_label = ttk.Label(easyocr_conf_frame, text="30", width=5)
        self.easyocr_conf_value_label.pack(side=tk.LEFT)

        # Contrast adjustment
        contrast_frame = ttk.Frame(easyocr_config_frame)
        contrast_frame.pack(fill=tk.X, pady=2)
        ttk.Label(contrast_frame, text="Contrast:", width=14).pack(side=tk.LEFT)

        contrast_slider = ttk.Scale(contrast_frame, from_=0, to=2, orient=tk.HORIZONTAL,
                                   variable=self.easyocr_contrast, command=lambda v: self.update_easyocr_contrast_label())
        contrast_slider.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)

        self.easyocr_contrast_value_label = ttk.Label(contrast_frame, text="1", width=5)
        self.easyocr_contrast_value_label.pack(side=tk.LEFT)

        ttk.Label(easyocr_config_frame, text="0=low, 1=normal, 2=high",
                 foreground="gray", font=("Arial", 7)).pack(fill=tk.X)

        # Paragraph mode checkbox
        ttk.Checkbutton(easyocr_config_frame, text="Paragraph mode (merge nearby text)",
                       variable=self.easyocr_paragraph).pack(fill=tk.X, pady=2)

        # Red text detection checkbox
        ttk.Checkbutton(easyocr_config_frame, text="Detect red text (2nd pass)",
                       variable=self.easyocr_detect_red).pack(fill=tk.X, pady=2)

        # Character whitelist
        easyocr_whitelist_frame = ttk.Frame(easyocr_config_frame)
        easyocr_whitelist_frame.pack(fill=tk.X, pady=2)
        ttk.Label(easyocr_whitelist_frame, text="Chars:", width=14).pack(side=tk.LEFT)

        easyocr_whitelist_entry = ttk.Entry(easyocr_whitelist_frame, textvariable=self.easyocr_whitelist, width=15)
        easyocr_whitelist_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)

        # Quick preset buttons
        easyocr_preset_frame = ttk.Frame(easyocr_config_frame)
        easyocr_preset_frame.pack(fill=tk.X, pady=2)

        ttk.Button(easyocr_preset_frame, text="#", width=3,
                  command=lambda: self.easyocr_whitelist.set("#")).pack(side=tk.LEFT, padx=1)
        ttk.Button(easyocr_preset_frame, text="A-Z", width=4,
                  command=lambda: self.easyocr_whitelist.set("ABCDEFGHIJKLMNOPQRSTUVWXYZ")).pack(side=tk.LEFT, padx=1)
        ttk.Button(easyocr_preset_frame, text="0-9", width=4,
                  command=lambda: self.easyocr_whitelist.set("0123456789")).pack(side=tk.LEFT, padx=1)
        ttk.Button(easyocr_preset_frame, text="Clear", width=5,
                  command=lambda: self.easyocr_whitelist.set("")).pack(side=tk.LEFT, padx=1)

        ttk.Label(easyocr_config_frame, text="Empty = all characters",
                 foreground="gray", font=("Arial", 7)).pack(fill=tk.X)

        # Bounding box padding slider
        padding_frame = ttk.Frame(easyocr_config_frame)
        padding_frame.pack(fill=tk.X, pady=2)
        ttk.Label(padding_frame, text="Box Padding:", width=14).pack(side=tk.LEFT)

        padding_slider = ttk.Scale(padding_frame, from_=0, to=50, orient=tk.HORIZONTAL,
                                   variable=self.easyocr_padding, command=lambda v: self.update_easyocr_padding_label())
        padding_slider.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)

        self.easyocr_padding_value_label = ttk.Label(padding_frame, text="10", width=5)
        self.easyocr_padding_value_label.pack(side=tk.LEFT)

        ttk.Label(easyocr_config_frame, text="Pixels added around detected text boxes",
                 foreground="gray", font=("Arial", 7)).pack(fill=tk.X)

        # Rotation detection checkbox
        ttk.Checkbutton(easyocr_config_frame, text="Detect rotated text (90° rotation pass)",
                       variable=self.easyocr_rotate_detection).pack(fill=tk.X, pady=2)

        # OpenCV Preprocessing Settings (collapsible)
        preproc_config_frame = ttk.LabelFrame(lead_frame, text="🔧 OpenCV Preprocessing Settings", padding=5)
        preproc_config_frame.pack(fill=tk.X, padx=5, pady=5)

        # Filter toggles
        ttk.Label(preproc_config_frame, text="Enable Filters:", font=("Arial", 9, "bold")).pack(fill=tk.X, pady=2)

        ttk.Checkbutton(preproc_config_frame, text="Convert to Grayscale",
                       variable=self.preproc_use_grayscale).pack(fill=tk.X, pady=1)

        ttk.Checkbutton(preproc_config_frame, text="Adaptive Thresholding",
                       variable=self.preproc_use_threshold).pack(fill=tk.X, pady=1)

        ttk.Checkbutton(preproc_config_frame, text="Denoising Filter",
                       variable=self.preproc_use_denoise).pack(fill=tk.X, pady=1)

        ttk.Checkbutton(preproc_config_frame, text="Morphology (for red text)",
                       variable=self.preproc_use_morphology).pack(fill=tk.X, pady=1)

        ttk.Separator(preproc_config_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)

        # Threshold parameters
        ttk.Label(preproc_config_frame, text="Adaptive Threshold:", font=("Arial", 9, "bold")).pack(fill=tk.X, pady=2)

        # Block size
        threshold_block_frame = ttk.Frame(preproc_config_frame)
        threshold_block_frame.pack(fill=tk.X, pady=2)
        ttk.Label(threshold_block_frame, text="Block Size:", width=14).pack(side=tk.LEFT)
        threshold_block_slider = ttk.Scale(threshold_block_frame, from_=3, to=51, orient=tk.HORIZONTAL,
                                          variable=self.preproc_threshold_blocksize,
                                          command=lambda v: self.update_threshold_block_label())
        threshold_block_slider.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
        self.threshold_block_value_label = ttk.Label(threshold_block_frame, text="11", width=5)
        self.threshold_block_value_label.pack(side=tk.LEFT)

        # C constant
        threshold_c_frame = ttk.Frame(preproc_config_frame)
        threshold_c_frame.pack(fill=tk.X, pady=2)
        ttk.Label(threshold_c_frame, text="C Constant:", width=14).pack(side=tk.LEFT)
        threshold_c_slider = ttk.Scale(threshold_c_frame, from_=0, to=20, orient=tk.HORIZONTAL,
                                      variable=self.preproc_threshold_c,
                                      command=lambda v: self.update_threshold_c_label())
        threshold_c_slider.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
        self.threshold_c_value_label = ttk.Label(threshold_c_frame, text="2", width=5)
        self.threshold_c_value_label.pack(side=tk.LEFT)

        ttk.Separator(preproc_config_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)

        # Denoise parameters
        ttk.Label(preproc_config_frame, text="Denoising:", font=("Arial", 9, "bold")).pack(fill=tk.X, pady=2)

        denoise_h_frame = ttk.Frame(preproc_config_frame)
        denoise_h_frame.pack(fill=tk.X, pady=2)
        ttk.Label(denoise_h_frame, text="Filter Strength:", width=14).pack(side=tk.LEFT)
        denoise_h_slider = ttk.Scale(denoise_h_frame, from_=1, to=30, orient=tk.HORIZONTAL,
                                     variable=self.preproc_denoise_h,
                                     command=lambda v: self.update_denoise_h_label())
        denoise_h_slider.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
        self.denoise_h_value_label = ttk.Label(denoise_h_frame, text="10", width=5)
        self.denoise_h_value_label.pack(side=tk.LEFT)

        ttk.Separator(preproc_config_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)

        # Red text detection parameters
        ttk.Label(preproc_config_frame, text="Red Text Detection:", font=("Arial", 9, "bold")).pack(fill=tk.X, pady=2)

        ttk.Checkbutton(preproc_config_frame, text="Isolate red text to binary mask",
                       variable=self.preproc_use_red_isolation).pack(fill=tk.X, pady=1)

        ttk.Label(preproc_config_frame, text="(If disabled, uses standard preprocessing on full color image)",
                 foreground="gray", font=("Arial", 7)).pack(fill=tk.X)

        red_r_frame = ttk.Frame(preproc_config_frame)
        red_r_frame.pack(fill=tk.X, pady=2)
        ttk.Label(red_r_frame, text="Red Min (R):", width=14).pack(side=tk.LEFT)
        red_r_slider = ttk.Scale(red_r_frame, from_=100, to=255, orient=tk.HORIZONTAL,
                                variable=self.preproc_red_r_min,
                                command=lambda v: self.update_red_r_label())
        red_r_slider.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
        self.red_r_value_label = ttk.Label(red_r_frame, text="200", width=5)
        self.red_r_value_label.pack(side=tk.LEFT)

        red_gb_frame = ttk.Frame(preproc_config_frame)
        red_gb_frame.pack(fill=tk.X, pady=2)
        ttk.Label(red_gb_frame, text="G/B Max:", width=14).pack(side=tk.LEFT)
        red_gb_slider = ttk.Scale(red_gb_frame, from_=0, to=150, orient=tk.HORIZONTAL,
                                 variable=self.preproc_red_gb_max,
                                 command=lambda v: self.update_red_gb_label())
        red_gb_slider.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
        self.red_gb_value_label = ttk.Label(red_gb_frame, text="50", width=5)
        self.red_gb_value_label.pack(side=tk.LEFT)

        ttk.Label(preproc_config_frame, text="Lower values = stricter red detection",
                 foreground="gray", font=("Arial", 7)).pack(fill=tk.X)

        ttk.Separator(preproc_config_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)

        # Morphology parameters
        ttk.Label(preproc_config_frame, text="Morphology:", font=("Arial", 9, "bold")).pack(fill=tk.X, pady=2)

        morph_kernel_frame = ttk.Frame(preproc_config_frame)
        morph_kernel_frame.pack(fill=tk.X, pady=2)
        ttk.Label(morph_kernel_frame, text="Kernel Size:", width=14).pack(side=tk.LEFT)
        morph_kernel_slider = ttk.Scale(morph_kernel_frame, from_=1, to=10, orient=tk.HORIZONTAL,
                                       variable=self.preproc_morph_kernel,
                                       command=lambda v: self.update_morph_kernel_label())
        morph_kernel_slider.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
        self.morph_kernel_value_label = ttk.Label(morph_kernel_frame, text="2", width=5)
        self.morph_kernel_value_label.pack(side=tk.LEFT)

        ttk.Separator(preproc_config_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5)

        # Preview button
        ttk.Button(preproc_config_frame, text="👁️ Preview Preprocessing",
                  command=self.preview_preprocessing).pack(fill=tk.X, pady=5)

        clear_btn_frame = ttk.Frame(lead_frame)
        clear_btn_frame.pack(fill=tk.X, pady=2)

        ttk.Button(clear_btn_frame, text="Clear Barriers",
                   command=self.clear_barriers).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)

        ttk.Button(clear_btn_frame, text="Clear OCR",
                   command=self.clear_ocr).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)

        self.barrier_status = ttk.Label(lead_frame, text="No barriers detected", 
                                       foreground="gray", wraplength=330)
        self.barrier_status.pack(pady=5)
        
        # Wall construction section
        wall_frame = ttk.LabelFrame(parent, text="Wall Construction", padding=5)
        wall_frame.pack(fill=tk.X, pady=5)
        
        frame = ttk.Frame(wall_frame)
        frame.pack(fill=tk.X, pady=2)
        ttk.Label(frame, text="Thickness(m):").pack(side=tk.LEFT)
        ttk.Entry(frame, textvariable=self.wall_thickness, width=6).pack(side=tk.LEFT, padx=5)
        
        ttk.Checkbutton(wall_frame, text="Include Drywall", 
                       variable=self.include_drywall).pack(anchor=tk.W, pady=2)
        
        frame2 = ttk.Frame(wall_frame)
        frame2.pack(fill=tk.X, pady=2)
        ttk.Label(frame2, text="Layers:").pack(side=tk.LEFT)
        ttk.Spinbox(frame2, from_=1, to=4, textvariable=self.drywall_layers, 
                   width=6).pack(side=tk.LEFT, padx=5)
        
        # X-ray parameters section
        param_frame = ttk.LabelFrame(parent, text="X-ray Parameters", padding=5)
        param_frame.pack(fill=tk.X, pady=5)
        
        params = [
            ("kVp:", self.kvp, 60, 90),
            ("mA:", self.ma, 5, 15),
            ("Time(s):", self.exposure_time, 0.1, 1.0),
            ("Workload:", self.workload, 50, 500)
        ]
        
        for label, var, min_val, max_val in params:
            frame = ttk.Frame(param_frame)
            frame.pack(fill=tk.X, pady=1)
            ttk.Label(frame, text=label).pack(side=tk.LEFT)
            ttk.Entry(frame, textvariable=var, width=6).pack(side=tk.LEFT, padx=5)
        
        # Calculate button
        calc_frame = ttk.Frame(parent)
        calc_frame.pack(fill=tk.X, pady=10)
        
        ttk.Button(calc_frame, text="Calculate Shielding Requirements", 
                   command=self.calculate_shielding,
                   style="Accent.TButton").pack(fill=tk.X, pady=2)
        
        # Heat map button
        self.heatmap_button = ttk.Button(calc_frame, text="🌡️ Show Radiation Heat Map", 
                   command=self.toggle_heatmap)
        self.heatmap_button.pack(fill=tk.X, pady=2)
        
        # Results section
        self.results_frame = ttk.LabelFrame(parent, text="Results", padding=10)
        self.results_frame.pack(fill=tk.BOTH, expand=True, pady=5)
        
        # Create frame for text and scrollbar
        text_container = ttk.Frame(self.results_frame)
        text_container.pack(fill=tk.BOTH, expand=True)
        
        self.results_text = tk.Text(text_container, height=20, wrap=tk.WORD, font=("Courier", 9))
        scrollbar = ttk.Scrollbar(text_container, command=self.results_text.yview)
        
        self.results_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.results_text.config(yscrollcommand=scrollbar.set)
        
    def setup_canvas(self, parent):
        # Canvas with scrollbars
        canvas_frame = ttk.Frame(parent)
        canvas_frame.pack(fill=tk.BOTH, expand=True)
        
        self.canvas = tk.Canvas(canvas_frame, bg="white", cursor="cross")
        
        v_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=self.canvas.yview)
        h_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.HORIZONTAL, command=self.canvas.xview)
        
        self.canvas.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)
        
        v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)
        self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        
        # Bind events
        self.canvas.bind("<Button-1>", self.canvas_click)
        self.canvas.bind("<Motion>", self.canvas_motion)
        
        # Status bar
        self.status_label = ttk.Label(parent, text="Load a PDF floor plan to begin", 
                                      relief=tk.SUNKEN, anchor=tk.W)
        self.status_label.pack(fill=tk.X, pady=2)
        
    def load_pdf(self):
        filename = filedialog.askopenfilename(
            title="Select Floor Plan PDF",
            filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")]
        )
        
        if filename:
            try:
                self.pdf_document = fitz.open(filename)
                self.current_page = 0
                self.display_pdf_page()
                self.status_label.config(text=f"Loaded: {filename}")
            except Exception as e:
                messagebox.showerror("Error", f"Failed to load PDF: {str(e)}")
    
    def display_pdf_page(self):
        if not self.pdf_document:
            return
        
        page = self.pdf_document[self.current_page]

        # Render page to pixmap with ultra high quality settings
        # Use 600 DPI for maximum quality (zoom = DPI/72)
        zoom = 600 / 72  # ~8.33x zoom = 600 DPI (ultra high quality)
        mat = fitz.Matrix(zoom, zoom)
        pix = page.get_pixmap(matrix=mat, alpha=False)  # No alpha channel for better quality
        
        # Convert to PIL Image at 600 DPI (high resolution for OCR)
        img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)

        # Store high-res version for OCR processing
        self.floor_plan_image = img

        # Create display version scaled down to 150 DPI (25% size for comfortable viewing)
        display_scale = 0.25  # Scale down from 600 DPI to 150 DPI
        display_width = int(img.width * display_scale)
        display_height = int(img.height * display_scale)
        display_img = img.resize((display_width, display_height), Image.Resampling.LANCZOS)

        # Convert display version to PhotoImage
        self.photo_image = ImageTk.PhotoImage(display_img)

        # Store display scale for coordinate mapping
        self.display_scale = display_scale
        
        # Update canvas
        self.canvas.delete("all")
        self.canvas_image = self.canvas.create_image(0, 0, anchor=tk.NW, image=self.photo_image)
        self.canvas.config(scrollregion=self.canvas.bbox(tk.ALL))
        
        self.update_display()
    
    def start_calibration(self):
        if not self.floor_plan_image:
            messagebox.showwarning("Warning", "Please load a floor plan first")
            return
        
        self.mode = "calibrate"
        self.calibration_points = []
        self.status_label.config(text="Click two points on the floor plan to calibrate distance")
    
    def start_place_tubehead(self):
        if not self.floor_plan_image:
            messagebox.showwarning("Warning", "Please load a floor plan first")
            return
        
        if self.scale_factor == 1.0:
            messagebox.showwarning("Warning", "Please calibrate the distance first")
            return
        
        self.mode = "place_tubehead"
        self.status_label.config(text="Click to place the X-ray tube head")
    
    def start_place_operator(self):
        if not self.floor_plan_image:
            messagebox.showwarning("Warning", "Please load a floor plan first")
            return
        
        if not self.tube_head_position:
            messagebox.showwarning("Warning", "Please place the tube head first")
            return
        
        self.mode = "place_operator"
        self.status_label.config(text="Click to place the operator position (exposure button)")
    
    def start_select_lead_08(self):
        if not self.floor_plan_image:
            messagebox.showwarning("Warning", "Please load a floor plan first")
            return
        
        self.mode = "select_lead_08"
        self.status_label.config(text="Click on a 0.8mm lead barrier line (color) on the floor plan")
    
    def start_select_lead_016(self):
        if not self.floor_plan_image:
            messagebox.showwarning("Warning", "Please load a floor plan first")
            return
        
        self.mode = "select_lead_016"
        self.status_label.config(text="Click on a 0.16mm lead barrier line (color) on the floor plan")
    
    def start_select_wall(self):
        """Start selecting two points on parallel wall lines to detect wall spacing"""
        if not self.floor_plan_image:
            messagebox.showwarning("Warning", "Please load a floor plan first")
            return
        
        self.mode = "select_wall_point1"
        self.wall_point1 = None
        self.wall_point2 = None
        self.status_label.config(text="Click on FIRST parallel line (outer edge of wall)")
        messagebox.showinfo("Wall Detection", 
            "Step 1: Click on the FIRST parallel line\n"
            "Step 2: Click on the SECOND parallel line\n\n"
            "Walls are drawn as two parallel black lines ||")
    
    def handle_wall_point1(self, x, y):
        """Handle first point selection for wall detection"""
        self.wall_point1 = (x, y)
        self.mode = "select_wall_point2"
        
        # Draw marker at first point
        self.canvas.delete("wall_temp")
        self.canvas.create_oval(x-5, y-5, x+5, y+5, fill="blue", outline="darkblue", 
                               width=2, tags="wall_temp")
        self.canvas.create_text(x, y-15, text="Point 1", fill="blue", 
                               font=("Arial", 10, "bold"), tags="wall_temp")
        
        self.status_label.config(text="Now click on SECOND parallel line (other edge of same wall)")
    
    def handle_wall_point2(self, x, y):
        """Handle second point selection and detect all walls with similar spacing"""
        self.wall_point2 = (x, y)
        
        # Draw marker at second point
        self.canvas.create_oval(x-5, y-5, x+5, y+5, fill="red", outline="darkred", 
                               width=2, tags="wall_temp")
        self.canvas.create_text(x, y-15, text="Point 2", fill="red", 
                               font=("Arial", 10, "bold"), tags="wall_temp")
        
        # Draw line between points
        self.canvas.create_line(self.wall_point1[0], self.wall_point1[1], 
                               x, y, fill="purple", width=2, tags="wall_temp")
        
        # Calculate wall spacing
        dx = x - self.wall_point1[0]
        dy = y - self.wall_point1[1]
        spacing_pixels = math.sqrt(dx**2 + dy**2)
        
        self.wall_spacing = spacing_pixels
        
        self.status_label.config(text=f"Wall spacing detected: {spacing_pixels:.1f} pixels. Click 'Detect Lead Barriers'")
        
        messagebox.showinfo("Wall Spacing Detected", 
            f"Wall spacing: {spacing_pixels:.1f} pixels\n\n"
            f"Now click 'Detect Lead Barriers' to find all walls")
        
        # Update status
        self.label_wall_status.config(text=f"{spacing_pixels:.0f}px", foreground="green")
        
        # Draw indicator on display canvas
        self.wall_display.delete("all")
        self.wall_display.create_line(5, 10, 25, 10, fill="black", width=2)
        self.wall_display.create_text(15, 5, text="||", fill="black", font=("Arial", 8, "bold"))
        
        self.mode = None
        self.canvas.delete("wall_temp")
    
    def update_tolerance(self):
        """Update color tolerance from spinbox"""
        self.color_tolerance = self.tolerance_var.get()
        print(f"Color tolerance updated to ±{self.color_tolerance} RGB units")
    
    def run_ocr_detection(self):
        """Run OCR to detect and visualize text regions"""
        if not self.floor_plan_image:
            messagebox.showwarning("Warning", "Please load a floor plan first")
            return

        # Clear cached text regions to force re-detection with current parameters
        if hasattr(self, '_cached_text_regions'):
            delattr(self, '_cached_text_regions')

        print("\n" + "="*60)
        print("RUNNING OCR TEXT DETECTION")
        print("="*60)

        # Flush GPU memory before OCR processing
        self.flush_gpu_memory()

        # Convert image to array
        img_array = np.array(self.floor_plan_image)
        height, width = img_array.shape[:2]

        # Get text regions
        self.ocr_text_regions = self._get_text_regions(img_array, width, height)
        
        if self.ocr_text_regions:
            messagebox.showinfo("OCR Complete", 
                f"Detected {len(self.ocr_text_regions)} text regions.\n"
                "Text areas are shown in semi-transparent red overlay.")
            self.barrier_status.config(
                text=f"OCR: {len(self.ocr_text_regions)} text regions detected", 
                foreground="blue"
            )
        else:
            messagebox.showinfo("OCR Complete", "No text regions detected.")
            self.barrier_status.config(text="OCR: No text found", foreground="gray")
        
        # Update display to show text regions
        self.update_display()

        # Flush GPU memory after OCR processing
        self.flush_gpu_memory()

        print("="*60 + "\n")

    def update_easyocr_conf_label(self):
        """Update EasyOCR confidence label"""
        value = int(self.easyocr_min_confidence.get())
        self.easyocr_conf_value_label.config(text=str(value))

    def update_easyocr_contrast_label(self):
        """Update EasyOCR contrast label"""
        value = int(self.easyocr_contrast.get())
        self.easyocr_contrast_value_label.config(text=str(value))

    def update_easyocr_padding_label(self):
        """Update EasyOCR padding label"""
        value = int(self.easyocr_padding.get())
        self.easyocr_padding_value_label.config(text=str(value))

    def update_threshold_block_label(self):
        """Update threshold block size label"""
        value = int(self.preproc_threshold_blocksize.get())
        # Ensure odd number for adaptive threshold
        if value % 2 == 0:
            value += 1
            self.preproc_threshold_blocksize.set(value)
        self.threshold_block_value_label.config(text=str(value))

    def update_threshold_c_label(self):
        """Update threshold C constant label"""
        value = int(self.preproc_threshold_c.get())
        self.threshold_c_value_label.config(text=str(value))

    def update_denoise_h_label(self):
        """Update denoise filter strength label"""
        value = int(self.preproc_denoise_h.get())
        self.denoise_h_value_label.config(text=str(value))

    def update_red_r_label(self):
        """Update red R minimum label"""
        value = int(self.preproc_red_r_min.get())
        self.red_r_value_label.config(text=str(value))

    def update_red_gb_label(self):
        """Update red G/B maximum label"""
        value = int(self.preproc_red_gb_max.get())
        self.red_gb_value_label.config(text=str(value))

    def update_morph_kernel_label(self):
        """Update morphology kernel size label"""
        value = int(self.preproc_morph_kernel.get())
        self.morph_kernel_value_label.config(text=str(value))

    def preview_preprocessing(self):
        """Preview the preprocessing with current settings without running OCR"""
        if not self.floor_plan_image:
            messagebox.showwarning("Warning", "Please load a floor plan first")
            return

        if not self.use_opencv_preprocessing.get():
            messagebox.showinfo("Preprocessing Disabled",
                               "OpenCV Preprocessing is currently disabled.\n\n"
                               "Enable it using the checkbox above the OCR button to see preprocessing effects.")
            return

        print("\n" + "="*60)
        print("PREPROCESSING PREVIEW")
        print("="*60)

        # Flush GPU memory before processing
        self.flush_gpu_memory()

        # Convert image to array
        img_array = np.array(self.floor_plan_image)

        # Show standard preprocessing
        print("\nStandard text preprocessing:")
        img_standard = self._preprocess_image_for_ocr(img_array, red_only=False)

        # Show red text preprocessing if enabled
        if self.easyocr_detect_red.get():
            print("\nRed text preprocessing:")
            img_red = self._preprocess_image_for_ocr(img_array, red_only=True)

        # Flush GPU memory after preview
        self.flush_gpu_memory()

        print("="*60 + "\n")

    def clear_ocr(self):
        """Clear OCR text regions"""
        self.ocr_text_regions = []
        if hasattr(self, '_cached_text_regions'):
            delattr(self, '_cached_text_regions')

        # Flush memory after clearing OCR data
        print("\n" + "="*60)
        print("CLEARING OCR DATA")
        print("="*60)
        self.flush_gpu_memory()
        print("="*60 + "\n")

        self.update_display()
        print("OCR text regions cleared")

    def clear_barriers(self):
        """Clear all detected barriers"""
        self.lead_barriers = []
        self.wall_barriers = []
        self.barrier_status.config(text="Barriers cleared", foreground="gray")
        self.show_heatmap = False
        self.heatmap_button.config(text="🌡️ Show Radiation Heat Map")

        # Flush memory after clearing barrier data
        print("\n" + "="*60)
        print("CLEARING BARRIER DATA")
        print("="*60)
        self.flush_gpu_memory()
        print("="*60 + "\n")

        self.update_display()
        print("Barriers cleared")

    def flush_gpu_memory(self):
        """Flush GPU and RAM memory buffers to prevent memory leaks"""
        print("    Flushing GPU and RAM memory...")

        # Clear any cached preprocessing results
        if hasattr(self, '_cached_text_regions'):
            delattr(self, '_cached_text_regions')
            print("      - Cleared cached OCR text regions")

        # Clear any temporary image data from preprocessing
        temp_attrs = ['_temp_preprocessed', '_temp_red_preprocessed', '_temp_rotated']
        for attr in temp_attrs:
            if hasattr(self, attr):
                delattr(self, attr)

        # Force Python garbage collection (RAM cleanup)
        collected = gc.collect()
        print(f"      - Python garbage collection: {collected} objects freed")

        # Force another aggressive garbage collection pass
        gc.collect(generation=2)  # Full collection of all generations

        # If CuPy is available, clear GPU memory pool
        if GPU_AVAILABLE and cp is not None:
            try:
                # Get memory pool and free all blocks
                mempool = cp.get_default_memory_pool()
                pinned_mempool = cp.get_default_pinned_memory_pool()

                mempool.free_all_blocks()
                pinned_mempool.free_all_blocks()
                print("      - CuPy memory pool cleared (GPU + pinned)")
            except Exception as e:
                print(f"      - CuPy cleanup warning: {e}")

        # If EasyOCR is using PyTorch (common), try to clear CUDA cache
        if EASYOCR_AVAILABLE:
            try:
                import torch
                if torch.cuda.is_available():
                    torch.cuda.empty_cache()
                    torch.cuda.synchronize()
                    torch.cuda.ipc_collect()  # Clear inter-process communication cache
                    print("      - PyTorch CUDA cache cleared")
            except ImportError:
                # PyTorch not available, skip
                pass
            except Exception as e:
                print(f"      - PyTorch cleanup warning: {e}")

        # Clear OpenCV memory cache if available
        if OPENCV_AVAILABLE:
            try:
                # OpenCV doesn't have a direct memory clear, but we can force it
                # by triggering internal cleanup
                cv2.setNumThreads(0)
                cv2.setNumThreads(-1)  # Reset to auto
                print("      - OpenCV thread pool reset")
            except:
                pass

        # Final aggressive garbage collection for both RAM and Python objects
        gc.collect()
        gc.collect(generation=2)

        print("    GPU and RAM memory flushed")

    def toggle_heatmap(self):
        """Toggle radiation heat map display"""
        if not self.tube_head_position:
            messagebox.showwarning("Warning", "Please place the tube head first")
            return
        
        if self.scale_factor == 1.0:
            messagebox.showwarning("Warning", "Please calibrate the distance first")
            return
        
        self.show_heatmap = not self.show_heatmap
        
        if self.show_heatmap:
            self.heatmap_button.config(text="🌡️ Hide Radiation Heat Map")
            self.status_label.config(text="Generating radiation heat map...")
            self.root.update()
            self.generate_heatmap()
        else:
            self.heatmap_button.config(text="🌡️ Show Radiation Heat Map")
            self.update_display()
    
    def generate_heatmap(self):
        """Generate and display radiation exposure heat map - ENHANCED DEBUG VERSION"""
        if not self.floor_plan_image:
            return

        # CRITICAL CHECK: Tube head must be set
        if not self.tube_head_position:
            messagebox.showwarning("Warning", "Tube head position not set!")
            return

        # Flush memory before heatmap generation
        self.flush_gpu_memory()

        width, height = self.floor_plan_image.size
        heatmap = np.zeros((height, width), dtype=np.float32)
        
        # GET TUBE HEAD AS ORIGIN - THIS IS THE KEY
        tx, ty = self.tube_head_position
        
        step = 10
        total_points = (width // step) * (height // step)
        processed = 0
        progress_step = max(1, total_points // 10)
        
        print(f"\n{'='*70}")
        print(f"GENERATING RADIATION HEAT MAP")
        print(f"{'='*70}")
        print(f"\n📍 TUBE HEAD ORIGIN (Source of Radiation):")
        print(f"   Position: ({tx:.0f}, {ty:.0f}) pixels on floor plan")
        print(f"   This is where X-rays originate from")
        print(f"\n📏 CALIBRATION:")
        print(f"   Scale: {self.scale_factor:.2f} pixels/meter")
        if self.scale_factor == 1.0:
            print(f"   ⚠️  WARNING: Scale = 1.0 means NOT CALIBRATED!")
            print(f"   ⚠️  ALL distances will be wrong!")
        print(f"\n📐 GEOMETRY:")
        print(f"   Floor plan: {width}x{height} pixels")
        print(f"   Primary beam: {self.primary_beam_direction.get()}° from horizontal")
        print(f"   Cone angle: ±30° (60° total)")
        
        # Calculate and display key parameters
        mas_per_exp = self.ma.get() * self.exposure_time.get()
        weekly_mas = mas_per_exp * self.workload.get()
        kvp_factor = (self.kvp.get() / 70.0) ** 2.5
        base_rate = 5 * kvp_factor
        
        print(f"\n⚡ X-RAY MACHINE PARAMETERS:")
        print(f"   kVp: {self.kvp.get():.0f}")
        print(f"   mA: {self.ma.get():.1f}")
        print(f"   Time: {self.exposure_time.get():.2f} s")
        print(f"   mAs/exposure: {mas_per_exp:.2f}")
        print(f"   Workload: {self.workload.get():.0f} exposures/week")
        print(f"   Weekly mAs: {weekly_mas:.1f}")
        print(f"   kVp factor: {kvp_factor:.3f}")
        print(f"   Base rate: {base_rate:.2f} mR per mAs at 1m")
        
        # Calculate expected dose at 1 meter
        dose_at_1m = (base_rate * weekly_mas) * 0.01
        print(f"\n📊 EXPECTED DOSES (unshielded, primary beam):")
        print(f"   At 1.0 m: {dose_at_1m:.4f} mSv/week ({dose_at_1m/self.weekly_dose_limit:.1f}x limit)")
        print(f"   At 2.0 m: {dose_at_1m/4:.4f} mSv/week ({dose_at_1m/4/self.weekly_dose_limit:.1f}x limit)")
        print(f"   At 3.0 m: {dose_at_1m/9:.4f} mSv/week ({dose_at_1m/9/self.weekly_dose_limit:.1f}x limit)")
        print(f"   Safety limit: {self.weekly_dose_limit:.3f} mSv/week")
        
        print(f"\n📊 EXPECTED DOSES (scatter radiation, NOT in primary beam):")
        print(f"   At 1.0 m: {dose_at_1m*0.001:.6f} mSv/week ({dose_at_1m*0.001/self.weekly_dose_limit:.3f}x limit)")
        print(f"   At 2.0 m: {dose_at_1m/4*0.001:.6f} mSv/week ({dose_at_1m/4*0.001/self.weekly_dose_limit:.3f}x limit)")
        print(f"   At 3.0 m: {dose_at_1m/9*0.001:.6f} mSv/week ({dose_at_1m/9*0.001/self.weekly_dose_limit:.3f}x limit)")
        
        print(f"\n⚠️  CRITICAL: Primary beam is 1000x stronger than scatter!")
        print(f"   Operator at 2m in PRIMARY: {dose_at_1m/4:.4f} mSv/week (OVER LIMIT!)")
        print(f"   Operator at 2m in SCATTER: {dose_at_1m/4*0.001:.6f} mSv/week (safe)")
        print(f"   This is why operators must stay OUT of primary beam!")
        
        print(f"\n🔄 Processing sample points within 5m radius...")
        
        primary_angle_rad = math.radians(self.primary_beam_direction.get())
        max_dose = 0
        max_location = (0, 0)
        min_dose = float('inf')
        dose_count = 0
        sample_calcs = []
        skipped_outside_radius = 0
        
        # Calculate 5-meter radius in pixels
        max_radius_m = 5.0
        max_radius_pixels = max_radius_m * self.scale_factor
        
        print(f"   5m radius = {max_radius_pixels:.0f} pixels")
        print(f"   Only calculating doses within this circle")
        
        for y in range(0, height, step):
            for x in range(0, width, step):
                # VECTOR FROM TUBE HEAD (ORIGIN) TO THIS POINT
                dx_pixels = x - tx
                dy_pixels = y - ty
                distance_pixels = math.sqrt(dx_pixels**2 + dy_pixels**2)
                
                # LIMIT TO 10-METER RADIUS
                if distance_pixels > max_radius_pixels:
                    skipped_outside_radius += 1
                    continue
                
                if distance_pixels < 10:
                    continue
                
                # CRITICAL: Convert pixels to meters using scale
                distance_m = distance_pixels / self.scale_factor
                
                if distance_m < 0.1:
                    continue
                
                # Calculate angle to this point FROM TUBE HEAD (needed for beam checks)
                angle_to_point = math.atan2(dy_pixels, dx_pixels)
                
                # Check which beam direction(s) this point is in
                # We have three beams: 0° (20%), 90° (40%), -90° (40%)
                primary_dir = self.primary_beam_direction.get()
                
                # Calculate weighted dose from all three beams
                total_dose_from_beams = 0
                in_any_primary = False
                
                for beam_angle_offset, workload_factor in zip(self.beam_angles, self.beam_workload_factors):
                    beam_angle = primary_dir + beam_angle_offset
                    beam_angle_rad = math.radians(beam_angle)
                    
                    # Calculate angle difference from this beam
                    angle_diff_beam = abs(angle_to_point - beam_angle_rad)
                    if angle_diff_beam > math.pi:
                        angle_diff_beam = 2 * math.pi - angle_diff_beam
                    
                    # Check if in this beam (within 30 degrees)
                    if angle_diff_beam < math.radians(30):
                        in_any_primary = True
                        # Calculate dose from this specific beam
                        beam_weekly_mas = weekly_mas * workload_factor
                        beam_exposure = base_rate * beam_weekly_mas / (distance_m ** 2)
                        beam_dose = beam_exposure * 0.01
                        total_dose_from_beams += beam_dose
                
                # If not in any primary beam, use scatter
                if in_any_primary:
                    dose_after_use = total_dose_from_beams
                    use_factor = 1.0  # For display purposes
                else:
                    # Scatter radiation from all beams combined
                    # Calculate total exposure at this distance
                    exposure_at_dist = base_rate * weekly_mas / (distance_m ** 2)
                    dose_unshielded = exposure_at_dist * 0.01
                    dose_after_use = dose_unshielded * 0.001
                    use_factor = 0.001
                
                # Shielding
                intersected = self.check_line_intersection_with_barriers(tx, ty, x, y)
                total_shield_mm = sum(b['thickness'] for b in intersected)
                
                tvl = 0.17
                if self.kvp.get() > 70:
                    tvl *= (1 + (self.kvp.get() - 70) * 0.01)
                elif self.kvp.get() < 70:
                    tvl *= (1 - (70 - self.kvp.get()) * 0.008)
                
                transmission = 10 ** (-total_shield_mm / tvl) if total_shield_mm > 0 else 1.0
                final_dose = dose_after_use * transmission
                
                # Save first few samples for debugging
                if len(sample_calcs) < 10 and 0.5 < distance_m < 4.0:
                    sample_calcs.append({
                        'pos': (x, y),
                        'dist_m': distance_m,
                        'angle_deg': math.degrees(angle_to_point),
                        'in_any_beam': in_any_primary,
                        'num_beams': sum(1 for b in self.beam_angles if abs(angle_to_point - math.radians(primary_dir + b)) < math.radians(30)),
                        'use_factor': use_factor,
                        'dose_after_use': dose_after_use,
                        'barriers': len(intersected),
                        'shield': total_shield_mm,
                        'transmission': transmission,
                        'dose': final_dose
                    })
                
                # ALSO save operator position specifically if we hit it
                if self.operator_position:
                    op_x, op_y = self.operator_position
                    # Check if this sample point is near operator position (within step pixels)
                    if abs(x - op_x) < step and abs(y - op_y) < step:
                        sample_calcs.append({
                            'pos': (x, y),
                            'dist_m': distance_m,
                            'angle_deg': math.degrees(angle_to_point),
                            'in_any_beam': in_any_primary,
                            'num_beams': sum(1 for b in self.beam_angles if abs(angle_to_point - math.radians(primary_dir + b)) < math.radians(30)),
                            'use_factor': use_factor,
                            'dose_after_use': dose_after_use,
                            'barriers': len(intersected),
                            'shield': total_shield_mm,
                            'transmission': transmission,
                            'dose': final_dose,
                            'IS_OPERATOR': True
                        })
                
                if final_dose > max_dose:
                    max_dose = final_dose
                    max_location = (x, y)
                if final_dose > 0 and final_dose < min_dose:
                    min_dose = final_dose
                dose_count += 1
                
                # Fill heatmap
                for dy in range(step):
                    for dx in range(step):
                        py, px = y + dy, x + dx
                        if py < height and px < width:
                            heatmap[py, px] = final_dose
                
                processed += 1
                if processed % progress_step == 0:
                    print(f"   {processed/total_points*100:.0f}% complete...")
        
        print(f"\n{'='*70}")
        print(f"SAMPLE CALCULATIONS (verify correctness):")
        print(f"{'='*70}")
        print(f"Primary beam direction: {self.primary_beam_direction.get()}°")
        print(f"Three beams: 0° (20%), 90° (40%), -90° (40%) from primary")
        print(f"Points within ±30° of any beam are in 'primary beam'")
        print(f"")
        for i, s in enumerate(sample_calcs, 1):
            is_op = s.get('IS_OPERATOR', False)
            marker = "*** OPERATOR POSITION ***" if is_op else ""
            print(f"\nSample {i}: {marker}")
            print(f"   Position: ({s['pos'][0]}, {s['pos'][1]}) pixels")
            print(f"   Distance from tube: {s['dist_m']:.2f} m")
            print(f"   Angle to point: {s['angle_deg']:.1f}°")
            print(f"   In any beam: {s['in_any_beam']}")
            print(f"   Number of beams: {s['num_beams']}")
            print(f"   ---")
            print(f"   Use factor: {s['use_factor']} ({'primary' if s['use_factor'] == 1.0 else 'scatter'})")
            print(f"   Dose after use factor: {s['dose_after_use']:.6f} mSv/week")
            print(f"   Barriers intersected: {s['barriers']}")
            print(f"   Total shielding: {s['shield']:.2f} mm Pb")
            print(f"   Transmission: {s['transmission']:.6f}")
            print(f"   FINAL DOSE: {s['dose']:.6f} mSv/week ({s['dose']/self.weekly_dose_limit:.2f}x limit)")
            if is_op:
                print(f"   *** This is the OPERATOR position! ***")
        
        max_dist = math.sqrt((max_location[0]-tx)**2 + (max_location[1]-ty)**2) / self.scale_factor
        
        print(f"\n{'='*70}")
        print(f"HEAT MAP RESULTS:")
        print(f"{'='*70}")
        print(f"Calculation radius: 5.0 meters ({max_radius_pixels:.0f} pixels)")
        print(f"Points within radius: {dose_count:,}")
        print(f"Points outside radius (skipped): {skipped_outside_radius:,}")
        print(f"Maximum dose: {max_dose:.6f} mSv/week")
        print(f"   Location: ({max_location[0]}, {max_location[1]}) pixels")
        print(f"   Distance: {max_dist:.2f} m from tube head")
        print(f"   Ratio to limit: {max_dose/self.weekly_dose_limit:.2f}x")
        print(f"Minimum dose: {min_dose:.6f} mSv/week")
        print(f"\n{'='*70}")
        
        if max_dose < self.weekly_dose_limit * 0.01:
            print(f"\n⚠️  PROBLEM DETECTED: Maximum dose is {max_dose:.8f} mSv/week")
            print(f"   This is {max_dose/self.weekly_dose_limit*100:.4f}% of the safety limit")
            print(f"   This is extremely low and indicates:")
            print(f"   1. Scale factor = {self.scale_factor:.2f} - Is this correct?")
            print(f"   2. Workload = {self.workload.get():.0f} - Is this realistic?")
            print(f"   3. Distance calculations may be wrong")
            print(f"\n   DIAGNOSE:")
            print(f"   - If scale = 1.0, you MUST calibrate!")
            print(f"   - If workload < 50, increase to 200-300")
            print(f"   - Expected max dose should be 0.02-0.20 mSv/week")
        elif max_dose > self.weekly_dose_limit * 10:
            print(f"\n⚠️  VERY HIGH DOSES DETECTED")
            print(f"   Maximum is {max_dose/self.weekly_dose_limit:.1f}x the safety limit")
            print(f"   This is expected in primary beam area")
        else:
            print(f"\n✓ Dose levels appear realistic")
            print(f"  Maximum: {max_dose/self.weekly_dose_limit:.1f}x limit")
        
        print(f"\nHeat map generation complete!\n")

        self.heatmap_data = heatmap
        self.update_display()

        # Flush memory after heatmap generation
        self.flush_gpu_memory()

    def check_point_near_barrier(self, x, y, barrier, tolerance=5):
        """Check if a point is near a barrier"""
        barrier_set = set(barrier['pixels'])
        for dx in range(-tolerance, tolerance + 1):
            for dy in range(-tolerance, tolerance + 1):
                if (x + dx, y + dy) in barrier_set:
                    return True
        return False
    
    def dose_to_color(self, dose_msv):
        """
        Convert dose to color for heat map
        Red (high) -> Orange -> Yellow -> Green (safe)
        Enhanced opacity for better visibility
        """
        # Safety threshold is 0.02 mSv/week
        safe_level = self.weekly_dose_limit
        
        # Handle zero or very small doses
        if dose_msv < 0.000001:  # Essentially zero
            return (0, 255, 0, 60)  # Very transparent green
        
        if dose_msv >= safe_level * 5:  # Very high (5x limit)
            return (255, 0, 0, 220)  # Red, more opaque
        elif dose_msv >= safe_level * 2:  # High (2-5x limit)
            # Red to Orange
            ratio = (dose_msv - safe_level * 2) / (safe_level * 3)
            r = 255
            g = int(100 + 65 * (1 - ratio))  # 165 to 100
            return (r, g, 0, 200)  # Orange
        elif dose_msv >= safe_level:  # Above limit (1-2x)
            # Orange to Yellow
            ratio = (dose_msv - safe_level) / safe_level
            r = 255
            g = int(165 + (255 - 165) * ratio)  # 165 to 255
            return (r, g, 0, 180)  # Yellow
        elif dose_msv >= safe_level * 0.5:  # Approaching limit (0.5-1x)
            # Yellow to Yellow-Green
            ratio = (dose_msv - safe_level * 0.5) / (safe_level * 0.5)
            r = int(255 * ratio)
            g = 255
            return (r, g, 0, 160)  # Yellow-green
        elif dose_msv >= safe_level * 0.1:  # Low (0.1-0.5x)
            # Yellow-Green to Green
            ratio = dose_msv / (safe_level * 0.5)
            r = int(128 * ratio)
            g = 255
            return (r, g, 0, 140)  # Green
        else:  # Very safe (<0.1x limit)
            return (0, 255, 0, 120)  # Green, semi-transparent
    
    def canvas_click(self, event):
        # Get canvas coordinates (display scale)
        x_display = self.canvas.canvasx(event.x)
        y_display = self.canvas.canvasy(event.y)

        # Convert to full resolution coordinates for processing
        scale = getattr(self, 'display_scale', 1.0)
        x = x_display / scale
        y = y_display / scale
        
        if self.mode == "calibrate":
            self.calibration_points.append((x, y))
            
            if len(self.calibration_points) == 2:
                # Calculate scale factor
                p1, p2 = self.calibration_points
                pixel_distance = math.sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)
                real_distance = self.calibration_distance.get()
                self.scale_factor = pixel_distance / real_distance
                
                self.cal_status.config(
                    text=f"Calibrated: {self.scale_factor:.1f} pixels/meter",
                    foreground="green"
                )
                
                self.mode = "view"
                self.status_label.config(text="Calibration complete")
                self.update_display()
        
        elif self.mode == "place_tubehead":
            self.tube_head_position = (x, y)
            self.mode = "view"
            self.status_label.config(text="Tube head placed")
            self.update_display()
        
        elif self.mode == "place_operator":
            self.operator_position = (x, y)
            self.mode = "view"
            self.status_label.config(text="Operator position placed")
            self.update_display()
        
        elif self.mode == "select_lead_08":
            # Get color from floor plan image at click position
            color = self.get_pixel_color(int(x), int(y))
            if color:
                self.lead_08mm_color = color
                self.color_08_display.config(bg=self.rgb_to_hex(color))
                self.label_08_status.config(text=f"RGB{color}", foreground="green")
                self.mode = "view"
                self.status_label.config(text="0.8mm lead color selected")
        
        elif self.mode == "select_lead_016":
            # Get color from floor plan image at click position
            color = self.get_pixel_color(int(x), int(y))
            if color:
                self.lead_016mm_color = color
                self.color_016_display.config(bg=self.rgb_to_hex(color))
                self.label_016_status.config(text=f"RGB{color}", foreground="green")
                self.mode = "view"
                self.status_label.config(text="0.16mm lead color selected")
        
        elif self.mode == "select_wall_point1":
            # First point of wall spacing
            self.handle_wall_point1(x, y)
        
        elif self.mode == "select_wall_point2":
            # Second point of wall spacing
            self.handle_wall_point2(x, y)
    
    def canvas_motion(self, event):
        # Get canvas coordinates (display scale)
        x_display = self.canvas.canvasx(event.x)
        y_display = self.canvas.canvasy(event.y)

        # Convert to full resolution coordinates
        display_scale = getattr(self, 'display_scale', 1.0)
        x = x_display / display_scale
        y = y_display / display_scale

        if self.scale_factor > 1.0 and self.tube_head_position:
            # Show distance from tube head in meters
            dx = (x - self.tube_head_position[0]) / self.scale_factor
            dy = (y - self.tube_head_position[1]) / self.scale_factor
            distance = math.sqrt(dx**2 + dy**2)
            self.status_label.config(text=f"Distance from tube head: {distance:.2f} meters")
    
    def get_pixel_color(self, x, y):
        """Get RGB color from floor plan image at specified coordinates"""
        if not self.floor_plan_image:
            return None
        
        try:
            # Ensure coordinates are within image bounds
            if 0 <= x < self.floor_plan_image.width and 0 <= y < self.floor_plan_image.height:
                pixel = self.floor_plan_image.getpixel((x, y))
                if isinstance(pixel, int):  # Grayscale
                    return (pixel, pixel, pixel)
                return pixel[:3]  # RGB only
        except:
            pass
        return None
    
    def rgb_to_hex(self, rgb):
        """Convert RGB tuple to hex color string"""
        return f'#{rgb[0]:02x}{rgb[1]:02x}{rgb[2]:02x}'
    
    def colors_match(self, color1, color2, tolerance=None):
        """Check if two RGB colors match within tolerance"""
        if not color1 or not color2:
            return False
        
        if tolerance is None:
            tolerance = self.color_tolerance
        
        return all(abs(c1 - c2) <= tolerance for c1, c2 in zip(color1, color2))
    
    def _show_preprocessing_preview(self, preprocessed_image, title="Preprocessed Image Preview"):
        """Show a popup window with the preprocessed image for debugging"""
        try:
            # Create a new top-level window
            preview_window = tk.Toplevel(self.root)
            preview_window.title(title)
            preview_window.geometry("900x700")

            # Add info label at top
            info_text = f"Image Shape: {preprocessed_image.shape} | Type: {preprocessed_image.dtype} | Full Size (use scrollbars to navigate)"
            info_label = ttk.Label(preview_window, text=info_text, foreground="blue", font=("Arial", 9, "bold"))
            info_label.pack(pady=5)

            # Create frame for canvas and scrollbars
            canvas_frame = ttk.Frame(preview_window)
            canvas_frame.pack(expand=True, fill=tk.BOTH, padx=10, pady=5)

            # Create canvas with scrollbars
            canvas = tk.Canvas(canvas_frame, bg="gray")

            # Vertical scrollbar
            v_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.VERTICAL, command=canvas.yview)
            v_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

            # Horizontal scrollbar
            h_scrollbar = ttk.Scrollbar(canvas_frame, orient=tk.HORIZONTAL, command=canvas.xview)
            h_scrollbar.pack(side=tk.BOTTOM, fill=tk.X)

            # Pack canvas
            canvas.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

            # Configure canvas scrolling
            canvas.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)

            # Convert numpy array to PIL Image at ORIGINAL SIZE
            if len(preprocessed_image.shape) == 2:
                # Grayscale
                pil_img = Image.fromarray(preprocessed_image)
            else:
                # RGB
                pil_img = Image.fromarray(preprocessed_image)

            # Convert to PhotoImage (full resolution)
            photo = ImageTk.PhotoImage(pil_img)

            # Store reference to prevent garbage collection
            preview_window.photo = photo

            # Display image on canvas
            canvas.create_image(0, 0, anchor=tk.NW, image=photo)

            # Update scroll region to match image size
            canvas.configure(scrollregion=(0, 0, pil_img.width, pil_img.height))

            # Add mouse wheel scrolling support
            def _on_mousewheel(event):
                canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")

            def _on_shift_mousewheel(event):
                canvas.xview_scroll(int(-1 * (event.delta / 120)), "units")

            canvas.bind_all("<MouseWheel>", _on_mousewheel)
            canvas.bind_all("<Shift-MouseWheel>", _on_shift_mousewheel)

            # Clean up bindings when window closes
            def on_close():
                canvas.unbind_all("<MouseWheel>")
                canvas.unbind_all("<Shift-MouseWheel>")
                preview_window.destroy()

            # Add close button
            close_btn = ttk.Button(preview_window, text="Close Preview", command=on_close)
            close_btn.pack(pady=5)

            preview_window.protocol("WM_DELETE_WINDOW", on_close)

            print(f"    Preview window opened: {title} ({pil_img.width}x{pil_img.height})")
        except Exception as e:
            print(f"    Error showing preview: {e}")

    def _preprocess_image_for_ocr(self, img_array, red_only=False):
        """Preprocess image using OpenCV for better OCR detection
        
        Args:
            img_array: Input image array
            red_only: If True, isolate only red pixels for detection
        """
        # Check if preprocessing is enabled
        if not self.use_opencv_preprocessing.get():
            # Preprocessing disabled - return original image (no conversion)
            print("      - OpenCV preprocessing disabled, returning original image")

            # Still show preview window with original image
            original_img = img_array[:, :, :3].astype(np.uint8)

            if red_only:
                self._show_preprocessing_preview(original_img, "RED Text Preview (No Preprocessing)")
            else:
                self._show_preprocessing_preview(original_img, "Standard Text Preview (No Preprocessing)")

            return original_img
        
        # Preprocessing enabled - continue with OpenCV
        if not OPENCV_AVAILABLE:
            # No preprocessing, return as-is
            return img_array[:, :, :3].astype(np.uint8)
        
        if red_only:
            # Check if red text isolation is enabled
            if self.preproc_use_red_isolation.get():
                print("    Preprocessing for RED text with binary mask isolation...")
                img_rgb = img_array[:, :, :3].astype(np.uint8)

                # Get configurable red detection parameters
                red_r_min = self.preproc_red_r_min.get()
                red_gb_max = self.preproc_red_gb_max.get()

                # Create mask for red pixels (with configurable tolerance)
                lower_red = np.array([red_r_min, 0, 0])  # R must be > red_r_min
                upper_red = np.array([255, red_gb_max, red_gb_max])  # G,B must be < red_gb_max

                # Create mask where red pixels are white, everything else black
                mask = cv2.inRange(img_rgb, lower_red, upper_red)

                # Invert: red text becomes black on white background (better for OCR)
                inverted = cv2.bitwise_not(mask)

                # Create color preview: original red pixels on white background
                color_preview = np.full_like(img_rgb, 255)  # White background
                color_preview[mask > 0] = img_rgb[mask > 0]  # Keep original red colors

                print(f"      - Isolated red pixels to binary mask (R>{red_r_min}, G/B<{red_gb_max})")

                # Apply morphological operations to clean up (if enabled)
                if self.preproc_use_morphology.get():
                    kernel_size = self.preproc_morph_kernel.get()
                    kernel = np.ones((kernel_size, kernel_size), np.uint8)
                    cleaned = cv2.morphologyEx(inverted, cv2.MORPH_CLOSE, kernel)
                    print(f"      - Applied morphology (kernel={kernel_size}x{kernel_size})")
                else:
                    cleaned = inverted
                    print(f"      - Morphology disabled")

                # Denoise (if enabled)
                if self.preproc_use_denoise.get():
                    denoise_h = self.preproc_denoise_h.get()
                    denoised = cv2.fastNlMeansDenoising(cleaned, None, denoise_h, 7, 21)
                    print(f"      - Applied denoising (h={denoise_h})")
                else:
                    denoised = cleaned
                    print(f"      - Denoising disabled")

                # Apply morphology/denoise cleanup to the color preview as well
                # Convert mask cleanup back to a clean color image
                # Use the cleaned mask to refine which pixels to keep
                if self.preproc_use_morphology.get() or self.preproc_use_denoise.get():
                    # The cleaned/denoised binary tells us which pixels survived cleanup
                    # Invert back: in denoised, text=black(0), background=white(255)
                    cleaned_mask = cv2.bitwise_not(denoised)
                    # Apply cleaned mask to original colors
                    color_result = np.full_like(img_rgb, 255)  # White background
                    color_result[cleaned_mask > 0] = img_rgb[cleaned_mask > 0]
                else:
                    color_result = color_preview

                print("      - Red text isolated with original colors preserved")

                # Show preview window with original red colors
                self._show_preprocessing_preview(color_result, "RED Text Preprocessing Preview (Isolated Red)")

                return color_result
            else:
                # Red isolation disabled - process as normal color image
                print("    Preprocessing for RED text (no binary isolation, using standard preprocessing)...")
                # Fall through to standard preprocessing below
        else:
            print("    Preprocessing image with OpenCV...")

            # Start with RGB
            img_rgb = img_array[:, :, :3].astype(np.uint8)
            current_img = img_rgb
            is_grayscale = False

            # Determine if we need grayscale for any operation
            needs_grayscale = self.preproc_use_grayscale.get() or self.preproc_use_threshold.get()

            # Convert to grayscale if needed
            if needs_grayscale:
                gray = cv2.cvtColor(current_img, cv2.COLOR_RGB2GRAY)
                current_img = gray
                is_grayscale = True
                if self.preproc_use_grayscale.get():
                    print("      - Converted to grayscale")
                else:
                    print("      - Converted to grayscale (required for threshold)")
            else:
                print("      - Keeping color image (grayscale disabled)")

            # Apply adaptive thresholding (if enabled) - requires grayscale
            if self.preproc_use_threshold.get():
                if not is_grayscale:
                    # Should not happen due to needs_grayscale check, but safety first
                    current_img = cv2.cvtColor(current_img, cv2.COLOR_RGB2GRAY)
                    is_grayscale = True
                    print("      - Converted to grayscale (required for threshold)")

                block_size = self.preproc_threshold_blocksize.get()
                c_constant = self.preproc_threshold_c.get()

                # Ensure block size is odd
                if block_size % 2 == 0:
                    block_size += 1

                binary = cv2.adaptiveThreshold(
                    current_img, 255,
                    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                    cv2.THRESH_BINARY,
                    block_size,
                    c_constant
                )
                current_img = binary
                print(f"      - Applied adaptive thresholding (block={block_size}, C={c_constant})")
            else:
                print("      - Adaptive thresholding disabled")

            # Denoise to remove small artifacts (if enabled)
            if self.preproc_use_denoise.get():
                denoise_h = self.preproc_denoise_h.get()

                if is_grayscale:
                    # Use grayscale denoising
                    denoised = cv2.fastNlMeansDenoising(current_img, None, denoise_h, 7, 21)
                    print(f"      - Applied grayscale denoising (h={denoise_h})")
                else:
                    # Use color denoising
                    denoised = cv2.fastNlMeansDenoisingColored(current_img, None, denoise_h, denoise_h, 7, 21)
                    print(f"      - Applied color denoising (h={denoise_h})")

                current_img = denoised
            else:
                print("      - Denoising disabled")

            # Convert to RGB for EasyOCR (it expects RGB)
            if is_grayscale:
                preprocessed = cv2.cvtColor(current_img, cv2.COLOR_GRAY2RGB)
                print("      - Converted grayscale to RGB for EasyOCR")
            else:
                preprocessed = current_img
                print("      - Already in RGB format")

            print("      - Preprocessing complete")

            # Show preview window for debugging
            if red_only and not self.preproc_use_red_isolation.get():
                # Red text pass but isolation is disabled - using standard preprocessing
                self._show_preprocessing_preview(preprocessed, "RED Text Preprocessing Preview (Standard Processing)")
            else:
                self._show_preprocessing_preview(preprocessed, "Standard Text Preprocessing Preview")

            return preprocessed
    
    def _get_text_regions(self, img_array, width, height):
        """Get text regions from image using EasyOCR (cached per detection run)"""
        if not EASYOCR_AVAILABLE:
            return []

        # Check if we already have text regions cached for this detection run
        if hasattr(self, '_cached_text_regions'):
            return self._cached_text_regions

        # Get configuration options
        languages = [lang.strip() for lang in self.easyocr_languages.get().split(',') if lang.strip()]
        use_gpu = self.easyocr_gpu.get() and GPU_AVAILABLE
        min_confidence = self.easyocr_min_confidence.get() / 100.0  # Convert to 0-1 range
        whitelist = self.easyocr_whitelist.get().strip()
        paragraph = self.easyocr_paragraph.get()
        detect_red = self.easyocr_detect_red.get()
        contrast = self.easyocr_contrast.get()
        padding = self.easyocr_padding.get()
        detect_rotated = self.easyocr_rotate_detection.get()

        print(f"    Configuration:")
        print(f"      Languages: {languages}")
        print(f"      GPU: {use_gpu}")
        print(f"      Min confidence: {min_confidence:.2f}")
        print(f"      Paragraph mode: {paragraph}")
        print(f"      Detect red text: {detect_red}")
        print(f"      Contrast: {contrast}")
        print(f"      Bounding box padding: {padding}px")
        print(f"      Detect rotated text: {detect_rotated}")
        if whitelist:
            print(f"      Character whitelist: '{whitelist}'")

        # Initialize OCR reader (lazy loading) - reinitialize if languages changed
        if self.ocr_reader is None:
            print(f"    Initializing EasyOCR with languages: {languages}")
            print("    (First time may take a moment - downloading models)...")
            if use_gpu:
                print("    Using GPU for OCR acceleration")
            self.ocr_reader = easyocr.Reader(languages, gpu=use_gpu)
            print("    OCR reader initialized")

        # Preprocess image for better OCR (grayscale, binary, denoise)
        img_preprocessed = self._preprocess_image_for_ocr(img_array)

        # Run OCR to detect text regions
        print("    Detecting text regions with EasyOCR...")

        # Build allowlist parameter
        allowlist = whitelist if whitelist else None

        # Enhanced parameters for rotated/angled text detection
        results = self.ocr_reader.readtext(
            img_preprocessed,
            paragraph=paragraph,       # User configurable
            allowlist=allowlist,       # Character whitelist
            rotation_info=None,        # Detect text at any rotation angle
            min_size=5,                # Detect very small text
            text_threshold=0.4,        # Lower text detection confidence
            low_text=0.2,              # Lower threshold for faint text
            link_threshold=0.2,        # Lower threshold for linking text
            canvas_size=2560,          # Larger canvas for better detection
            mag_ratio=1.5,             # Magnification for small text
            slope_ths=0.3,             # More tolerant to text slope/rotation
            ycenter_ths=0.5,           # More tolerant to vertical alignment
            height_ths=0.7,            # More tolerant to height variations
            width_ths=0.9,             # More tolerant to width variations
            add_margin=0.15,           # Add margin around detected text
            contrast_ths=contrast / 2.0  # Contrast adjustment (0-1 range)
        )

        print(f"    Found {len(results)} text regions")

        # Convert text bounding boxes to exclusion regions
        text_regions = []
        low_conf_count = 0

        for (bbox, text, conf) in results:
            # Apply minimum confidence filter
            if conf < min_confidence:
                continue

            # bbox is [[x1,y1], [x2,y2], [x3,y3], [x4,y4]]
            xs = [point[0] for point in bbox]
            ys = [point[1] for point in bbox]

            min_x = int(min(xs))
            max_x = int(max(xs))
            min_y = int(min(ys))
            max_y = int(max(ys))

            # Add padding around text to ensure we catch all text pixels
            text_regions.append({
                'min_x': max(0, min_x - padding),
                'max_x': min(width, max_x + padding),
                'min_y': max(0, min_y - padding),
                'max_y': min(height, max_y + padding),
                'text': text,
                'confidence': conf,
                'source': 'easyocr'
            })

            # Show confidence level in output
            if conf < 0.7:
                low_conf_count += 1
                print(f"      Text: '{text}' at ({min_x},{min_y})-({max_x},{max_y}) conf={conf:.2f} [LOW CONF]")
            else:
                print(f"      Text: '{text}' at ({min_x},{min_y})-({max_x},{max_y}) conf={conf:.2f}")

        if low_conf_count > 0:
            print(f"    Note: {low_conf_count} text regions detected with low confidence (may include noise)")

        # SECOND PASS: Detect RED text specifically (if enabled)
        red_count = 0
        if detect_red:
            print("\n    Running SECOND PASS for red text...")
            img_red_only = self._preprocess_image_for_ocr(img_array, red_only=True)

            results_red = self.ocr_reader.readtext(
                img_red_only,
                paragraph=paragraph,
                allowlist=allowlist,
                rotation_info=None,
                min_size=3,                # Even smaller for single letters like "H"
                text_threshold=0.3,        # Lower threshold for colored text
                low_text=0.15,
                link_threshold=0.15,
                canvas_size=2560,
                mag_ratio=2.0,             # Higher magnification for small red text
                slope_ths=0.3,
                ycenter_ths=0.5,
                height_ths=0.7,
                width_ths=0.9,
                add_margin=0.2,
                contrast_ths=contrast / 2.0
            )

            print(f"    Found {len(results_red)} RED text regions")

            # Add red text regions
            for (bbox, text, conf) in results_red:
                # Apply minimum confidence filter
                if conf < min_confidence:
                    continue

                xs = [point[0] for point in bbox]
                ys = [point[1] for point in bbox]

                min_x = int(min(xs))
                max_x = int(max(xs))
                min_y = int(min(ys))
                max_y = int(max(ys))

                text_regions.append({
                    'min_x': max(0, min_x - padding),
                    'max_x': min(width, max_x + padding),
                    'min_y': max(0, min_y - padding),
                    'max_y': min(height, max_y + padding),
                    'text': text,
                    'confidence': conf,
                    'color': 'red',  # Mark as red text
                    'source': 'easyocr'
                })

                red_count += 1
                print(f"      RED Text: '{text}' at ({min_x},{min_y})-({max_x},{max_y}) conf={conf:.2f}")

            if red_count > 0:
                print(f"    Total: {len(results)} standard + {red_count} red = {len(text_regions)} text regions")
        else:
            print("\n    Red text detection disabled (skipping 2nd pass)")

        # THIRD PASS: Detect text at 90-degree rotation (if enabled)
        rotated_count = 0
        if detect_rotated:
            print("\n    Running THIRD PASS for 90° rotated text...")

            # Rotate image 90 degrees clockwise
            import cv2
            img_rotated = cv2.rotate(img_preprocessed, cv2.ROTATE_90_CLOCKWISE)

            # Show preview of rotated preprocessed image (if preprocessing was enabled)
            if self.use_opencv_preprocessing.get():
                self._show_preprocessing_preview(img_rotated, "Rotated 90° Preprocessing Preview")

            # Run OCR on rotated image
            results_rotated = self.ocr_reader.readtext(
                img_rotated,
                paragraph=paragraph,
                allowlist=allowlist,
                rotation_info=None,
                min_size=5,
                text_threshold=0.4,
                low_text=0.2,
                link_threshold=0.2,
                canvas_size=2560,
                mag_ratio=1.5,
                slope_ths=0.3,
                ycenter_ths=0.5,
                height_ths=0.7,
                width_ths=0.9,
                add_margin=0.15,
                contrast_ths=contrast / 2.0
            )

            print(f"    Found {len(results_rotated)} text regions in rotated image")

            # Translate bounding boxes back to original orientation
            # Original image: (width, height)
            # Rotated image (90° clockwise): (height, width)
            # For cv2.ROTATE_90_CLOCKWISE, to convert point (x_rot, y_rot) back to original:
            #   x_orig = y_rot
            #   y_orig = height - x_rot

            for (bbox, text, conf) in results_rotated:
                # Apply minimum confidence filter
                if conf < min_confidence:
                    continue

                # bbox in rotated image coordinates
                # Translate each point back to original image coordinates
                original_bbox = []
                for point in bbox:
                    x_rot, y_rot = point
                    x_orig = y_rot
                    y_orig = height - x_rot
                    original_bbox.append([x_orig, y_orig])

                # Get bounding box in original coordinates
                xs = [point[0] for point in original_bbox]
                ys = [point[1] for point in original_bbox]

                min_x = int(min(xs))
                max_x = int(max(xs))
                min_y = int(min(ys))
                max_y = int(max(ys))

                text_regions.append({
                    'min_x': max(0, min_x - padding),
                    'max_x': min(width, max_x + padding),
                    'min_y': max(0, min_y - padding),
                    'max_y': min(height, max_y + padding),
                    'text': text,
                    'confidence': conf,
                    'rotated': True,  # Mark as detected from rotated image
                    'source': 'easyocr'
                })

                rotated_count += 1
                print(f"      ROTATED Text: '{text}' at ({min_x},{min_y})-({max_x},{max_y}) conf={conf:.2f}")

            if rotated_count > 0:
                print(f"    Total: {len(results)} standard + {red_count} red + {rotated_count} rotated = {len(text_regions)} text regions")

            # FOURTH PASS: Detect RED text in rotated image (if red detection is enabled)
            rotated_red_count = 0
            if detect_red:
                print("\n    Running FOURTH PASS for RED text in 90° rotated image...")

                # Rotate the red-only preprocessed image
                img_red_only = self._preprocess_image_for_ocr(img_array, red_only=True)
                img_red_rotated = cv2.rotate(img_red_only, cv2.ROTATE_90_CLOCKWISE)

                # Show preview of rotated red image (now in original colors)
                if self.use_opencv_preprocessing.get():
                    self._show_preprocessing_preview(img_red_rotated, "Rotated 90° RED Text Preview (Isolated Red)")

                results_red_rotated = self.ocr_reader.readtext(
                    img_red_rotated,
                    paragraph=paragraph,
                    allowlist=allowlist,
                    rotation_info=None,
                    min_size=3,
                    text_threshold=0.3,
                    low_text=0.15,
                    link_threshold=0.15,
                    canvas_size=2560,
                    mag_ratio=2.0,
                    slope_ths=0.3,
                    ycenter_ths=0.5,
                    height_ths=0.7,
                    width_ths=0.9,
                    add_margin=0.2,
                    contrast_ths=contrast / 2.0
                )

                print(f"    Found {len(results_red_rotated)} RED text regions in rotated image")

                # Translate bounding boxes back to original orientation
                for (bbox, text, conf) in results_red_rotated:
                    if conf < min_confidence:
                        continue

                    # Translate each point back to original image coordinates
                    original_bbox = []
                    for point in bbox:
                        x_rot, y_rot = point
                        x_orig = y_rot
                        y_orig = height - x_rot
                        original_bbox.append([x_orig, y_orig])

                    xs = [point[0] for point in original_bbox]
                    ys = [point[1] for point in original_bbox]

                    min_x = int(min(xs))
                    max_x = int(max(xs))
                    min_y = int(min(ys))
                    max_y = int(max(ys))

                    text_regions.append({
                        'min_x': max(0, min_x - padding),
                        'max_x': min(width, max_x + padding),
                        'min_y': max(0, min_y - padding),
                        'max_y': min(height, max_y + padding),
                        'text': text,
                        'confidence': conf,
                        'color': 'red',  # Mark as red text
                        'rotated': True,  # Mark as detected from rotated image
                        'source': 'easyocr'
                    })

                    rotated_red_count += 1
                    print(f"      ROTATED RED Text: '{text}' at ({min_x},{min_y})-({max_x},{max_y}) conf={conf:.2f}")

                if rotated_red_count > 0:
                    print(f"    Total: {len(results)} standard + {red_count} red + {rotated_count} rotated + {rotated_red_count} rotated red = {len(text_regions)} text regions")
        else:
            print("\n    Rotated text detection disabled (skipping 3rd pass)")

        # Cache the results for this detection run
        self._cached_text_regions = text_regions
        
        # Also store in instance variable for visualization
        self.ocr_text_regions = text_regions
        
        return text_regions
    
    def _filter_text_regions(self, pixels, img_array, width, height):
        """Filter out text regions from pixels using OCR"""
        if not EASYOCR_AVAILABLE:
            return pixels
        
        # Get text regions (cached if already run)
        text_regions = self._get_text_regions(img_array, width, height)
        
        if not text_regions:
            return pixels
        
        # Filter out pixels that fall within text regions
        filtered_pixels = set()
        removed_count = 0
        
        for (x, y) in pixels:
            in_text_region = False
            for region in text_regions:
                if (region['min_x'] <= x <= region['max_x'] and 
                    region['min_y'] <= y <= region['max_y']):
                    in_text_region = True
                    removed_count += 1
                    break
            
            if not in_text_region:
                filtered_pixels.add((x, y))
        
        print(f"    Removed {removed_count:,} pixels in text regions, kept {len(filtered_pixels):,}")
        
        return filtered_pixels
    
    def detect_lead_barriers(self):
        """Detect lead barriers and walls using GPU acceleration (if available)"""
        if not self.floor_plan_image:
            messagebox.showwarning("Warning", "Please load a floor plan first")
            return
        
        if not self.lead_08mm_color and not self.lead_016mm_color and not self.wall_spacing:
            messagebox.showwarning("Warning", "Please select at least one barrier color or detect wall spacing first")
            return
        
        self.lead_barriers = []
        self.wall_barriers = []
        width, height = self.floor_plan_image.size

        print(f"\n{'='*60}")
        print(f"BARRIER DETECTION")
        print(f"{'='*60}")
        print(f"Image size: {width}x{height} = {width*height:,} pixels")
        print(f"Processing method: {'GPU (CuPy)' if GPU_AVAILABLE else 'CPU (NumPy)'}")

        # Flush memory before barrier detection
        self.flush_gpu_memory()

        import time
        start_time = time.time()

        # Convert image to numpy array
        img_array = np.array(self.floor_plan_image)
        
        # Clear OCR text region cache for new detection run
        if hasattr(self, '_cached_text_regions'):
            delattr(self, '_cached_text_regions')
        
        if GPU_AVAILABLE:
            # GPU-ACCELERATED VERSION
            barrier_pixels_08, barrier_pixels_016, drywall_pixels = self._detect_barriers_gpu(img_array, width, height)
        else:
            # CPU VERSION (fallback)
            barrier_pixels_08, barrier_pixels_016, drywall_pixels = self._detect_barriers_cpu(img_array, width, height)
        
        elapsed = time.time() - start_time
        
        print(f"\n{'='*60}")
        print(f"DETECTION COMPLETE")
        print(f"{'='*60}")
        print(f"Time: {elapsed:.2f} seconds")
        print(f"Found {len(barrier_pixels_08):,} pixels of 0.8mm lead")
        print(f"Found {len(barrier_pixels_016):,} pixels of 0.16mm lead")
        print(f"Found {len(drywall_pixels):,} pixels of walls")
        print(f"Processing speed: {(width*height/elapsed/1000000):.2f} megapixels/second")
        print(f"{'='*60}\n")
        
        # Store detected barriers with their type
        if barrier_pixels_08:
            self.lead_barriers.append({
                'type': '0.8mm',
                'thickness': 0.8,
                'pixels': barrier_pixels_08,
                'color': self.lead_08mm_color
            })
        
        if barrier_pixels_016:
            self.lead_barriers.append({
                'type': '0.16mm',
                'thickness': 0.16,
                'pixels': barrier_pixels_016,
                'color': self.lead_016mm_color
            })
        
        if drywall_pixels:
            self.wall_barriers.append({
                'type': 'wall',
                'thickness': 0.25,
                'pixels': drywall_pixels,
                'spacing': self.wall_spacing
            })
        
        # Update status
        total_items = len(self.lead_barriers) + len(self.wall_barriers)
        if total_items > 0:
            barrier_info = []
            if barrier_pixels_08:
                barrier_info.append(f"{len(barrier_pixels_08):,} pixels of 0.8mm lead")
            if barrier_pixels_016:
                barrier_info.append(f"{len(barrier_pixels_016):,} pixels of 0.16mm lead")
            if drywall_pixels:
                barrier_info.append(f"{len(drywall_pixels):,} pixels of walls")
            
            status_text = "Detected: " + ", ".join(barrier_info)
            self.barrier_status.config(text=status_text, foreground="green")
            self.update_display()
            
            speed_info = f"\n\nProcessing speed: {(width*height/elapsed/1000000):.2f} MP/s"
            if GPU_AVAILABLE:
                speed_info += "\n(GPU accelerated)"
            
            messagebox.showinfo("Success", f"Barriers detected in {elapsed:.2f}s!{speed_info}\n\n{status_text}")
        else:
            self.barrier_status.config(text="No barriers detected - try different color or tolerance", foreground="red")
            messagebox.showwarning("No Barriers",
                "No barrier pixels found with selected colors.\n\n" +
                "Tips:\n" +
                "- Try clicking on a different part of the barrier line\n" +
                "- Increase tolerance if needed\n" +
                "- Ensure the color is distinct and solid")

        # Flush memory after barrier detection
        self.flush_gpu_memory()

    def _detect_barriers_gpu(self, img_array, width, height):
        """GPU-accelerated barrier detection using CuPy"""
        print("Using GPU acceleration...")
        
        # Transfer image to GPU
        img_gpu = cp.asarray(img_array[:, :, :3])  # RGB only
        
        barrier_pixels_08 = []
        barrier_pixels_016 = []
        drywall_pixels = []
        
        tolerance = self.color_tolerance
        
        # Detect 0.8mm lead barriers
        if self.lead_08mm_color:
            target = cp.array(self.lead_08mm_color, dtype=cp.uint8)
            
            # Calculate color distance on GPU
            diff = cp.abs(img_gpu.astype(cp.int16) - target)
            matches = cp.all(diff <= tolerance, axis=2)
            
            # Get matching pixel coordinates
            y_coords, x_coords = cp.where(matches)
            
            # Transfer back to CPU
            pixels = list(zip(cp.asnumpy(x_coords), cp.asnumpy(y_coords)))
            
            print(f"  0.8mm lead: {len(pixels):,} pixels detected")
            
            # Filter out text regions
            if len(pixels) > 0:
                pixels_filtered = self._filter_text_regions(set(pixels), img_array, width, height)
                barrier_pixels_08 = list(pixels_filtered)
                print(f"  0.8mm lead: {len(barrier_pixels_08):,} pixels after text filtering")
            else:
                barrier_pixels_08 = []
        
        # Detect 0.16mm lead barriers
        if self.lead_016mm_color:
            target = cp.array(self.lead_016mm_color, dtype=cp.uint8)
            
            diff = cp.abs(img_gpu.astype(cp.int16) - target)
            matches = cp.all(diff <= tolerance, axis=2)
            
            y_coords, x_coords = cp.where(matches)
            pixels = list(zip(cp.asnumpy(x_coords), cp.asnumpy(y_coords)))
            
            print(f"  0.16mm lead: {len(pixels):,} pixels detected")
            
            # Filter out text regions
            if len(pixels) > 0:
                pixels_filtered = self._filter_text_regions(set(pixels), img_array, width, height)
                barrier_pixels_016 = list(pixels_filtered)
                print(f"  0.16mm lead: {len(barrier_pixels_016):,} pixels after text filtering")
            else:
                barrier_pixels_016 = []
        
        # Detect walls (parallel black lines)
        if self.wall_spacing:
            print(f"  Detecting walls with {self.wall_spacing:.1f}px spacing...")
            
            # Find black pixels (walls are typically black)
            black_threshold = 50  # RGB values below this are considered black
            is_black = cp.all(img_gpu < black_threshold, axis=2)
            
            y_coords, x_coords = cp.where(is_black)
            black_pixels_list = list(zip(cp.asnumpy(x_coords), cp.asnumpy(y_coords)))
            
            if len(black_pixels_list) > 0:
                print(f"    Found {len(black_pixels_list):,} black pixels")
                
                # Create a set for fast lookup
                black_pixels_set = set(black_pixels_list)
                
                # Check perpendicular directions to find parallel lines
                tolerance_px = int(self.wall_spacing * 0.35)  # 35% tolerance
                spacing = int(self.wall_spacing)
                min_line_length = max(30, spacing * 4)  # Walls must be LONG (30+ pixels minimum)
                
                print(f"    Searching for parallel lines (min length: {min_line_length}px, excluding text)...")
                
                # For better performance, sample but check more thoroughly
                sample_step = max(1, len(black_pixels_list) // 50000)
                
                verified_wall_pixels = set()
                
                for idx, (x, y) in enumerate(black_pixels_list[::sample_step]):
                    if idx % 10000 == 0 and idx > 0:
                        print(f"      Processed {idx:,} sampled pixels...")
                    
                    # Check perpendicular to potential line direction
                    found_parallel = False
                    
                    for angle_offset in range(-25, 26, 5):  # Check ±25° in 5° increments
                        if found_parallel:
                            break
                            
                        rad = math.radians(angle_offset)
                        
                        # Check both perpendicular directions
                        for base_angle in [90, 0]:
                            check_angle = math.radians(base_angle) + rad
                            
                            dx = int(spacing * math.cos(check_angle))
                            dy = int(spacing * math.sin(check_angle))
                            
                            # Check for black pixel at spacing distance
                            found = False
                            parallel_x, parallel_y = None, None
                            
                            for search_dx in range(-tolerance_px, tolerance_px + 1, 3):
                                for search_dy in range(-tolerance_px, tolerance_px + 1, 3):
                                    check_x = x + dx + search_dx
                                    check_y = y + dy + search_dy
                                    
                                    if (check_x, check_y) in black_pixels_set:
                                        parallel_x, parallel_y = check_x, check_y
                                        found = True
                                        break
                                
                                if found:
                                    break
                            
                            if found:
                                # Found parallel pixel - now verify both are part of VERY LONG lines
                                line_angle = check_angle + math.pi/2  # Along the line
                                
                                # Check line 1 continuity - much stricter now
                                line1_length = 0
                                for step in range(-40, 41, 2):  # Check ±40 pixels along line
                                    if step == 0:
                                        continue
                                    nx = int(x + step * math.cos(line_angle))
                                    ny = int(y + step * math.sin(line_angle))
                                    # Check nearby (±2 pixels) for black
                                    found_neighbor = False
                                    for ox in range(-2, 3):
                                        for oy in range(-2, 3):
                                            if (nx + ox, ny + oy) in black_pixels_set:
                                                line1_length += 1
                                                found_neighbor = True
                                                break
                                        if found_neighbor:
                                            break
                                
                                # Check line 2 continuity
                                line2_length = 0
                                for step in range(-40, 41, 2):
                                    if step == 0:
                                        continue
                                    nx = int(parallel_x + step * math.cos(line_angle))
                                    ny = int(parallel_y + step * math.sin(line_angle))
                                    found_neighbor = False
                                    for ox in range(-2, 3):
                                        for oy in range(-2, 3):
                                            if (nx + ox, ny + oy) in black_pixels_set:
                                                line2_length += 1
                                                found_neighbor = True
                                                break
                                        if found_neighbor:
                                            break
                                
                                # STRICT: Both lines must be LONG (at least 20 segments = ~40px continuous)
                                # This filters out text which is typically 5-10px tall
                                if line1_length >= 20 and line2_length >= 20:
                                    # Additional check: Count actual continuous segments
                                    # Text has breaks between letters, walls are continuous
                                    
                                    # Count gaps in line 1
                                    line1_gaps = 0
                                    prev_found = False
                                    for step in range(-40, 41, 2):
                                        if step == 0:
                                            continue
                                        nx = int(x + step * math.cos(line_angle))
                                        ny = int(y + step * math.sin(line_angle))
                                        
                                        found_here = False
                                        for ox in range(-2, 3):
                                            for oy in range(-2, 3):
                                                if (nx + ox, ny + oy) in black_pixels_set:
                                                    found_here = True
                                                    break
                                            if found_here:
                                                break
                                        
                                        # If previous had black but this doesn't = gap
                                        if prev_found and not found_here:
                                            line1_gaps += 1
                                        prev_found = found_here
                                    
                                    # Count gaps in line 2
                                    line2_gaps = 0
                                    prev_found = False
                                    for step in range(-40, 41, 2):
                                        if step == 0:
                                            continue
                                        nx = int(parallel_x + step * math.cos(line_angle))
                                        ny = int(parallel_y + step * math.sin(line_angle))
                                        
                                        found_here = False
                                        for ox in range(-2, 3):
                                            for oy in range(-2, 3):
                                                if (nx + ox, ny + oy) in black_pixels_set:
                                                    found_here = True
                                                    break
                                            if found_here:
                                                break
                                        
                                        if prev_found and not found_here:
                                            line2_gaps += 1
                                        prev_found = found_here
                                    
                                    # Walls are continuous (few gaps), text has many gaps (between letters)
                                    # Allow max 5 small gaps (for imperfect lines)
                                    if line1_gaps <= 5 and line2_gaps <= 5:
                                        # This is a valid wall pair!
                                        # Add all pixels along both lines, not just the sampled ones
                                        for step in range(-40, 41):
                                            nx1 = int(x + step * math.cos(line_angle))
                                            ny1 = int(y + step * math.sin(line_angle))
                                            nx2 = int(parallel_x + step * math.cos(line_angle))
                                            ny2 = int(parallel_y + step * math.sin(line_angle))
                                            
                                            # Add nearby pixels to fill in the line
                                            for ox in range(-1, 2):
                                                for oy in range(-1, 2):
                                                    if (nx1 + ox, ny1 + oy) in black_pixels_set:
                                                        verified_wall_pixels.add((nx1 + ox, ny1 + oy))
                                                    if (nx2 + ox, ny2 + oy) in black_pixels_set:
                                                        verified_wall_pixels.add((nx2 + ox, ny2 + oy))
                                        
                                        found_parallel = True
                                        break
                            
                            if found_parallel:
                                break
                
                # Filter out text regions using OCR
                print(f"    Detected {len(verified_wall_pixels):,} potential wall pixels")
                wall_pixels_filtered = self._filter_text_regions(verified_wall_pixels, img_array, width, height)
                
                drywall_pixels = list(wall_pixels_filtered)
                print(f"  Walls: {len(drywall_pixels):,} pixels after OCR text filtering")
            else:
                drywall_pixels = []
                print(f"  Walls: No black pixels found")
        
        return barrier_pixels_08, barrier_pixels_016, drywall_pixels
    
    def _detect_barriers_cpu(self, img_array, width, height):
        """CPU fallback barrier detection using NumPy"""
        print("Using CPU processing (install cupy for GPU acceleration)...")
        
        # Extract RGB channels
        img_rgb = img_array[:, :, :3]
        
        barrier_pixels_08 = []
        barrier_pixels_016 = []
        drywall_pixels = []
        
        tolerance = self.color_tolerance
        total_pixels = width * height
        progress_step = total_pixels // 10
        processed = 0
        
        # Detect 0.8mm lead barriers
        if self.lead_08mm_color:
            target = np.array(self.lead_08mm_color, dtype=np.uint8)
            diff = np.abs(img_rgb.astype(np.int16) - target)
            matches = np.all(diff <= tolerance, axis=2)
            y_coords, x_coords = np.where(matches)
            pixels = list(zip(x_coords, y_coords))
            print(f"  0.8mm lead: {len(pixels):,} pixels detected")
            
            # Filter out text regions
            if len(pixels) > 0:
                pixels_filtered = self._filter_text_regions(set(pixels), img_array, width, height)
                barrier_pixels_08 = list(pixels_filtered)
                print(f"  0.8mm lead: {len(barrier_pixels_08):,} pixels after text filtering")
            else:
                barrier_pixels_08 = []
        
        # Detect 0.16mm lead barriers
        if self.lead_016mm_color:
            target = np.array(self.lead_016mm_color, dtype=np.uint8)
            diff = np.abs(img_rgb.astype(np.int16) - target)
            matches = np.all(diff <= tolerance, axis=2)
            y_coords, x_coords = np.where(matches)
            pixels = list(zip(x_coords, y_coords))
            print(f"  0.16mm lead: {len(pixels):,} pixels detected")
            
            # Filter out text regions
            if len(pixels) > 0:
                pixels_filtered = self._filter_text_regions(set(pixels), img_array, width, height)
                barrier_pixels_016 = list(pixels_filtered)
                print(f"  0.16mm lead: {len(barrier_pixels_016):,} pixels after text filtering")
            else:
                barrier_pixels_016 = []
        
        # Detect walls (parallel black lines)
        if self.wall_spacing:
            print(f"  Detecting walls with {self.wall_spacing:.1f}px spacing...")
            
            # Find black pixels
            black_threshold = 50
            is_black = np.all(img_rgb < black_threshold, axis=2)
            y_coords, x_coords = np.where(is_black)
            black_pixels_list = list(zip(x_coords, y_coords))
            
            if len(black_pixels_list) > 0:
                print(f"    Found {len(black_pixels_list):,} black pixels")
                
                black_pixels_set = set(black_pixels_list)
                
                tolerance_px = int(self.wall_spacing * 0.35)
                spacing = int(self.wall_spacing)
                min_line_length = max(30, spacing * 4)
                
                print(f"    Searching for parallel lines (min length: {min_line_length}px, excluding text)...")
                
                sample_step = max(1, len(black_pixels_list) // 50000)
                
                verified_wall_pixels = set()
                
                for idx, (x, y) in enumerate(black_pixels_list[::sample_step]):
                    if idx % 10000 == 0 and idx > 0:
                        print(f"      Processed {idx:,} sampled pixels...")
                    
                    found_parallel = False
                    
                    for angle_offset in range(-25, 26, 5):
                        if found_parallel:
                            break
                            
                        rad = math.radians(angle_offset)
                        
                        for base_angle in [90, 0]:
                            check_angle = math.radians(base_angle) + rad
                            
                            dx = int(spacing * math.cos(check_angle))
                            dy = int(spacing * math.sin(check_angle))
                            
                            found = False
                            parallel_x, parallel_y = None, None
                            
                            for search_dx in range(-tolerance_px, tolerance_px + 1, 3):
                                for search_dy in range(-tolerance_px, tolerance_px + 1, 3):
                                    check_x = x + dx + search_dx
                                    check_y = y + dy + search_dy
                                    
                                    if (check_x, check_y) in black_pixels_set:
                                        parallel_x, parallel_y = check_x, check_y
                                        found = True
                                        break
                                
                                if found:
                                    break
                            
                            if found:
                                line_angle = check_angle + math.pi/2
                                
                                line1_length = 0
                                for step in range(-40, 41, 2):
                                    if step == 0:
                                        continue
                                    nx = int(x + step * math.cos(line_angle))
                                    ny = int(y + step * math.sin(line_angle))
                                    found_neighbor = False
                                    for ox in range(-2, 3):
                                        for oy in range(-2, 3):
                                            if (nx + ox, ny + oy) in black_pixels_set:
                                                line1_length += 1
                                                found_neighbor = True
                                                break
                                        if found_neighbor:
                                            break
                                
                                line2_length = 0
                                for step in range(-40, 41, 2):
                                    if step == 0:
                                        continue
                                    nx = int(parallel_x + step * math.cos(line_angle))
                                    ny = int(parallel_y + step * math.sin(line_angle))
                                    found_neighbor = False
                                    for ox in range(-2, 3):
                                        for oy in range(-2, 3):
                                            if (nx + ox, ny + oy) in black_pixels_set:
                                                line2_length += 1
                                                found_neighbor = True
                                                break
                                        if found_neighbor:
                                            break
                                
                                if line1_length >= 20 and line2_length >= 20:
                                    # Count gaps - text has many (between letters), walls have few
                                    line1_gaps = 0
                                    prev_found = False
                                    for step in range(-40, 41, 2):
                                        if step == 0:
                                            continue
                                        nx = int(x + step * math.cos(line_angle))
                                        ny = int(y + step * math.sin(line_angle))
                                        
                                        found_here = False
                                        for ox in range(-2, 3):
                                            for oy in range(-2, 3):
                                                if (nx + ox, ny + oy) in black_pixels_set:
                                                    found_here = True
                                                    break
                                            if found_here:
                                                break
                                        
                                        if prev_found and not found_here:
                                            line1_gaps += 1
                                        prev_found = found_here
                                    
                                    line2_gaps = 0
                                    prev_found = False
                                    for step in range(-40, 41, 2):
                                        if step == 0:
                                            continue
                                        nx = int(parallel_x + step * math.cos(line_angle))
                                        ny = int(parallel_y + step * math.sin(line_angle))
                                        
                                        found_here = False
                                        for ox in range(-2, 3):
                                            for oy in range(-2, 3):
                                                if (nx + ox, ny + oy) in black_pixels_set:
                                                    found_here = True
                                                    break
                                            if found_here:
                                                break
                                        
                                        if prev_found and not found_here:
                                            line2_gaps += 1
                                        prev_found = found_here
                                    
                                    # Max 5 gaps allowed (walls continuous, text has many gaps)
                                    if line1_gaps <= 5 and line2_gaps <= 5:
                                        # Add all pixels along both lines
                                        for step in range(-40, 41):
                                            nx1 = int(x + step * math.cos(line_angle))
                                            ny1 = int(y + step * math.sin(line_angle))
                                            nx2 = int(parallel_x + step * math.cos(line_angle))
                                            ny2 = int(parallel_y + step * math.sin(line_angle))
                                            
                                            for ox in range(-1, 2):
                                                for oy in range(-1, 2):
                                                    if (nx1 + ox, ny1 + oy) in black_pixels_set:
                                                        verified_wall_pixels.add((nx1 + ox, ny1 + oy))
                                                    if (nx2 + ox, ny2 + oy) in black_pixels_set:
                                                        verified_wall_pixels.add((nx2 + ox, ny2 + oy))
                                        
                                        found_parallel = True
                                        break
                            
                            if found_parallel:
                                break
                
                # Filter out text regions using OCR  
                print(f"    Detected {len(verified_wall_pixels):,} potential wall pixels")
                wall_pixels_filtered = self._filter_text_regions(verified_wall_pixels, img_array, width, height)
                
                drywall_pixels = list(wall_pixels_filtered)
                print(f"  Walls: {len(drywall_pixels):,} pixels after OCR text filtering")
            else:
                drywall_pixels = []
                print(f"  Walls: No black pixels found")
        
        return barrier_pixels_08, barrier_pixels_016, drywall_pixels
    
    def check_line_intersection_with_barriers(self, x1, y1, x2, y2):
        """
        Check if a line from (x1,y1) to (x2,y2) intersects with any lead barriers or drywall
        Returns list of intersected barriers with their properties
        Uses efficient spatial lookup instead of checking every pixel
        """
        intersected_barriers = []
        
        # Combine all barriers
        all_barriers = self.lead_barriers + self.wall_barriers
        
        if not all_barriers:
            return intersected_barriers
        
        # Get all pixels along the line
        line_pixels = self.get_line_pixels(int(x1), int(y1), int(x2), int(y2))
        
        # Create a set of line pixel positions for fast lookup
        line_pixel_set = set(line_pixels)
        
        # For each barrier, create a spatial grid for faster lookup
        for barrier in all_barriers:
            intersection_found = False
            
            # Create a set of barrier pixels for fast lookup
            barrier_pixel_set = set(barrier['pixels'])
            
            # Check direct intersection first (exact matches)
            if line_pixel_set & barrier_pixel_set:
                intersected_barriers.append(barrier)
                print(f"Direct intersection found with {barrier['type']} barrier")
                continue
            
            # Check nearby pixels (within tolerance)
            # This is more thorough but slower
            tolerance = 5  # pixels
            for lx, ly in line_pixels:
                for dx in range(-tolerance, tolerance + 1):
                    for dy in range(-tolerance, tolerance + 1):
                        if (lx + dx, ly + dy) in barrier_pixel_set:
                            intersected_barriers.append(barrier)
                            intersection_found = True
                            print(f"Near intersection found with {barrier['type']} barrier at offset ({dx},{dy})")
                            break
                    if intersection_found:
                        break
                if intersection_found:
                    break
        
        return intersected_barriers
    
    def get_line_pixels(self, x1, y1, x2, y2):
        """Get all pixels along a line using Bresenham's algorithm"""
        pixels = []
        dx = abs(x2 - x1)
        dy = abs(y2 - y1)
        sx = 1 if x1 < x2 else -1
        sy = 1 if y1 < y2 else -1
        err = dx - dy
        
        x, y = x1, y1
        
        while True:
            pixels.append((x, y))
            
            if x == x2 and y == y2:
                break
            
            e2 = 2 * err
            if e2 > -dy:
                err -= dy
                x += sx
            if e2 < dx:
                err += dx
                y += sy
        
        return pixels
    
    def update_display(self):
        if not self.canvas_image:
            return

        print(f"Update display called. Show heatmap: {self.show_heatmap}")

        # Get display scale factor for coordinate conversion
        scale = getattr(self, 'display_scale', 1.0)

        # Remove annotations (but keep floor plan)
        self.canvas.delete("annotation")
        
        # Draw heat map FIRST (if enabled) so it appears as overlay on floor plan
        if self.show_heatmap and hasattr(self, 'heatmap_data') and self.heatmap_data is not None:
            print("Drawing heat map overlay...")
            self.draw_heatmap_overlay()
        else:
            # Remove heatmap if turned off
            self.canvas.delete("heatmap_layer")
        
        # Draw calibration line
        if len(self.calibration_points) == 2:
            p1, p2 = self.calibration_points
            # Scale coordinates for display
            p1_display = (p1[0] * scale, p1[1] * scale)
            p2_display = (p2[0] * scale, p2[1] * scale)
            self.canvas.create_line(p1_display[0], p1_display[1], p2_display[0], p2_display[1],
                                   fill="blue", width=3, tags="annotation")
            self.canvas.create_oval(p1_display[0]-5, p1_display[1]-5, p1_display[0]+5, p1_display[1]+5,
                                   fill="blue", tags="annotation")
            self.canvas.create_oval(p2_display[0]-5, p2_display[1]-5, p2_display[0]+5, p2_display[1]+5,
                                   fill="blue", tags="annotation")
        
        # Draw tube head and patient position with three beam directions
        if self.tube_head_position:
            x, y = self.tube_head_position
            # Scale for display
            x_display = x * scale
            y_display = y * scale

            # Draw patient head as "H"
            self.canvas.create_text(x_display, y_display, text="H", fill="darkred",
                                   font=("Arial", 20, "bold"), tags="annotation")
            self.canvas.create_oval(x_display-15, y_display-15, x_display+15, y_display+15,
                                   outline="darkred", width=3, tags="annotation")
            
            # Get primary direction
            primary_dir = self.primary_beam_direction.get()
            
            # Draw three beam arrows: 0°, -45°, +45° from primary direction
            # With workload percentages: 20%, 40%, 40%
            arrow_length = 2.5 * self.scale_factor  # 2.5 meters
            
            for i, (angle_offset, workload_pct) in enumerate(zip(self.beam_angles, self.beam_workload_factors)):
                # Calculate actual angle
                actual_angle = primary_dir + angle_offset
                angle_rad = math.radians(actual_angle)
                
                # Determine arrow color/width based on workload
                if workload_pct == 0.40:
                    color = "red"
                    width = 4
                    alpha_color = "#FF4444"
                else:  # 0.20
                    color = "orange"
                    width = 3
                    alpha_color = "#FF8844"
                
                # Calculate arrow endpoint (full resolution)
                end_x = x + arrow_length * math.cos(angle_rad)
                end_y = y + arrow_length * math.sin(angle_rad)

                # Scale for display
                end_x_display = end_x * scale
                end_y_display = end_y * scale

                # Draw cone (60-degree spread)
                cone_half_angle = 30
                left_angle = math.radians(actual_angle - cone_half_angle)
                right_angle = math.radians(actual_angle + cone_half_angle)

                # Create cone polygon
                points = [x_display, y_display]
                for ang in np.linspace(left_angle, right_angle, 20):
                    px = x + arrow_length * math.cos(ang)
                    py = y + arrow_length * math.sin(ang)
                    points.extend([px * scale, py * scale])

                self.canvas.create_polygon(points, fill=alpha_color, outline=color,
                                          width=width, stipple="gray50", tags="annotation")

                # Draw center arrow
                self.canvas.create_line(x_display, y_display, end_x_display, end_y_display, fill=color, width=width,
                                       arrow=tk.LAST, arrowshape=(16, 20, 6), tags="annotation")
                
                # Add workload percentage label
                label_x = x + (arrow_length * 0.6) * math.cos(angle_rad)
                label_y = y + (arrow_length * 0.6) * math.sin(angle_rad)
                self.canvas.create_text(label_x * scale, label_y * scale,
                                       text=f"{int(workload_pct*100)}%",
                                       fill=color, font=("Arial", 11, "bold"),
                                       tags="annotation")

            # Label
            self.canvas.create_text(x_display, y_display-30, text="Patient Head\n(Tube Position)",
                                   fill="darkred", font=("Arial", 10, "bold"),
                                   tags="annotation")

            # Draw 5-meter radius circle
            if self.scale_factor > 1.0:
                radius_5m = 5.0 * self.scale_factor
                radius_5m_display = radius_5m * scale
                self.canvas.create_oval(x_display - radius_5m_display, y_display - radius_5m_display,
                                       x_display + radius_5m_display, y_display + radius_5m_display,
                                       outline="blue", width=2, dash=(10, 5),
                                       tags="annotation")

                # Label the 5m circle
                label_x = (x + radius_5m * 0.707) * scale  # 45 degrees
                label_y = (y - radius_5m * 0.707) * scale
                self.canvas.create_text(label_x, label_y,
                                       text="5m radius\n(heat map limit)",
                                       fill="blue", font=("Arial", 9),
                                       tags="annotation")
        
        # Draw detected lead barriers with better visibility
        if self.lead_barriers:
            for barrier in self.lead_barriers:
                # Use different colors for different barrier types
                if barrier['type'] == '0.8mm':
                    color = "magenta"
                    size = 3
                else:  # 0.16mm
                    color = "cyan"
                    size = 2

                # Draw every 5th pixel for better visibility and performance
                for bx, by in barrier['pixels'][::5]:
                    bx_display = bx * scale
                    by_display = by * scale
                    self.canvas.create_rectangle(bx_display-size, by_display-size, bx_display+size, by_display+size,
                                                fill=color, outline=color, tags="annotation")
        
        # Draw detected wall barriers (parallel lines)
        if self.wall_barriers:
            for barrier in self.wall_barriers:
                color = "yellow"
                size = 2

                # Draw every 4th pixel (was 8th) for better line visibility
                for bx, by in barrier["pixels"]:
                    bx_display = bx * scale
                    by_display = by * scale
                    self.canvas.create_rectangle(bx_display-size, by_display-size, bx_display+size, by_display+size,
                                                fill=color, outline=color, tags="annotation")
        
        # Draw OCR text regions (semi-transparent overlay)
        if self.ocr_text_regions:
            for region in self.ocr_text_regions:
                x1 = region['min_x']
                y1 = region['min_y']
                x2 = region['max_x']
                y2 = region['max_y']
                text = region['text']
                conf = region['confidence']
                is_red_text = region.get('color') == 'red'

                # Scale coordinates for display
                x1_display = x1 * scale
                y1_display = y1 * scale
                x2_display = x2 * scale
                y2_display = y2 * scale

                # Use different colors for red text vs normal text
                if is_red_text:
                    fill_color = "orange"
                    outline_color = "darkorange"
                    text_color = "darkorange"
                else:
                    fill_color = "red"
                    outline_color = "red"
                    text_color = "red"
                
                # Semi-transparent rectangle
                self.canvas.create_rectangle(
                    x1_display, y1_display, x2_display, y2_display,
                    fill=fill_color, stipple="gray50",  # Stipple creates transparency effect
                    outline=outline_color, width=2,
                    tags="annotation"
                )

                # Text label
                label_y = y1_display - 5 if y1_display > 20 else y2_display + 15
                label_text = f"🔴 {text} ({conf:.0%})" if is_red_text else f"{text} ({conf:.0%})"
                self.canvas.create_text(
                    (x1_display + x2_display) / 2, label_y,
                    text=label_text,
                    fill=text_color, font=("Arial", 8, "bold"),
                    tags="annotation"
                )
        
        # Draw operator position
        if self.operator_position:
            x, y = self.operator_position
            x_display = x * scale
            y_display = y * scale
            self.canvas.create_oval(x_display-8, y_display-8, x_display+8, y_display+8,
                                   fill="green", outline="darkgreen", width=2, tags="annotation")
            self.canvas.create_text(x_display, y_display-20, text="Operator",
                                   fill="darkgreen", font=("Arial", 10, "bold"),
                                   tags="annotation")

            # Draw line to tube head
            if self.tube_head_position:
                tube_x, tube_y = self.tube_head_position
                self.canvas.create_line(tube_x * scale, tube_y * scale,
                                       x_display, y_display, fill="green", width=2, dash=(5, 5), tags="annotation")
    
    def draw_heatmap_overlay(self):
        """Draw heat map overlay on canvas"""
        if not hasattr(self, 'heatmap_data') or self.heatmap_data is None:
            return
        
        height, width = self.heatmap_data.shape
        
        print(f"Drawing heat map overlay...")
        print(f"Heat map data range: {self.heatmap_data.min():.6f} to {self.heatmap_data.max():.6f} mSv/week")
        
        # Create a PIL Image for the heat map
        heatmap_image = Image.new('RGBA', (width, height), (0, 0, 0, 0))
        pixels = heatmap_image.load()
        
        # Color statistics
        color_counts = {'red': 0, 'orange': 0, 'yellow': 0, 'green': 0}
        non_zero_pixels = 0
        
        # Color each pixel based on dose
        for y in range(height):
            for x in range(width):
                dose = self.heatmap_data[y, x]
                if dose > 0:
                    non_zero_pixels += 1
                    color = self.dose_to_color(dose)
                    pixels[x, y] = color
                    
                    # Track color distribution
                    if color[0] > 200 and color[1] < 100:  # Red
                        color_counts['red'] += 1
                    elif color[0] > 200 and color[1] < 200:  # Orange/Yellow
                        color_counts['orange'] += 1
                    elif color[0] > 100:  # Yellow-green
                        color_counts['yellow'] += 1
                    else:  # Green
                        color_counts['green'] += 1
        
        total_colored = sum(color_counts.values())
        print(f"Heat map color distribution:")
        for color, count in color_counts.items():
            if total_colored > 0:
                pct = (count / total_colored) * 100
                print(f"  {color.capitalize()}: {pct:.1f}%")
        
        print(f"Non-zero pixels: {non_zero_pixels:,} ({non_zero_pixels/total_colored*100:.1f}%)")
        
        # CRITICAL: Store the image as an instance variable before converting to PhotoImage
        # This prevents garbage collection
        self.heatmap_pil_image = heatmap_image

        # Scale heatmap to match display scale
        scale = getattr(self, 'display_scale', 1.0)
        display_width = int(width * scale)
        display_height = int(height * scale)
        heatmap_display = heatmap_image.resize((display_width, display_height), Image.Resampling.LANCZOS)

        # Convert to PhotoImage
        self.heatmap_photo = ImageTk.PhotoImage(heatmap_display)
        
        print(f"PhotoImage created: {self.heatmap_photo.width()}x{self.heatmap_photo.height()}")
        
        # Draw on canvas at correct position
        self.canvas.delete("heatmap_layer")  # Remove old heatmap
        
        # Create the image at position 0,0 (top-left of canvas)
        heatmap_id = self.canvas.create_image(0, 0, anchor=tk.NW, 
                                              image=self.heatmap_photo, 
                                              tags="heatmap_layer")
        
        print(f"Heat map added to canvas with ID: {heatmap_id}")
        
        # Move heatmap BEHIND other elements but ABOVE the floor plan
        # First get the floor plan image ID
        if hasattr(self, 'canvas_image') and self.canvas_image:
            self.canvas.tag_raise("heatmap_layer", self.canvas_image)
        
        # Move annotations on top
        self.canvas.tag_raise("annotation")
        
        print(f"Canvas layers ordered: floor plan -> heatmap -> annotations")
        
        # Add legend
        self.draw_heatmap_legend()
        
        # Force canvas update
        self.canvas.update_idletasks()
        
        print(f"Heat map overlay drawing complete!\n")
    
    def draw_heatmap_legend(self):
        """Draw heat map legend"""
        legend_x = 20
        legend_y = 50
        
        # Background
        self.canvas.create_rectangle(legend_x - 10, legend_y - 10,
                                     legend_x + 200, legend_y + 160,
                                     fill="white", outline="black", width=2, tags="annotation")
        
        self.canvas.create_text(legend_x + 95, legend_y + 5,
                               text="Radiation Heat Map", font=("Arial", 11, "bold"),
                               tags="annotation")
        
        # Color scale
        levels = [
            ("Very High (>5x limit)", (255, 0, 0)),
            ("High (2-5x limit)", (255, 100, 0)),
            ("Above Limit (1-2x)", (255, 200, 0)),
            ("Approaching (0.5-1x)", (200, 255, 0)),
            ("Safe (<0.5x limit)", (0, 255, 0))
        ]
        
        y_offset = legend_y + 30
        for label, color in levels:
            # Color box
            hex_color = f'#{color[0]:02x}{color[1]:02x}{color[2]:02x}'
            self.canvas.create_rectangle(legend_x, y_offset,
                                        legend_x + 20, y_offset + 15,
                                        fill=hex_color, outline="black", tags="annotation")
            
            # Label
            self.canvas.create_text(legend_x + 30, y_offset + 7,
                                   text=label, anchor=tk.W, font=("Arial", 9),
                                   tags="annotation")
            y_offset += 25
        
        # Note
        self.canvas.create_text(legend_x + 95, y_offset + 10,
                               text=f"Limit: {self.weekly_dose_limit} mSv/week\nRadius: 5m from source",
                               font=("Arial", 8), tags="annotation")
    
    def calculate_shielding(self):
        """
        Calculate radiation exposure and shielding requirements based on Ontario standards
        Includes: lead barriers, wall thickness, and drywall shielding
        """
        if not self.tube_head_position or not self.operator_position:
            messagebox.showwarning("Warning", "Please place both tube head and operator position")
            return
        
        # Calculate distance
        dx = (self.operator_position[0] - self.tube_head_position[0]) / self.scale_factor
        dy = (self.operator_position[1] - self.tube_head_position[1]) / self.scale_factor
        distance = math.sqrt(dx**2 + dy**2)
        
        # Calculate angle to operator from tube head
        angle_to_operator_rad = math.atan2(dy, dx)
        angle_to_operator = math.degrees(angle_to_operator_rad) % 360
        
        # Check which beam direction(s) the operator is in
        # Three beams: 0° (20%), -45° (40%), +45° (40%) from primary direction
        primary_direction = self.primary_beam_direction.get()
        
        in_beams = []
        total_beam_exposure = 0
        
        for beam_angle_offset, workload_factor in zip(self.beam_angles, self.beam_workload_factors):
            beam_angle = (primary_direction + beam_angle_offset) % 360
            angle_diff = abs(angle_to_operator - beam_angle)
            if angle_diff > 180:
                angle_diff = 360 - angle_diff
            
            if angle_diff < 30:  # Within this beam's cone
                in_beams.append({
                    'angle': beam_angle,
                    'offset': beam_angle_offset,
                    'workload_factor': workload_factor,
                    'angle_diff': angle_diff
                })
        
        in_primary_beam = len(in_beams) > 0
        
        # Check for lead barriers along the line from tube to operator
        intersected_barriers = self.check_line_intersection_with_barriers(
            self.tube_head_position[0], self.tube_head_position[1],
            self.operator_position[0], self.operator_position[1]
        )
        
        # Calculate total lead shielding from barriers
        total_lead_mm = sum(barrier['thickness'] for barrier in intersected_barriers)
        
        # Calculate wall and drywall shielding - ONLY if barriers were detected
        # (Wall thickness should only apply if there's actually a wall in the path)
        wall_lead_equiv = 0.0
        drywall_lead_equiv = 0.0
        
        # Only apply wall construction shielding if we detected actual barriers on the path
        # This prevents applying wall shielding when operator is in open space
        if len(intersected_barriers) > 0:
            # Concrete/masonry wall shielding
            # Typical: 100mm concrete ≈ 2.0mm Pb, so 1mm concrete ≈ 0.02mm Pb
            if self.wall_thickness.get() > 0:
                wall_thickness_mm = self.wall_thickness.get() * 1000  # convert to mm
                # Using conservative estimate: 1 inch (25.4mm) concrete ≈ 0.5mm Pb
                wall_lead_equiv = wall_thickness_mm * 0.02
            
            # Drywall shielding (gypsum board)
            # Typical: 2 layers of 5/8" drywall ≈ 0.25mm Pb at 70 kVp
            # Single layer ≈ 0.125mm Pb
            if self.include_drywall.get():
                drywall_lead_equiv = self.drywall_layers.get() * 0.125
        else:
            # No barriers detected = operator is in open space
            # Do NOT apply wall construction shielding
            wall_lead_equiv = 0.0
            drywall_lead_equiv = 0.0
        
        # Total shielding
        total_shielding_mm = total_lead_mm + wall_lead_equiv + drywall_lead_equiv
        
        # Calculate unshielded dose using workload from specific beams
        mas_per_exposure = self.ma.get() * self.exposure_time.get()
        total_weekly_mas = mas_per_exposure * self.workload.get()
        
        # Exposure rate at 1 meter (approximate for 70 kVp intraoral)
        # Using kVp adjustment factor
        kvp_factor = (self.kvp.get() / 70.0) ** 2.5  # Approximate kVp dependence
        exposure_rate_1m = 5 * kvp_factor  # mR per mAs at 1 meter
        
        # Calculate dose from each beam the operator is in
        if in_primary_beam:
            # Sum dose from all beams operator is within
            total_beam_dose = 0
            beam_details = []
            
            for beam_info in in_beams:
                # Calculate dose from this specific beam with its workload factor
                beam_weekly_mas = total_weekly_mas * beam_info['workload_factor']
                beam_exposure = exposure_rate_1m * beam_weekly_mas / (distance ** 2)
                beam_dose_msv = beam_exposure * 0.01
                total_beam_dose += beam_dose_msv
                
                beam_details.append(f"{beam_info['offset']:+.0f}° beam ({beam_info['workload_factor']*100:.0f}% workload)")
            
            unshielded_dose_msv = total_beam_dose
            use_factor = 1.0
            beam_type = f"PRIMARY BEAM ({', '.join(beam_details)})"
        else:
            # Scatter radiation from all beams
            exposure_at_distance = exposure_rate_1m * total_weekly_mas / (distance ** 2)
            unshielded_dose_msv = exposure_at_distance * 0.01
            use_factor = 0.001  # Scatter is ~0.1% of primary
            beam_type = "SCATTER RADIATION"
        
        unshielded_weekly_dose = unshielded_dose_msv
        
        # Calculate transmission through shielding
        # TVL (Tenth Value Layer) at 70 kVp: approximately 0.17mm Pb
        # Adjust for actual kVp
        tvl_base = 0.17  # mm Pb at 70 kVp
        if self.kvp.get() > 70:
            tvl = tvl_base * (1 + (self.kvp.get() - 70) * 0.01)  # Increases with kVp
        else:
            tvl = tvl_base * (1 - (70 - self.kvp.get()) * 0.008)
        
        # Calculate transmission factor
        if total_shielding_mm > 0:
            transmission = 10 ** (-total_shielding_mm / tvl)
        else:
            transmission = 1.0
        
        # Shielded dose
        weekly_dose = unshielded_weekly_dose * transmission
        
        # Determine if additional shielding is required
        if weekly_dose <= self.weekly_dose_limit:
            additional_shielding_required = 0
            shielding_required = "NO ADDITIONAL SHIELDING REQUIRED"
            shielding_description = "Current shielding is adequate for Ontario safety standards."
            status = "✓ COMPLIANT"
            status_color = "green"
        else:
            # Calculate required additional transmission
            required_transmission = self.weekly_dose_limit / unshielded_weekly_dose
            required_total_shielding = -math.log10(required_transmission) * tvl
            additional_shielding_required = max(0, required_total_shielding - total_shielding_mm)
            
            shielding_required = f"{additional_shielding_required:.2f} mm ADDITIONAL LEAD EQUIVALENT"
            shielding_description = "Additional shielding is required to meet Ontario safety standards."
            status = "⚠ ADDITIONAL SHIELDING NEEDED"
            status_color = "red"
        
        # Prepare barrier information
        barrier_info = ""
        if intersected_barriers:
            barrier_info = "\nDETECTED BARRIERS ON PATH:\n"
            for barrier in intersected_barriers:
                barrier_info += f"  • {barrier['type']} lead barrier\n"
            barrier_info += f"  • Total Lead: {total_lead_mm:.2f} mm Pb\n"
        else:
            barrier_info = "\n  • No lead barriers detected on path to operator\n"
        
        # Wall construction info
        wall_info = f"\nWALL CONSTRUCTION SHIELDING:\n"
        if len(intersected_barriers) > 0:
            # Barriers detected, wall construction may apply
            if self.wall_thickness.get() > 0:
                wall_info += f"  • Wall Thickness: {self.wall_thickness.get()*1000:.0f} mm ({self.wall_thickness.get()*39.37:.1f} inches)\n"
                wall_info += f"  • Wall Lead Equivalent: {wall_lead_equiv:.2f} mm Pb\n"
            else:
                wall_info += f"  • No wall thickness specified\n"
            
            if self.include_drywall.get():
                wall_info += f"  • Drywall: {self.drywall_layers.get()} layer(s)\n"
                wall_info += f"  • Drywall Lead Equivalent: {drywall_lead_equiv:.2f} mm Pb\n"
        else:
            # No barriers detected = open space
            wall_info += f"  • No barriers detected on path to operator\n"
            wall_info += f"  • Operator appears to be in OPEN SPACE (no wall between)\n"
            wall_info += f"  • Wall construction shielding NOT applied\n"
            if self.wall_thickness.get() > 0 or self.include_drywall.get():
                wall_info += f"  • Note: Wall settings configured but not used (no barrier detected)\n"
        
        # Display results
        self.results_text.delete(1.0, tk.END)
        
        results = f"""
╔══════════════════════════════════════════════════════════════╗
║  RADIATION SHIELDING ANALYSIS                                ║
║  Ontario Ministry of Health Standards                        ║
╚══════════════════════════════════════════════════════════════╝

MEASUREMENT PARAMETERS:
  • Distance to Operator: {distance:.2f} meters
  • Beam Type: {beam_type}
  • Operator angle: {angle_to_operator:.1f}°
  • Primary direction: {primary_direction}°
  • Beams present: 0° (20%), -45° (40%), +45° (40%) from primary
  
X-RAY PARAMETERS:
  • Tube Voltage: {self.kvp.get():.0f} kVp
  • Tube Current: {self.ma.get():.1f} mA
  • Exposure Time: {self.exposure_time.get():.2f} seconds
  • Workload: {self.workload.get():.0f} exposures/week
  • mAs per exposure: {mas_per_exposure:.1f} mAs
{barrier_info}{wall_info}
TOTAL SHIELDING:
  • Total Lead Equivalent: {total_shielding_mm:.2f} mm Pb
  • Transmission Factor: {transmission:.6f} ({transmission*100:.4f}%)
  • TVL at {self.kvp.get():.0f} kVp: {tvl:.2f} mm Pb

DOSE CALCULATION:
  • Unshielded Weekly Dose: {unshielded_weekly_dose:.4f} mSv/week
  • Shielded Weekly Dose: {weekly_dose:.6f} mSv/week
  • Regulatory Limit: {self.weekly_dose_limit:.3f} mSv/week (public)
  • Ratio to Limit: {(weekly_dose/self.weekly_dose_limit)*100:.2f}%

SHIELDING REQUIREMENTS:
  • Status: {status}
  • Required Shielding: {shielding_required}
  • {shielding_description}

RECOMMENDATIONS:
"""
        
        if weekly_dose <= self.weekly_dose_limit:
            results += f"""
  ✓ Current shielding configuration is SAFE
  ✓ No additional shielding required
  ✓ Maintain minimum distance of {distance:.2f} meters
  ✓ Ensure operator never stands in primary beam path
"""
            if total_shielding_mm > 0:
                results += f"""
  ✓ Existing barriers provide adequate protection
  ✓ Total effective shielding: {total_shielding_mm:.2f} mm Pb
"""
        else:
            results += f"""
  ⚠ Install additional lead-equivalent barrier: {additional_shielding_required:.2f} mm Pb
  ⚠ Current shielding: {total_shielding_mm:.2f} mm Pb
  ⚠ Total required: {total_shielding_mm + additional_shielding_required:.2f} mm Pb
  ⚠ Alternative: Increase distance to {math.sqrt(unshielded_weekly_dose/self.weekly_dose_limit) * distance:.2f} meters
  ⚠ Ensure operator is behind protective barrier during exposure
  ⚠ Post radiation warning signs
  ⚠ Implement radiation safety protocol
"""
        
        results += f"""
SHIELDING MATERIAL EQUIVALENCIES (at {self.kvp.get():.0f} kVp):
  • 1 layer drywall (5/8"): ≈ 0.125 mm Pb
  • 2 layers drywall: ≈ 0.25 mm Pb
  • 100mm (4") concrete: ≈ 2.0 mm Pb
  • 150mm (6") concrete: ≈ 3.0 mm Pb
  • Lead sheet 0.16mm: primary scatter protection
  • Lead sheet 0.8mm: primary beam protection

ONTARIO REGULATORY NOTES:
  • Operators must be positioned outside primary beam
  • Controlled area designation may be required
  • Annual dose monitoring recommended
  • Maintain exposure logs
  • Review and update safety protocol annually
  • Barriers must be inspected annually
  • Lead barriers must be clearly marked

CALCULATION STANDARDS:
  • Based on NCRP Report No. 147
  • Ontario Healing Arts Radiation Protection Act (HARP)
  • Includes wall and drywall shielding effects
  • Barrier detection from floor plan colors
  • Conservative estimates used for safety margin
"""
        
        self.results_text.insert(1.0, results)
        
        # Tag the status line with color
        status_line = results.split('\n').index([s for s in results.split('\n') if 'Status:' in s][0]) + 1
        self.results_text.tag_add("status", f"{status_line}.0", f"{status_line}.end")
        self.results_text.tag_config("status", foreground=status_color, font=("Arial", 10, "bold"))

def main():
    root = tk.Tk()
    
    # Set style
    style = ttk.Style()
    style.theme_use('clam')
    
    app = XrayShieldingCalculator(root)
    root.mainloop()

if __name__ == "__main__":
    main()