Compare commits

...

4 Commits

Author SHA1 Message Date
VALLONGOL
ac6acc2246 pulite i file temporanei 2026-01-19 15:27:31 +01:00
VALLONGOL
ac1d9e23da sistemati alcuni salvataggi delle immagini/video 2026-01-19 15:07:20 +01:00
VALLONGOL
bef6cf6f08 configurazione salvataggi esterni al modulo 2026-01-19 14:37:12 +01:00
VALLONGOL
28f5fda397 sistemate funzioni di registrazione di sar e mfd 2026-01-19 13:56:46 +01:00
7 changed files with 489 additions and 96 deletions

View File

@ -18,7 +18,11 @@ SAVE_RAW_UNKNOWN = True
# Flags to also save decoded previews for quick inspection # Flags to also save decoded previews for quick inspection
# MFD: save BMP; SAR/UNKNOWN: save BMP by default for lossless grayscale # MFD: save BMP; SAR/UNKNOWN: save BMP by default for lossless grayscale
SAVE_BMP_MFD = False SAVE_BMP_MFD = False
SAVE_BMP_SAR = True # By default do not automatically save decoded SAR previews; leave saving
# under user control via the UI checkboxes. Previously this was True and
# caused SAR previews to be written even when the user only enabled video
# recording. Set to False to avoid unexpected saves.
SAVE_BMP_SAR = False
SAVE_BMP_UNKNOWN = True SAVE_BMP_UNKNOWN = True
# Video recording defaults # Video recording defaults

View File

@ -43,9 +43,14 @@ class DumpManager:
'frame': deque() 'frame': deque()
} }
# Video recording state # Video recording state
self._video_writers: Dict[str, cv2.VideoWriter] = {} self._video_writers = {}
self._video_start_times: Dict[str, float] = {} self._video_start_times: Dict[str, float] = {}
self._last_frame_timestamps: Dict[str, float] = {} self._last_frame_timestamps: Dict[str, float] = {}
self._video_fps_map: Dict[str, float] = {}
# Maximum duplicate frames to write when spacing is large (safety cap)
self._max_duplicate_frames = 1000
# Track last produced video file paths per category
self._video_paths: Dict[str, str] = {}
# prune existing files at startup # prune existing files at startup
for cat in list(self._saved.keys()): for cat in list(self._saved.keys()):
@ -109,6 +114,13 @@ class DumpManager:
return False return False
self._video_writers[category] = writer self._video_writers[category] = writer
self._video_start_times[category] = time.time() self._video_start_times[category] = time.time()
# remember video path for later retrieval
self._video_paths[category] = path
# store configured fps per category
try:
self._video_fps_map[category] = float(fps)
except Exception:
self._video_fps_map[category] = float(20)
self._last_frame_timestamps[category] = time.time() self._last_frame_timestamps[category] = time.time()
logger.info("Started video recording: %s (%dx%d @ %d FPS)", path, width, height, fps) logger.info("Started video recording: %s (%dx%d @ %d FPS)", path, width, height, fps)
return True return True
@ -116,7 +128,7 @@ class DumpManager:
logger.exception("Error starting video recording") logger.exception("Error starting video recording")
return False return False
def write_video_frame(self, frame: Image.Image | np.ndarray, category: str): def write_video_frame(self, frame, category: str):
"""Write a frame to the active video writer, converting from PIL/RGB if needed.""" """Write a frame to the active video writer, converting from PIL/RGB if needed."""
if category not in self._video_writers: if category not in self._video_writers:
return return
@ -130,7 +142,24 @@ class DumpManager:
# Ensure correct format (uint8, BGR) # Ensure correct format (uint8, BGR)
if cv_frame.dtype != np.uint8: if cv_frame.dtype != np.uint8:
cv_frame = cv_frame.astype(np.uint8) cv_frame = cv_frame.astype(np.uint8)
writer.write(cv_frame) # Determine how many frames to write to reflect real arrival timing.
now = time.time()
last = self._last_frame_timestamps.get(category, now)
fps = float(self._video_fps_map.get(category, 20.0))
# delta seconds since last written frame
delta = max(0.0, now - last)
# compute duplicates: at least 1 frame
try:
dup = max(1, int(round(delta * fps)))
except Exception:
dup = 1
# cap duplicates to avoid runaway file sizes
if dup > self._max_duplicate_frames:
dup = self._max_duplicate_frames
for _ in range(dup):
writer.write(cv_frame)
# update last timestamp to now
self._last_frame_timestamps[category] = now
except Exception: except Exception:
logger.exception("Error writing frame to video %s", category) logger.exception("Error writing frame to video %s", category)
@ -143,8 +172,11 @@ class DumpManager:
except Exception: except Exception:
logger.exception("Error closing video writer") logger.exception("Error closing video writer")
finally: finally:
# Remove the active writer and runtime timestamp but keep
# the recorded video's start time/path so the UI can show
# the last produced file even after recording stopped.
self._video_writers.pop(category, None) self._video_writers.pop(category, None)
self._video_start_times.pop(category, None) # keep _video_start_times[category] and _video_paths[category]
self._last_frame_timestamps.pop(category, None) self._last_frame_timestamps.pop(category, None)
def _enqueue(self, path: str, category: str) -> None: def _enqueue(self, path: str, category: str) -> None:
@ -153,6 +185,38 @@ class DumpManager:
self._saved[cat].append(path) self._saved[cat].append(path)
self._prune_category(cat) self._prune_category(cat)
def get_last_saved(self, category: str) -> Optional[str]:
"""Return the last saved preview path for a category, or None."""
try:
cat = category if category in self._saved else 'unknown'
if not self._saved.get(cat):
return None
return self._saved[cat][-1]
except Exception:
return None
def get_last_video_path(self, category: Optional[str] = None) -> Optional[str]:
"""Return the last started video path.
If `category` is provided, return that category's last video path.
Otherwise return the most-recently-started video across categories.
"""
try:
if category:
return self._video_paths.get(category)
# choose latest by _video_start_times
best_cat = None
best_ts = 0.0
for cat, ts in self._video_start_times.items():
if ts and ts > best_ts:
best_ts = ts
best_cat = cat
if best_cat:
return self._video_paths.get(best_cat)
return None
except Exception:
return None
def _prune_category(self, category: str) -> None: def _prune_category(self, category: str) -> None:
"""Remove oldest files if they exceed the keep limit.""" """Remove oldest files if they exceed the keep limit."""
prefix = f'VideoReceiverSFP_{category}_' prefix = f'VideoReceiverSFP_{category}_'

