163 lines
6.1 KiB
Python
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
|