SXXXXXXX_ControlPanel/VideoReceiverSFP/core/image_processing.py
2026-01-13 14:21:21 +01:00

163 lines
6.1 KiB
Python

target_min: Minimum output value (inclusive).
target_max: Maximum output value (inclusive).
target_type: NumPy dtype for output array (default: np.uint8).
Returns:
Normalized NumPy array of dtype `target_type`, or None on error.
"""
"""Image processing helpers copied into VideoReceiverSFP for standalone
operation. Implements the same behavior used by ControlPanel so output
images are visually identical.
Provides:
- normalize_image: uses OpenCV `cv2.normalize` with dtype handling
- create_brightness_contrast_lut
- apply_brightness_contrast
- apply_color_palette: wraps `cv2.applyColorMap`
- resize_image: thin wrapper over `cv2.resize`
"""
from typing import Optional
import logging
import numpy as np
try:
import cv2
except Exception:
cv2 = None # type: ignore
def create_brightness_contrast_lut(brightness: int = 0, contrast: float = 1.0) -> np.ndarray:
"""Create a 256-element uint8 LUT equivalent to ControlPanel's.
Args:
brightness: int (-255..255)
contrast: float (>0)
Returns:
numpy.ndarray shape (256,) dtype uint8
"""
log_prefix = "[VRSFP B/C LUT]"
logging.getLogger().debug(f"{log_prefix} Creating B/C LUT B={brightness} C={contrast}")
contrast = max(0.01, float(contrast))
lut_values = np.arange(256, dtype=np.float32)
adjusted = (lut_values * contrast) + float(brightness)
lut = np.clip(np.round(adjusted), 0, 255).astype(np.uint8)
return lut
def apply_brightness_contrast(image: np.ndarray, brightness: int = 0, contrast: float = 1.0) -> np.ndarray:
"""Apply B/C using a LUT (expects uint8 input). Returns image or original on error."""
log_prefix = "[VRSFP B/C Apply]"
if image is None or not hasattr(image, 'dtype'):
logging.getLogger().warning(f"{log_prefix} invalid image")
return image
if image.dtype != np.uint8:
logging.getLogger().warning(f"{log_prefix} expected uint8, got {image.dtype}")
return image
try:
lut = create_brightness_contrast_lut(brightness, contrast)
if cv2 is not None:
return cv2.LUT(image, lut)
else:
return image
except Exception:
logging.getLogger().exception(f"{log_prefix} failed to apply B/C")
return image
def apply_color_palette(image: np.ndarray, palette: str) -> np.ndarray:
"""Apply an OpenCV colormap by name (returns BGR image).
palette should be names like 'JET', 'HOT', etc. Case-insensitive.
"""
log_prefix = "[VRSFP Palette]"
if image is None or cv2 is None:
logging.getLogger().warning(f"{log_prefix} cv2 not available or image is None")
return image
if image.dtype != np.uint8:
logging.getLogger().warning(f"{log_prefix} expected uint8 image for palette application")
try:
img_gray = image.astype(np.uint8)
except Exception:
return image
else:
img_gray = image
try:
colormap_name = f"COLORMAP_{palette.upper()}"
colormap = getattr(cv2, colormap_name)
colorized = cv2.applyColorMap(img_gray, colormap)
return colorized
except AttributeError:
logging.getLogger().error(f"{log_prefix} palette {palette} not found; returning BGR grayscale")
return cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)
except Exception:
logging.getLogger().exception(f"{log_prefix} error applying palette")
return cv2.cvtColor(img_gray, cv2.COLOR_GRAY2BGR)
def normalize_image(image: Optional[np.ndarray], target_min: int = 0, target_max: int = 255, target_type=np.uint8) -> Optional[np.ndarray]:
"""Normalize image using OpenCV to match ControlPanel behavior.
Uses cv2.normalize with dtype mapping and final cast to `target_type`.
"""
log_prefix = "[VRSFP Normalize]"
logging.getLogger().debug(f"{log_prefix} Normalizing image (dtype={getattr(image,'dtype',None)}) to {target_type}")
if image is None or getattr(image, 'size', 0) == 0:
logging.getLogger().warning(f"{log_prefix} called with None or empty image")
return None
try:
# decide cv dtype
if target_type == np.uint8:
cv_dtype = cv2.CV_8U if cv2 is not None else -1
elif target_type == np.uint16:
cv_dtype = cv2.CV_16U
elif target_type == np.int16:
cv_dtype = cv2.CV_16S
elif target_type == np.int32:
cv_dtype = cv2.CV_32S
elif target_type == np.float32:
cv_dtype = cv2.CV_32F
elif target_type == np.float64:
cv_dtype = cv2.CV_64F
else:
cv_dtype = -1
if cv2 is not None:
normalized = cv2.normalize(src=image, dst=None, alpha=target_min, beta=target_max, norm_type=cv2.NORM_MINMAX, dtype=cv_dtype)
else:
# fallback to simple numpy minmax
arr = np.asarray(image)
vmin = float(np.min(arr))
vmax = float(np.max(arr))
if vmin == vmax:
return np.full(arr.shape, target_min, dtype=target_type)
scale = (target_max - target_min) / (vmax - vmin)
out = (arr.astype(np.float64) - vmin) * scale + target_min
normalized = np.clip(out, target_min, target_max)
if normalized.dtype != target_type:
try:
normalized = normalized.astype(target_type)
except Exception:
logging.getLogger().exception(f"{log_prefix} failed casting to target_type")
return image
return normalized
except Exception:
logging.getLogger().exception(f"{log_prefix} unexpected error")
return image
def resize_image(image: np.ndarray, width: int, height: int, interpolation=None) -> Optional[np.ndarray]:
if interpolation is None:
interpolation = cv2.INTER_LINEAR if cv2 is not None else None
if image is None or width <= 0 or height <= 0:
return image
try:
if cv2 is not None:
return cv2.resize(image, (width, height), interpolation=interpolation)
else:
return image
except Exception:
logging.getLogger().exception('[VRSFP Resize] failed')
return None