team-project-gui / src /features.py
nikos99n's picture
copy from GH
fe66586
Raw
History Blame Contribute Delete
6.14 kB
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)