""" Minimal texture feature extraction. """ import numpy as np import torch import torch.nn.functional as F from skimage.feature import local_binary_pattern, hog from skimage import exposure from scipy import ndimage from typing import Dict, Tuple, Optional import logging logger = logging.getLogger(__name__) class TextureExtractor: """Minimal texture extraction (LBP, HOG, Lacunarity only).""" def __init__(self, lbp_points: int = 8, lbp_radius: int = 1, hog_orientations: int = 9, hog_pixels_per_cell: Tuple[int, int] = (8, 8), hog_cells_per_block: Tuple[int, int] = (2, 2), lacunarity_window: int = 15, ehd_threshold: float = 0.3, angle_resolution: int = 45): """Initialize with defaults.""" self.lbp_points = lbp_points self.lbp_radius = lbp_radius self.hog_orientations = hog_orientations self.hog_pixels_per_cell = hog_pixels_per_cell self.hog_cells_per_block = hog_cells_per_block self.lacunarity_window = lacunarity_window def extract_lbp(self, gray_image: np.ndarray) -> np.ndarray: """Extract Local Binary Pattern.""" try: lbp = local_binary_pattern(gray_image, self.lbp_points, self.lbp_radius, method='uniform') return self._convert_to_uint8(lbp) except Exception as e: logger.error(f"LBP failed: {e}") return np.zeros_like(gray_image, dtype=np.uint8) def extract_hog(self, gray_image: np.ndarray) -> np.ndarray: """Extract HOG features.""" try: _, vis = hog(gray_image, orientations=self.hog_orientations, pixels_per_cell=self.hog_pixels_per_cell, cells_per_block=self.hog_cells_per_block, visualize=True, feature_vector=True) return exposure.rescale_intensity(vis, out_range=(0, 255)).astype(np.uint8) except Exception as e: logger.error(f"HOG failed: {e}") return np.zeros_like(gray_image, dtype=np.uint8) def compute_local_lacunarity(self, gray_image: np.ndarray) -> np.ndarray: """Compute lacunarity.""" try: arr = gray_image.astype(np.float32) m1 = ndimage.uniform_filter(arr, size=self.lacunarity_window) m2 = ndimage.uniform_filter(arr * arr, size=self.lacunarity_window) var = m2 - m1 * m1 lac = var / (m1 * m1 + 1e-6) + 1 lac[m1 <= 1e-6] = 0 return self._convert_to_uint8(lac) except Exception as e: logger.error(f"Lacunarity failed: {e}") return np.zeros_like(gray_image, dtype=np.uint8) def extract_all_texture_features(self, gray_image: np.ndarray) -> Dict[str, np.ndarray]: """Extract LBP, HOG, and Lacunarity.""" return { 'lbp': self.extract_lbp(gray_image), 'hog': self.extract_hog(gray_image), 'lac2': self.compute_local_lacunarity(gray_image) } def _convert_to_uint8(self, arr: np.ndarray) -> np.ndarray: """Convert to uint8.""" arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0) ptp = np.ptp(arr) if ptp > 0: normalized = (arr - arr.min()) / (ptp + 1e-6) * 255 else: normalized = np.zeros_like(arr) return np.clip(normalized, 0, 255).astype(np.uint8) def compute_texture_statistics(self, features: Dict[str, np.ndarray], mask: Optional[np.ndarray] = None) -> Dict[str, Dict[str, float]]: """Compute basic statistics.""" stats = {} for feature_name, feature_data in features.items(): if mask is not None and mask.shape == feature_data.shape: masked_data = np.where(mask > 0, feature_data, np.nan) else: masked_data = feature_data valid_data = masked_data[~np.isnan(masked_data)] if len(valid_data) > 0: stats[feature_name] = { 'mean': float(np.mean(valid_data)), 'std': float(np.std(valid_data)), } return stats