| """ | |
| 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 |