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)