Source code for catvision.spectral

"""Spectral sensitivity processing for cat vision simulation."""

import numpy as np
import cv2
from typing import Dict


[docs] class SpectralMixin: """ Mixin class for spectral sensitivity processing methods. Implements biologically accurate spectral sensitivity curves based on cat photoreceptor characteristics (S-cones, L-cones, and rods). """ def _init_spectral_curves(self) -> None: """ Initialize biological spectral sensitivity curves for cat photoreceptors. Based on published research on cat retinal photoreceptor spectral sensitivities: - S-cone peak: ~450nm (blue) - L-cone peak: ~556nm (green-yellow) - Rod peak: ~498nm (blue-green) """ # Wavelength range (nm) self.wavelengths = np.linspace(380, 700, 321) # Cat S-cone spectral sensitivity (peak ~450nm) s_sigma = 40 # nm bandwidth self.s_cone_sensitivity = np.exp( -0.5 * ((self.wavelengths - self.s_cone_peak) / s_sigma) ** 2 ) # Cat L-cone spectral sensitivity (peak ~556nm) l_sigma = 50 # nm bandwidth self.l_cone_sensitivity = np.exp( -0.5 * ((self.wavelengths - self.l_cone_peak) / l_sigma) ** 2 ) # Rod spectral sensitivity (peak ~498nm) rod_sigma = 45 # nm bandwidth self.rod_sensitivity = np.exp( -0.5 * ((self.wavelengths - self.rod_peak) / rod_sigma) ** 2 ) # Normalize curves self.s_cone_sensitivity /= np.max(self.s_cone_sensitivity) self.l_cone_sensitivity /= np.max(self.l_cone_sensitivity) self.rod_sensitivity /= np.max(self.rod_sensitivity) # RGB to wavelength mapping (approximate) self.rgb_wavelengths = {'red': 630, 'green': 530, 'blue': 470}
[docs] def apply_spectral_sensitivity_curves(self, image: np.ndarray) -> np.ndarray: """ Apply biological spectral sensitivity curves instead of simple RGB weights. This method maps RGB channels to the biological spectral sensitivity of cat photoreceptors, accounting for the rod-dominated vision (25:1 rod/cone ratio). Args: image: Input image in BGR format (OpenCV convention) Returns: Spectrally corrected image with cat-like color perception """ if len(image.shape) != 3: return image img_float = image.astype(np.float32) / 255.0 b, g, r = cv2.split(img_float) # Map RGB channels to spectral sensitivities # Blue channel (~470nm) blue_idx = np.argmin(np.abs(self.wavelengths - self.rgb_wavelengths['blue'])) s_response_blue = self.s_cone_sensitivity[blue_idx] l_response_blue = self.l_cone_sensitivity[blue_idx] rod_response_blue = self.rod_sensitivity[blue_idx] # Green channel (~530nm) green_idx = np.argmin(np.abs(self.wavelengths - self.rgb_wavelengths['green'])) s_response_green = self.s_cone_sensitivity[green_idx] l_response_green = self.l_cone_sensitivity[green_idx] rod_response_green = self.rod_sensitivity[green_idx] # Red channel (~630nm) red_idx = np.argmin(np.abs(self.wavelengths - self.rgb_wavelengths['red'])) s_response_red = self.s_cone_sensitivity[red_idx] l_response_red = self.l_cone_sensitivity[red_idx] rod_response_red = self.rod_sensitivity[red_idx] # Apply rod dominance (25:1 ratio) rod_weight = self.rod_cone_ratio / (self.rod_cone_ratio + 1) cone_weight = 1 / (self.rod_cone_ratio + 1) # Calculate weighted responses b_corrected = ( rod_weight * rod_response_blue + cone_weight * (s_response_blue + l_response_blue) ) * b g_corrected = ( rod_weight * rod_response_green + cone_weight * (s_response_green + l_response_green) ) * g r_corrected = ( rod_weight * rod_response_red + cone_weight * (s_response_red + l_response_red) ) * r # Normalize and clip corrected = cv2.merge([ np.clip(b_corrected, 0, 1), np.clip(g_corrected, 0, 1), np.clip(r_corrected, 0, 1) ]) return (corrected * 255).astype(np.uint8)
[docs] def adjust_color_sensitivity(self, image: np.ndarray) -> np.ndarray: """ Adjust color sensitivity to match cat vision (legacy method). Cats have peak sensitivity around 500nm (blue-green) and reduced red sensitivity compared to humans. This is a simplified legacy method for backward compatibility. Args: image: Input image in BGR format Returns: Color-adjusted image """ if len(image.shape) != 3: return image # Convert to float for processing img_float = image.astype(np.float32) / 255.0 # Split channels (BGR format) b, g, r = cv2.split(img_float) # Apply cat-specific color weights b_enhanced = np.clip(b * self.color_weights['blue'], 0, 1) g_enhanced = np.clip(g * self.color_weights['green'], 0, 1) r_reduced = np.clip(r * self.color_weights['red'], 0, 1) # Merge channels adjusted = cv2.merge([b_enhanced, g_enhanced, r_reduced]) # Convert back to uint8 return (adjusted * 255).astype(np.uint8)