View File

@ -91,11 +91,12 @@ def dispatch_frame(module: Any, frame: Any, leader: Optional[Any]) -> None:
except Exception: except Exception:
pass pass
if w and h: if w and h:
started = module._dump_manager.start_video_record('sar', w, h, fps=getattr(module, '_video_fps', 20)) sar_fps = getattr(module, '_sar_video_fps', getattr(module, '_video_fps', 20))
started = module._dump_manager.start_video_record('sar', w, h, fps=sar_fps)
if started: if started:
module._sar_video_active = True module._sar_video_active = True
try: try:
logging.getLogger().info("VideoReceiverSFP: started SAR video writer (%dx%d @ %s FPS)", w, h, getattr(module, '_video_fps', 20)) logging.getLogger().info("VideoReceiverSFP: started SAR video writer (%dx%d @ %s FPS)", w, h, sar_fps)
except Exception: except Exception:
pass pass
except Exception: except Exception:
@ -167,19 +168,27 @@ def dispatch_frame(module: Any, frame: Any, leader: Optional[Any]) -> None:
pass pass
# generic callbacks and saving # generic callbacks and saving
try:
is_mfd = (image_type_id == getattr(module, 'IMAGE_TYPE_MFD', 1))
except Exception:
is_mfd = False
for cb in list(getattr(module, '_callbacks', [])): for cb in list(getattr(module, '_callbacks', [])):
try: try:
if getattr(module, '_save_png', False) and Image is not None and hasattr(frame, 'save'): # Only the MFD generic save is controlled by _save_png. SAR has its
# own _sar_save_png flag handled earlier. Save MFD previews under
# the 'mfd' category so get_last_saved_mfd() can find them.
if is_mfd and getattr(module, '_save_png', False) and Image is not None and hasattr(frame, 'save'):
try: try:
if getattr(module, '_dump_manager', None) is not None: if getattr(module, '_dump_manager', None) is not None:
saved = module._dump_manager.save_preview_from_pil(frame, category='frame', fmt='png') saved = module._dump_manager.save_preview_from_pil(frame, category='mfd', fmt='png')
if saved: if saved:
logging.getLogger().info("VideoReceiverSFP: saved frame to %s", saved) logging.getLogger().info("VideoReceiverSFP: saved MFD preview to %s", saved)
else: else:
ts = time.strftime('%Y%m%d_%H%M%S') + (f"_{int(time.time() * 1000) % 1000:03d}") ts = time.strftime('%Y%m%d_%H%M%S') + (f"_{int(time.time() * 1000) % 1000:03d}")
dumps_dir = getattr(module, '_dumps_dir', os.path.join(os.getcwd(), 'dumps')) dumps_dir = getattr(module, '_dumps_dir', os.path.join(os.getcwd(), 'dumps'))
os.makedirs(dumps_dir, exist_ok=True) os.makedirs(dumps_dir, exist_ok=True)
pngpath = os.path.join(dumps_dir, f'VideoReceiverSFP_frame_{ts}.png') pngpath = os.path.join(dumps_dir, f'VideoReceiverSFP_mfd_{ts}.png')
frame.save(pngpath) frame.save(pngpath)
module._saved_pngs.append(pngpath) module._saved_pngs.append(pngpath)
while len(module._saved_pngs) > module._dump_keep: while len(module._saved_pngs) > module._dump_keep:
@ -189,7 +198,7 @@ def dispatch_frame(module: Any, frame: Any, leader: Optional[Any]) -> None:
except Exception: except Exception:
pass pass
except Exception: except Exception:
logging.getLogger().exception('VideoReceiverSFP: failed to save preview png') logging.getLogger().exception('VideoReceiverSFP: failed to save MFD preview png')
try: try:
cb(frame) cb(frame)
except Exception: except Exception:

