|
|
--- |
|
|
license: mit |
|
|
tags: |
|
|
- medical-imaging |
|
|
- pcos-detection |
|
|
- explainable-ai |
|
|
- grad-cam |
|
|
- ultrasound |
|
|
- image Classification |
|
|
- pcos |
|
|
language: en |
|
|
metrics: |
|
|
- accuracy |
|
|
library_name: tensorflow |
|
|
--- |
|
|
|
|
|
# PCOS Detection with Explainable AI |
|
|
|
|
|
A deep learning model for **Polycystic Ovary Syndrome (PCOS)** detection from ultrasound images with **Grad-CAM** visualization for clinical interpretability. |
|
|
|
|
|
## Model Overview |
|
|
|
|
|
- **Architecture**: Dual-path CNN with multi-head attention |
|
|
- **Input**: 224Γ224 RGB ultrasound images |
|
|
- **Output**: Binary classification (PCOS-positive / Healthy) |
|
|
- **Accuracy**: ~95%+ on test set |
|
|
- **XAI**: Grad-CAM heatmaps for interpretability |
|
|
|
|
|
## π Quick Start |
|
|
|
|
|
```bash |
|
|
pip install tensorflow opencv-python matplotlib numpy requests huggingface-hub |
|
|
``` |
|
|
|
|
|
### Complete Working Example |
|
|
|
|
|
```python |
|
|
# ============================================================ |
|
|
# π PCOS Prediction + Grad-CAM (HF VERSION) |
|
|
# ============================================================ |
|
|
|
|
|
import numpy as np |
|
|
import cv2 |
|
|
import tensorflow as tf |
|
|
import matplotlib.pyplot as plt |
|
|
from tensorflow.keras import Model, Input |
|
|
from tensorflow.keras.layers import ( |
|
|
Conv2D, MaxPooling2D, Flatten, Dense, |
|
|
Lambda, Reshape, Concatenate, |
|
|
MultiHeadAttention, GlobalAveragePooling1D |
|
|
) |
|
|
import requests |
|
|
from huggingface_hub import hf_hub_download |
|
|
|
|
|
# ============================================================ |
|
|
# Config |
|
|
# ============================================================ |
|
|
IMG_SIZE = (224, 224) |
|
|
HF_MODEL_REPO = "Dehsahk-AI/Pcos-Detect" |
|
|
MODEL_FILENAME = "best_pcos_model.h5" |
|
|
IMAGE_URL = "https://example.com/ultrasound.jpg" # Your image URL |
|
|
CLASS_NAMES = ["infected", "noninfected"] |
|
|
|
|
|
# ============================================================ |
|
|
# Download model from HF |
|
|
# ============================================================ |
|
|
MODEL_PATH = hf_hub_download(repo_id=HF_MODEL_REPO, filename=MODEL_FILENAME) |
|
|
print(f" Model downloaded to: {MODEL_PATH}") |
|
|
|
|
|
# ============================================================ |
|
|
# Custom Lambda Functions |
|
|
# ============================================================ |
|
|
def split_image(image): |
|
|
upper = image[:, :IMG_SIZE[0]//2, :, :] |
|
|
lower = image[:, IMG_SIZE[0]//2:, :, :] |
|
|
return upper, lower |
|
|
|
|
|
def flip_lower(lower_half): |
|
|
return tf.image.flip_left_right(lower_half) |
|
|
|
|
|
# ============================================================ |
|
|
# Rebuild Model Architecture |
|
|
# ============================================================ |
|
|
input_layer = Input(shape=(224,224,3)) |
|
|
|
|
|
upper_half, lower_half = Lambda(split_image)(input_layer) |
|
|
lower_half = Lambda(flip_lower)(lower_half) |
|
|
|
|
|
# Upper CNN |
|
|
u = Conv2D(32, 3, activation="relu", padding="same")(upper_half) |
|
|
u = MaxPooling2D(2)(u) |
|
|
u = Conv2D(64, 3, activation="relu", padding="same")(u) |
|
|
u = MaxPooling2D(2)(u) |
|
|
u = Conv2D(128, 3, activation="relu", padding="same", name="upper_last_conv")(u) |
|
|
u = MaxPooling2D(2)(u) |
|
|
u = Flatten()(u) |
|
|
|
|
|
# Lower CNN |
|
|
l = Conv2D(32, 3, activation="relu", padding="same")(lower_half) |
|
|
l = MaxPooling2D(2)(l) |
|
|
l = Conv2D(64, 3, activation="relu", padding="same")(l) |
|
|
l = MaxPooling2D(2)(l) |
|
|
l = Conv2D(128, 3, activation="relu", padding="same", name="lower_last_conv")(l) |
|
|
l = MaxPooling2D(2)(l) |
|
|
l = Flatten()(l) |
|
|
|
|
|
u_dense = Dense(512, activation="relu")(u) |
|
|
l_dense = Dense(512, activation="relu")(l) |
|
|
|
|
|
u_r = Reshape((1,512))(u_dense) |
|
|
l_r = Reshape((1,512))(l_dense) |
|
|
|
|
|
concat = Concatenate(axis=1)([u_r, l_r]) |
|
|
|
|
|
att = MultiHeadAttention(num_heads=4, key_dim=64)(concat, concat) |
|
|
att = GlobalAveragePooling1D()(att) |
|
|
|
|
|
fc = Dense(256, activation="relu")(att) |
|
|
fc = Dense(128, activation="relu")(fc) |
|
|
|
|
|
# Logits for Grad-CAM |
|
|
logits = Dense(2, name="logits")(fc) |
|
|
output = tf.keras.layers.Activation('softmax', name='softmax')(logits) |
|
|
|
|
|
model = Model(input_layer, output) |
|
|
model.load_weights(MODEL_PATH) |
|
|
print(" Weights loaded successfully") |
|
|
|
|
|
# ============================================================ |
|
|
# Load & Preprocess Image |
|
|
# ============================================================ |
|
|
response = requests.get(IMAGE_URL) |
|
|
img_array_raw = np.asarray(bytearray(response.content), dtype=np.uint8) |
|
|
img = cv2.imdecode(img_array_raw, cv2.IMREAD_COLOR) |
|
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) |
|
|
img = cv2.resize(img, IMG_SIZE) |
|
|
img = img.astype(np.float32) / 255.0 |
|
|
img_array = np.expand_dims(img, axis=0) |
|
|
|
|
|
# ============================================================ |
|
|
# Prediction |
|
|
# ============================================================ |
|
|
pred = model.predict(img_array, verbose=0)[0] |
|
|
pred_class = np.argmax(pred) |
|
|
confidence = pred[pred_class] |
|
|
|
|
|
print(f"\n Prediction: {CLASS_NAMES[pred_class]}") |
|
|
print(f" Confidence: {confidence:.2%}") |
|
|
|
|
|
# ============================================================ |
|
|
# Grad-CAM |
|
|
# ============================================================ |
|
|
def gradcam(img_array, model, layer_name, pred_index): |
|
|
logits_layer = model.get_layer('logits') |
|
|
grad_model = Model( |
|
|
model.input, |
|
|
[model.get_layer(layer_name).output, logits_layer.output] |
|
|
) |
|
|
|
|
|
with tf.GradientTape() as tape: |
|
|
conv_out, logits = grad_model(img_array) |
|
|
loss = logits[:, pred_index] |
|
|
|
|
|
grads = tape.gradient(loss, conv_out) |
|
|
pooled = tf.reduce_mean(grads, axis=(0,1,2)) |
|
|
conv_out = conv_out[0] |
|
|
|
|
|
heatmap = conv_out @ pooled[..., tf.newaxis] |
|
|
heatmap = tf.squeeze(heatmap) |
|
|
heatmap = tf.maximum(heatmap, 0) |
|
|
|
|
|
if tf.reduce_max(heatmap) > 0: |
|
|
heatmap /= tf.reduce_max(heatmap) |
|
|
|
|
|
return heatmap.numpy() |
|
|
|
|
|
upper = gradcam(img_array, model, "upper_last_conv", pred_class) |
|
|
lower = gradcam(img_array, model, "lower_last_conv", pred_class) |
|
|
|
|
|
h = IMG_SIZE[0] // 2 |
|
|
upper = cv2.resize(upper, (IMG_SIZE[1], h)) |
|
|
lower = cv2.resize(lower, (IMG_SIZE[1], h)) |
|
|
lower = cv2.flip(lower, 1) |
|
|
|
|
|
heatmap = np.vstack([upper, lower]) |
|
|
|
|
|
heatmap_color = cv2.applyColorMap(np.uint8(255*heatmap), cv2.COLORMAP_JET) |
|
|
heatmap_color = cv2.cvtColor(heatmap_color, cv2.COLOR_BGR2RGB) / 255.0 |
|
|
|
|
|
overlay = 0.5 * heatmap_color + 0.5 * img |
|
|
|
|
|
# ============================================================ |
|
|
# Visualization |
|
|
# ============================================================ |
|
|
plt.figure(figsize=(15,5)) |
|
|
|
|
|
plt.subplot(1,3,1) |
|
|
plt.imshow(img) |
|
|
plt.title("Original") |
|
|
plt.axis("off") |
|
|
|
|
|
plt.subplot(1,3,2) |
|
|
plt.imshow(heatmap, cmap="jet") |
|
|
plt.title("Grad-CAM") |
|
|
plt.axis("off") |
|
|
|
|
|
plt.subplot(1,3,3) |
|
|
plt.imshow(overlay) |
|
|
plt.title(f"{CLASS_NAMES[pred_class]} ({confidence:.2%})") |
|
|
plt.axis("off") |
|
|
|
|
|
plt.tight_layout() |
|
|
plt.show() |
|
|
``` |
|
|
|
|
|
### Load from Local File |
|
|
|
|
|
```python |
|
|
# Replace URL loading with: |
|
|
img = cv2.imread('path/to/ultrasound.jpg') |
|
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) |
|
|
img = cv2.resize(img, IMG_SIZE) |
|
|
img = img.astype(np.float32) / 255.0 |
|
|
img_array = np.expand_dims(img, axis=0) |
|
|
``` |
|
|
|
|
|
## Understanding Grad-CAM Output |
|
|
|
|
|
- **Red/Hot regions**: High importance for prediction (follicles, cysts) |
|
|
- **Blue/Cool regions**: Low influence on decision |
|
|
- **Dual visualization**: Separate heatmaps for upper and lower ovarian regions |
|
|
|
|
|
## Model Architecture |
|
|
|
|
|
``` |
|
|
Input (224Γ224Γ3) |
|
|
βββ Split horizontally (upper/lower) |
|
|
βββ Upper Path: Conv32 β Conv64 β Conv128 β Dense512 |
|
|
βββ Lower Path: Conv32 β Conv64 β Conv128 β Dense512 |
|
|
βββ Multi-Head Attention (4 heads, dim=64) |
|
|
βββ Classification: Dense256 β Dense128 β Dense2 |
|
|
``` |
|
|
|
|
|
**Key Features:** |
|
|
- Dual-path CNN for separate ovarian region analysis |
|
|
- Lower region flipped for symmetry normalization |
|
|
- Multi-head attention for feature fusion |
|
|
- Logits-based Grad-CAM (fixes saturated softmax gradients) |
|
|
|
|
|
## Dataset |
|
|
|
|
|
- **Total**: 11,784 ultrasound images |
|
|
- **PCOS-positive**: 6,784 images (57.5%) |
|
|
- **Healthy**: 5,000 images (42.5%) |
|
|
- **Source**: 3 clinics (2018-2022), expert-annotated |
|
|
- **Dataset**: [PCOS XAI Ultrasound](https://www.kaggle.com/datasets/ibadeus/pcos-xai-ultrasound-dataset) |
|
|
|
|
|
## Important Notes |
|
|
|
|
|
**Clinical Use:** |
|
|
- Research purposes only - NOT FDA approved |
|
|
- Not a diagnostic tool - requires professional validation |
|
|
- Must be validated on local datasets before clinical deployment |
|
|
|
|
|
**Technical:** |
|
|
- Fixed 224Γ224 input size required |
|
|
- RGB images only |
|
|
- Model performance may vary across different ultrasound machines |
|
|
|
|
|
## Citation |
|
|
|
|
|
```bibtex |
|
|
@misc{pcos_xai_2024, |
|
|
title={PCOS Detection with Explainable AI}, |
|
|
author={Dehsahk-AI}, |
|
|
year={2025}, |
|
|
url={https://huggingface.co/Dehsahk-AI/Pcos-Detect} |
|
|
} |
|
|
``` |
|
|
|
|
|
## License |
|
|
|
|
|
MIT License - See LICENSE file for details. |
|
|
|
|
|
|
|
|
--- |
|
|
|
|
|
**Model Version**: 1.0 | **Last Updated**: December 2025 |
|
|
license: MIT |
|
|
--- |