Download features.py from Jennigwen/comvis: direct link, hf CLI and curl.
- Browser
- Download file 6.99 kB
-
https://huggingface.co/spaces/Jennigwen/comvis/resolve/main/features.py
- Command line
-
hf download hf://spaces/Jennigwen/comvis/features.py
-
curl -L -o features.py https://huggingface.co/spaces/Jennigwen/comvis/resolve/main/features.py
6.99 kB
| """ | |
| features.py β 120-dimensional handcrafted CV feature extraction. | |
| Pipeline (matches training notebook exactly): | |
| preprocess(img) β get_mask(img) β extract_features(img, mask) | |
| Feature vector layout: | |
| [0:24] GLCM texture (6 props Γ 4 angles) | |
| [24:50] LBP texture (26-bin uniform histogram, P=24 R=3) | |
| [50:74] Gabor texture (4 freqs Γ 3 orientations, mean+std) | |
| [74:106] Colour histogram (16H + 8S + 8V, normalised, within mask) | |
| [106:115] Colour moments (mean, std, skewness per HSV channel) | |
| [115:120] ABCD morphology (asymmetry, compactness, cvar, diam, elong) | |
| Total: 120 dimensions | |
| """ | |
| import numpy as np | |
| import cv2 | |
| from skimage.feature import graycomatrix, graycoprops, local_binary_pattern | |
| # ββ Preprocessing ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def preprocess(img_bgr: np.ndarray, size: tuple = (256, 256)) -> np.ndarray: | |
| """Hair removal (black-hat) β Gaussian blur β CLAHE β resize.""" | |
| gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) | |
| kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (17, 17)) | |
| bhat = cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, kernel) | |
| _, hmask = cv2.threshold(bhat, 10, 255, cv2.THRESH_BINARY) | |
| img_bgr = cv2.inpaint(img_bgr, hmask, 3, cv2.INPAINT_TELEA) | |
| img_bgr = cv2.GaussianBlur(img_bgr, (5, 5), 0) | |
| lab = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2LAB) | |
| clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) | |
| lab[:, :, 0] = clahe.apply(lab[:, :, 0]) | |
| img_bgr = cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) | |
| return cv2.resize(img_bgr, size, interpolation=cv2.INTER_AREA) | |
| def get_mask(img_bgr: np.ndarray) -> np.ndarray: | |
| """Otsu thresholding + morphological refinement to isolate lesion.""" | |
| gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) | |
| blur = cv2.GaussianBlur(gray, (7, 7), 0) | |
| _, mask = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) | |
| k = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) | |
| mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, k) | |
| mask = cv2.morphologyEx( | |
| mask, cv2.MORPH_OPEN, | |
| cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) | |
| ) | |
| return mask | |
| # ββ Feature groups βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def feat_glcm(gray: np.ndarray) -> np.ndarray: | |
| """24-dim GLCM at 4 angles Γ 6 properties.""" | |
| glcm = graycomatrix( | |
| gray, distances=[1], | |
| angles=[0, np.pi / 4, np.pi / 2, 3 * np.pi / 4], | |
| levels=256, symmetric=True, normed=True | |
| ) | |
| props = ["contrast", "correlation", "energy", "homogeneity", "dissimilarity", "ASM"] | |
| return np.concatenate([graycoprops(glcm, p).flatten() for p in props]) | |
| def feat_lbp(gray: np.ndarray) -> np.ndarray: | |
| """26-dim LBP histogram β rotation-invariant uniform, P=24 R=3.""" | |
| lbp = local_binary_pattern(gray, P=24, R=3, method="uniform") | |
| hist, _ = np.histogram(lbp.ravel(), bins=26, range=(0, 26), density=True) | |
| return hist.astype(np.float32) | |
| def feat_gabor(gray: np.ndarray) -> np.ndarray: | |
| """24-dim Gabor responses at 4 scales Γ 3 orientations (mean + std each).""" | |
| feats = [] | |
| for freq in [0.1, 0.2, 0.3, 0.4]: | |
| for theta in [0, np.pi / 3, 2 * np.pi / 3]: | |
| kernel = cv2.getGaborKernel( | |
| (21, 21), sigma=4.0, theta=theta, | |
| lambd=1.0 / freq, gamma=0.5, psi=0 | |
| ) | |
| resp = cv2.filter2D(gray.astype(np.float32), cv2.CV_32F, kernel) | |
| feats.extend([resp.mean(), resp.std()]) | |
| return np.array(feats, dtype=np.float32) | |
| def feat_colour(img_bgr: np.ndarray, mask: np.ndarray) -> np.ndarray: | |
| """32-dim normalised HSV histogram within lesion mask (16H + 8S + 8V).""" | |
| hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) | |
| m = (mask > 0).astype(np.uint8) * 255 | |
| h = cv2.calcHist([hsv], [0], m, [16], [0, 180]).flatten() | |
| s = cv2.calcHist([hsv], [1], m, [8], [0, 256]).flatten() | |
| v = cv2.calcHist([hsv], [2], m, [8], [0, 256]).flatten() | |
| norm = lambda x: x / (x.sum() + 1e-8) | |
| return np.concatenate([norm(h), norm(s), norm(v)]).astype(np.float32) | |
| def feat_colour_moments(img_bgr: np.ndarray, mask: np.ndarray) -> np.ndarray: | |
| """9-dim colour moments (mean, std, skewness) per HSV channel.""" | |
| hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV) | |
| feats = [] | |
| for ch in range(3): | |
| pixels = hsv[:, :, ch][mask > 0].astype(float) | |
| if pixels.size == 0: | |
| feats.extend([0.0, 0.0, 0.0]) | |
| continue | |
| mu = pixels.mean() | |
| sigma = pixels.std() + 1e-8 | |
| skew = float(np.mean(((pixels - mu) / sigma) ** 3)) | |
| feats.extend([mu, sigma, skew]) | |
| return np.array(feats, dtype=np.float32) | |
| def feat_abcd(mask: np.ndarray, hsv: np.ndarray) -> np.ndarray: | |
| """5-dim ABCD dermoscopy features (asymmetry, compactness, cvar, diam, elong).""" | |
| cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| if not cnts: | |
| return np.zeros(5, dtype=np.float32) | |
| cnt = max(cnts, key=cv2.contourArea) | |
| area = cv2.contourArea(cnt) + 1e-6 | |
| perim = cv2.arcLength(cnt, True) + 1e-6 | |
| if len(cnt) >= 5: | |
| axes_sorted = sorted(cv2.fitEllipse(cnt)[1]) | |
| asym = axes_sorted[0] / (axes_sorted[1] + 1e-6) | |
| else: | |
| asym = 1.0 | |
| comp = (4 * np.pi * area) / (perim ** 2) | |
| hue_pixels = hsv[:, :, 0][mask > 0] | |
| cvar = float(hue_pixels.std()) if hue_pixels.size else 0.0 | |
| diam = np.sqrt(4 * area / np.pi) | |
| elong = perim / (2 * np.sqrt(np.pi * area) + 1e-6) | |
| return np.array([asym, comp, cvar, diam, elong], dtype=np.float32) | |
| # ββ Full 120D extraction βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract_features(img_bgr: np.ndarray) -> np.ndarray: | |
| """ | |
| Full pipeline: preprocess β mask β 120D feature vector. | |
| Input : BGR image (any size) | |
| Output: float32 ndarray of shape (120,) | |
| """ | |
| proc = preprocess(img_bgr) # 256Γ256 BGR | |
| mask = get_mask(proc) # binary mask | |
| gray = cv2.cvtColor(proc, cv2.COLOR_BGR2GRAY) | |
| hsv = cv2.cvtColor(proc, cv2.COLOR_BGR2HSV) | |
| vec = np.concatenate([ | |
| feat_glcm(gray), # 24 | |
| feat_lbp(gray), # 26 | |
| feat_gabor(gray), # 24 | |
| feat_colour(proc, mask), # 32 | |
| feat_colour_moments(proc, mask), # 9 | |
| feat_abcd(mask, hsv), # 5 | |
| ]).astype(np.float32) | |
| assert vec.shape == (120,), f"Expected 120 dims, got {vec.shape}" | |
| return vec | |