View File

@ -123,6 +123,8 @@ class SfpConnectorModule:
self._sar_video_active = False self._sar_video_active = False
# desired video fps when starting writers (default 20) # desired video fps when starting writers (default 20)
self._video_fps = 20 self._video_fps = 20
# default SAR recording fps (respect arrival timing, lower default)
self._sar_video_fps = 1
try: try:
from .config import DUMP_KEEP_COUNT, DUMPS_DIR from .config import DUMP_KEEP_COUNT, DUMPS_DIR
self._dump_keep = int(DUMP_KEEP_COUNT) self._dump_keep = int(DUMP_KEEP_COUNT)
@ -166,6 +168,37 @@ class SfpConnectorModule:
logging.getLogger().info("VideoReceiverSFP: simulated frames received=%d", self._frames_received) logging.getLogger().info("VideoReceiverSFP: simulated frames received=%d", self._frames_received)
except Exception: except Exception:
pass pass
# In simulation mode we may not pass through the full dispatch
# pipeline that handles saving. If the UI requested preview
# saves via `_save_png`, perform a save here so users see the
# last MFD preview even when running simulated frames.
try:
if getattr(self, '_save_png', False):
try:
if getattr(self, '_dump_manager', None) is not None:
saved = self._dump_manager.save_preview_from_pil(frame, category='mfd', fmt='png')
if saved:
logging.getLogger().info("VideoReceiverSFP: saved simulated MFD preview %s", saved)
else:
ts = time.strftime('%Y%m%d_%H%M%S') + (f"_{int(time.time() * 1000) % 1000:03d}")
dumps_dir = getattr(self, '_dumps_dir', os.path.join(os.getcwd(), 'dumps'))
os.makedirs(dumps_dir, exist_ok=True)
pngpath = os.path.join(dumps_dir, f'VideoReceiverSFP_mfd_{ts}.png')
try:
frame.save(pngpath)
self._saved_pngs.append(pngpath)
except Exception:
pass
while len(self._saved_pngs) > self._dump_keep:
old = self._saved_pngs.popleft()
try:
pathlib.Path(old).unlink()
except Exception:
pass
except Exception:
logging.getLogger().exception('VideoReceiverSFP: failed saving simulated preview')
except Exception:
pass
for cb in list(self._callbacks): for cb in list(self._callbacks):
try: try:
cb(frame) cb(frame)
@ -873,67 +906,9 @@ class SfpConnectorModule:
if frame is None: if frame is None:
frame = payload frame = payload
# Save the first decoded frame to disk so user can verify output # First-frame preview file generation removed: do not save automatic
if (not getattr(self, "_first_frame_saved", False)) and Image is not None and hasattr(frame, "save"): # 'VideoReceiverSFP_first_frame.png' or related preview files unless
try: # explicitly requested by the user via UI controls.
dumps_dir = os.path.join(os.getcwd(), "dumps")
os.makedirs(dumps_dir, exist_ok=True)
path = os.path.join(dumps_dir, "VideoReceiverSFP_first_frame.png")
frame.save(path)
self._first_frame_saved = True
self._first_frame_path = path
try:
logging.getLogger().info("VideoReceiverSFP: saved first decoded frame to %s", path)
except Exception:
pass
except Exception:
try:
logging.getLogger().exception("VideoReceiverSFP: failed to save first decoded frame")
except Exception:
pass
# Additionally save an enhanced preview image to help Windows thumbnail/preview
try:
from PIL import ImageOps
import numpy as _np
try:
arr = _np.array(frame.convert('RGB'))
nonzero = _np.count_nonzero(arr)
pct_nonzero = float(nonzero) / arr.size if arr.size > 0 else 0.0
except Exception:
arr = None
pct_nonzero = 0.0
enhanced = None
# If image is very sparse (few non-zero pixels) scale non-zero values to full range
if arr is not None and pct_nonzero < 0.05:
try:
mask = _np.any(arr != 0, axis=2)
if _np.any(mask):
maxv = int(arr[mask].max())
if maxv > 0:
scale = 255.0 / float(maxv)
arr2 = _np.clip(arr.astype(_np.float32) * scale, 0, 255).astype(_np.uint8)
enhanced = Image.fromarray(arr2, mode='RGB')
except Exception:
enhanced = None
# Fallback to PIL autocontrast if not produced above
if enhanced is None:
try:
enhanced = ImageOps.autocontrast(frame)
except Exception:
enhanced = None
if enhanced is not None:
try:
preview_path = os.path.join(dumps_dir, "VideoReceiverSFP_first_frame_preview.png")
enhanced.save(preview_path)
logging.getLogger().info("VideoReceiverSFP: saved enhanced preview to %s", preview_path)
except Exception:
logging.getLogger().exception("VideoReceiverSFP: failed to save enhanced preview")
except Exception:
# Do not break the main flow if preview creation fails
pass
with self._frame_lock: with self._frame_lock:
self._last_frame = frame self._last_frame = frame
@ -1017,13 +992,14 @@ class SfpConnectorModule:
except Exception: except Exception:
pass pass
if w and h: if w and h:
started = self._dump_manager.start_video_record('sar', w, h, fps=getattr(self, '_video_fps', 20)) sar_fps = getattr(self, '_sar_video_fps', getattr(self, '_video_fps', 20))
if started: started = self._dump_manager.start_video_record('sar', w, h, fps=sar_fps)
self._sar_video_active = True if started:
try: self._sar_video_active = True
logging.getLogger().info("VideoReceiverSFP: started SAR video writer (%dx%d @ %s FPS)", w, h, getattr(self, '_video_fps', 20)) try:
except Exception: logging.getLogger().info("VideoReceiverSFP: started SAR video writer (%dx%d @ %s FPS)", w, h, sar_fps)
pass except Exception:
pass
except Exception: except Exception:
logging.getLogger().exception("VideoReceiverSFP: failed to start sar video writer") logging.getLogger().exception("VideoReceiverSFP: failed to start sar video writer")
# write frame if active # write frame if active
@ -1219,8 +1195,8 @@ class SfpConnectorModule:
try: try:
self._record_sar_video = bool(enabled) self._record_sar_video = bool(enabled)
if fps is not None: if fps is not None:
self._video_fps = int(fps) self._sar_video_fps = int(fps)
logging.getLogger().info("VideoReceiverSFP: record_sar_video set to %s (fps=%s)", self._record_sar_video, self._video_fps) logging.getLogger().info("VideoReceiverSFP: record_sar_video set to %s (fps=%s)", self._record_sar_video, getattr(self, '_sar_video_fps', getattr(self, '_video_fps', 20)))
if not self._record_sar_video and getattr(self, '_dump_manager', None) is not None: if not self._record_sar_video and getattr(self, '_dump_manager', None) is not None:
try: try:
self._dump_manager.stop_video_record('sar') self._dump_manager.stop_video_record('sar')
@ -1259,6 +1235,36 @@ class SfpConnectorModule:
except Exception: except Exception:
pass pass
# --- API: last saved file paths -------------------------------------
def get_last_saved_mfd(self) -> Optional[str]:
try:
if getattr(self, '_dump_manager', None) is not None:
return self._dump_manager.get_last_saved('mfd')
# fallback: check module-level saved lists
if getattr(self, '_saved_pngs', None):
return self._saved_pngs[-1] if len(self._saved_pngs) else None
except Exception:
pass
return None
def get_last_saved_sar(self) -> Optional[str]:
try:
if getattr(self, '_dump_manager', None) is not None:
return self._dump_manager.get_last_saved('sar')
if getattr(self, '_saved_sar_pngs', None):
return self._saved_sar_pngs[-1] if len(self._saved_sar_pngs) else None
except Exception:
pass
return None
def get_last_video_path(self, category: Optional[str] = None) -> Optional[str]:
try:
if getattr(self, '_dump_manager', None) is not None:
return self._dump_manager.get_last_video_path(category)
except Exception:
pass
return None
def update_mfd_lut(self, lut) -> None: def update_mfd_lut(self, lut) -> None:
"""Receive an MFD LUT (numpy array or similar) from the UI and store it. """Receive an MFD LUT (numpy array or similar) from the UI and store it.

