Spaces:
Runtime error
Runtime error
File size: 6,138 Bytes
fe66586 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | import cv2
import numpy as np
from scipy.stats import skew
from . import config
def get_feature_names():
"""
Returns the list of feature names in the exact order they are extracted
by the pipeline. Used for Explainable AI plots.
"""
names = []
# 1. Color Stats (Mean, Std, Skew for B, G, R)
# OpenCV loads images as BGR
for c in ['Blue', 'Green', 'Red']:
names.extend([f'{c}_Mean', f'{c}_Std', f'{c}_Skew'])
# 2. Histogram (Bins for B, G, R)
for c in ['Blue', 'Green', 'Red']:
for i in range(config.HIST_BINS):
names.append(f'{c}_Hist_Bin_{i}')
# 3. Shape
names.extend(['Area', 'Perimeter', 'Compactness'])
# 4. Texture
names.append('Texture_EdgeDensity')
return names
def center_crop_and_resize(img, target_size=224):
"""
Take the largest possible center square from the image (no distortion)
Resize that square to (target_size x target_size)
"""
h, w = img.shape[:2]
# Determine the size of the largest possible center square
min_side = min(h, w)
# Starting points for center crop
start_x = (w - min_side) // 2
start_y = (h - min_side) // 2
# Perform center crop
img_cropped = img[start_y:start_y + min_side,
start_x:start_x + min_side]
# Resize the center crop to target_size x target_size
img_resized = cv2.resize(
img_cropped,
(target_size, target_size),
interpolation=cv2.INTER_AREA if min_side > target_size else cv2.INTER_CUBIC
)
return img_resized
def preprocess_image(img):
"""Standardizes, Grayscale, CLAHE, and Blur."""
if img is None: return None, None, None, None
img_resized = center_crop_and_resize(img, target_size=config.IMG_SIZE)
img_gray = cv2.cvtColor(img_resized, cv2.COLOR_BGR2GRAY)
clahe = cv2.createCLAHE(clipLimit=config.CLAHE_CLIP, tileGridSize=config.CLAHE_GRID)
img_eq = clahe.apply(img_gray)
img_blur = cv2.GaussianBlur(img_eq, config.BLUR_KERNEL, 0)
return img_resized, img_gray, img_eq, img_blur
def extract_color_stats(img, mask=None):
"""Calculates Mean, Std, Skew for R, G, B."""
stats = []
for i in range(3):
channel = img[:, :, i]
if mask is not None:
pixels = channel[mask > 0]
else:
pixels = channel.flatten()
if len(pixels) == 0:
stats.extend([0, 0, 0])
else:
stats.append(np.mean(pixels))
stats.append(np.std(pixels))
stats.append(skew(pixels))
return stats
def extract_histogram_features(img, mask=None):
"""Calculates Color Histogram for lesion area."""
hist_features = []
for i in range(3):
hist = cv2.calcHist([img], [i], mask, [config.HIST_BINS], [0, 256])
cv2.normalize(hist, hist)
hist_features.extend(hist.flatten())
return hist_features
def segment_lesion(img_blur):
"""Pipeline: Otsu Thresholding -> Open (Clean) -> Dilate (Connect)."""
_, mask_raw = cv2.threshold(img_blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
kernel_open = cv2.getStructuringElement(cv2.MORPH_RECT, config.MORPH_OPEN_KERNEL)
mask_clean = cv2.morphologyEx(mask_raw, cv2.MORPH_OPEN, kernel_open, iterations=2)
kernel_dilate = cv2.getStructuringElement(cv2.MORPH_RECT, config.MORPH_DILATE_KERNEL)
mask_connected = cv2.dilate(mask_clean, kernel_dilate, iterations=2)
return mask_raw, mask_clean, mask_connected
def isolate_largest_component(mask):
"""Filters all blobs except the largest one."""
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
final_mask = np.zeros_like(mask)
area, perimeter, compactness = 0, 0, 0
if contours:
sorted_contours = sorted(contours, key=cv2.contourArea, reverse=True)
cnt = sorted_contours[0]
area = cv2.contourArea(cnt)
img_area = mask.shape[0] * mask.shape[1]
if 50 < area < (img_area * 0.95):
cv2.drawContours(final_mask, [cnt], -1, 255, -1)
perimeter = cv2.arcLength(cnt, True)
if perimeter > 0:
compactness = (4 * np.pi * area) / (perimeter ** 2)
elif len(sorted_contours) > 1:
cnt2 = sorted_contours[1]
area2 = cv2.contourArea(cnt2)
if area2 > 50:
cv2.drawContours(final_mask, [cnt2], -1, 255, -1)
area = area2
perimeter = cv2.arcLength(cnt2, True)
if perimeter > 0:
compactness = (4 * np.pi * area) / (perimeter ** 2)
return final_mask, area, perimeter, compactness
def compute_texture_canny(img_gray, mask=None):
"""Calculates texture score using Canny Edge Detection."""
edges = cv2.Canny(img_gray, 100, 200)
if mask is not None:
lesion_edges = edges[mask > 0]
if len(lesion_edges) > 0:
texture_score = np.mean(lesion_edges)
else:
texture_score = 0
else:
texture_score = np.mean(edges)
edges_vis = edges.copy()
if mask is not None:
edges_vis = cv2.bitwise_and(edges_vis, edges_vis, mask=mask)
return texture_score, edges_vis
def extract_all_features_pipeline(image_path_or_array):
"""Master Orchestrator."""
if isinstance(image_path_or_array, str):
img = cv2.imread(image_path_or_array)
else:
img = image_path_or_array
if img is None: return None
# Preprocess
img_resized, img_gray, img_eq, img_blur = preprocess_image(img)
# Segmentation
_, _, mask_connected = segment_lesion(img_blur)
mask_final, area, perimeter, compactness = isolate_largest_component(mask_connected)
# Texture
texture_score, _ = compute_texture_canny(img_gray, mask=mask_final)
features = []
# Color Analysis
features.extend(extract_color_stats(img_resized, mask=mask_final))
features.extend(extract_histogram_features(img_resized, mask=mask_final))
# Shape
features.extend([area, perimeter, compactness])
features.append(texture_score)
return np.array(features) |