Guest User

Untitled

a guest
Aug 3rd, 2025
37
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 33.46 KB | None | 0 0
  1. import tkinter as tk
  2. from tkinter import ttk, filedialog, messagebox, scrolledtext
  3. import requests
  4. import urllib.parse
  5. import numpy as np
  6. from PIL import Image, ImageTk
  7. import threading
  8. import time
  9. import matplotlib.cm as cm
  10. import struct
  11. import json
  12. import os
  13. from matplotlib.colors import LinearSegmentedColormap
  14.  
  15. class ImageStreamViewer:
  16.     SETTINGS_FILE = "viewer_settings.json"
  17.  
  18.     def __init__(self, root):
  19.         self.root = root
  20.         self.root.title("Image Stream Viewer - Fast")
  21.         self.root.geometry("1800x1000")
  22.  
  23.         self.current_image_array = None
  24.         self.current_pil_image = None
  25.         self.resolution = None
  26.         self.raw_data = None
  27.        
  28.         self.streaming = False
  29.         self.stream_thread = None
  30.         self.locked_pixel = None  # Add this to your __init__ or setup_gui
  31.  
  32.         # Add custom palettes (example: custom1)
  33.         def custom1_cmap():
  34.             # HSV sweep from 0 to 360°
  35.             N = 256
  36.             hsv_colors = [(i / (N - 1), 1.0, 1.0) for i in range(N)]
  37.             rgb_colors = [tuple(int(x * 255) for x in cm.colors.hsv_to_rgb(hsv)) for hsv in hsv_colors]
  38.             rgb_hex = ['#%02x%02x%02x' % rgb for rgb in rgb_colors]
  39.             return LinearSegmentedColormap.from_list("custom1", rgb_hex)
  40.  
  41.  
  42.         # Simple palette mapping
  43.         self.palettes = {
  44.             'Grayscale': None,
  45.             'Hot': cm.hot,
  46.             'Jet': cm.jet,
  47.             'Cool': cm.cool,
  48.             'Viridis': cm.viridis,
  49.             'Plasma': cm.plasma,
  50.             'Inferno': cm.inferno,
  51.             'Magma': cm.magma,
  52.             'Cividis': cm.cividis,
  53.             'Rainbow': cm.rainbow,
  54.             'Bone': cm.bone,
  55.             'Copper': cm.copper,
  56.             'Spring': cm.spring,
  57.             'Summer': cm.summer,
  58.             'Autumn': cm.autumn,
  59.             'Winter': cm.winter,
  60.             'Twilight': cm.twilight,
  61.             '!Twilight': cm.twilight_shifted,
  62.             'Custom1': custom1_cmap(),  
  63.         }
  64.         self.first = True
  65.        
  66.         # Performance tracking
  67.         self.frame_count = 0
  68.         self.last_fps_time = time.time()
  69.        
  70.        
  71.         self.setup_gui()
  72.         self.load_settings()
  73.         self.root.protocol("WM_DELETE_WINDOW", self.on_close)
  74.        
  75.  
  76.     def setup_gui(self):
  77.         main_frame = ttk.Frame(self.root, padding="1")
  78.         main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  79.        
  80.         # Left panel for controls
  81.         left_frame = ttk.Frame(main_frame, width=700)
  82.         left_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
  83.         left_frame.grid_propagate(False)
  84.        
  85.         # Right panel for image
  86.         right_frame = ttk.Frame(main_frame)
  87.         right_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
  88.        
  89.         # === CONTROLS ===
  90.        
  91.         # Connection
  92.         conn_frame = ttk.LabelFrame(left_frame, text="Connection", padding="5")
  93.         conn_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
  94.        
  95.         ttk.Label(conn_frame, text="IP:").grid(row=0, column=0, sticky=tk.W)
  96.         self.ip_var = tk.StringVar(value="192.168.178.92")
  97.         ttk.Entry(conn_frame, textvariable=self.ip_var, width=15).grid(row=0, column=1, padx=(5, 10))
  98.        
  99.         self.start_button = ttk.Button(conn_frame, text="Start Stream", command=self.toggle_stream)
  100.         self.start_button.grid(row=0, column=2, padx=(0, 5))
  101.        
  102.         self.save_button = ttk.Button(conn_frame, text="Save", command=self.save_image)
  103.         self.save_button.grid(row=0, column=3, padx=(0, 5))
  104.        
  105.         ttk.Label(conn_frame, text="FPS:").grid(row=0, column=4, padx=(10, 5))
  106.         self.fps_var = tk.StringVar(value="20")
  107.         ttk.Entry(conn_frame, textvariable=self.fps_var, width=4).grid(row=0, column=5)
  108.        
  109.         self.status_label = ttk.Label(conn_frame, text="Status: Stopped")
  110.         self.status_label.grid(row=1, column=0, columnspan=3, sticky=tk.W)
  111.        
  112.         self.fps_label = ttk.Label(conn_frame, text="Actual FPS: 0")
  113.         self.fps_label.grid(row=1, column=3, columnspan=3, sticky=tk.W)
  114.        
  115.         # Format
  116.         format_frame = ttk.LabelFrame(left_frame, text="Format", padding="5")
  117.         format_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
  118.        
  119.         ttk.Label(format_frame, text="Width:").grid(row=0, column=0, sticky=tk.W)
  120.         self.width_var = tk.StringVar(value="640")
  121.         ttk.Entry(format_frame, textvariable=self.width_var, width=6).grid(row=0, column=1, padx=(5, 10))
  122.        
  123.         ttk.Label(format_frame, text="Height:").grid(row=0, column=2, sticky=tk.W)
  124.         self.height_var = tk.StringVar(value="480")
  125.         ttk.Entry(format_frame, textvariable=self.height_var, width=6).grid(row=0, column=3, padx=(5, 10))
  126.        
  127.         ttk.Label(format_frame, text="Low offset:").grid(row=1, column=0, sticky=tk.W)
  128.         self.low_offset_var = tk.StringVar(value="0x238")
  129.         ttk.Entry(format_frame, textvariable=self.low_offset_var, width=8).grid(row=1, column=1, padx=(5, 10))
  130.        
  131.         ttk.Label(format_frame, text="High offset:").grid(row=1, column=2, sticky=tk.W)
  132.         self.high_offset_var = tk.StringVar(value="0x4B238")
  133.         ttk.Entry(format_frame, textvariable=self.high_offset_var, width=8).grid(row=1, column=3, padx=(5, 10))
  134.        
  135.         ttk.Button(format_frame, text="Parse Header", command=self.parse_header).grid(row=0, column=4, padx=(10, 0))
  136.        
  137.         # Display
  138.         display_frame = ttk.LabelFrame(left_frame, text="Display", padding="5")
  139.         display_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
  140.        
  141.         ttk.Label(display_frame, text="Palette:").grid(row=0, column=0, sticky=tk.W)
  142.         self.palette_var = tk.StringVar(value="Grayscale")
  143.         palette_combo = ttk.Combobox(display_frame, textvariable=self.palette_var,
  144.                                    values=list(self.palettes.keys()), state="readonly", width=10)
  145.         palette_combo.grid(row=0, column=1, padx=(5, 10))
  146.         palette_combo.bind("<<ComboboxSelected>>", self.on_palette_change)
  147.        
  148.         self.auto_range_var = tk.BooleanVar(value=False)
  149.         ttk.Checkbutton(display_frame, text="Auto Range", variable=self.auto_range_var).grid(row=0, column=2)
  150.        
  151.         # Range controls
  152.         ttk.Button(display_frame, text="Auto Now", command=self.auto_range).grid(row=1, column=0, padx=(10, 0))
  153.         ttk.Button(display_frame, text="Reset", command=self.reset_range).grid(row=2, column=0, padx=(10, 0))
  154.        
  155.         # Offset controls
  156.         ttk.Label(display_frame, text="Offset:").grid(row=1, column=1, sticky=tk.W)
  157.         self.offset_var = tk.IntVar(value=8192)
  158.         offset_frame = ttk.Frame(display_frame)
  159.         offset_frame.grid(row=1, column=2, columnspan=2, sticky=(tk.W, tk.E))
  160.        
  161.         ttk.Button(offset_frame, text="-", width=3, command=lambda: self.adjust_offset(-5)).grid(row=0, column=0)
  162.         self.offset_scale = ttk.Scale(offset_frame, from_=7000, to=8000, variable=self.offset_var,
  163.                                      orient=tk.HORIZONTAL, length=360, command=self.on_offset_change)
  164.         self.offset_scale.grid(row=0, column=1, padx=(2, 2))
  165.         ttk.Button(offset_frame, text="+", width=3, command=lambda: self.adjust_offset(5)).grid(row=0, column=2)
  166.         self.offset_label = ttk.Label(offset_frame, text="8192", width=6)
  167.         self.offset_label.grid(row=0, column=3, padx=(5, 0))
  168.        
  169.         # Range controls
  170.         ttk.Label(display_frame, text="Range:").grid(row=2, column=1, sticky=tk.W)
  171.         self.range_var = tk.IntVar(value=1024)
  172.         range_frame = ttk.Frame(display_frame)
  173.         range_frame.grid(row=2, column=2, columnspan=2, sticky=(tk.W, tk.E))
  174.        
  175.         ttk.Button(range_frame, text="-", width=3, command=lambda: self.adjust_range(-5)).grid(row=0, column=0)
  176.         self.range_scale = ttk.Scale(range_frame, from_=1, to=1024, variable=self.range_var,
  177.                                     orient=tk.HORIZONTAL, length=360, command=self.on_range_change)
  178.         self.range_scale.grid(row=0, column=1, padx=(2, 2))
  179.         ttk.Button(range_frame, text="+", width=3, command=lambda: self.adjust_range(5)).grid(row=0, column=2)
  180.         self.range_label = ttk.Label(range_frame, text="1024", width=6)
  181.         self.range_label.grid(row=0, column=3, padx=(5, 0))
  182.        
  183.         ttk.Label(display_frame, text="Min:").grid(row=3, column=1, sticky=tk.W)
  184.         self.min_display_label = ttk.Label(display_frame, text="0", width=6, relief="sunken")
  185.         self.min_display_label.grid(row=3, column=2, sticky=tk.W)
  186.  
  187.         ttk.Label(display_frame, text="Max:").grid(row=3, column=3, sticky=tk.W)
  188.         self.max_display_label = ttk.Label(display_frame, text="16383", width=6, relief="sunken")
  189.         self.max_display_label.grid(row=3, column=4, sticky=tk.W)
  190.  
  191.         # Raw data min/max info
  192.    
  193.         self.raw_min_label = ttk.Label(display_frame, text="0", width=6, relief="sunken")
  194.         self.raw_min_label.grid(row=4, column=0, sticky=tk.W)
  195.  
  196.  
  197.         self.raw_avg_label = ttk.Label(display_frame, text="0", width=6, relief="sunken")
  198.         self.raw_avg_label.grid(row=4, column=1, sticky=tk.W)
  199.  
  200.    
  201.         self.raw_max_label = ttk.Label(display_frame, text="0", width=6, relief="sunken")
  202.         self.raw_max_label.grid(row=4, column=2, sticky=tk.W)
  203.        
  204.         # Commands
  205.         cmd_frame = ttk.LabelFrame(left_frame, text="Commands", padding="5")
  206.         cmd_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
  207.        
  208.         ttk.Label(cmd_frame, text="Cmd:").grid(row=0, column=0, sticky=tk.W)
  209.         self.cmd_entry = ttk.Entry(cmd_frame, width=15)
  210.         self.cmd_entry.grid(row=0, column=1, padx=(5, 10))
  211.        
  212.         ttk.Label(cmd_frame, text="Value:").grid(row=0, column=2, sticky=tk.W)
  213.         self.value_entry = ttk.Entry(cmd_frame, width=15)
  214.         self.value_entry.grid(row=0, column=3, padx=(5, 10))
  215.        
  216.         ttk.Button(cmd_frame, text="Send", command=self.send_command).grid(row=0, column=4)
  217.        
  218.         # FFC Button
  219.         ttk.Button(cmd_frame, text="FFC", command=self.send_ffc_command).grid(row=0, column=5, padx=(10, 0))
  220.        
  221.         self.cmd_response = scrolledtext.ScrolledText(cmd_frame, height=3, width=60, font=("Courier", 8))
  222.         self.cmd_response.grid(row=1, column=0, columnspan=6, pady=(5, 0), sticky=(tk.W, tk.E))
  223.        
  224.         # Info
  225.         info_frame = ttk.LabelFrame(left_frame, text="Info", padding="5")
  226.         info_frame.grid(row=4, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
  227.        
  228.         self.info_text = tk.Text(info_frame, height=20, width=90, font=("Courier", 8))
  229.         info_scroll = ttk.Scrollbar(info_frame, orient=tk.VERTICAL, command=self.info_text.yview)
  230.         self.info_text.configure(yscrollcommand=info_scroll.set)
  231.         self.info_text.grid(row=0, column=0, sticky=(tk.W, tk.E))
  232.         info_scroll.grid(row=0, column=1, sticky=(tk.N, tk.S))
  233.        
  234.         # === IMAGE DISPLAY ===
  235.        
  236.         image_frame = ttk.LabelFrame(right_frame, text="Image", padding="5")
  237.         image_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  238.        
  239.         self.canvas = tk.Canvas(image_frame, bg="black", width=1000, height=800)
  240.         self.canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  241.         self.canvas.bind("<Button-1>", self.on_canvas_click)
  242.         self.canvas.bind("<Button-3>", self.on_canvas_right_click)
  243.  
  244.         # Pixel info
  245.         self.pixel_label = ttk.Label(image_frame, text="Pixel: Move mouse over image")
  246.         self.pixel_label.grid(row=1, column=0, sticky=tk.W)
  247.  
  248.         self.temp_label = ttk.Label(image_frame, text="Temperature: -- °C", font=("Arial", 12, "bold"))
  249.         self.temp_label.grid(row=2, column=0, sticky=tk.W, pady=(10,0))
  250.  
  251.         self.pixel_label = ttk.Label(image_frame, text="Pixel: Move mouse over image")
  252.         self.pixel_label.grid(row=3, column=0, sticky=tk.W)
  253.  
  254.         # Reference input fields
  255.         ref_frame = ttk.Frame(image_frame)
  256.         ref_frame.grid(row=4, column=0, sticky=tk.W, pady=(5,0))
  257.  
  258.         ttk.Label(ref_frame, text="Ref1 °C:").grid(row=0, column=0, sticky=tk.W)
  259.         self.ref_temp_var = tk.DoubleVar(value=25.0)
  260.         ttk.Entry(ref_frame, textvariable=self.ref_temp_var, width=6).grid(row=0, column=1, padx=(5, 15))
  261.  
  262.         ttk.Label(ref_frame, text="Ref1 Pixel:").grid(row=0, column=2, sticky=tk.W)
  263.         self.ref_pixel_var = tk.IntVar(value=7750)
  264.         self.ref_pixel_entry = ttk.Entry(ref_frame, textvariable=self.ref_pixel_var, width=8)
  265.         self.ref_pixel_entry.grid(row=0, column=3, padx=(5, 15))
  266.  
  267.         ttk.Label(ref_frame, text="Ref2 °C:").grid(row=0, column=4, sticky=tk.W)
  268.         self.ref2_temp_var = tk.DoubleVar(value=36.0)
  269.         ttk.Entry(ref_frame, textvariable=self.ref2_temp_var, width=6).grid(row=0, column=5, padx=(5, 15))
  270.  
  271.         ttk.Label(ref_frame, text="Ref2 Pixel:").grid(row=0, column=6, sticky=tk.W)
  272.         self.ref2_pixel_var = tk.IntVar(value=7880)
  273.         self.ref2_pixel_entry = ttk.Entry(ref_frame, textvariable=self.ref2_pixel_var, width=8)
  274.         self.ref2_pixel_entry.grid(row=0, column=7, padx=(5, 15))
  275.  
  276.         # Optionally, add a label for pixels per degree
  277.  
  278.         self.canvas.bind("<Motion>", self.on_mouse_move)
  279.        
  280.         # Grid weights
  281.         self.root.columnconfigure(0, weight=1)
  282.         self.root.rowconfigure(0, weight=1)
  283.         main_frame.columnconfigure(0, weight=0)
  284.         main_frame.columnconfigure(1, weight=1)
  285.         main_frame.rowconfigure(0, weight=1)
  286.         left_frame.columnconfigure(0, weight=1)
  287.         right_frame.columnconfigure(0, weight=1)
  288.         right_frame.rowconfigure(0, weight=1)
  289.         image_frame.columnconfigure(0, weight=1)
  290.         image_frame.rowconfigure(0, weight=1)
  291.        
  292.         # Bind Enter keys
  293.         self.cmd_entry.bind("<Return>", lambda e: self.send_command())
  294.         self.value_entry.bind("<Return>", lambda e: self.send_command())
  295.        
  296.         # Initialize min/max display
  297.         self.update_min_max_display()
  298.        
  299.     def log(self, text):
  300.         self.info_text.insert(tk.END, f"{text}\n")
  301.         self.info_text.see(tk.END)
  302.  
  303.     def on_canvas_click(self, event):
  304.         if self.current_image_array is None or not hasattr(self, 'scale_factor'):
  305.             return
  306.         x = int(event.x / self.scale_factor)
  307.         y = int(event.y / self.scale_factor)
  308.         h, w = self.current_image_array.shape
  309.         if 0 <= x < w and 0 <= y < h:
  310.             self.locked_pixel = (x, y)
  311.             self.update_display()
  312.  
  313.     def on_canvas_right_click(self, event):
  314.         self.locked_pixel = None
  315.         self.update_display()
  316.  
  317.     def parse_header(self):
  318.         if self.raw_data is None:
  319.             return
  320.            
  321.         try:
  322.             data = self.raw_data
  323.             width = struct.unpack('<I', data[8:12])[0]
  324.             height = struct.unpack('<I', data[12:16])[0]
  325.            
  326.             self.width_var.set(str(width))
  327.             self.height_var.set(str(height))
  328.             self.log(f"Parsed: {width}x{height}")
  329.            
  330.         except Exception as e:
  331.             self.log(f"Parse error: {e}")
  332.    
  333.     def get_offset(self, offset_str):
  334.         try:
  335.             if offset_str.startswith('0x'):
  336.                 return int(offset_str, 16)
  337.             else:
  338.                 return int(offset_str)
  339.         except:
  340.             return 0
  341.    
  342.     def send_command(self):
  343.         cmd = self.cmd_entry.get().strip()
  344.         value = self.value_entry.get().strip()
  345.         if not cmd:
  346.             return
  347.            
  348.         command = f"{cmd},{value}" if value else f"{cmd},"
  349.         ip = self.ip_var.get()
  350.         url = f"http://{ip}/cgi-bin/dmcmd?Command={urllib.parse.quote(command)}"
  351.        
  352.         def make_request():
  353.             try:
  354.                 response = requests.get(url, timeout=5)
  355.                 self.root.after(0, lambda: self.cmd_response.insert(tk.END, f"OK: {response.text}\n"))
  356.             except Exception as e:
  357.                 self.root.after(0, lambda: self.cmd_response.insert(tk.END, f"Error: {e}\n"))
  358.        
  359.         threading.Thread(target=make_request, daemon=True).start()
  360.    
  361.     def send_ffc_command(self):
  362.         """Send FFC command (KBD with value M)"""
  363.         command = "KBD,C"
  364.         ip = self.ip_var.get()
  365.         url = f"http://{ip}/cgi-bin/dmcmd?Command={urllib.parse.quote(command)}"
  366.        
  367.         def make_request():
  368.             try:
  369.                 response = requests.get(url, timeout=5)
  370.                 self.root.after(0, lambda: self.cmd_response.insert(tk.END, f"FFC OK: {response.text}\n"))
  371.             except Exception as e:
  372.                 self.root.after(0, lambda: self.cmd_response.insert(tk.END, f"FFC Error: {e}\n"))
  373.        
  374.         threading.Thread(target=make_request, daemon=True).start()
  375.    
  376.     def adjust_offset(self, delta):
  377.         """Adjust offset by delta amount"""
  378.         current = self.offset_var.get()
  379.         new_value = max(0, min(16383, current + delta))
  380.         self.offset_var.set(new_value)
  381.         self.on_offset_change()
  382.    
  383.     def adjust_range(self, delta):
  384.         """Adjust range by delta amount"""
  385.         current = self.range_var.get()
  386.         new_value = max(1, min(16383, current + delta))
  387.         self.range_var.set(new_value)
  388.         self.on_range_change()
  389.    
  390.     def on_mouse_move(self, event):
  391.         if self.current_image_array is None or not hasattr(self, 'scale_factor'):
  392.             return
  393.  
  394.         try:
  395.             img_h, img_w = self.current_image_array.shape
  396.             canvas_w = self.canvas.winfo_width()
  397.             canvas_h = self.canvas.winfo_height()
  398.  
  399.             # Calculate scale for each axis
  400.             scale_x = canvas_w / img_w
  401.             scale_y = canvas_h / img_h
  402.             scale = min(scale_x, scale_y, 3.0)  # Max 3x scale
  403.  
  404.             display_w = int(img_w * scale)
  405.             display_h = int(img_h * scale)
  406.             offset_x = (canvas_w - display_w) // 2
  407.             offset_y = (canvas_h - display_h) // 2
  408.  
  409.             # Map mouse position to image coordinates using separate scales
  410.             x = int((event.x - offset_x) / scale)
  411.             y = int((event.y - offset_y) / scale)
  412.  
  413.             if 0 <= x < img_w and 0 <= y < img_h:
  414.                 value = self.current_image_array[y, x]
  415.                 self.pixel_label.config(text=f"Pixel ({x},{y}): {value}")
  416.  
  417.                 # Calculate temperature using two reference points
  418.                 ref1_temp = self.ref_temp_var.get()
  419.                 ref1_pixel = self.ref_pixel_var.get()
  420.                 ref2_temp = self.ref2_temp_var.get()
  421.                 ref2_pixel = self.ref2_pixel_var.get()
  422.  
  423.                 if ref2_pixel != ref1_pixel:
  424.                     temperature = ref1_temp + (value - ref1_pixel) * (ref2_temp - ref1_temp) / (ref2_pixel - ref1_pixel)
  425.                     self.temp_label.config(text=f"Temperature: {temperature:.1f} °C")
  426.                 else:
  427.                     self.temp_label.config(text="Temperature: -- °C")
  428.             else:
  429.                 self.temp_label.config(text="Temperature: -- °C")
  430.         except:
  431.             self.temp_label.config(text="Temperature: -- °C")
  432.    
  433.     def on_palette_change(self, event=None):
  434.         if self.current_image_array is not None:
  435.             self.update_display()
  436.    
  437.     def offset_range_to_min_max(self):
  438.         """Convert offset and range to min and max values"""
  439.         offset = self.offset_var.get()
  440.         range_val = self.range_var.get()
  441.        
  442.         min_val = max(0, offset - range_val)
  443.         max_val = min(16383, offset + range_val)
  444.        
  445.         return min_val, max_val
  446.    
  447.     def min_max_to_offset_range(self, min_val, max_val):
  448.         """Convert min and max values to offset and range"""
  449.         offset = (min_val + max_val) // 2
  450.         range_val = (max_val - min_val) // 2
  451.        
  452.         return offset, range_val
  453.    
  454.     def update_min_max_display(self):
  455.         """Update the min/max display labels"""
  456.         min_val, max_val = self.offset_range_to_min_max()
  457.         self.min_display_label.config(text=str(min_val))
  458.         self.max_display_label.config(text=str(max_val))
  459.    
  460.     def on_offset_change(self, event=None):
  461.         offset = int(self.offset_var.get())
  462.         self.offset_label.config(text=str(offset))
  463.         self.update_min_max_display()
  464.        
  465.         if self.current_image_array is not None:
  466.             self.update_display()
  467.    
  468.     def on_range_change(self, event=None):
  469.         range_val = int(self.range_var.get())
  470.         self.range_label.config(text=str(range_val))
  471.         self.update_min_max_display()
  472.        
  473.         if self.current_image_array is not None:
  474.             self.update_display()
  475.    
  476.     def auto_range(self):
  477.         if self.current_image_array is not None:
  478.             data = self.current_image_array.flatten()
  479.             if data.size >= 200:
  480.                 min_val = int(np.partition(data, 100)[100])
  481.                 max_val = int(np.partition(data, -100)[-100])
  482.             else:
  483.                 min_val = int(np.min(data))
  484.                 max_val = int(np.max(data))
  485.             offset, range_val = self.min_max_to_offset_range(min_val, max_val)
  486.             self.offset_var.set(offset)
  487.             self.range_var.set(range_val)
  488.             self.on_offset_change()
  489.             self.on_range_change()
  490.    
  491.     def reset_range(self):
  492.         offset, range_val = self.min_max_to_offset_range(0, 16383)
  493.         self.offset_var.set(offset)
  494.         self.range_var.set(range_val)
  495.         self.on_offset_change()
  496.         self.on_range_change()
  497.    
  498.     def get_url(self):
  499.         ip = self.ip_var.get()
  500.         return f"http://{ip}:5000/stream?Type=RAW&Source=Raw&Mode=TCP&Heart-beat=No&Frames=1&Snap=No&Channel=1"
  501.    
  502.     def fetch_data(self):
  503.         try:
  504.             url = self.get_url()
  505.             response = requests.get(url, timeout=1.0)
  506.             if response.status_code == 200:
  507.                 self.raw_data = response.content
  508.                 self.process_data()
  509.                
  510.                 # FPS calculation
  511.                 self.frame_count += 1
  512.                 current_time = time.time()
  513.                 if current_time - self.last_fps_time >= 1.0:
  514.                     fps = self.frame_count / (current_time - self.last_fps_time)
  515.                     self.root.after(0, lambda: self.fps_label.config(text=f"Actual FPS: {fps:.1f}"))
  516.                     self.frame_count = 0
  517.                     self.last_fps_time = current_time
  518.                    
  519.         except Exception as e:
  520.             self.log(f"Fetch error: {e}")
  521.    
  522.     def process_data(self):
  523.         if self.raw_data is None:
  524.             return
  525.            
  526.         try:
  527.             width = int(self.width_var.get())
  528.             height = int(self.height_var.get())
  529.             total_pixels = width * height
  530.  
  531.             low_offset = self.get_offset(self.low_offset_var.get())
  532.             high_offset = self.get_offset(self.high_offset_var.get())
  533.  
  534.             if len(self.raw_data) >= high_offset + total_pixels:
  535.                 # Extract bytes
  536.                 low_bytes = np.frombuffer(self.raw_data[low_offset:low_offset + total_pixels], dtype=np.uint8)
  537.                 high_bytes = np.frombuffer(self.raw_data[high_offset:high_offset + total_pixels], dtype=np.uint8)
  538.  
  539.                 # Combine to 16-bit
  540.                 image_data = low_bytes.astype(np.uint16) + (high_bytes.astype(np.uint16) << 8)
  541.  
  542.                 # Reshape
  543.                 self.current_image_array = image_data.reshape((height, width))
  544.  
  545.                 # Update raw min/max info
  546.                 data = self.current_image_array.flatten()
  547.                 raw_min = int(np.partition(data, 10)[10])
  548.                 raw_max = int(np.partition(data, -10)[-10])
  549.                 self.raw_min_label.config(text=str(raw_min))
  550.                 self.raw_max_label.config(text=str(raw_max))
  551.                 self.raw_avg_label.config(text=str(int(np.mean(self.current_image_array))))
  552.  
  553.                 # Auto range if enabled
  554.                 if self.first or self.auto_range_var.get():
  555.                     data = self.current_image_array.flatten()
  556.                     if data.size >= 20:
  557.                         min_val = int(np.partition(data, 10)[10])
  558.                         max_val = int(np.partition(data, -10)[-10])
  559.                     else:
  560.                         min_val = int(np.min(data))
  561.                         max_val = int(np.max(data))
  562.                     offset, range_val = self.min_max_to_offset_range(min_val, max_val)
  563.                     self.offset_var.set(offset)
  564.                     self.range_var.set(range_val)
  565.                     self.offset_label.config(text=str(offset))
  566.                     self.range_label.config(text=str(range_val))
  567.                     self.update_min_max_display()
  568.                     self.first = False
  569.  
  570.                 self.root.after(0, self.update_display)
  571.  
  572.         except Exception as e:
  573.             self.log(f"Process error: {e}")
  574.  
  575.  
  576.     def get_settings(self):
  577.         return {
  578.             "ip": self.ip_var.get(),
  579.             "fps": self.fps_var.get(),
  580.             "width": self.width_var.get(),
  581.             "height": self.height_var.get(),
  582.             "low_offset": self.low_offset_var.get(),
  583.             "high_offset": self.high_offset_var.get(),
  584.             "palette": self.palette_var.get(),
  585.             "auto_range": self.auto_range_var.get(),
  586.             "offset": self.offset_var.get(),
  587.             "range": self.range_var.get(),
  588.             "ref_temp": self.ref_temp_var.get(),
  589.             "ref_pixel": self.ref_pixel_var.get(),
  590.             "ref2_temp": self.ref2_temp_var.get(),
  591.             "ref2_pixel": self.ref2_pixel_var.get(),
  592.         }
  593.  
  594.     def set_settings(self, settings):
  595.         self.ip_var.set(settings.get("ip", "192.168.178.92"))
  596.         self.fps_var.set(settings.get("fps", "20"))
  597.         self.width_var.set(settings.get("width", "640"))
  598.         self.height_var.set(settings.get("height", "480"))
  599.         self.low_offset_var.set(settings.get("low_offset", "0x238"))
  600.         self.high_offset_var.set(settings.get("high_offset", "0x4B238"))
  601.         self.palette_var.set(settings.get("palette", "Grayscale"))
  602.         self.auto_range_var.set(settings.get("auto_range", False))
  603.         self.offset_var.set(settings.get("offset", 8192))
  604.         self.range_var.set(settings.get("range", 32767))
  605.         self.ref_temp_var.set(settings.get("ref_temp", 35.0))
  606.         self.ref_pixel_var.set(settings.get("ref_pixel", 7800))
  607.         self.ref2_temp_var.set(settings.get("ref2_temp", 35.0))
  608.         self.ref2_pixel_var.set(settings.get("ref2_pixel", 7800))
  609.  
  610.     def save_settings(self):
  611.         try:
  612.             with open(self.SETTINGS_FILE, "w") as f:
  613.                 json.dump(self.get_settings(), f)
  614.         except Exception as e:
  615.             self.log(f"Settings save error: {e}")
  616.  
  617.     def load_settings(self):
  618.         if os.path.exists(self.SETTINGS_FILE):
  619.             try:
  620.                 with open(self.SETTINGS_FILE, "r") as f:
  621.                     settings = json.load(f)
  622.                 self.set_settings(settings)
  623.             except Exception as e:
  624.                 self.log(f"Settings load error: {e}")
  625.  
  626.     def on_close(self):
  627.         self.save_settings()
  628.         self.root.destroy()  
  629.     def update_display(self):
  630.         if self.current_image_array is None:
  631.             return
  632.  
  633.         try:
  634.             min_val, max_val = self.offset_range_to_min_max()
  635.            
  636.             # Cache normalized array if image or min/max changed
  637.             if not hasattr(self, '_normalized_cache'):
  638.                 self._normalized_cache = None
  639.                 self._cache_image_id = None
  640.                 self._cache_min = None
  641.                 self._cache_max = None
  642.  
  643.             image_id = id(self.current_image_array)
  644.             if (self._normalized_cache is None or
  645.                 self._cache_image_id != image_id or
  646.                 self._cache_min != min_val or
  647.                 self._cache_max != max_val):
  648.                 if max_val > min_val:
  649.                     normalized = np.clip((self.current_image_array.astype(np.float32) - min_val) / (max_val - min_val), 0, 1)
  650.                 else:
  651.                     normalized = np.zeros_like(self.current_image_array, dtype=np.float32)
  652.                 self._normalized_cache = normalized
  653.                 self._cache_image_id = image_id
  654.                 self._cache_min = min_val
  655.                 self._cache_max = max_val
  656.             else:
  657.                 normalized = self._normalized_cache
  658.            
  659.             # Apply colormap
  660.             palette = self.palette_var.get()
  661.             if palette == "Grayscale":
  662.                 image_8bit = (normalized * 255).astype(np.uint8)
  663.                 pil_image = Image.fromarray(image_8bit, mode='L')
  664.             else:
  665.                 cmap = self.palettes[palette]
  666.                 colored = cmap(normalized)
  667.                 rgb_array = (colored[:, :, :3] * 255).astype(np.uint8)
  668.                 pil_image = Image.fromarray(rgb_array, mode='RGB')
  669.            
  670.             self.current_pil_image = pil_image
  671.            
  672.             # Scale for display
  673.             canvas_w = self.canvas.winfo_width()
  674.             canvas_h = self.canvas.winfo_height()
  675.  
  676.             if canvas_w > 1 and canvas_h > 1:
  677.                 img_w, img_h = pil_image.size
  678.                 scale_x = canvas_w / img_w
  679.                 scale_y = canvas_h / img_h
  680.                 scale = min(scale_x, scale_y, 3.0)  # Max 3x scale
  681.  
  682.                 if scale > 1.0:
  683.                     new_w = int(img_w * scale)
  684.                     new_h = int(img_h * scale)
  685.                     display_image = pil_image.resize((new_w, new_h), Image.Resampling.NEAREST)
  686.                 else:
  687.                     display_image = pil_image
  688.                 self.scale_factor = scale
  689.  
  690.                 self.photo = ImageTk.PhotoImage(display_image)
  691.                 self.canvas.delete("all")
  692.                 self.canvas.create_image(canvas_w//2, canvas_h//2, image=self.photo, anchor=tk.CENTER)
  693.  
  694.                 # Draw red marker if locked_pixel is set
  695.                 if self.locked_pixel is not None:
  696.                     lx, ly = self.locked_pixel
  697.                     marker_x = int(lx * self.scale_factor)
  698.                     marker_y = int(ly * self.scale_factor)
  699.                     r = 8  # marker radius
  700.                     self.canvas.create_text(marker_x+1, marker_y - r + 10, text="X", fill="red", font=("Arial", 8))
  701.                     #self.canvas.create_oval(
  702.                     #    marker_x - r, marker_y - r, marker_x + r, marker_y + r,
  703.                     #    outline="red", width=3
  704.                     #)
  705.                     # Show temperature for locked pixel
  706.                     value = self.current_image_array[ly, lx]
  707.                     ref1_temp = self.ref_temp_var.get()
  708.                     ref1_pixel = self.ref_pixel_var.get()
  709.                     ref2_temp = self.ref2_temp_var.get()
  710.                     ref2_pixel = self.ref2_pixel_var.get()
  711.                     if ref2_pixel != ref1_pixel:
  712.                         temperature = ref1_temp + (value - ref1_pixel) * (ref2_temp - ref1_temp) / (ref2_pixel - ref1_pixel)
  713.                         self.temp_label.config(text=f"Temperature (locked): {temperature:.1f} °C")
  714.                     else:
  715.                         self.temp_label.config(text="Temperature: -- °C")
  716.  
  717.         except Exception as e:
  718.             self.log(f"Display error: {e}")
  719.             self.canvas.delete("all")
  720.             self.current_pil_image = None
  721.    
  722.     def stream_worker(self):
  723.         while self.streaming:
  724.             start_time = time.time()
  725.             self.fetch_data()
  726.            
  727.             # Frame rate control
  728.             try:
  729.                 target_fps = int(self.fps_var.get())
  730.                 frame_time = 1.0 / target_fps
  731.                 elapsed = time.time() - start_time
  732.                 sleep_time = max(0, frame_time - elapsed)
  733.                 time.sleep(sleep_time)
  734.             except:
  735.                 time.sleep(0.05)  # 20 FPS fallback
  736.    
  737.     def toggle_stream(self):
  738.         if not self.streaming:
  739.             self.first = True
  740.             self.streaming = True
  741.             self.start_button.config(text="Stop")
  742.             self.status_label.config(text="Status: Streaming")
  743.             self.stream_thread = threading.Thread(target=self.stream_worker, daemon=True)
  744.             self.stream_thread.start()
  745.             # Set reference pixel to avg on stream start
  746.             if self.current_image_array is not None:
  747.                 avg_pixel = int(np.mean(self.current_image_array))
  748.                 self.ref_pixel_var.set(avg_pixel)
  749.         else:
  750.             self.streaming = False
  751.             self.start_button.config(text="Start Stream")
  752.             self.status_label.config(text="Status: Stopped")
  753.    
  754.     def save_image(self):
  755.         if self.current_pil_image is None:
  756.             messagebox.showwarning("No Image", "No image to save.")
  757.             return
  758.        
  759.         file_path = filedialog.asksaveasfilename(
  760.             defaultextension=".png",
  761.             filetypes=[("PNG files", "*.png"), ("JPEG files", "*.jpg")]
  762.         )
  763.        
  764.         if file_path:
  765.             try:
  766.                 self.current_pil_image.save(file_path)
  767.                 messagebox.showinfo("Success", f"Image saved: {file_path}")
  768.             except Exception as e:
  769.                 messagebox.showerror("Error", f"Save failed: {e}")
  770.  
  771. def main():
  772.     root = tk.Tk()
  773.     app = ImageStreamViewer(root)
  774.     root.mainloop()
  775.  
  776. if __name__ == "__main__":
  777.     main()
Add Comment
Please, Sign In to add comment