import streamlit as st import cv2 import numpy as np import pandas as pd import joblib import os import matplotlib.pyplot as plt import lime import lime.lime_tabular from src import features, config st.set_page_config(page_title="DermAI Classification", layout="centered") # Custom styling to widen the center container st.markdown( """ """, unsafe_allow_html=True ) st.title("🔬 Skin Lesion Classification") st.markdown(""" This system uses **Classical Machine Vision** techniques (CLAHE, Otsu Thresholding, Morphology) to classify skin lesions. """) try: model_path = os.path.join(config.MODEL_DIR, 'skin_cancer_model.pkl') scaler_path = os.path.join(config.MODEL_DIR, 'scaler.pkl') classes_path = os.path.join(config.MODEL_DIR, 'classes.pkl') model = joblib.load(model_path) scaler = joblib.load(scaler_path) classes = joblib.load(classes_path) st.success("System Ready: Model Loaded Successfully") except FileNotFoundError: st.error("Model files not found. Please run 'train_main.py' first.") st.stop() uploaded_file = st.file_uploader("Choose a dermoscopy image...", type=["jpg", "jpeg", "png"]) if uploaded_file is not None: file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8) image = cv2.imdecode(file_bytes, 1) col1, col2 = st.columns(2) with col1: st.image(image, channels="BGR", caption="Uploaded Image", use_container_width=True) with st.spinner('Extracting Handcrafted Features...'): feat_vector = features.extract_all_features_pipeline(image) # Reshape for model input feat_vector_reshaped = feat_vector.reshape(1, -1) feat_scaled = scaler.transform(feat_vector_reshaped) # Predict probs = model.predict_proba(feat_scaled) pred_idx = np.argmax(probs) pred_label = classes[pred_idx] with col2: st.subheader(f"Prediction: **{pred_label}**") st.metric("Confidence", f"{probs[0][pred_idx] * 100:.2f}%") # --- Charts --- st.subheader("Class Probabilities") chart_data = pd.DataFrame({"Class": classes, "Probability": probs[0] * 100}) st.bar_chart(chart_data.set_index("Class")) with st.expander("Abbreviation information"): df_legend = pd.DataFrame(config.LEGEND_DATA) st.dataframe( df_legend, column_config={ "More Info": st.column_config.LinkColumn( "More", help="Click to visit Wikipedia page", display_text="🔍" ) }, hide_index=True, use_container_width=True ) # --- LIME EXPLANATION (Local XAI) --- st.divider() st.subheader("Explainable AI (LIME)") st.write(f"#### Why was this specific image classified as **{pred_label}**?") st.write( "The charts below show which features supported (Green) or contradicted (Red) the decision for **EACH** possible class.") try: # 1. Load the training sample (needed to initialize LIME) train_sample_path = os.path.join(config.MODEL_DIR, 'X_train_sample.npy') if os.path.exists(train_sample_path): X_train_sample = np.load(train_sample_path) feature_names = features.get_feature_names() # Check for feature mismatch if X_train_sample.shape[1] != len(feature_names): st.warning( f"Feature count mismatch (Model: {X_train_sample.shape[1]}, Code: {len(feature_names)}). Falling back to generic names.") feature_names = [f"Feature_{i}" for i in range(X_train_sample.shape[1])] # 2. Initialize Explainer explainer = lime.lime_tabular.LimeTabularExplainer( training_data=X_train_sample, feature_names=feature_names, class_names=classes, mode='classification', verbose=False ) # 3. Explain this specific instance for ALL classes # We pass labels=range(len(classes)) to calculate explanations for every class index exp = explainer.explain_instance( data_row=feat_scaled[0], predict_fn=model.predict_proba, num_features=10, labels=range(len(classes)) ) # 4. Plot using Tabs # Create a tab for each class so the user can switch between them tabs = st.tabs(list(classes)) for i, class_name in enumerate(classes): with tabs[i]: st.write(f"**Evidence For/Against: {class_name}**") # LIME uses the index (i) to retrieve the specific explanation fig = exp.as_pyplot_figure(label=i) st.pyplot(fig) else: st.warning("LIME initialization data (X_train_sample.npy) not found. Re-run training.") except Exception as e: st.error(f"Could not generate explanation: {type(e).__name__}: {e}") # --- Pipeline Visualization --- st.divider() with st.expander("See Internal Logic (Computer Vision Pipeline Steps)", expanded=True): st.info("Visualizing the exact steps performed by `src.features.py`") img_resized, img_gray, img_eq, img_blur = features.preprocess_image(image) mask_raw, mask_clean, mask_connected = features.segment_lesion(img_blur) mask_final, _, _, _ = features.isolate_largest_component(mask_connected) _, texture_vis = features.compute_texture_canny(img_gray, mask=mask_final) img_lesion_only = cv2.bitwise_and(img_resized, img_resized, mask=mask_final) # Row 1: Preprocessing st.markdown("### Phase 1: Preprocessing") c1, c2, c3, c4 = st.columns(4) c1.image(img_resized, channels="BGR", caption="1. Resize") c2.image(img_gray, caption="2. Grayscale") c3.image(img_eq, caption="3. CLAHE (Smart Contrast)") c4.image(img_blur, caption="4. Blur (Reduce Noise)") st.divider() # Row 2: Segmentation st.markdown("### Phase 2: Segmentation") c5, c6 = st.columns(2) c5.image(mask_raw, caption="5. Otsu Threshold") c6.image(mask_clean, caption="6. Morph Opening") st.divider() # Row 3: Connection & Selection c7, c8 = st.columns(2) c7.image(mask_connected, caption="7. Morph Dilation") c8.image(mask_final, caption="8. Final Mask") st.divider() # Row 4: Analysis st.markdown("### Phase 3: Analysis") c9, c10 = st.columns(2) c9.image(img_lesion_only, channels="BGR", caption="9. Masked Source") c10.image(texture_vis, caption="10. Canny Edges (Masked)") # Histogram st.write("**11. Lesion Color Histogram**") fig, ax = plt.subplots(figsize=(10, 3)) colors = ('b', 'g', 'r') for i, color in enumerate(colors): hist = cv2.calcHist([img_resized], [i], mask_final, [256], [0, 256]) ax.plot(hist, color=color) ax.set_xlim([0, 256]) ax.set_title("Color Frequency") st.pyplot(fig)