Source code for catvision.spatial

"""Spatial processing methods for cat vision simulation."""

import numpy as np
import cv2
from scipy.ndimage import gaussian_filter
from typing import Optional


[docs] class SpatialMixin: """ Mixin class for spatial processing methods. Implements cat-specific spatial characteristics including: - Vertical slit pupil (3:1 aspect ratio) - Reduced spatial acuity (1/6 human acuity) - Wide-angle field of view (200°×140°) - Center-surround acuity mapping """
[docs] def create_pupil_kernel(self, kernel_size: int = 15) -> np.ndarray: """ Create vertical slit pupil convolution kernel. Cats have a distinctive vertical slit pupil with a 3:1 aspect ratio, which affects light distribution and depth of field. Args: kernel_size: Size of the kernel (should be odd) Returns: Normalized convolution kernel representing cat pupil """ if kernel_size % 2 == 0: kernel_size += 1 # Ensure odd size # Create elliptical Gaussian with 3:1 aspect ratio (vertical slit) center = kernel_size // 2 y, x = np.ogrid[:kernel_size, :kernel_size] # Elliptical parameters sigma_x = kernel_size / 8.0 # Narrow horizontal spread sigma_y = sigma_x * self.pupil_aspect_ratio # Wider vertical spread # Create elliptical Gaussian kernel = np.exp( -((x - center)**2 / (2 * sigma_x**2) + (y - center)**2 / (2 * sigma_y**2)) ) # Normalize kernel kernel = kernel / np.sum(kernel) return kernel
[docs] def apply_pupil_filter(self, image: np.ndarray, kernel_size: int = 15) -> np.ndarray: """ Apply vertical slit pupil convolution filter. Args: image: Input image kernel_size: Size of pupil kernel Returns: Pupil-filtered image """ kernel = self.create_pupil_kernel(kernel_size) if len(image.shape) == 3: # Apply to each channel filtered_channels = [] for i in range(3): filtered_channel = cv2.filter2D(image[:, :, i], -1, kernel) filtered_channels.append(filtered_channel) filtered_image = cv2.merge(filtered_channels) else: filtered_image = cv2.filter2D(image, -1, kernel) return filtered_image
[docs] def simulate_spatial_acuity_reduction( self, image: np.ndarray, acuity_factor: Optional[float] = None ) -> np.ndarray: """ Simulate cat spatial acuity reduction using frequency domain filtering. Cats have approximately 1/6 the spatial acuity of humans, corresponding to visual acuity of about 20/100 to 20/200 in human terms. Args: image: Input image acuity_factor: Acuity reduction factor (default: 0.167 = 1/6 human acuity) Returns: Spatially filtered image """ if acuity_factor is None: acuity_factor = self.spatial_acuity_factor if len(image.shape) == 3: # Process each channel separately filtered_channels = [] for i in range(3): filtered_channel = self._apply_spatial_filter(image[:, :, i], acuity_factor) filtered_channels.append(filtered_channel) return cv2.merge(filtered_channels) else: return self._apply_spatial_filter(image, acuity_factor)
def _apply_spatial_filter(self, channel: np.ndarray, acuity_factor: float) -> np.ndarray: """ Apply spatial frequency filtering to simulate reduced acuity. Args: channel: Single channel image acuity_factor: Acuity reduction factor Returns: Filtered channel """ # Convert to frequency domain f_transform = np.fft.fft2(channel) f_shifted = np.fft.fftshift(f_transform) # Create frequency coordinates rows, cols = channel.shape crow, ccol = rows // 2, cols // 2 # Create distance matrix from center y, x = np.ogrid[:rows, :cols] distance = np.sqrt((x - ccol)**2 + (y - crow)**2) # Create low-pass filter based on acuity factor # Higher acuity_factor = more high frequencies preserved cutoff_freq = acuity_factor * min(rows, cols) / 4 filter_mask = np.exp(-(distance**2) / (2 * cutoff_freq**2)) # Apply filter f_filtered = f_shifted * filter_mask # Convert back to spatial domain f_ishifted = np.fft.ifftshift(f_filtered) filtered = np.fft.ifft2(f_ishifted) filtered = np.real(filtered) return np.clip(filtered, 0, 255).astype(np.uint8)
[docs] def apply_spatial_field_transformation( self, image: np.ndarray, fov_horizontal: int = 200, fov_vertical: int = 140 ) -> np.ndarray: """ Apply wide-angle peripheral vision transformation. Cats have a wider field of view than humans (200°×140° vs 180°×120°), with enhanced peripheral vision but reduced central acuity. Args: image: Input image fov_horizontal: Horizontal field of view in degrees (default: 200°) fov_vertical: Vertical field of view in degrees (default: 140°) Returns: Transformed image with cat-like field of view """ h, w = image.shape[:2] # Create coordinate grids y, x = np.mgrid[0:h, 0:w] # Normalize coordinates to [-1, 1] x_norm = (x - w/2) / (w/2) y_norm = (y - h/2) / (h/2) # Apply barrel distortion for wider field of view # Cat FOV is wider than human, so we need to compress the image fov_ratio_h = fov_horizontal / 180 # Human ~180° fov_ratio_v = fov_vertical / 120 # Human ~120° # Barrel distortion parameters k1 = 0.1 * (fov_ratio_h - 1) # Horizontal distortion k2 = 0.1 * (fov_ratio_v - 1) # Vertical distortion # Apply distortion r_squared = x_norm**2 + y_norm**2 x_distorted = x_norm * (1 + k1 * r_squared) y_distorted = y_norm * (1 + k2 * r_squared) # Convert back to image coordinates x_new = (x_distorted * w/2 + w/2).astype(np.float32) y_new = (y_distorted * h/2 + h/2).astype(np.float32) # Apply remapping transformed = cv2.remap( image, x_new, y_new, cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT ) # Apply center-surround acuity mapping center_x, center_y = w//2, h//2 max_distance = np.sqrt((w/2)**2 + (h/2)**2) # Create acuity mask (sharp center, blurred periphery) distance_from_center = np.sqrt((x - center_x)**2 + (y - center_y)**2) acuity_mask = np.exp(-(distance_from_center / max_distance)**2 * 3) # Apply variable blur based on distance from center if len(image.shape) == 3: for i in range(3): # Create peripheral blur blurred = gaussian_filter(transformed[:, :, i], sigma=3) # Blend based on acuity mask transformed[:, :, i] = ( acuity_mask * transformed[:, :, i] + (1 - acuity_mask) * blurred ).astype(np.uint8) else: blurred = gaussian_filter(transformed, sigma=3) transformed = ( acuity_mask * transformed + (1 - acuity_mask) * blurred ).astype(np.uint8) return transformed