View File

@ -12,6 +12,7 @@ import logging
import threading import threading
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from tkinter import messagebox
from .sfp_module import SfpConnectorModule from .sfp_module import SfpConnectorModule
@ -39,6 +40,10 @@ def run_orchestrator():
# Module Initialization # Module Initialization
module = SfpConnectorModule() module = SfpConnectorModule()
init_cfg = {"fps": args.fps} init_cfg = {"fps": args.fps}
# Ensure recording does not start automatically at app launch; recordings
# should only start after the user presses Start Recording.
init_cfg['record_mfd_video'] = False
init_cfg['record_sar_video'] = False
if args.normalize and args.normalize != 'none': if args.normalize and args.normalize != 'none':
init_cfg['normalize'] = args.normalize init_cfg['normalize'] = args.normalize
if args.image_type != 'ALL': if args.image_type != 'ALL':
@ -124,7 +129,119 @@ def run_orchestrator():
main_frame.columnconfigure(1, weight=1) main_frame.columnconfigure(1, weight=1)
main_frame.rowconfigure(1, weight=1) # The viewer row is the one that grows main_frame.rowconfigure(1, weight=1) # The viewer row is the one that grows
# --- Row 0b: Last saved paths (for copy/paste) ---
paths_frame = ttk.Frame(controls_box)
paths_frame.pack(fill='x', expand=False, pady=(6,0))
mfd_img_save_var = tk.BooleanVar(value=bool(getattr(module, '_save_png', False)))
mfd_img_cb = ttk.Checkbutton(paths_frame, text='Last MFD file image', variable=mfd_img_save_var)
mfd_img_cb.grid(row=0, column=0, sticky='w', padx=5)
last_mfd_var = tk.StringVar(value="")
last_mfd_entry = ttk.Entry(paths_frame, textvariable=last_mfd_var, width=80, state='readonly')
last_mfd_entry.grid(row=0, column=1, padx=5, sticky='w')
def _copy_mfd():
try:
root.clipboard_clear()
root.clipboard_append(last_mfd_var.get() or '')
except Exception:
pass
ttk.Button(paths_frame, text='Copy', command=_copy_mfd, width=6).grid(row=0, column=2, padx=4)
sar_img_save_var = tk.BooleanVar(value=bool(getattr(module, '_sar_save_png', False)))
sar_img_cb = ttk.Checkbutton(paths_frame, text='Last SAR file image', variable=sar_img_save_var)
sar_img_cb.grid(row=1, column=0, sticky='w', padx=5)
last_sar_var = tk.StringVar(value="")
last_sar_entry = ttk.Entry(paths_frame, textvariable=last_sar_var, width=80, state='readonly')
last_sar_entry.grid(row=1, column=1, padx=5, sticky='w')
def _copy_sar():
try:
root.clipboard_clear()
root.clipboard_append(last_sar_var.get() or '')
except Exception:
pass
ttk.Button(paths_frame, text='Copy', command=_copy_sar, width=6).grid(row=1, column=2, padx=4)
mfd_vid_save_var = tk.BooleanVar(value=bool(getattr(module, '_record_mfd_video', False)))
mfd_vid_cb = ttk.Checkbutton(paths_frame, text='Last MFD file video', variable=mfd_vid_save_var)
mfd_vid_cb.grid(row=2, column=0, sticky='w', padx=5)
last_mfd_vid_var = tk.StringVar(value="")
last_mfd_vid_entry = ttk.Entry(paths_frame, textvariable=last_mfd_vid_var, width=80, state='readonly')
last_mfd_vid_entry.grid(row=2, column=1, padx=5, sticky='w')
def _copy_mfd_vid():
try:
root.clipboard_clear()
root.clipboard_append(last_mfd_vid_var.get() or '')
except Exception:
pass
ttk.Button(paths_frame, text='Copy', command=_copy_mfd_vid, width=6).grid(row=2, column=2, padx=4)
sar_vid_save_var = tk.BooleanVar(value=bool(getattr(module, '_record_sar_video', False)))
sar_vid_cb = ttk.Checkbutton(paths_frame, text='Last SAR file video', variable=sar_vid_save_var)
sar_vid_cb.grid(row=3, column=0, sticky='w', padx=5)
last_sar_vid_var = tk.StringVar(value="")
last_sar_vid_entry = ttk.Entry(paths_frame, textvariable=last_sar_vid_var, width=80, state='readonly')
last_sar_vid_entry.grid(row=3, column=1, padx=5, sticky='w')
def _copy_sar_vid():
try:
root.clipboard_clear()
root.clipboard_append(last_sar_vid_var.get() or '')
except Exception:
pass
ttk.Button(paths_frame, text='Copy', command=_copy_sar_vid, width=6).grid(row=3, column=2, padx=4)
def _refresh_paths():
try:
m = module.get_last_saved_mfd()
s = module.get_last_saved_sar()
vm = module.get_last_video_path('mfd')
vs = module.get_last_video_path('sar')
try:
last_mfd_var.set(m or '')
except Exception:
pass
try:
last_sar_var.set(s or '')
except Exception:
pass
try:
last_mfd_vid_var.set(vm or '')
except Exception:
pass
try:
last_sar_vid_var.set(vs or '')
except Exception:
pass
except Exception:
pass
ttk.Button(paths_frame, text='Refresh Paths', command=_refresh_paths).grid(row=0, column=3, rowspan=4, padx=8)
# initial refresh
try:
_refresh_paths()
except Exception:
pass
# Periodic automatic refresh of last-saved paths (every 2 seconds)
def _periodic_refresh():
try:
_refresh_paths()
except Exception:
pass
try:
root.after(2000, _periodic_refresh)
except Exception:
pass
try:
root.after(2000, _periodic_refresh)
except Exception:
pass
# Callbacks for Parameters # Callbacks for Parameters
# Track whether the user has started a 'manual' recording session
user_recording_active_var = tk.BooleanVar(value=False)
def on_mfd_param_changed(param_type, name, value): def on_mfd_param_changed(param_type, name, value):
try: try:
if param_type == 'save_png': if param_type == 'save_png':
@ -204,24 +321,205 @@ def run_orchestrator():
# Button Actions # Button Actions
def on_start_recording(): def on_start_recording():
# Read desired targets from checkboxes and only apply when starting
save_mfd = bool(mfd_img_save_var.get())
save_sar = bool(sar_img_save_var.get())
rec_mfd = bool(mfd_vid_save_var.get())
rec_sar = bool(sar_vid_save_var.get())
# If nothing selected, inform the user and abort
if not (save_mfd or save_sar or rec_mfd or rec_sar):
try:
messagebox.showwarning("No selection", "Please select at least one save or record option before starting.")
except Exception:
pass
return
# Apply selections to module: image saving and video recording only while 'recording'
try:
module.set_save_png(save_mfd)
except Exception:
pass
try:
module.set_sar_save_png(save_sar)
except Exception:
pass
try:
module.set_record_mfd_video(rec_mfd)
except Exception:
pass
try:
module.set_record_sar_video(rec_sar)
except Exception:
pass
# Mark that user requested recording (even if only image saving)
try:
user_recording_active_var.set(True)
except Exception:
pass
# Lock checkboxes while recording/armed
try:
mfd_img_cb.config(state=tk.DISABLED)
except Exception:
pass
try:
sar_img_cb.config(state=tk.DISABLED)
except Exception:
pass
try:
mfd_vid_cb.config(state=tk.DISABLED)
except Exception:
pass
try:
sar_vid_cb.config(state=tk.DISABLED)
except Exception:
pass
start_rec_btn.config(state=tk.DISABLED) start_rec_btn.config(state=tk.DISABLED)
stop_rec_btn.config(state=tk.NORMAL) stop_rec_btn.config(state=tk.NORMAL)
rec_status_var.set("Recording...") rec_status_var.set("Recording...")
rec_status_label.config(foreground="red") rec_status_label.config(foreground="red")
module.set_record_mfd_video(True)
module.set_record_sar_video(True)
def on_stop_recording(): def on_stop_recording():
# Stop all recording/saving targets and unlock checkboxes
try:
module.set_record_mfd_video(False)
except Exception:
pass
try:
module.set_record_sar_video(False)
except Exception:
pass
try:
module.set_save_png(False)
except Exception:
pass
try:
module.set_sar_save_png(False)
except Exception:
pass
try:
mfd_img_cb.config(state=tk.NORMAL)
except Exception:
pass
try:
sar_img_cb.config(state=tk.NORMAL)
except Exception:
pass
try:
mfd_vid_cb.config(state=tk.NORMAL)
except Exception:
pass
try:
sar_vid_cb.config(state=tk.NORMAL)
except Exception:
pass
try:
user_recording_active_var.set(False)
except Exception:
pass
start_rec_btn.config(state=tk.NORMAL) start_rec_btn.config(state=tk.NORMAL)
stop_rec_btn.config(state=tk.DISABLED) stop_rec_btn.config(state=tk.DISABLED)
rec_status_var.set("Not Recording") rec_status_var.set("Not Recording")
rec_status_label.config(foreground="gray") rec_status_label.config(foreground="gray")
module.set_record_mfd_video(False)
module.set_record_sar_video(False)
start_rec_btn.config(command=on_start_recording) start_rec_btn.config(command=on_start_recording)
stop_rec_btn.config(command=on_stop_recording) stop_rec_btn.config(command=on_stop_recording)
# Periodically update recording status indicator based on module state
def _update_record_status():
try:
# consider user-initiated recording as 'armed' even if video flags are not set
manual = bool(user_recording_active_var.get())
armed = manual or bool(getattr(module, '_record_mfd_video', False) or getattr(module, '_record_sar_video', False))
active_mfd = bool(getattr(module, '_mfd_video_active', False))
active_sar = bool(getattr(module, '_sar_video_active', False))
if active_mfd or active_sar:
parts = []
if active_mfd:
parts.append('MFD')
if active_sar:
parts.append('SAR')
rec_status_var.set(f"Recording ({'/'.join(parts)})")
rec_status_label.config(foreground="red")
start_rec_btn.config(state=tk.DISABLED)
stop_rec_btn.config(state=tk.NORMAL)
try:
mfd_vid_cb.config(state=tk.DISABLED)
except Exception:
pass
try:
sar_vid_cb.config(state=tk.DISABLED)
except Exception:
pass
try:
mfd_img_cb.config(state=tk.DISABLED)
except Exception:
pass
try:
sar_img_cb.config(state=tk.DISABLED)
except Exception:
pass
elif armed:
rec_status_var.set("Armed (waiting for first frame)")
rec_status_label.config(foreground="orange")
start_rec_btn.config(state=tk.DISABLED)
stop_rec_btn.config(state=tk.NORMAL)
try:
mfd_vid_cb.config(state=tk.DISABLED)
except Exception:
pass
try:
sar_vid_cb.config(state=tk.DISABLED)
except Exception:
pass
try:
mfd_img_cb.config(state=tk.DISABLED)
except Exception:
pass
try:
sar_img_cb.config(state=tk.DISABLED)
except Exception:
pass
else:
rec_status_var.set("Not Recording")
rec_status_label.config(foreground="gray")
start_rec_btn.config(state=tk.NORMAL)
stop_rec_btn.config(state=tk.DISABLED)
try:
mfd_vid_cb.config(state=tk.NORMAL)
except Exception:
pass
try:
sar_vid_cb.config(state=tk.NORMAL)
except Exception:
pass
try:
mfd_img_cb.config(state=tk.NORMAL)
except Exception:
pass
try:
sar_img_cb.config(state=tk.NORMAL)
except Exception:
pass
except Exception:
pass
try:
root.after(500, _update_record_status)
except Exception:
pass
# kickoff periodic status updates
try:
root.after(500, _update_record_status)
except Exception:
pass
root.minsize(1000, 600) root.minsize(1000, 600)
logging.info("VideoReceiverSFP: GUI initialized with dynamic layout") logging.info("VideoReceiverSFP: GUI initialized with dynamic layout")

