Compare commits
4 Commits
v.0.0.0.26
...
SarRawData
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac6acc2246 | ||
|
|
ac1d9e23da | ||
|
|
bef6cf6f08 | ||
|
|
28f5fda397 |
@ -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
|
||||||
|
|||||||
@ -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}_'
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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")
|
||||||
|
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
10
todos.md
10
todos.md
@ -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.
|
||||||
Loading…
Reference in New Issue
Block a user