nikos99n commited on
Commit
fe66586
·
1 Parent(s): 121d4e6

copy from GH

Browse files
README.md CHANGED
@@ -1,19 +1,29 @@
1
- ---
2
- title: Team Project Gui
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Streamlit template space
12
- ---
13
 
14
- # Welcome to Streamlit!
 
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
-
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # By Kolia Aimilia, Kontoudakis Nikos, Skiada Kyriaki, Lampropoulou Nancy
2
+ The project aims to classify lesions.
 
 
 
 
 
 
 
 
 
 
3
 
4
+ # Project Structure
5
+ The project follows a modular structure to separate configuration, data processing, computer vision logic, and model training.
6
 
7
+ ```text
8
+ ham10000_project/
9
+
10
+ ├── data/
11
+ │ ├── images/ # Original dermoscopy images (e.g., ISIC_0024306.jpg)
12
+ │ └── GroundTruth.csv # Metadata and labels
13
+
14
+ ├── models/ # Generated automatically during training
15
+ │ ├── skin_cancer_model.pkl # Trained Random Forest/SVM model
16
+ │ ├── scaler.pkl # StandardScaler for feature normalization
17
+ │ ├── classes.pkl # List of class names (MEL, NV, etc.)
18
+ │ └── comparison_results.png # Confusion matrix plot
19
+
20
+ ├── src/ # Core Logic Package
21
+ │ ├── __init__.py # Makes this folder a Python package
22
+ │ ├── config.py # Configuration, Constants, and Hyperparameters
23
+ │ ├── data.py # Data loading and stratified splitting logic
24
+ │ ├── features.py # Computer Vision pipeline (CLAHE, Otsu, Sobel, etc.)
25
+ │ └── model.py # Model training, evaluation, and saving
26
+
27
+ ├── train_main.py # Script 1: Main entry point to train the model
28
+ ├── app.py # Script 2: Streamlit Web Interface for inference
29
+ └── requirements.txt # Project dependencies
app.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import cv2
3
+ import numpy as np
4
+ import pandas as pd
5
+ import joblib
6
+ import os
7
+ import matplotlib.pyplot as plt
8
+ import lime
9
+ import lime.lime_tabular
10
+ from src import features, config
11
+
12
+ st.set_page_config(page_title="DermAI Classification", layout="centered")
13
+
14
+ # Custom styling to widen the center container
15
+ st.markdown(
16
+ """
17
+ <style>
18
+ .block-container {
19
+ max-width: 1000px;
20
+ padding-top: 2rem;
21
+ padding-bottom: 2rem;
22
+ }
23
+ </style>
24
+ """,
25
+ unsafe_allow_html=True
26
+ )
27
+
28
+ st.title("🔬 Skin Lesion Classification")
29
+ st.markdown("""
30
+ This system uses **Classical Machine Vision** techniques (CLAHE, Otsu Thresholding, Morphology)
31
+ to classify skin lesions.
32
+ """)
33
+
34
+ try:
35
+ model_path = os.path.join(config.MODEL_DIR, 'skin_cancer_model.pkl')
36
+ scaler_path = os.path.join(config.MODEL_DIR, 'scaler.pkl')
37
+ classes_path = os.path.join(config.MODEL_DIR, 'classes.pkl')
38
+
39
+ model = joblib.load(model_path)
40
+ scaler = joblib.load(scaler_path)
41
+ classes = joblib.load(classes_path)
42
+ st.success("System Ready: Model Loaded Successfully")
43
+ except FileNotFoundError:
44
+ st.error("Model files not found. Please run 'train_main.py' first.")
45
+ st.stop()
46
+
47
+ uploaded_file = st.file_uploader("Choose a dermoscopy image...", type=["jpg", "jpeg", "png"])
48
+
49
+ if uploaded_file is not None:
50
+ file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)
51
+ image = cv2.imdecode(file_bytes, 1)
52
+
53
+ col1, col2 = st.columns(2)
54
+ with col1:
55
+ st.image(image, channels="BGR", caption="Uploaded Image", use_container_width=True)
56
+
57
+ with st.spinner('Extracting Handcrafted Features...'):
58
+ feat_vector = features.extract_all_features_pipeline(image)
59
+
60
+ # Reshape for model input
61
+ feat_vector_reshaped = feat_vector.reshape(1, -1)
62
+ feat_scaled = scaler.transform(feat_vector_reshaped)
63
+
64
+ # Predict
65
+ probs = model.predict_proba(feat_scaled)
66
+ pred_idx = np.argmax(probs)
67
+ pred_label = classes[pred_idx]
68
+
69
+ with col2:
70
+ st.subheader(f"Prediction: **{pred_label}**")
71
+ st.metric("Confidence", f"{probs[0][pred_idx] * 100:.2f}%")
72
+
73
+ # --- Charts ---
74
+ st.subheader("Class Probabilities")
75
+ chart_data = pd.DataFrame({"Class": classes, "Probability": probs[0] * 100})
76
+ st.bar_chart(chart_data.set_index("Class"))
77
+
78
+ with st.expander("Abbreviation information"):
79
+ df_legend = pd.DataFrame(config.LEGEND_DATA)
80
+ st.dataframe(
81
+ df_legend,
82
+ column_config={
83
+ "More Info": st.column_config.LinkColumn(
84
+ "More",
85
+ help="Click to visit Wikipedia page",
86
+ display_text="🔍"
87
+ )
88
+ },
89
+ hide_index=True,
90
+ use_container_width=True
91
+ )
92
+
93
+ # --- LIME EXPLANATION (Local XAI) ---
94
+ st.divider()
95
+ st.subheader("Explainable AI (LIME)")
96
+ st.write(f"#### Why was this specific image classified as **{pred_label}**?")
97
+ st.write(
98
+ "The charts below show which features supported (Green) or contradicted (Red) the decision for **EACH** possible class.")
99
+
100
+ try:
101
+ # 1. Load the training sample (needed to initialize LIME)
102
+ train_sample_path = os.path.join(config.MODEL_DIR, 'X_train_sample.npy')
103
+ if os.path.exists(train_sample_path):
104
+ X_train_sample = np.load(train_sample_path)
105
+ feature_names = features.get_feature_names()
106
+
107
+ # Check for feature mismatch
108
+ if X_train_sample.shape[1] != len(feature_names):
109
+ st.warning(
110
+ f"Feature count mismatch (Model: {X_train_sample.shape[1]}, Code: {len(feature_names)}). Falling back to generic names.")
111
+ feature_names = [f"Feature_{i}" for i in range(X_train_sample.shape[1])]
112
+
113
+ # 2. Initialize Explainer
114
+ explainer = lime.lime_tabular.LimeTabularExplainer(
115
+ training_data=X_train_sample,
116
+ feature_names=feature_names,
117
+ class_names=classes,
118
+ mode='classification',
119
+ verbose=False
120
+ )
121
+
122
+ # 3. Explain this specific instance for ALL classes
123
+ # We pass labels=range(len(classes)) to calculate explanations for every class index
124
+ exp = explainer.explain_instance(
125
+ data_row=feat_scaled[0],
126
+ predict_fn=model.predict_proba,
127
+ num_features=10,
128
+ labels=range(len(classes))
129
+ )
130
+
131
+ # 4. Plot using Tabs
132
+ # Create a tab for each class so the user can switch between them
133
+ tabs = st.tabs(list(classes))
134
+
135
+ for i, class_name in enumerate(classes):
136
+ with tabs[i]:
137
+ st.write(f"**Evidence For/Against: {class_name}**")
138
+ # LIME uses the index (i) to retrieve the specific explanation
139
+ fig = exp.as_pyplot_figure(label=i)
140
+ st.pyplot(fig)
141
+
142
+ else:
143
+ st.warning("LIME initialization data (X_train_sample.npy) not found. Re-run training.")
144
+
145
+ except Exception as e:
146
+ st.error(f"Could not generate explanation: {type(e).__name__}: {e}")
147
+
148
+ # --- Pipeline Visualization ---
149
+ st.divider()
150
+ with st.expander("See Internal Logic (Computer Vision Pipeline Steps)", expanded=True):
151
+ st.info("Visualizing the exact steps performed by `src.features.py`")
152
+
153
+ img_resized, img_gray, img_eq, img_blur = features.preprocess_image(image)
154
+ mask_raw, mask_clean, mask_connected = features.segment_lesion(img_blur)
155
+ mask_final, _, _, _ = features.isolate_largest_component(mask_connected)
156
+ _, texture_vis = features.compute_texture_canny(img_gray, mask=mask_final)
157
+ img_lesion_only = cv2.bitwise_and(img_resized, img_resized, mask=mask_final)
158
+
159
+ # Row 1: Preprocessing
160
+ st.markdown("### Phase 1: Preprocessing")
161
+ c1, c2, c3, c4 = st.columns(4)
162
+ c1.image(img_resized, channels="BGR", caption="1. Resize")
163
+ c2.image(img_gray, caption="2. Grayscale")
164
+ c3.image(img_eq, caption="3. CLAHE (Smart Contrast)")
165
+ c4.image(img_blur, caption="4. Blur (Reduce Noise)")
166
+ st.divider()
167
+
168
+ # Row 2: Segmentation
169
+ st.markdown("### Phase 2: Segmentation")
170
+ c5, c6 = st.columns(2)
171
+ c5.image(mask_raw, caption="5. Otsu Threshold")
172
+ c6.image(mask_clean, caption="6. Morph Opening")
173
+ st.divider()
174
+
175
+ # Row 3: Connection & Selection
176
+ c7, c8 = st.columns(2)
177
+ c7.image(mask_connected, caption="7. Morph Dilation")
178
+ c8.image(mask_final, caption="8. Final Mask")
179
+ st.divider()
180
+
181
+ # Row 4: Analysis
182
+ st.markdown("### Phase 3: Analysis")
183
+ c9, c10 = st.columns(2)
184
+ c9.image(img_lesion_only, channels="BGR", caption="9. Masked Source")
185
+ c10.image(texture_vis, caption="10. Canny Edges (Masked)")
186
+
187
+ # Histogram
188
+ st.write("**11. Lesion Color Histogram**")
189
+ fig, ax = plt.subplots(figsize=(10, 3))
190
+ colors = ('b', 'g', 'r')
191
+ for i, color in enumerate(colors):
192
+ hist = cv2.calcHist([img_resized], [i], mask_final, [256], [0, 256])
193
+ ax.plot(hist, color=color)
194
+ ax.set_xlim([0, 256])
195
+ ax.set_title("Color Frequency")
196
+ st.pyplot(fig)
models/X_train_sample.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dacc23f02577d428bb68c9f15def2373ee2dfac9ed3e55f4f328add123f54384
3
+ size 11027608
models/classes.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:be4e4643483c57dcb12b3052e4f1bfc75ef9d81569c95216c93ad4047d0baafb
3
+ size 59
models/scaler.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:27ae467c5aa5c69269cb2c2524518b1985eda83b9bed02ebf0fd1ad16e61d8a2
3
+ size 1487
models/skin_cancer_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7867c457b12b6d092ccadcb34d0c6c2921620b2416f6f1e53b925a0ad24d700c
3
+ size 118655649
requirements.txt CHANGED
@@ -1,3 +1,8 @@
1
- altair
 
2
  pandas
3
- streamlit
 
 
 
 
 
1
+ opencv-python-headless
2
+ numpy
3
  pandas
4
+ scikit-learn
5
+ matplotlib
6
+ seaborn
7
+ streamlit
8
+ joblib
src/augmentation.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ from tensorflow.keras.preprocessing.image import ImageDataGenerator
4
+ from . import config
5
+
6
+ # Define the generator with your specific settings
7
+ # Note: We removed 'preprocessing_function' because our feature pipeline handles color/contrast.
8
+ # This generator focuses on GEOMETRIC variations.
9
+ datagen = ImageDataGenerator(
10
+ rotation_range=30,
11
+ width_shift_range=0.1,
12
+ height_shift_range=0.1,
13
+ shear_range=0.1,
14
+ zoom_range=0.2,
15
+ horizontal_flip=True,
16
+ fill_mode='nearest'
17
+ )
18
+
19
+
20
+ def get_augmentation_factor(class_name, class_counts, max_count):
21
+ """
22
+ Calculates how many augmented versions we need per image
23
+ to reach the majority class count.
24
+ """
25
+ current_count = class_counts.get(class_name, 0)
26
+ if current_count == 0: return 0
27
+
28
+ # Example: If Max=1000 and Current=100, factor is 10.
29
+ # We need 9 new images for every 1 original image.
30
+ factor = int(max_count / current_count)
31
+ return factor
32
+
33
+
34
+ def generate_augmented_images(img, count=1):
35
+ """
36
+ Takes an OpenCV image, converts to Keras format, generates 'count' variations,
37
+ and returns them as a list of OpenCV images.
38
+ """
39
+ if count <= 0: return []
40
+
41
+ # 1. Keras expects RGB, OpenCV is BGR. Convert for safety (though geometric ops don't care)
42
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
43
+
44
+ # 2. Keras expects 4D array (Batch Size, Height, Width, Channels)
45
+ img_expanded = np.expand_dims(img_rgb, 0)
46
+
47
+ augmented_images = []
48
+
49
+ # 3. Generate
50
+ # flow() generates batches indefinitely, so we loop 'count' times
51
+ i = 0
52
+ for batch in datagen.flow(img_expanded, batch_size=1):
53
+ # Retrieve the single image from batch
54
+ aug_img = batch[0].astype('uint8')
55
+
56
+ # Convert back to BGR for our feature pipeline
57
+ aug_img_bgr = cv2.cvtColor(aug_img, cv2.COLOR_RGB2BGR)
58
+
59
+ augmented_images.append(aug_img_bgr)
60
+ i += 1
61
+ if i >= count:
62
+ break
63
+
64
+ return augmented_images
src/config.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # src/config.py
2
+ import os
3
+
4
+ # Paths
5
+ base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
6
+ IMAGE_FOLDER = os.path.join(base_dir, 'data', 'images')
7
+ CSV_PATH = os.path.join(base_dir, 'data', 'GroundTruth.csv')
8
+ MODEL_DIR = os.path.join(base_dir, 'models')
9
+
10
+ # Constants
11
+ IMG_SIZE = 224
12
+ CLASSES = ['MEL', 'NV', 'BCC', 'AKIEC', 'BKL', 'DF', 'VASC']
13
+
14
+ # Hyperparameters
15
+ BLUR_KERNEL = (9, 9)
16
+ MORPH_OPEN_KERNEL = (5, 5) # Increase to remove more noise but risk losing information
17
+ MORPH_DILATE_KERNEL = (5, 5)
18
+
19
+ # CLAHE Settings (Smart Equalization)
20
+ CLAHE_CLIP = 2.0 # Threshold for contrast limiting
21
+ CLAHE_GRID = (8, 8) # Grid size for local equalization
22
+
23
+ # Histogram Config
24
+ HIST_BINS = 8
25
+
26
+ LEGEND_DATA = {
27
+ "Abbreviation": ["MEL", "NV", "BCC", "AKIEC", "BKL", "DF", "VASC"],
28
+ "Full Diagnosis": [
29
+ "Melanoma",
30
+ "Melanocytic nevus",
31
+ "Basal cell carcinoma",
32
+ "Actinic keratoses",
33
+ "Benign keratosis-like lesions",
34
+ "Dermatofibroma",
35
+ "Vascular lesions"
36
+ ],
37
+ "Description": [
38
+ "Malignant skin tumor (Cancerous).",
39
+ "Benign melanocytic proliferations (Moles).",
40
+ "Common variant of skin cancer (Cancerous).",
41
+ "Pre-cancerous skin lesions.",
42
+ "Non-cancerous skin growths (e.g., solar lentigines).",
43
+ "Benign skin lesion (nodules).",
44
+ "Benign blood vessel lesions."
45
+ ],
46
+ "More Info": [
47
+ "https://en.wikipedia.org/wiki/Melanoma",
48
+ "https://en.wikipedia.org/wiki/Melanocytic_nevus",
49
+ "https://en.wikipedia.org/wiki/Basal-cell_carcinoma",
50
+ "https://en.wikipedia.org/wiki/Actinic_keratosis",
51
+ "https://en.wikipedia.org/wiki/Seborrheic_keratosis",
52
+ "https://en.wikipedia.org/wiki/Dermatofibroma",
53
+ "https://en.wikipedia.org/wiki/Cherry_angioma"
54
+ ]
55
+ }
src/data.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import os
3
+ from sklearn.model_selection import train_test_split
4
+ from sklearn.utils import resample
5
+ from . import config
6
+
7
+
8
+ def balance_dataset(df):
9
+ """
10
+ Upsamples minority classes to match the count of the majority class.
11
+ This ensures the model sees an equal number of examples for each lesion type.
12
+ """
13
+ print("Balancing dataset (Upsampling)...")
14
+
15
+ # 1. Find the maximum class count
16
+ max_count = df['target'].value_counts().max()
17
+
18
+ balanced_dfs = []
19
+
20
+ # 2. Resample each class
21
+ for class_name in df['target'].unique():
22
+ class_subset = df[df['target'] == class_name]
23
+
24
+ # Upsample (replace=True means we duplicate rows)
25
+ df_resampled = resample(
26
+ class_subset,
27
+ replace=True,
28
+ n_samples=max_count,
29
+ random_state=42
30
+ )
31
+ balanced_dfs.append(df_resampled)
32
+
33
+ # 3. Combine back together
34
+ df_balanced = pd.concat(balanced_dfs)
35
+
36
+ # Shuffle the dataset so classes aren't grouped together
37
+ df_balanced = df_balanced.sample(frac=1, random_state=42).reset_index(drop=True)
38
+
39
+ print(f"Original size: {len(df)} -> Balanced size: {len(df_balanced)}")
40
+ return df_balanced
41
+
42
+
43
+ def load_metadata(limit=None, balance=True):
44
+ """
45
+ Loads CSV, parses classes, prepares file paths, and optionally balances data.
46
+ """
47
+ print("Loading Metadata...")
48
+ if not os.path.exists(config.CSV_PATH):
49
+ raise FileNotFoundError(f"CSV not found at {config.CSV_PATH}")
50
+
51
+ df = pd.read_csv(config.CSV_PATH)
52
+
53
+ # Validate Classes
54
+ classes = config.CLASSES
55
+ available = [c for c in classes if c in df.columns]
56
+
57
+ df['target'] = df[available].idxmax(axis=1)
58
+ df['label_idx'] = df['target'].apply(lambda x: available.index(x))
59
+ df['path'] = df['image'].apply(lambda x: os.path.join(config.IMAGE_FOLDER, x + '.jpg'))
60
+
61
+ # Apply limit first (if testing)
62
+ if limit:
63
+ print(f"Subsampling to {limit}...")
64
+ actual_limit = min(limit, len(df))
65
+ df, _ = train_test_split(df, train_size=actual_limit, stratify=df['label_idx'], random_state=42)
66
+
67
+ # Apply balancing (Upsampling)
68
+ # if balance:
69
+ # df = balance_dataset(df)
70
+
71
+ return df, available
src/explainability.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+ import pandas as pd
3
+ import numpy as np
4
+ import os
5
+ import lime
6
+ import lime.lime_tabular
7
+ from . import config
8
+
9
+
10
+ def plot_rf_feature_importance(model, feature_names):
11
+ """
12
+ Plots and saves the Feature Importance for a Random Forest model.
13
+ """
14
+ if not hasattr(model, 'feature_importances_'):
15
+ return None
16
+
17
+ importances = model.feature_importances_
18
+ indices = np.argsort(importances)[::-1]
19
+ sorted_names = [feature_names[i] for i in indices]
20
+
21
+ plt.figure(figsize=(14, 8))
22
+ plt.title("Random Forest: Feature Importance (Global XAI)")
23
+ plt.bar(range(len(importances)), importances[indices], align="center", color='teal')
24
+ plt.xticks(range(len(importances)), sorted_names, rotation=90)
25
+ plt.xlim([-1, len(importances)])
26
+ plt.ylabel("Relative Importance")
27
+ plt.tight_layout()
28
+
29
+ save_path = os.path.join(config.MODEL_DIR, 'rf_feature_importance.png')
30
+ plt.savefig(save_path)
31
+ plt.close()
32
+ print(f"Global XAI Plot saved to {save_path}")
33
+
34
+
35
+ def generate_lime_explanations(model, X_train, X_test, y_test, feature_names, class_names, model_name, num_samples=3):
36
+ """
37
+ Generates LIME (Local Interpretable Model-agnostic Explanations) for specific test instances.
38
+ This works for ANY model (RF, SVM, etc.).
39
+ """
40
+ print(f" Initializing LIME Explainer for {model_name}...")
41
+
42
+ # 1. Initialize Explainer
43
+ # LIME needs the training data to learn the distribution of features (mean, std, etc.)
44
+ explainer = lime.lime_tabular.LimeTabularExplainer(
45
+ training_data=np.array(X_train),
46
+ feature_names=feature_names,
47
+ class_names=class_names,
48
+ mode='classification',
49
+ verbose=False
50
+ )
51
+
52
+ # 2. Pick sample indices to explain
53
+ # We pick evenly spaced samples from the test set to get a variety
54
+ indices = np.linspace(0, len(X_test) - 1, num_samples, dtype=int)
55
+
56
+ output_dir = os.path.join(config.MODEL_DIR, 'lime_explanations')
57
+ os.makedirs(output_dir, exist_ok=True)
58
+
59
+ for i in indices:
60
+ # 3. Generate Explanation
61
+ # LIME perturbs this specific instance and sees how the model's prediction changes
62
+ exp = explainer.explain_instance(
63
+ data_row=X_test[i],
64
+ predict_fn=model.predict_proba,
65
+ num_features=10,
66
+ top_labels=1
67
+ )
68
+
69
+ # 4. Save Plot
70
+ # We title it with the True Label for context
71
+ true_label = class_names[y_test[i]]
72
+ fig = exp.as_pyplot_figure()
73
+ plt.title(f"LIME ({model_name}): Test Instance {i} | True Label: {true_label}")
74
+ plt.tight_layout()
75
+
76
+ save_path = os.path.join(output_dir, f'{model_name}_inst_{i}_lime.png')
77
+ plt.savefig(save_path)
78
+ plt.close()
79
+
80
+ print(f" LIME explanations saved to {output_dir}")
src/features.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from scipy.stats import skew
4
+ from . import config
5
+
6
+
7
+ def get_feature_names():
8
+ """
9
+ Returns the list of feature names in the exact order they are extracted
10
+ by the pipeline. Used for Explainable AI plots.
11
+ """
12
+ names = []
13
+
14
+ # 1. Color Stats (Mean, Std, Skew for B, G, R)
15
+ # OpenCV loads images as BGR
16
+ for c in ['Blue', 'Green', 'Red']:
17
+ names.extend([f'{c}_Mean', f'{c}_Std', f'{c}_Skew'])
18
+
19
+ # 2. Histogram (Bins for B, G, R)
20
+ for c in ['Blue', 'Green', 'Red']:
21
+ for i in range(config.HIST_BINS):
22
+ names.append(f'{c}_Hist_Bin_{i}')
23
+
24
+ # 3. Shape
25
+ names.extend(['Area', 'Perimeter', 'Compactness'])
26
+
27
+ # 4. Texture
28
+ names.append('Texture_EdgeDensity')
29
+
30
+ return names
31
+
32
+ def center_crop_and_resize(img, target_size=224):
33
+ """
34
+ Take the largest possible center square from the image (no distortion)
35
+ Resize that square to (target_size x target_size)
36
+ """
37
+
38
+ h, w = img.shape[:2]
39
+
40
+ # Determine the size of the largest possible center square
41
+ min_side = min(h, w)
42
+
43
+ # Starting points for center crop
44
+ start_x = (w - min_side) // 2
45
+ start_y = (h - min_side) // 2
46
+
47
+ # Perform center crop
48
+ img_cropped = img[start_y:start_y + min_side,
49
+ start_x:start_x + min_side]
50
+
51
+ # Resize the center crop to target_size x target_size
52
+ img_resized = cv2.resize(
53
+ img_cropped,
54
+ (target_size, target_size),
55
+ interpolation=cv2.INTER_AREA if min_side > target_size else cv2.INTER_CUBIC
56
+ )
57
+
58
+ return img_resized
59
+
60
+
61
+ def preprocess_image(img):
62
+ """Standardizes, Grayscale, CLAHE, and Blur."""
63
+ if img is None: return None, None, None, None
64
+
65
+ img_resized = center_crop_and_resize(img, target_size=config.IMG_SIZE)
66
+ img_gray = cv2.cvtColor(img_resized, cv2.COLOR_BGR2GRAY)
67
+
68
+ clahe = cv2.createCLAHE(clipLimit=config.CLAHE_CLIP, tileGridSize=config.CLAHE_GRID)
69
+ img_eq = clahe.apply(img_gray)
70
+
71
+ img_blur = cv2.GaussianBlur(img_eq, config.BLUR_KERNEL, 0)
72
+
73
+ return img_resized, img_gray, img_eq, img_blur
74
+
75
+
76
+ def extract_color_stats(img, mask=None):
77
+ """Calculates Mean, Std, Skew for R, G, B."""
78
+ stats = []
79
+ for i in range(3):
80
+ channel = img[:, :, i]
81
+ if mask is not None:
82
+ pixels = channel[mask > 0]
83
+ else:
84
+ pixels = channel.flatten()
85
+
86
+ if len(pixels) == 0:
87
+ stats.extend([0, 0, 0])
88
+ else:
89
+ stats.append(np.mean(pixels))
90
+ stats.append(np.std(pixels))
91
+ stats.append(skew(pixels))
92
+ return stats
93
+
94
+
95
+ def extract_histogram_features(img, mask=None):
96
+ """Calculates Color Histogram for lesion area."""
97
+ hist_features = []
98
+ for i in range(3):
99
+ hist = cv2.calcHist([img], [i], mask, [config.HIST_BINS], [0, 256])
100
+ cv2.normalize(hist, hist)
101
+ hist_features.extend(hist.flatten())
102
+ return hist_features
103
+
104
+
105
+ def segment_lesion(img_blur):
106
+ """Pipeline: Otsu Thresholding -> Open (Clean) -> Dilate (Connect)."""
107
+ _, mask_raw = cv2.threshold(img_blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
108
+
109
+ kernel_open = cv2.getStructuringElement(cv2.MORPH_RECT, config.MORPH_OPEN_KERNEL)
110
+ mask_clean = cv2.morphologyEx(mask_raw, cv2.MORPH_OPEN, kernel_open, iterations=2)
111
+
112
+ kernel_dilate = cv2.getStructuringElement(cv2.MORPH_RECT, config.MORPH_DILATE_KERNEL)
113
+ mask_connected = cv2.dilate(mask_clean, kernel_dilate, iterations=2)
114
+
115
+ return mask_raw, mask_clean, mask_connected
116
+
117
+
118
+ def isolate_largest_component(mask):
119
+ """Filters all blobs except the largest one."""
120
+ contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
121
+ final_mask = np.zeros_like(mask)
122
+ area, perimeter, compactness = 0, 0, 0
123
+
124
+ if contours:
125
+ sorted_contours = sorted(contours, key=cv2.contourArea, reverse=True)
126
+ cnt = sorted_contours[0]
127
+ area = cv2.contourArea(cnt)
128
+
129
+ img_area = mask.shape[0] * mask.shape[1]
130
+
131
+ if 50 < area < (img_area * 0.95):
132
+ cv2.drawContours(final_mask, [cnt], -1, 255, -1)
133
+ perimeter = cv2.arcLength(cnt, True)
134
+ if perimeter > 0:
135
+ compactness = (4 * np.pi * area) / (perimeter ** 2)
136
+ elif len(sorted_contours) > 1:
137
+ cnt2 = sorted_contours[1]
138
+ area2 = cv2.contourArea(cnt2)
139
+ if area2 > 50:
140
+ cv2.drawContours(final_mask, [cnt2], -1, 255, -1)
141
+ area = area2
142
+ perimeter = cv2.arcLength(cnt2, True)
143
+ if perimeter > 0:
144
+ compactness = (4 * np.pi * area) / (perimeter ** 2)
145
+
146
+ return final_mask, area, perimeter, compactness
147
+
148
+
149
+ def compute_texture_canny(img_gray, mask=None):
150
+ """Calculates texture score using Canny Edge Detection."""
151
+ edges = cv2.Canny(img_gray, 100, 200)
152
+
153
+ if mask is not None:
154
+ lesion_edges = edges[mask > 0]
155
+ if len(lesion_edges) > 0:
156
+ texture_score = np.mean(lesion_edges)
157
+ else:
158
+ texture_score = 0
159
+ else:
160
+ texture_score = np.mean(edges)
161
+
162
+ edges_vis = edges.copy()
163
+ if mask is not None:
164
+ edges_vis = cv2.bitwise_and(edges_vis, edges_vis, mask=mask)
165
+
166
+ return texture_score, edges_vis
167
+
168
+
169
+ def extract_all_features_pipeline(image_path_or_array):
170
+ """Master Orchestrator."""
171
+ if isinstance(image_path_or_array, str):
172
+ img = cv2.imread(image_path_or_array)
173
+ else:
174
+ img = image_path_or_array
175
+
176
+ if img is None: return None
177
+
178
+ # Preprocess
179
+ img_resized, img_gray, img_eq, img_blur = preprocess_image(img)
180
+
181
+ # Segmentation
182
+ _, _, mask_connected = segment_lesion(img_blur)
183
+ mask_final, area, perimeter, compactness = isolate_largest_component(mask_connected)
184
+
185
+ # Texture
186
+ texture_score, _ = compute_texture_canny(img_gray, mask=mask_final)
187
+
188
+ features = []
189
+ # Color Analysis
190
+ features.extend(extract_color_stats(img_resized, mask=mask_final))
191
+ features.extend(extract_histogram_features(img_resized, mask=mask_final))
192
+ # Shape
193
+ features.extend([area, perimeter, compactness])
194
+ features.append(texture_score)
195
+
196
+ return np.array(features)
src/model.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import joblib
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import seaborn as sns
6
+ from sklearn.ensemble import RandomForestClassifier
7
+ from sklearn.svm import SVC
8
+ from sklearn.preprocessing import StandardScaler
9
+ from sklearn.metrics import accuracy_score
10
+ from . import config, plots, explainability, features
11
+
12
+
13
+ def train_and_evaluate_split(X_train, y_train, X_test, y_test, classes):
14
+ """
15
+ Accepts PRE-SPLIT and PRE-AUGMENTED data.
16
+ Trains models, generates plots, and saves artifacts.
17
+ """
18
+
19
+ # 1. Define Models
20
+ techniques = {
21
+ "RF": RandomForestClassifier(n_estimators=100, class_weight='balanced', random_state=42),
22
+ "SVM": SVC(probability=True, class_weight='balanced', random_state=42)
23
+ }
24
+
25
+ best_score = 0
26
+ best_model = None
27
+ best_scaler = None
28
+ best_name = ""
29
+
30
+ os.makedirs(config.MODEL_DIR, exist_ok=True)
31
+
32
+ # 2. Prepare Feature Names for XAI
33
+ # We fetch these once so we can use them for LIME (all models) and RF Importance
34
+ feature_names = features.get_feature_names()
35
+ # Safety fallback if feature count mismatches name list
36
+ if len(feature_names) != X_train.shape[1]:
37
+ print(f"Warning: Feature names count ({len(feature_names)}) != Data columns ({X_train.shape[1]})")
38
+ feature_names = [f"Feature_{i}" for i in range(X_train.shape[1])]
39
+
40
+ # 3. Scaling
41
+ # Important: Fit on Train, Transform Test
42
+ scaler = StandardScaler()
43
+ X_train_s = scaler.fit_transform(X_train)
44
+ X_test_s = scaler.transform(X_test)
45
+
46
+ # --- NEW: Save Training Sample for LIME in App ---
47
+ # We save a subset (e.g., 500 samples) to keep the app lightweight and fast.
48
+ # LIME needs this to understand the "background" distribution of features.
49
+ print("Saving training sample for App LIME initialization...")
50
+ # if X_train_s.shape[0] > 500:
51
+ # indices = np.random.choice(X_train_s.shape[0], 500, replace=False)
52
+ # X_sample = X_train_s[indices]
53
+ # else:
54
+ # keep all results for XAI
55
+ X_sample = X_train_s
56
+ np.save(os.path.join(config.MODEL_DIR, 'X_train_sample.npy'), X_sample)
57
+ # -------------------------------------------------
58
+
59
+ # 4. Training Loop
60
+ for name, model in techniques.items():
61
+ print(f"\n--- Training {name} ---")
62
+ model.fit(X_train_s, y_train)
63
+ preds = model.predict(X_test_s)
64
+ acc = accuracy_score(y_test, preds)
65
+
66
+ print(f"--> {name} Accuracy on Test Set: {acc:.4f}")
67
+
68
+ # --- PLOTTING METRICS ---
69
+ print(f"Generating ROC and Confusion Matrix for {name}...")
70
+ plots.plot_confusion_matrix(y_test, preds, classes, name)
71
+ plots.plot_multiclass_roc(model, X_test_s, y_test, classes, name)
72
+ plots.save_classification_report(y_test, preds, classes, name)
73
+
74
+ # --- EXPLAINABLE AI (Global: Feature Importance) ---
75
+ if name == "RF":
76
+ print("Generating Global Feature Importance Plot (RF)...")
77
+ explainability.plot_rf_feature_importance(model, feature_names)
78
+
79
+ # --- EXPLAINABLE AI (Local: LIME) ---
80
+ # This works for BOTH RF and SVM
81
+ print(f"Generating Local LIME Explanations for {name}...")
82
+ explainability.generate_lime_explanations(
83
+ model=model,
84
+ X_train=X_train_s, # LIME needs training distribution
85
+ X_test=X_test_s, # Instances to explain
86
+ y_test=y_test, # For labeling plots
87
+ feature_names=feature_names,
88
+ class_names=classes,
89
+ model_name=name
90
+ )
91
+
92
+ # Track Best
93
+ if acc > best_score:
94
+ best_score = acc
95
+ best_model = model
96
+ best_name = name
97
+ best_scaler = scaler
98
+
99
+ # Save Artifacts
100
+ print(f"\nSaving Best Model: {best_name}")
101
+ joblib.dump(best_model, os.path.join(config.MODEL_DIR, 'skin_cancer_model.pkl'))
102
+ joblib.dump(best_scaler, os.path.join(config.MODEL_DIR, 'scaler.pkl'))
103
+ joblib.dump(classes, os.path.join(config.MODEL_DIR, 'classes.pkl'))
src/plots.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import matplotlib.pyplot as plt
2
+ import seaborn as sns
3
+ import numpy as np
4
+ import os
5
+ from sklearn.metrics import confusion_matrix, roc_curve, auc, classification_report
6
+ from sklearn.preprocessing import label_binarize
7
+ from . import config
8
+
9
+
10
+ def plot_confusion_matrix(y_true, y_pred, classes, model_name):
11
+ """Generates and saves a confusion matrix heatmap."""
12
+ cm = confusion_matrix(y_true, y_pred)
13
+
14
+ plt.figure(figsize=(10, 8))
15
+ sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
16
+ xticklabels=classes, yticklabels=classes)
17
+ plt.title(f"{model_name} Confusion Matrix")
18
+ plt.ylabel('True Label')
19
+ plt.xlabel('Predicted Label')
20
+ plt.tight_layout()
21
+
22
+ filename = f"{model_name.lower()}_confusion_matrix.png"
23
+ plt.savefig(os.path.join(config.MODEL_DIR, filename))
24
+ plt.close()
25
+
26
+
27
+ def plot_multiclass_roc(model, X_test, y_test, classes, model_name):
28
+ """Generates and saves a Multi-class ROC Curve."""
29
+ # 1. Binarize labels (One-vs-Rest)
30
+ y_test_bin = label_binarize(y_test, classes=range(len(classes)))
31
+ n_classes = y_test_bin.shape[1]
32
+
33
+ # 2. Get probabilities
34
+ if hasattr(model, "predict_proba"):
35
+ y_score = model.predict_proba(X_test)
36
+ else:
37
+ print(f"{model_name} does not support probability prediction. Skipping ROC.")
38
+ return
39
+
40
+ # 3. Compute ROC curve and ROC area for each class
41
+ fpr = dict()
42
+ tpr = dict()
43
+ roc_auc = dict()
44
+
45
+ for i in range(n_classes):
46
+ fpr[i], tpr[i], _ = roc_curve(y_test_bin[:, i], y_score[:, i])
47
+ roc_auc[i] = auc(fpr[i], tpr[i])
48
+
49
+ # 4. Plot
50
+ plt.figure(figsize=(10, 8))
51
+ colors = plt.cm.rainbow(np.linspace(0, 1, n_classes))
52
+
53
+ for i, color in zip(range(n_classes), colors):
54
+ plt.plot(fpr[i], tpr[i], color=color, lw=2,
55
+ label=f'{classes[i]} (AUC = {roc_auc[i]:.2f})')
56
+
57
+ plt.plot([0, 1], [0, 1], 'k--', lw=2) # Diagonal line
58
+ plt.xlim([0.0, 1.0])
59
+ plt.ylim([0.0, 1.05])
60
+ plt.xlabel('False Positive Rate')
61
+ plt.ylabel('True Positive Rate')
62
+ plt.title(f'{model_name} Multi-class ROC Curve')
63
+ plt.legend(loc="lower right")
64
+ plt.tight_layout()
65
+
66
+ filename = f"{model_name.lower()}_roc_curve.png"
67
+ plt.savefig(os.path.join(config.MODEL_DIR, filename))
68
+ plt.close()
69
+
70
+
71
+ def save_classification_report(y_true, y_pred, classes, model_name):
72
+ """Saves the text classification report."""
73
+ report = classification_report(y_true, y_pred, target_names=classes)
74
+ filename = f"{model_name.lower()}_report.txt"
75
+ with open(os.path.join(config.MODEL_DIR, filename), "w") as f:
76
+ f.write(report)
77
+ print(f"Report saved to {filename}")
src/streamlit_app.py DELETED
@@ -1,40 +0,0 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
- import streamlit as st
5
-
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
train_main.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ import pandas as pd
4
+ from sklearn.model_selection import train_test_split
5
+ from src import data, features, model, augmentation
6
+
7
+
8
+ def process_dataset_with_augmentation(df, is_training=False):
9
+ """
10
+ Loops through the dataframe.
11
+ If is_training=True, it augments minority classes to balance the data.
12
+ """
13
+ X = []
14
+ y = []
15
+
16
+ # 1. Calculate Statistics for Balancing (Only needed for training)
17
+ if is_training:
18
+ class_counts = df['target'].value_counts().to_dict()
19
+ max_count = max(class_counts.values())
20
+ print(f" [Augmentation] Balancing classes to match majority count: {max_count}")
21
+
22
+ total = len(df)
23
+
24
+ for idx, row in df.iterrows():
25
+ if idx % 100 == 0: print(f" Processing image {idx}/{total}...")
26
+
27
+ # Load Original Image
28
+ img = cv2.imread(row['path'])
29
+ if img is None: continue
30
+
31
+ # A. Extract Features for Original Image
32
+ feats = features.extract_all_features_pipeline(img)
33
+ if feats is not None:
34
+ X.append(feats)
35
+ y.append(row['label_idx'])
36
+
37
+ # B. Augmentation Logic (Training Only)
38
+ if is_training:
39
+ # Check how many extra copies we need
40
+ class_name = row['target']
41
+
42
+ # Calculate factor. e.g., if factor is 5, we generate 4 NEW images
43
+ # so total = 1 original + 4 augmented = 5
44
+ factor = augmentation.get_augmentation_factor(class_name, class_counts, max_count)
45
+ num_new_images = factor - 1
46
+
47
+ if num_new_images > 0:
48
+ # Generate variations
49
+ aug_imgs = augmentation.generate_augmented_images(img, count=num_new_images)
50
+
51
+ # Extract features for every augmented variation
52
+ for aug_img in aug_imgs:
53
+ aug_feats = features.extract_all_features_pipeline(aug_img)
54
+ if aug_feats is not None:
55
+ X.append(aug_feats)
56
+ y.append(row['label_idx'])
57
+
58
+ return np.array(X), np.array(y)
59
+
60
+
61
+ def main():
62
+ # 1. Load Data (Metadata only)
63
+ df, classes = data.load_metadata(limit=None) # Adjust limit as needed
64
+
65
+ print("-" * 50)
66
+ print("STEP 1: Splitting Data (Train/Test) on File Paths")
67
+ print("-" * 50)
68
+
69
+ # Split DataFrame FIRST to avoid data leakage
70
+ df_train, df_test = train_test_split(
71
+ df, test_size=0.2, stratify=df['label_idx'], random_state=42
72
+ )
73
+
74
+ print(f"Training Samples (Files): {len(df_train)}")
75
+ print(f"Test Samples (Files): {len(df_test)}")
76
+
77
+ # 2. Process Test Data (No Augmentation, just feature extraction)
78
+ print("\n" + "-" * 50)
79
+ print("STEP 2: Extracting Test Features (Standard)")
80
+ print("-" * 50)
81
+ X_test, y_test = process_dataset_with_augmentation(df_test, is_training=False)
82
+
83
+ # 3. Process Training Data (WITH Augmentation)
84
+ print("\n" + "-" * 50)
85
+ print("STEP 3: Extracting Training Features (With Keras Augmentation)")
86
+ print("-" * 50)
87
+ X_train, y_train = process_dataset_with_augmentation(df_train, is_training=True)
88
+
89
+ print(f"\nFinal Feature Matrix Shapes:")
90
+ print(f"X_train: {X_train.shape}, y_train: {y_train.shape}")
91
+ print(f"X_test: {X_test.shape}, y_test: {y_test.shape}")
92
+
93
+ # 4. Train & Evaluate
94
+ # We pass the pre-split arrays directly to a modified train function
95
+ if len(X_train) > 0 and len(X_test) > 0:
96
+ model.train_and_evaluate_split(X_train, y_train, X_test, y_test, classes)
97
+ else:
98
+ print("Error: Feature extraction failed.")
99
+
100
+
101
+ if __name__ == "__main__":
102
+ main()