import cv2 import numpy as np def bgr_to_lab(image: np.ndarray) -> np.ndarray: """Convert BGR uint8 to float32 CIE LAB (L: 0-100, a/b: -128..127).""" lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB).astype(np.float32) lab[:, :, 0] *= 100.0 / 255.0 lab[:, :, 1:] -= 128.0 return lab def lab_to_bgr(lab: np.ndarray) -> np.ndarray: """Convert float32 CIE LAB back to BGR uint8.""" lab_cv = lab.copy() lab_cv[:, :, 0] = lab_cv[:, :, 0] * 255.0 / 100.0 lab_cv[:, :, 1:] += 128.0 return cv2.cvtColor(np.clip(lab_cv, 0, 255).astype(np.uint8), cv2.COLOR_LAB2BGR) def bgr_to_hsv(image: np.ndarray) -> np.ndarray: """Convert BGR uint8 to float32 HSV (H: 0-360, S/V: 0-100).""" hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV).astype(np.float32) hsv[:, :, 0] *= 2.0 # OpenCV H: 0-180 -> 0-360 hsv[:, :, 1:] /= 255.0 * 100 # S,V to percent return hsv def hsv_to_bgr(hsv: np.ndarray) -> np.ndarray: """Convert float32 HSV (H:0-360, S/V:0-100) back to BGR uint8.""" hsv_cv = hsv.copy() hsv_cv[:, :, 0] /= 2.0 hsv_cv[:, :, 1:] *= 255.0 / 100.0 return cv2.cvtColor(np.clip(hsv_cv, 0, 255).astype(np.uint8), cv2.COLOR_HSV2BGR) def histogram_match(source: np.ndarray, reference: np.ndarray) -> np.ndarray: """ Match the histogram of source image to reference image. Operates channel-by-channel on BGR images. """ matched = np.empty_like(source) for ch in range(source.shape[2]): src_ch = source[:, :, ch].ravel() ref_ch = reference[:, :, ch].ravel() src_hist, _ = np.histogram(src_ch, 256, [0, 256]) ref_hist, _ = np.histogram(ref_ch, 256, [0, 256]) src_cdf = np.cumsum(src_hist).astype(np.float64) ref_cdf = np.cumsum(ref_hist).astype(np.float64) src_cdf /= src_cdf[-1] ref_cdf /= ref_cdf[-1] lut = np.interp(src_cdf, ref_cdf, np.arange(256)) matched[:, :, ch] = lut[source[:, :, ch]] return matched.astype(np.uint8) def compute_mean_lab(image: np.ndarray, mask: np.ndarray | None = None) -> tuple: """Return mean L*, a*, b* for an image (optionally masked).""" lab = bgr_to_lab(image) if mask is not None: region = lab[mask > 0] else: region = lab.reshape(-1, 3) return tuple(region.mean(axis=0))