| """ | |
| Hand-crafted feature extraction: LBP (texture) + GLCM (texture) + HSV histogram (color). | |
| Used by the texture branch and color branch of SpiceFusionNet. | |
| """ | |
| import numpy as np | |
| import cv2 | |
| try: | |
| from skimage.feature import local_binary_pattern, graycomatrix, graycoprops | |
| _SKIMAGE = True | |
| except ImportError: | |
| _SKIMAGE = False | |
| import config | |
| def _require_skimage(): | |
| if not _SKIMAGE: | |
| raise ImportError( | |
| "scikit-image is required for texture features.\n" | |
| "Install: pip install scikit-image" | |
| ) | |
| def extract_lbp(img_rgb: np.ndarray) -> np.ndarray: | |
| """Local Binary Pattern histogram — 10-d vector.""" | |
| _require_skimage() | |
| gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY) | |
| lbp = local_binary_pattern(gray, config.LBP_P, config.LBP_R, method="uniform") | |
| n_bins = config.LBP_P + 2 | |
| hist, _ = np.histogram(lbp.ravel(), bins=n_bins, range=(0, n_bins), density=True) | |
| return hist.astype(np.float32) | |
| def extract_glcm(img_rgb: np.ndarray) -> np.ndarray: | |
| """GLCM texture features — 48-d vector (6 props × 2 dist × 4 angles).""" | |
| _require_skimage() | |
| gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY) | |
| gray = (gray // (256 // config.GLCM_LEVELS)).astype(np.uint8) | |
| gray = np.clip(gray, 0, config.GLCM_LEVELS - 1) | |
| angles_rad = [np.deg2rad(a) for a in config.GLCM_ANGLES_DEG] | |
| glcm = graycomatrix( | |
| gray, | |
| distances=config.GLCM_DISTANCES, | |
| angles=angles_rad, | |
| levels=config.GLCM_LEVELS, | |
| symmetric=True, | |
| normed=True, | |
| ) | |
| props = ["contrast", "dissimilarity", "homogeneity", "energy", "correlation", "ASM"] | |
| feats = [] | |
| for p in props: | |
| feats.extend(graycoprops(glcm, p).ravel()) | |
| return np.array(feats, dtype=np.float32) | |
| def extract_texture(img_rgb: np.ndarray) -> np.ndarray: | |
| """Concatenate LBP + GLCM → 58-d texture vector.""" | |
| return np.concatenate([extract_lbp(img_rgb), extract_glcm(img_rgb)]) | |
| def extract_hsv(img_rgb: np.ndarray) -> np.ndarray: | |
| """HSV histogram — 100-d color vector (H=36 + S=32 + V=32).""" | |
| hsv = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2HSV) | |
| h = np.histogram(hsv[:, :, 0], bins=config.HSV_H_BINS, range=(0, 180), density=True)[0] | |
| s = np.histogram(hsv[:, :, 1], bins=config.HSV_S_BINS, range=(0, 256), density=True)[0] | |
| v = np.histogram(hsv[:, :, 2], bins=config.HSV_V_BINS, range=(0, 256), density=True)[0] | |
| return np.concatenate([h, s, v]).astype(np.float32) | |
| def extract_all(img_rgb: np.ndarray): | |
| """Returns (texture: 58-d, color: 100-d).""" | |
| return extract_texture(img_rgb), extract_hsv(img_rgb) | |