Guest User

viewer

a guest
Jul 28th, 2025
23
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 23.05 KB | Science | 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.  
  12. class ImageStreamViewer:
  13.     def __init__(self, root):
  14.         self.root = root
  15.         self.root.title("Image Stream Viewer - Fast")
  16.         self.root.geometry("1800x1000")
  17.        
  18.         self.current_image_array = None
  19.         self.current_pil_image = None
  20.         self.resolution = None
  21.         self.raw_data = None
  22.        
  23.         self.streaming = False
  24.         self.stream_thread = None
  25.        
  26.         # Simple palette mapping
  27.         self.palettes = {
  28.             'Grayscale': None,
  29.             'Hot': cm.hot,
  30.             'Jet': cm.jet,
  31.             'Cool': cm.cool,
  32.             'Viridis': cm.viridis,
  33.             'Plasma': cm.plasma,
  34.         }
  35.         self.first = True
  36.        
  37.         # Performance tracking
  38.         self.frame_count = 0
  39.         self.last_fps_time = time.time()
  40.        
  41.         self.setup_gui()
  42.        
  43.     def setup_gui(self):
  44.         main_frame = ttk.Frame(self.root, padding="1")
  45.         main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  46.        
  47.         # Left panel for controls
  48.         left_frame = ttk.Frame(main_frame, width=700)
  49.         left_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 10))
  50.         left_frame.grid_propagate(False)
  51.        
  52.         # Right panel for image
  53.         right_frame = ttk.Frame(main_frame)
  54.         right_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S))
  55.        
  56.         # === CONTROLS ===
  57.        
  58.         # Connection
  59.         conn_frame = ttk.LabelFrame(left_frame, text="Connection", padding="5")
  60.         conn_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
  61.        
  62.         ttk.Label(conn_frame, text="IP:").grid(row=0, column=0, sticky=tk.W)
  63.         self.ip_var = tk.StringVar(value="192.168.178.92")
  64.         ttk.Entry(conn_frame, textvariable=self.ip_var, width=15).grid(row=0, column=1, padx=(5, 10))
  65.        
  66.         self.start_button = ttk.Button(conn_frame, text="Start Stream", command=self.toggle_stream)
  67.         self.start_button.grid(row=0, column=2, padx=(0, 5))
  68.        
  69.         self.save_button = ttk.Button(conn_frame, text="Save", command=self.save_image)
  70.         self.save_button.grid(row=0, column=3, padx=(0, 5))
  71.        
  72.         ttk.Label(conn_frame, text="FPS:").grid(row=0, column=4, padx=(10, 5))
  73.         self.fps_var = tk.StringVar(value="20")
  74.         ttk.Entry(conn_frame, textvariable=self.fps_var, width=4).grid(row=0, column=5)
  75.        
  76.         self.status_label = ttk.Label(conn_frame, text="Status: Stopped")
  77.         self.status_label.grid(row=1, column=0, columnspan=3, sticky=tk.W)
  78.        
  79.         self.fps_label = ttk.Label(conn_frame, text="Actual FPS: 0")
  80.         self.fps_label.grid(row=1, column=3, columnspan=3, sticky=tk.W)
  81.        
  82.         # Format
  83.         format_frame = ttk.LabelFrame(left_frame, text="Format", padding="5")
  84.         format_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
  85.        
  86.         ttk.Label(format_frame, text="Width:").grid(row=0, column=0, sticky=tk.W)
  87.         self.width_var = tk.StringVar(value="640")
  88.         ttk.Entry(format_frame, textvariable=self.width_var, width=6).grid(row=0, column=1, padx=(5, 10))
  89.        
  90.         ttk.Label(format_frame, text="Height:").grid(row=0, column=2, sticky=tk.W)
  91.         self.height_var = tk.StringVar(value="480")
  92.         ttk.Entry(format_frame, textvariable=self.height_var, width=6).grid(row=0, column=3, padx=(5, 10))
  93.        
  94.         ttk.Label(format_frame, text="Low offset:").grid(row=1, column=0, sticky=tk.W)
  95.         self.low_offset_var = tk.StringVar(value="0x238")
  96.         ttk.Entry(format_frame, textvariable=self.low_offset_var, width=8).grid(row=1, column=1, padx=(5, 10))
  97.        
  98.         ttk.Label(format_frame, text="High offset:").grid(row=1, column=2, sticky=tk.W)
  99.         self.high_offset_var = tk.StringVar(value="0x4B238")
  100.         ttk.Entry(format_frame, textvariable=self.high_offset_var, width=8).grid(row=1, column=3, padx=(5, 10))
  101.        
  102.         ttk.Button(format_frame, text="Parse Header", command=self.parse_header).grid(row=0, column=4, padx=(10, 0))
  103.        
  104.         # Display
  105.         display_frame = ttk.LabelFrame(left_frame, text="Display", padding="5")
  106.         display_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
  107.        
  108.         ttk.Label(display_frame, text="Palette:").grid(row=0, column=0, sticky=tk.W)
  109.         self.palette_var = tk.StringVar(value="Grayscale")
  110.         palette_combo = ttk.Combobox(display_frame, textvariable=self.palette_var,
  111.                                    values=list(self.palettes.keys()), state="readonly", width=10)
  112.         palette_combo.grid(row=0, column=1, padx=(5, 10))
  113.         palette_combo.bind("<<ComboboxSelected>>", self.on_palette_change)
  114.        
  115.         self.auto_range_var = tk.BooleanVar(value=False)
  116.         ttk.Checkbutton(display_frame, text="Auto Range", variable=self.auto_range_var).grid(row=0, column=2)
  117.        
  118.         # Range controls
  119.         ttk.Button(display_frame, text="Auto Now", command=self.auto_range).grid(row=1, column=0, padx=(10, 0))
  120.         ttk.Button(display_frame, text="Reset", command=self.reset_range).grid(row=2, column=0, padx=(10, 0))
  121.        
  122.         # Offset controls
  123.         ttk.Label(display_frame, text="Offset:").grid(row=1, column=1, sticky=tk.W)
  124.         self.offset_var = tk.IntVar(value=32767)
  125.         offset_frame = ttk.Frame(display_frame)
  126.         offset_frame.grid(row=1, column=2, columnspan=2, sticky=(tk.W, tk.E))
  127.        
  128.         ttk.Button(offset_frame, text="-", width=3, command=lambda: self.adjust_offset(-5)).grid(row=0, column=0)
  129.         self.offset_scale = ttk.Scale(offset_frame, from_=0, to=65535, variable=self.offset_var,
  130.                                      orient=tk.HORIZONTAL, length=360, command=self.on_offset_change)
  131.         self.offset_scale.grid(row=0, column=1, padx=(2, 2))
  132.         ttk.Button(offset_frame, text="+", width=3, command=lambda: self.adjust_offset(5)).grid(row=0, column=2)
  133.         self.offset_label = ttk.Label(offset_frame, text="32767", width=6)
  134.         self.offset_label.grid(row=0, column=3, padx=(5, 0))
  135.        
  136.         # Range controls
  137.         ttk.Label(display_frame, text="Range:").grid(row=2, column=1, sticky=tk.W)
  138.         self.range_var = tk.IntVar(value=32767)
  139.         range_frame = ttk.Frame(display_frame)
  140.         range_frame.grid(row=2, column=2, columnspan=2, sticky=(tk.W, tk.E))
  141.        
  142.         ttk.Button(range_frame, text="-", width=3, command=lambda: self.adjust_range(-5)).grid(row=0, column=0)
  143.         self.range_scale = ttk.Scale(range_frame, from_=1, to=65535, variable=self.range_var,
  144.                                     orient=tk.HORIZONTAL, length=360, command=self.on_range_change)
  145.         self.range_scale.grid(row=0, column=1, padx=(2, 2))
  146.         ttk.Button(range_frame, text="+", width=3, command=lambda: self.adjust_range(5)).grid(row=0, column=2)
  147.         self.range_label = ttk.Label(range_frame, text="32767", width=6)
  148.         self.range_label.grid(row=0, column=3, padx=(5, 0))
  149.        
  150.         # Min/Max display (read-only)
  151.         ttk.Label(display_frame, text="Min:").grid(row=3, column=1, sticky=tk.W)
  152.         self.min_display_label = ttk.Label(display_frame, text="0", width=6, relief="sunken")
  153.         self.min_display_label.grid(row=3, column=2, sticky=tk.W)
  154.        
  155.         ttk.Label(display_frame, text="Max:").grid(row=3, column=3, sticky=tk.W)
  156.         self.max_display_label = ttk.Label(display_frame, text="65535", width=6, relief="sunken")
  157.         self.max_display_label.grid(row=3, column=4, sticky=tk.W)
  158.        
  159.         # Commands
  160.         cmd_frame = ttk.LabelFrame(left_frame, text="Commands", padding="5")
  161.         cmd_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
  162.        
  163.         ttk.Label(cmd_frame, text="Cmd:").grid(row=0, column=0, sticky=tk.W)
  164.         self.cmd_entry = ttk.Entry(cmd_frame, width=15)
  165.         self.cmd_entry.grid(row=0, column=1, padx=(5, 10))
  166.        
  167.         ttk.Label(cmd_frame, text="Value:").grid(row=0, column=2, sticky=tk.W)
  168.         self.value_entry = ttk.Entry(cmd_frame, width=15)
  169.         self.value_entry.grid(row=0, column=3, padx=(5, 10))
  170.        
  171.         ttk.Button(cmd_frame, text="Send", command=self.send_command).grid(row=0, column=4)
  172.        
  173.         # FFC Button
  174.         ttk.Button(cmd_frame, text="FFC", command=self.send_ffc_command).grid(row=0, column=5, padx=(10, 0))
  175.        
  176.         self.cmd_response = scrolledtext.ScrolledText(cmd_frame, height=3, width=60, font=("Courier", 8))
  177.         self.cmd_response.grid(row=1, column=0, columnspan=6, pady=(5, 0), sticky=(tk.W, tk.E))
  178.        
  179.         # Info
  180.         info_frame = ttk.LabelFrame(left_frame, text="Info", padding="5")
  181.         info_frame.grid(row=4, column=0, sticky=(tk.W, tk.E), pady=(0, 5))
  182.        
  183.         self.info_text = tk.Text(info_frame, height=20, width=90, font=("Courier", 8))
  184.         info_scroll = ttk.Scrollbar(info_frame, orient=tk.VERTICAL, command=self.info_text.yview)
  185.         self.info_text.configure(yscrollcommand=info_scroll.set)
  186.         self.info_text.grid(row=0, column=0, sticky=(tk.W, tk.E))
  187.         info_scroll.grid(row=0, column=1, sticky=(tk.N, tk.S))
  188.        
  189.         # === IMAGE DISPLAY ===
  190.        
  191.         image_frame = ttk.LabelFrame(right_frame, text="Image", padding="5")
  192.         image_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  193.        
  194.         self.canvas = tk.Canvas(image_frame, bg="black", width=1000, height=800)
  195.         self.canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  196.        
  197.         # Pixel info
  198.         self.pixel_label = ttk.Label(image_frame, text="Pixel: Move mouse over image")
  199.         self.pixel_label.grid(row=1, column=0, sticky=tk.W)
  200.        
  201.         self.canvas.bind("<Motion>", self.on_mouse_move)
  202.        
  203.         # Grid weights
  204.         self.root.columnconfigure(0, weight=1)
  205.         self.root.rowconfigure(0, weight=1)
  206.         main_frame.columnconfigure(0, weight=0)
  207.         main_frame.columnconfigure(1, weight=1)
  208.         main_frame.rowconfigure(0, weight=1)
  209.         left_frame.columnconfigure(0, weight=1)
  210.         right_frame.columnconfigure(0, weight=1)
  211.         right_frame.rowconfigure(0, weight=1)
  212.         image_frame.columnconfigure(0, weight=1)
  213.         image_frame.rowconfigure(0, weight=1)
  214.        
  215.         # Bind Enter keys
  216.         self.cmd_entry.bind("<Return>", lambda e: self.send_command())
  217.         self.value_entry.bind("<Return>", lambda e: self.send_command())
  218.        
  219.         # Initialize min/max display
  220.         self.update_min_max_display()
  221.        
  222.     def log(self, text):
  223.         self.info_text.insert(tk.END, f"{text}\n")
  224.         self.info_text.see(tk.END)
  225.        
  226.     def parse_header(self):
  227.         if self.raw_data is None:
  228.             return
  229.            
  230.         try:
  231.             data = self.raw_data
  232.             width = struct.unpack('<I', data[8:12])[0]
  233.             height = struct.unpack('<I', data[12:16])[0]
  234.            
  235.             self.width_var.set(str(width))
  236.             self.height_var.set(str(height))
  237.             self.log(f"Parsed: {width}x{height}")
  238.            
  239.         except Exception as e:
  240.             self.log(f"Parse error: {e}")
  241.    
  242.     def get_offset(self, offset_str):
  243.         try:
  244.             if offset_str.startswith('0x'):
  245.                 return int(offset_str, 16)
  246.             else:
  247.                 return int(offset_str)
  248.         except:
  249.             return 0
  250.    
  251.     def send_command(self):
  252.         cmd = self.cmd_entry.get().strip()
  253.         value = self.value_entry.get().strip()
  254.         if not cmd:
  255.             return
  256.            
  257.         command = f"{cmd},{value}" if value else f"{cmd},"
  258.         ip = self.ip_var.get()
  259.         url = f"http://{ip}/cgi-bin/dmcmd?Command={urllib.parse.quote(command)}"
  260.        
  261.         def make_request():
  262.             try:
  263.                 response = requests.get(url, timeout=5)
  264.                 self.root.after(0, lambda: self.cmd_response.insert(tk.END, f"OK: {response.text}\n"))
  265.             except Exception as e:
  266.                 self.root.after(0, lambda: self.cmd_response.insert(tk.END, f"Error: {e}\n"))
  267.        
  268.         threading.Thread(target=make_request, daemon=True).start()
  269.    
  270.     def send_ffc_command(self):
  271.         """Send FFC command (KBD with value M)"""
  272.         command = "KBD,C"
  273.         ip = self.ip_var.get()
  274.         url = f"http://{ip}/cgi-bin/dmcmd?Command={urllib.parse.quote(command)}"
  275.        
  276.         def make_request():
  277.             try:
  278.                 response = requests.get(url, timeout=5)
  279.                 self.root.after(0, lambda: self.cmd_response.insert(tk.END, f"FFC OK: {response.text}\n"))
  280.             except Exception as e:
  281.                 self.root.after(0, lambda: self.cmd_response.insert(tk.END, f"FFC Error: {e}\n"))
  282.        
  283.         threading.Thread(target=make_request, daemon=True).start()
  284.    
  285.     def adjust_offset(self, delta):
  286.         """Adjust offset by delta amount"""
  287.         current = self.offset_var.get()
  288.         new_value = max(0, min(65535, current + delta))
  289.         self.offset_var.set(new_value)
  290.         self.on_offset_change()
  291.    
  292.     def adjust_range(self, delta):
  293.         """Adjust range by delta amount"""
  294.         current = self.range_var.get()
  295.         new_value = max(1, min(65535, current + delta))
  296.         self.range_var.set(new_value)
  297.         self.on_range_change()
  298.    
  299.     def on_mouse_move(self, event):
  300.         if self.current_image_array is None or not hasattr(self, 'scale_factor'):
  301.             return
  302.            
  303.         try:
  304.             x = int(event.x / self.scale_factor)
  305.             y = int(event.y / self.scale_factor)
  306.             h, w = self.current_image_array.shape
  307.            
  308.             if 0 <= x < w and 0 <= y < h:
  309.                 value = self.current_image_array[y, x]
  310.                 self.pixel_label.config(text=f"Pixel ({x},{y}): {value}")
  311.         except:
  312.             pass
  313.    
  314.     def on_palette_change(self, event=None):
  315.         if self.current_image_array is not None:
  316.             self.update_display()
  317.    
  318.     def offset_range_to_min_max(self):
  319.         """Convert offset and range to min and max values"""
  320.         offset = self.offset_var.get()
  321.         range_val = self.range_var.get()
  322.        
  323.         min_val = max(0, offset - range_val)
  324.         max_val = min(65535, offset + range_val)
  325.        
  326.         return min_val, max_val
  327.    
  328.     def min_max_to_offset_range(self, min_val, max_val):
  329.         """Convert min and max values to offset and range"""
  330.         offset = (min_val + max_val) // 2
  331.         range_val = (max_val - min_val) // 2
  332.        
  333.         return offset, range_val
  334.    
  335.     def update_min_max_display(self):
  336.         """Update the min/max display labels"""
  337.         min_val, max_val = self.offset_range_to_min_max()
  338.         self.min_display_label.config(text=str(min_val))
  339.         self.max_display_label.config(text=str(max_val))
  340.    
  341.     def on_offset_change(self, event=None):
  342.         offset = int(self.offset_var.get())
  343.         self.offset_label.config(text=str(offset))
  344.         self.update_min_max_display()
  345.        
  346.         if self.current_image_array is not None:
  347.             self.update_display()
  348.    
  349.     def on_range_change(self, event=None):
  350.         range_val = int(self.range_var.get())
  351.         self.range_label.config(text=str(range_val))
  352.         self.update_min_max_display()
  353.        
  354.         if self.current_image_array is not None:
  355.             self.update_display()
  356.    
  357.     def auto_range(self):
  358.         if self.current_image_array is not None:
  359.             data = self.current_image_array.flatten()
  360.             min_val = int(np.percentile(data, 1))
  361.             max_val = int(np.percentile(data, 99))
  362.            
  363.             offset, range_val = self.min_max_to_offset_range(min_val, max_val)
  364.             self.offset_var.set(offset)
  365.             self.range_var.set(range_val)
  366.             self.on_offset_change()
  367.             self.on_range_change()
  368.    
  369.     def reset_range(self):
  370.         offset, range_val = self.min_max_to_offset_range(0, 65535)
  371.         self.offset_var.set(offset)
  372.         self.range_var.set(range_val)
  373.         self.on_offset_change()
  374.         self.on_range_change()
  375.    
  376.     def get_url(self):
  377.         ip = self.ip_var.get()
  378.         return f"http://{ip}:5000/stream?Type=RAW&Source=Raw&Mode=TCP&Heart-beat=No&Frames=1&Snap=No&Channel=1"
  379.    
  380.     def fetch_data(self):
  381.         try:
  382.             url = self.get_url()
  383.             response = requests.get(url, timeout=1.0)
  384.             if response.status_code == 200:
  385.                 self.raw_data = response.content
  386.                 self.process_data()
  387.                
  388.                 # FPS calculation
  389.                 self.frame_count += 1
  390.                 current_time = time.time()
  391.                 if current_time - self.last_fps_time >= 1.0:
  392.                     fps = self.frame_count / (current_time - self.last_fps_time)
  393.                     self.root.after(0, lambda: self.fps_label.config(text=f"Actual FPS: {fps:.1f}"))
  394.                     self.frame_count = 0
  395.                     self.last_fps_time = current_time
  396.                    
  397.         except Exception as e:
  398.             self.log(f"Fetch error: {e}")
  399.    
  400.     def process_data(self):
  401.         if self.raw_data is None:
  402.             return
  403.            
  404.         try:
  405.             width = int(self.width_var.get())
  406.             height = int(self.height_var.get())
  407.             total_pixels = width * height
  408.            
  409.             low_offset = self.get_offset(self.low_offset_var.get())
  410.             high_offset = self.get_offset(self.high_offset_var.get())
  411.            
  412.             if len(self.raw_data) >= high_offset + total_pixels:
  413.                 # Extract bytes
  414.                 low_bytes = np.frombuffer(self.raw_data[low_offset:low_offset + total_pixels], dtype=np.uint8)
  415.                 high_bytes = np.frombuffer(self.raw_data[high_offset:high_offset + total_pixels], dtype=np.uint8)
  416.                
  417.                 # Combine to 16-bit
  418.                 image_data = low_bytes.astype(np.uint16) + (high_bytes.astype(np.uint16) << 8)
  419.                
  420.                 # Reshape
  421.                 self.current_image_array = image_data.reshape((height, width))
  422.                
  423.                 # Auto range if enabled
  424.                 if self.first or self.auto_range_var.get():
  425.                     data = self.current_image_array.flatten()
  426.                     min_val = int(np.percentile(data, 1))
  427.                     max_val = int(np.percentile(data, 99))
  428.                     offset, range_val = self.min_max_to_offset_range(min_val, max_val)
  429.                     self.offset_var.set(offset)
  430.                     self.range_var.set(range_val)
  431.                     self.offset_label.config(text=str(offset))
  432.                     self.range_label.config(text=str(range_val))
  433.                     self.update_min_max_display()
  434.                     self.first = False
  435.                
  436.                 self.root.after(0, self.update_display)
  437.                
  438.         except Exception as e:
  439.             self.log(f"Process error: {e}")
  440.    
  441.     def update_display(self):
  442.         if self.current_image_array is None:
  443.             return
  444.        
  445.         try:
  446.             min_val, max_val = self.offset_range_to_min_max()
  447.            
  448.             # Normalize
  449.             if max_val > min_val:
  450.                 normalized = np.clip((self.current_image_array.astype(np.float32) - min_val) / (max_val - min_val), 0, 1)
  451.             else:
  452.                 normalized = np.zeros_like(self.current_image_array, dtype=np.float32)
  453.            
  454.             # Apply colormap
  455.             palette = self.palette_var.get()
  456.             if palette == "Grayscale":
  457.                 image_8bit = (normalized * 255).astype(np.uint8)
  458.                 pil_image = Image.fromarray(image_8bit, mode='L')
  459.             else:
  460.                 cmap = self.palettes[palette]
  461.                 colored = cmap(normalized)
  462.                 rgb_array = (colored[:, :, :3] * 255).astype(np.uint8)
  463.                 pil_image = Image.fromarray(rgb_array, mode='RGB')
  464.            
  465.             self.current_pil_image = pil_image
  466.            
  467.             # Scale for display
  468.             canvas_w = self.canvas.winfo_width()
  469.             canvas_h = self.canvas.winfo_height()
  470.            
  471.             if canvas_w > 1 and canvas_h > 1:
  472.                 img_w, img_h = pil_image.size
  473.                 scale_x = canvas_w / img_w
  474.                 scale_y = canvas_h / img_h
  475.                 scale = min(scale_x, scale_y, 3.0)  # Max 3x scale
  476.                
  477.                 if scale > 1.0:
  478.                     new_w = int(img_w * scale)
  479.                     new_h = int(img_h * scale)
  480.                     display_image = pil_image.resize((new_w, new_h), Image.Resampling.NEAREST)
  481.                     self.scale_factor = scale
  482.                 else:
  483.                     display_image = pil_image
  484.                     self.scale_factor = 1.0
  485.                
  486.                 self.photo = ImageTk.PhotoImage(display_image)
  487.                 self.canvas.delete("all")
  488.                 self.canvas.create_image(canvas_w//2, canvas_h//2, image=self.photo)
  489.                
  490.         except Exception as e:
  491.             self.log(f"Display error: {e}")
  492.    
  493.     def stream_worker(self):
  494.         while self.streaming:
  495.             start_time = time.time()
  496.             self.fetch_data()
  497.            
  498.             # Frame rate control
  499.             try:
  500.                 target_fps = int(self.fps_var.get())
  501.                 frame_time = 1.0 / target_fps
  502.                 elapsed = time.time() - start_time
  503.                 sleep_time = max(0, frame_time - elapsed)
  504.                 time.sleep(sleep_time)
  505.             except:
  506.                 time.sleep(0.05)  # 20 FPS fallback
  507.    
  508.     def toggle_stream(self):
  509.         if not self.streaming:
  510.             self.first = True
  511.             self.streaming = True
  512.             self.start_button.config(text="Stop")
  513.             self.status_label.config(text="Status: Streaming")
  514.             self.stream_thread = threading.Thread(target=self.stream_worker, daemon=True)
  515.             self.stream_thread.start()
  516.         else:
  517.             self.streaming = False
  518.             self.start_button.config(text="Start Stream")
  519.             self.status_label.config(text="Status: Stopped")
  520.    
  521.     def save_image(self):
  522.         if self.current_pil_image is None:
  523.             messagebox.showwarning("No Image", "No image to save.")
  524.             return
  525.        
  526.         file_path = filedialog.asksaveasfilename(
  527.             defaultextension=".png",
  528.             filetypes=[("PNG files", "*.png"), ("JPEG files", "*.jpg")]
  529.         )
  530.        
  531.         if file_path:
  532.             try:
  533.                 self.current_pil_image.save(file_path)
  534.                 messagebox.showinfo("Success", f"Image saved: {file_path}")
  535.             except Exception as e:
  536.                 messagebox.showerror("Error", f"Save failed: {e}")
  537.  
  538. def main():
  539.     root = tk.Tk()
  540.     app = ImageStreamViewer(root)
  541.     root.mainloop()
  542.  
  543. if __name__ == "__main__":
  544.     main()
Add Comment
Please, Sign In to add comment