"""MFD parameters state management for VideoReceiverSFP. Self-contained state manager for MFD category parameters matching ControlPanel behavior. """ import logging import numpy as np from typing import Dict, Any class MfdState: """Manages MFD parameters (categories, intensities, colors, raw_map) and LUT generation.""" def __init__(self): self.mfd_params: Dict[str, Any] = self._initialize_mfd_params() self.mfd_lut: np.ndarray = np.zeros((256, 3), dtype=np.uint8) self._rebuild_lut() def _initialize_mfd_params(self) -> Dict[str, Any]: """Initialize MFD parameters matching ControlPanel defaults.""" params = { "categories": { "Occlusion": {"color": (0, 0, 0), "intensity": 255, "pixels": [0, 1]}, "Cat A": {"color": (255, 255, 255), "intensity": 255, "pixels": [2]}, "Cat B": {"color": (255, 255, 255), "intensity": 255, "pixels": [3, 18]}, "Cat C": {"color": (255, 255, 255), "intensity": 255, "pixels": [4, 5, 6, 16]}, "Cat C1": {"color": (255, 255, 255), "intensity": 255, "pixels": [7, 8, 9]}, "Cat C2": {"color": (255, 255, 255), "intensity": 255, "pixels": [10, 11, 12]}, "Cat C3": {"color": (255, 255, 255), "intensity": 255, "pixels": [13, 14, 15]}, "Reserved": {"color": (0, 0, 0), "intensity": 255, "pixels": list(range(17, 32))}, }, "raw_map_intensity": 255, } # Create reverse mapping pixel_to_category params["pixel_to_category"] = { p: cat for cat, data in params["categories"].items() for p in data["pixels"] } return params def update_category_intensity(self, category_name: str, intensity_value: int) -> bool: """Update intensity for a category and rebuild LUT. Returns True if updated.""" if category_name not in self.mfd_params["categories"]: logging.warning(f"[MfdState] Unknown category: {category_name}") return False clamped = int(np.clip(intensity_value, 0, 255)) self.mfd_params["categories"][category_name]["intensity"] = clamped self._rebuild_lut() return True def update_category_color(self, category_name: str, bgr_tuple: tuple) -> bool: """Update color (BGR) for a category and rebuild LUT. Returns True if updated.""" if category_name not in self.mfd_params["categories"]: logging.warning(f"[MfdState] Unknown category: {category_name}") return False # ensure BGR tuple with clamping new_bgr = tuple(int(np.clip(c, 0, 255)) for c in bgr_tuple) self.mfd_params["categories"][category_name]["color"] = new_bgr self._rebuild_lut() return True def update_raw_map_intensity(self, intensity_value: int) -> bool: """Update raw_map_intensity and rebuild LUT. Returns True if updated.""" clamped = int(np.clip(intensity_value, 0, 255)) self.mfd_params["raw_map_intensity"] = clamped self._rebuild_lut() return True def _rebuild_lut(self): """Rebuild the MFD LUT from current parameters (matches ControlPanel logic).""" try: params = self.mfd_params raw_map_factor = params["raw_map_intensity"] / 255.0 pixel_map = params["pixel_to_category"] categories = params["categories"] # Build in BGR order (ControlPanel convention) bgr_lut = np.zeros((256, 3), dtype=np.uint8) for idx in range(256): cat_name = pixel_map.get(idx) if cat_name: cat_data = categories[cat_name] bgr = cat_data["color"] intensity_factor = cat_data["intensity"] / 255.0 bgr_lut[idx, 0] = int(np.clip(int(round(float(bgr[0]) * intensity_factor)), 0, 255)) bgr_lut[idx, 1] = int(np.clip(int(round(float(bgr[1]) * intensity_factor)), 0, 255)) bgr_lut[idx, 2] = int(np.clip(int(round(float(bgr[2]) * intensity_factor)), 0, 255)) elif 32 <= idx <= 255: raw_intensity = (float(idx) - 32.0) * (255.0 / 223.0) final_gray = int(round(np.clip(raw_intensity * raw_map_factor, 0, 255))) bgr_lut[idx, :] = final_gray # Convert BGR -> RGB for PIL consumers self.mfd_lut = bgr_lut[:, ::-1].copy() except Exception: logging.exception("[MfdState] Error rebuilding LUT") self.mfd_lut = np.zeros((256, 3), dtype=np.uint8) def get_lut(self) -> np.ndarray: """Return current MFD LUT (256x3 RGB uint8).""" return self.mfd_lut