View File

@ -53,6 +53,7 @@ class SfpSarViewer:
self._brightness_var = tk.IntVar(value=0) self._brightness_var = tk.IntVar(value=0)
self._contrast_var = tk.IntVar(value=0) self._contrast_var = tk.IntVar(value=0)
self._save_png_var = tk.BooleanVar(value=False) self._save_png_var = tk.BooleanVar(value=False)
self._autocontrast = False
try: try:
self._context_menu = tk.Menu(self._root, tearoff=0) self._context_menu = tk.Menu(self._root, tearoff=0)
@ -108,10 +109,15 @@ class SfpSarViewer:
pass pass
def _on_autocontrast(self): def _on_autocontrast(self):
if self._on_sar_param_changed: # Toggle local autocontrast state and notify upstream
self._on_sar_param_changed("autocontrast", None, True) try:
with self._lock: with self._lock:
self._params_changed = True self._autocontrast = not self._autocontrast
self._params_changed = True
if self._on_sar_param_changed:
self._on_sar_param_changed("autocontrast", None, bool(self._autocontrast))
except Exception:
pass
def _on_save_current_image(self): def _on_save_current_image(self):
"""Save the current equalized SAR image to file.""" """Save the current equalized SAR image to file."""
@ -230,6 +236,12 @@ class SfpSarViewer:
if Image is None or ImageEnhance is None: if Image is None or ImageEnhance is None:
return img return img
try: try:
# Apply autocontrast first if enabled
try:
if self._autocontrast and ImageOps is not None:
img = ImageOps.autocontrast(img)
except Exception:
pass
with self._lock: with self._lock:
bf = max(0.0, 1.0 + (self._current_brightness / 100.0)) bf = max(0.0, 1.0 + (self._current_brightness / 100.0))
cf = max(0.0, 1.0 + (self._current_contrast / 100.0)) cf = max(0.0, 1.0 + (self._current_contrast / 100.0))

View File

@ -5,16 +5,16 @@
- [ ] VRSFP: aggiungere la possibilità di modificare la configurazione rutime, com e poter aggiungere la possibilità di salvare le immagini invece che il video o altro: creare una funzione setConfig che permetta di modificare runtime la configurazione del modulo. La stessa informazioni come formato deve essere la stessa che viene passata in fase di inizializzazione. Visto che la configurazione possa essere modificata anche in altre parti sarebbe utile anche a vere una funzione che restituisca il dizionario della configurazione come get_config(). - [ ] VRSFP: aggiungere la possibilità di modificare la configurazione rutime, com e poter aggiungere la possibilità di salvare le immagini invece che il video o altro: creare una funzione setConfig che permetta di modificare runtime la configurazione del modulo. La stessa informazioni come formato deve essere la stessa che viene passata in fase di inizializzazione. Visto che la configurazione possa essere modificata anche in altre parti sarebbe utile anche a vere una funzione che restituisca il dizionario della configurazione come get_config().
- [ ] VRSFP: poter aggiungere le funzioni di start salvataggio video e stop salvataggio video come api - [x] VRSFP: poter aggiungere le funzioni di start salvataggio video e stop salvataggio video come api
- [ ] VRSFP: poter impostare runtime il nome e la posizione del file immagine o video prodotto - [ ] VRSFP: poter impostare runtime il nome e la posizione del file immagine o video prodotto
- [ ] VRSFP: avere una funzione che restituisce nome e path dell'ultimo file salvato per mfd, sar e video - [x] VRSFP: avere una funzione che restituisce nome e path dell'ultimo file salvato per mfd, sar e video
- [x] VRSFP: aggiungere un log stile modulo aggiuntivo e collegarlo al modulo che poi sarà comune a tutti i moduli - [x] VRSFP: aggiungere un log stile modulo aggiuntivo e collegarlo al modulo che poi sarà comune a tutti i moduli
- [ ] VRSFP: aggiungere un pannello al test_orchestator dove poter aggiungere i controlli per testare le funzionalità di registrazione ondemand ecc - [x] VRSFP: aggiungere un pannello al test_orchestator dove poter aggiungere i controlli per testare le funzionalità di registrazione ondemand ecc
- [ ] VRSFP: poter impostare a priori la dimensione in pixel della finestra mfd e della finestra sar. Per la finestra mfd, se viene indicato "0, 0 " viene usata la dimensione dell'immagine che viene spedita. se è diversa da 0,0 viene ridmensionata la finestra. Invece per il sar la dimensione è sempre quella specificata e deve sempre essere diversa da 0,0. - [x] VRSFP: poter impostare a priori la dimensione in pixel della finestra mfd e della finestra sar. Per la finestra mfd, se viene indicato "0, 0 " viene usata la dimensione dell'immagine che viene spedita. se è diversa da 0,0 viene ridmensionata la finestra. Invece per il sar la dimensione è sempre quella specificata e deve sempre essere diversa da 0,0.
# FIXME List # FIXME List
- [ ] - [x] sistemare la funzione di autocostrans nella immagine sar.