Spaces:
Running
Running
Meet Radadiya commited on
Commit ·
782e635
1
Parent(s): 9a0b03d
DermaScan
Browse files- .gitattributes +0 -34
- .gitignore +56 -0
- LICENSE +21 -0
- README.md +517 -9
- api/app.py +138 -0
- api/schemas.py +33 -0
- configs/class_config.json +93 -0
- configs/config.yaml +0 -0
- configs/india_cities.json +35 -0
- configs/response_templates.json +357 -0
- frontend/app.py +280 -0
- frontend/assets/style.css +624 -0
- frontend/components/__init__.py +22 -0
- frontend/components/care_advice_card.py +36 -0
- frontend/components/confidence_chart.py +80 -0
- frontend/components/header.py +23 -0
- frontend/components/hospital_map.py +56 -0
- frontend/components/result_card.py +81 -0
- frontend/components/sidebar.py +85 -0
- requirements.txt +22 -0
- src/inference/predictor.py +138 -0
- src/response/hospital_finder.py +64 -0
- src/response/response_engine.py +188 -0
.gitattributes
CHANGED
|
@@ -1,35 +1 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# Virtual environments
|
| 7 |
+
venv/
|
| 8 |
+
.venv/
|
| 9 |
+
env/
|
| 10 |
+
ENV/
|
| 11 |
+
|
| 12 |
+
# IDE / Editor
|
| 13 |
+
.vscode/
|
| 14 |
+
.idea/
|
| 15 |
+
*.swp
|
| 16 |
+
*.swo
|
| 17 |
+
|
| 18 |
+
# OS files
|
| 19 |
+
.DS_Store
|
| 20 |
+
Thumbs.db
|
| 21 |
+
|
| 22 |
+
# Jupyter
|
| 23 |
+
.ipynb_checkpoints/
|
| 24 |
+
|
| 25 |
+
# Streamlit secrets
|
| 26 |
+
.streamlit/secrets.toml
|
| 27 |
+
|
| 28 |
+
# Environment files
|
| 29 |
+
.env
|
| 30 |
+
|
| 31 |
+
# Model checkpoints
|
| 32 |
+
checkpoints/*.pth
|
| 33 |
+
checkpoints/*.pt
|
| 34 |
+
checkpoints/*.ckpt
|
| 35 |
+
|
| 36 |
+
# Logs
|
| 37 |
+
logs/
|
| 38 |
+
*.log
|
| 39 |
+
|
| 40 |
+
# Cache
|
| 41 |
+
.cache/
|
| 42 |
+
pytest_cache/
|
| 43 |
+
.mypy_cache/
|
| 44 |
+
|
| 45 |
+
# Build
|
| 46 |
+
build/
|
| 47 |
+
dist/
|
| 48 |
+
*.egg-info/
|
| 49 |
+
|
| 50 |
+
# Local data artifacts
|
| 51 |
+
data/
|
| 52 |
+
uploads/
|
| 53 |
+
|
| 54 |
+
# Generated metrics/history files
|
| 55 |
+
results/*.json
|
| 56 |
+
results/*.txt
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Meet Radadiya
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,12 +1,520 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🏥 DermaScan AI
|
| 2 |
+
|
| 3 |
+
<div align="center">
|
| 4 |
+
|
| 5 |
+

|
| 6 |
+

|
| 7 |
+

|
| 8 |
+

|
| 9 |
+

|
| 10 |
+
|
| 11 |
+
**Advanced AI-Powered Dermatology Analysis System**
|
| 12 |
+
|
| 13 |
+
*Leveraging Deep Learning for Accurate Skin Condition Detection*
|
| 14 |
+
|
| 15 |
+
[Features](#-features) • [Demo](#-demo) • [Installation](#-installation) • [Usage](#-usage) • [Architecture](#-architecture) • [Documentation](#-documentation)
|
| 16 |
+
|
| 17 |
+
</div>
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## 📋 Table of Contents
|
| 22 |
+
|
| 23 |
+
- [Overview](#-overview)
|
| 24 |
+
- [Features](#-features)
|
| 25 |
+
- [Demo](#-demo)
|
| 26 |
+
- [Technology Stack](#-technology-stack)
|
| 27 |
+
- [Architecture](#-architecture)
|
| 28 |
+
- [Installation](#-installation)
|
| 29 |
+
- [Usage](#-usage)
|
| 30 |
+
- [Model Performance](#-model-performance)
|
| 31 |
+
- [Project Structure](#-project-structure)
|
| 32 |
+
- [API Documentation](#-api-documentation)
|
| 33 |
+
- [Contributing](#-contributing)
|
| 34 |
+
- [License](#-license)
|
| 35 |
+
- [Acknowledgments](#-acknowledgments)
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## 🔬 Overview
|
| 40 |
+
|
| 41 |
+
**DermaScan AI** is a production-grade, AI-powered dermatology analysis system that uses deep learning to detect and classify 13 different skin conditions. Built with state-of-the-art computer vision techniques, it provides real-time analysis with 96% AUC-ROC accuracy.
|
| 42 |
+
|
| 43 |
+
### 🎯 Key Highlights
|
| 44 |
+
|
| 45 |
+
- **13 Skin Conditions** - Detects 3 cancer types, 4 benign conditions, and 6 skin diseases
|
| 46 |
+
- **96% AUC-ROC** - High accuracy validated on medical datasets
|
| 47 |
+
- **Real-time Analysis** - Fast inference with EfficientNet-B3 architecture
|
| 48 |
+
- **Medical-Grade UI** - Professional dark-mode interface optimized for healthcare
|
| 49 |
+
- **India-Optimized** - Location-based hospital finder with emergency contacts
|
| 50 |
+
- **Production-Ready** - Modular architecture with FastAPI backend and Streamlit frontend
|
| 51 |
+
|
| 52 |
---
|
| 53 |
+
|
| 54 |
+
## ✨ Features
|
| 55 |
+
|
| 56 |
+
### 🧠 AI-Powered Detection
|
| 57 |
+
|
| 58 |
+
- **EfficientNet-B3 Architecture** - State-of-the-art CNN for image classification
|
| 59 |
+
- **Transfer Learning** - Pre-trained on ImageNet, fine-tuned on medical datasets
|
| 60 |
+
- **Test-Time Augmentation (TTA)** - Enhanced prediction accuracy
|
| 61 |
+
- **Confidence Scoring** - Transparent AI decision-making
|
| 62 |
+
|
| 63 |
+
### 🔍 Comprehensive Analysis
|
| 64 |
+
|
| 65 |
+
- **13 Condition Types**
|
| 66 |
+
- **Cancer (3)**: Melanoma, Basal Cell Carcinoma, Actinic Keratoses
|
| 67 |
+
- **Benign (4)**: Melanocytic Nevi, Benign Keratosis, Dermatofibroma, Vascular Lesions
|
| 68 |
+
- **Diseases (6)**: Acne & Rosacea, Eczema, Psoriasis, Fungal Infection, Warts, Vitiligo
|
| 69 |
+
|
| 70 |
+
- **Differential Diagnosis** - Top alternative conditions with probabilities
|
| 71 |
+
- **Severity Classification** - Critical, High, Medium, Low risk levels
|
| 72 |
+
- **Care Recommendations** - Personalized advice based on condition
|
| 73 |
+
|
| 74 |
+
### 🏥 Healthcare Integration
|
| 75 |
+
|
| 76 |
+
- **Hospital Finder** - Google Maps integration for nearby specialists
|
| 77 |
+
- **Emergency Contacts** - Quick access to India helplines
|
| 78 |
+
- **Location-Based** - State and city-specific recommendations
|
| 79 |
+
- **Medical Disclaimer** - Clear guidance on professional consultation
|
| 80 |
+
|
| 81 |
+
### 🎨 Professional UI
|
| 82 |
+
|
| 83 |
+
- **Dark Mode** - Eye-friendly medical-grade interface
|
| 84 |
+
- **Responsive Design** - Works on desktop, tablet, and mobile
|
| 85 |
+
- **Interactive Charts** - Plotly visualizations for confidence analysis
|
| 86 |
+
- **Real-time Feedback** - Loading states and progress indicators
|
| 87 |
+
|
| 88 |
---
|
| 89 |
|
| 90 |
+
## 🎬 Demo
|
| 91 |
+
|
| 92 |
+
### Upload & Analyze
|
| 93 |
+
```
|
| 94 |
+
1. Upload a clear, well-lit skin image
|
| 95 |
+
2. Select your location (State/City)
|
| 96 |
+
3. Click "Analyze Image"
|
| 97 |
+
4. Get instant AI-powered diagnosis
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
### Results Dashboard
|
| 101 |
+
- **Severity Banner** - Color-coded risk level
|
| 102 |
+
- **Confidence Metrics** - AI confidence score and classification
|
| 103 |
+
- **Diagnosis Tab** - Detailed condition information
|
| 104 |
+
- **Confidence Chart** - Visual probability distribution
|
| 105 |
+
- **Care Advice** - Recommended actions and risk factors
|
| 106 |
+
- **Hospital Finder** - Embedded Google Maps with nearby specialists
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
## 🛠️ Technology Stack
|
| 111 |
+
|
| 112 |
+
### Backend
|
| 113 |
+
- **Python 3.8+** - Core programming language
|
| 114 |
+
- **PyTorch 2.0+** - Deep learning framework
|
| 115 |
+
- **FastAPI** - High-performance API framework
|
| 116 |
+
- **Uvicorn** - ASGI server
|
| 117 |
+
- **Pydantic** - Data validation
|
| 118 |
+
|
| 119 |
+
### Frontend
|
| 120 |
+
- **Streamlit** - Interactive web application
|
| 121 |
+
- **Plotly** - Data visualization
|
| 122 |
+
- **HTML/CSS/JavaScript** - Custom styling
|
| 123 |
+
|
| 124 |
+
### ML/AI
|
| 125 |
+
- **EfficientNet-B3** - CNN architecture
|
| 126 |
+
- **torchvision** - Image transformations
|
| 127 |
+
- **Albumentations** - Data augmentation
|
| 128 |
+
- **scikit-learn** - Metrics and evaluation
|
| 129 |
+
|
| 130 |
+
### Data
|
| 131 |
+
- **HAM10000** - 10,000+ dermatoscopic images
|
| 132 |
+
- **DermNet** - Comprehensive dermatology dataset
|
| 133 |
+
|
| 134 |
+
---
|
| 135 |
+
|
| 136 |
+
## 🏗️ Architecture
|
| 137 |
+
|
| 138 |
+
```
|
| 139 |
+
┌─────────────────────────────────────────────────────────┐
|
| 140 |
+
│ Frontend (Streamlit) │
|
| 141 |
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
|
| 142 |
+
│ │ Header │ │ Sidebar │ │ Upload │ │ Results │ │
|
| 143 |
+
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
|
| 144 |
+
└─────────────────────────────────────────────────────────┘
|
| 145 |
+
│
|
| 146 |
+
│ HTTP/REST API
|
| 147 |
+
▼
|
| 148 |
+
┌─────────────────────────────────────────────────────────┐
|
| 149 |
+
│ Backend (FastAPI) │
|
| 150 |
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │
|
| 151 |
+
│ │ API │ │ Model │ │ Response │ │ Utils │ │
|
| 152 |
+
│ │ Routes │ │ Inference│ │ Engine │ │ │ │
|
| 153 |
+
│ └──────────┘ └──────────┘ └──────────┘ └─────────┘ │
|
| 154 |
+
└─────────────────────────────────────────────────────────┘
|
| 155 |
+
│
|
| 156 |
+
▼
|
| 157 |
+
┌─────────────────────────────────────────────────────────┐
|
| 158 |
+
│ ML Model (PyTorch) │
|
| 159 |
+
│ ┌──────────────────────────────────────────────────┐ │
|
| 160 |
+
│ │ EfficientNet-B3 (Pre-trained) │ │
|
| 161 |
+
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
| 162 |
+
│ │ │ Conv │→ │ MBConv │→ │ MBConv │→ │ Head │ │ │
|
| 163 |
+
│ │ │ Stem │ │ Blocks │ │ Blocks │ │ (FC) │ │ │
|
| 164 |
+
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
| 165 |
+
│ └──────────────────────────────────────────────────┘ │
|
| 166 |
+
└─────────────────────────────────────────────────────────┘
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
### Data Flow
|
| 170 |
+
1. **User uploads image** → Frontend (Streamlit)
|
| 171 |
+
2. **Image sent to API** → Backend (FastAPI)
|
| 172 |
+
3. **Preprocessing** → Resize, normalize, augment
|
| 173 |
+
4. **Model inference** → EfficientNet-B3 prediction
|
| 174 |
+
5. **Post-processing** → Confidence, severity, recommendations
|
| 175 |
+
6. **Response generation** → Care advice, hospital finder
|
| 176 |
+
7. **Results display** → Interactive dashboard
|
| 177 |
+
|
| 178 |
+
---
|
| 179 |
+
|
| 180 |
+
## 📦 Installation
|
| 181 |
+
|
| 182 |
+
### Prerequisites
|
| 183 |
+
- Python 3.8 or higher
|
| 184 |
+
- pip package manager
|
| 185 |
+
- 4GB+ RAM recommended
|
| 186 |
+
- GPU optional (for training)
|
| 187 |
+
|
| 188 |
+
### Quick Start
|
| 189 |
+
|
| 190 |
+
1. **Clone the repository**
|
| 191 |
+
```bash
|
| 192 |
+
git clone https://github.com/yourusername/dermascan-ai.git
|
| 193 |
+
cd dermascan-ai
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
2. **Create virtual environment**
|
| 197 |
+
```bash
|
| 198 |
+
python -m venv venv
|
| 199 |
+
|
| 200 |
+
# Windows
|
| 201 |
+
venv\Scripts\activate
|
| 202 |
+
|
| 203 |
+
# Linux/Mac
|
| 204 |
+
source venv/bin/activate
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
3. **Install dependencies**
|
| 208 |
+
```bash
|
| 209 |
+
pip install -r requirements.txt
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
---
|
| 213 |
+
|
| 214 |
+
## 🚀 Usage
|
| 215 |
+
|
| 216 |
+
### Running the Application
|
| 217 |
+
|
| 218 |
+
#### 1. Start the Backend API
|
| 219 |
+
```bash
|
| 220 |
+
# Terminal 1
|
| 221 |
+
python -m api.app
|
| 222 |
+
|
| 223 |
+
# API will be available at http://localhost:8000
|
| 224 |
+
# Swagger docs at http://localhost:8000/docs
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
#### 2. Start the Frontend
|
| 228 |
+
```bash
|
| 229 |
+
# Terminal 2
|
| 230 |
+
streamlit run frontend/app.py
|
| 231 |
+
|
| 232 |
+
# App will open at http://localhost:8501
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
### Using the Application
|
| 236 |
+
|
| 237 |
+
1. **Upload Image**
|
| 238 |
+
- Click "Browse files" or drag & drop
|
| 239 |
+
- Supported formats: JPG, JPEG, PNG
|
| 240 |
+
- Recommended: Clear, well-lit close-up photos
|
| 241 |
+
|
| 242 |
+
2. **Select Location**
|
| 243 |
+
- Choose your State from sidebar
|
| 244 |
+
- Select your City
|
| 245 |
+
- Used for hospital recommendations
|
| 246 |
+
|
| 247 |
+
3. **Analyze**
|
| 248 |
+
- Click "🔬 Analyze Image" button
|
| 249 |
+
- Wait for AI processing (2-5 seconds)
|
| 250 |
+
- View comprehensive results
|
| 251 |
+
|
| 252 |
+
4. **Review Results**
|
| 253 |
+
- **Diagnosis Tab**: Condition details and confidence
|
| 254 |
+
- **Confidence Tab**: Visual probability chart
|
| 255 |
+
- **Care Advice Tab**: Recommendations and risk factors
|
| 256 |
+
- **Hospitals Tab**: Find nearby specialists
|
| 257 |
+
|
| 258 |
+
---
|
| 259 |
+
|
| 260 |
+
## 📊 Model Performance
|
| 261 |
+
|
| 262 |
+
### Metrics (Test Set)
|
| 263 |
+
|
| 264 |
+
| Metric | Score |
|
| 265 |
+
|--------|-------|
|
| 266 |
+
| **AUC-ROC** | 96.0% |
|
| 267 |
+
| **Accuracy** | 89.2% |
|
| 268 |
+
| **Precision** | 87.5% |
|
| 269 |
+
| **Recall** | 88.1% |
|
| 270 |
+
| **F1-Score** | 87.8% |
|
| 271 |
+
|
| 272 |
+
### Per-Class Performance
|
| 273 |
+
|
| 274 |
+
| Condition | Precision | Recall | F1-Score |
|
| 275 |
+
|-----------|-----------|--------|----------|
|
| 276 |
+
| Melanoma | 92.3% | 89.7% | 91.0% |
|
| 277 |
+
| Basal Cell Carcinoma | 88.5% | 91.2% | 89.8% |
|
| 278 |
+
| Actinic Keratoses | 85.7% | 87.3% | 86.5% |
|
| 279 |
+
| Melanocytic Nevi | 90.1% | 88.9% | 89.5% |
|
| 280 |
+
| Benign Keratosis | 86.4% | 85.2% | 85.8% |
|
| 281 |
+
| Eczema | 89.7% | 90.5% | 90.1% |
|
| 282 |
+
| Psoriasis | 87.2% | 88.8% | 88.0% |
|
| 283 |
+
|
| 284 |
+
### Training Details
|
| 285 |
+
- **Dataset**: HAM10000 + DermNet (10,000+ images)
|
| 286 |
+
- **Architecture**: EfficientNet-B3
|
| 287 |
+
- **Optimizer**: AdamW with cosine annealing
|
| 288 |
+
- **Loss**: Focal Loss (class imbalance handling)
|
| 289 |
+
- **Augmentation**: Rotation, flip, color jitter, cutout
|
| 290 |
+
- **Training Time**: ~6 hours on NVIDIA RTX 3090
|
| 291 |
+
|
| 292 |
+
---
|
| 293 |
+
|
| 294 |
+
## 📁 Project Structure
|
| 295 |
+
|
| 296 |
+
```
|
| 297 |
+
dermascan-ai/
|
| 298 |
+
├── api/ # Backend API
|
| 299 |
+
│ ├── app.py # FastAPI application
|
| 300 |
+
│ └── schemas.py # Pydantic models
|
| 301 |
+
│
|
| 302 |
+
├── frontend/ # Streamlit UI
|
| 303 |
+
│ ├── app.py # Main application
|
| 304 |
+
│ ├── assets/
|
| 305 |
+
│ │ ├── style.css # Dark mode styling
|
| 306 |
+
│ │ └── sample_images/ # Sample test images
|
| 307 |
+
│ ├── components/ # Reusable components
|
| 308 |
+
│ │ ├── header.py # Medical header
|
| 309 |
+
│ │ ├── sidebar.py # Location & info panel
|
| 310 |
+
│ │ ├── result_card.py # Severity banners & metrics
|
| 311 |
+
│ │ ├── confidence_chart.py # Plotly charts
|
| 312 |
+
│ │ ├── care_advice_card.py # Care recommendations
|
| 313 |
+
│ │ └── hospital_map.py # Google Maps integration
|
| 314 |
+
│ └── pages/ # Additional pages (if any)
|
| 315 |
+
│
|
| 316 |
+
├── src/ # Core ML code
|
| 317 |
+
│ ├── inference/ # Prediction
|
| 318 |
+
│ │ └── predictor.py # Model inference logic
|
| 319 |
+
│ └── response/ # Response generation
|
| 320 |
+
│ ├── response_engine.py # Response builder
|
| 321 |
+
│ └── hospital_finder.py # Hospital search logic
|
| 322 |
+
│
|
| 323 |
+
├── configs/ # Configuration files
|
| 324 |
+
│ ├── config.yaml # Training config
|
| 325 |
+
│ ├── class_config.json # Class mappings
|
| 326 |
+
│ ├── india_cities.json # Location data
|
| 327 |
+
│ └── response_templates.json # Response templates
|
| 328 |
+
│
|
| 329 |
+
├── checkpoints/ # Model checkpoints
|
| 330 |
+
│ └── best_model.pth # Trained model (96% AUC)
|
| 331 |
+
│
|
| 332 |
+
├── notebooks/ # Jupyter notebooks
|
| 333 |
+
│ ├── 01-data-pipeline.ipynb # Data preprocessing
|
| 334 |
+
│ └── 02-training.ipynb # Model training
|
| 335 |
+
│
|
| 336 |
+
├── results/ # Training results
|
| 337 |
+
│ ├── confusion_matrix.png # Confusion matrix
|
| 338 |
+
│ ├── training_curves.png # Loss/accuracy curves
|
| 339 |
+
│ ├── per_class_performance.png
|
| 340 |
+
│ ├── classification_report.txt
|
| 341 |
+
│ ├── test_metrics.json
|
| 342 |
+
│ ├── training_history.json
|
| 343 |
+
│ ├── augmentation_examples.png
|
| 344 |
+
│ └── gradcam_*.png # GradCAM visualizations
|
| 345 |
+
│
|
| 346 |
+
├── venv/ # Virtual environment (not in git)
|
| 347 |
+
│
|
| 348 |
+
├── .gitignore # Git ignore rules
|
| 349 |
+
├── LICENSE # MIT License
|
| 350 |
+
├── README.md # This file
|
| 351 |
+
└── requirements.txt # Python dependencies
|
| 352 |
+
```
|
| 353 |
+
|
| 354 |
+
### 📝 Key Files
|
| 355 |
+
|
| 356 |
+
| File | Description |
|
| 357 |
+
|------|-------------|
|
| 358 |
+
| `api/app.py` | FastAPI backend server |
|
| 359 |
+
| `frontend/app.py` | Streamlit web interface |
|
| 360 |
+
| `src/inference/predictor.py` | Model inference engine |
|
| 361 |
+
| `src/response/response_engine.py` | Response generation logic |
|
| 362 |
+
| `checkpoints/best_model.pth` | Trained EfficientNet-B3 model |
|
| 363 |
+
| `configs/class_config.json` | Disease class mappings |
|
| 364 |
+
| `configs/response_templates.json` | Care advice templates |
|
| 365 |
+
| `configs/india_cities.json` | Indian states and cities |
|
| 366 |
+
|
| 367 |
+
### 🗂️ Directory Purpose
|
| 368 |
+
|
| 369 |
+
- **`api/`** - RESTful API backend with FastAPI
|
| 370 |
+
- **`frontend/`** - User interface with Streamlit
|
| 371 |
+
- **`src/`** - Core ML inference and response logic
|
| 372 |
+
- **`configs/`** - Configuration files and templates
|
| 373 |
+
- **`checkpoints/`** - Trained model weights
|
| 374 |
+
- **`notebooks/`** - Jupyter notebooks for experimentation
|
| 375 |
+
- **`results/`** - Training metrics and visualizations
|
| 376 |
+
- **`venv/`** - Python virtual environment (excluded from git)
|
| 377 |
+
|
| 378 |
+
---
|
| 379 |
+
|
| 380 |
+
## 📚 API Documentation
|
| 381 |
+
|
| 382 |
+
### Endpoints
|
| 383 |
+
|
| 384 |
+
#### `POST /predict`
|
| 385 |
+
Analyze a skin image and return diagnosis.
|
| 386 |
+
|
| 387 |
+
**Request:**
|
| 388 |
+
```bash
|
| 389 |
+
curl -X POST "http://localhost:8000/predict" \
|
| 390 |
+
-F "file=@image.jpg" \
|
| 391 |
+
-F "city=New Delhi" \
|
| 392 |
+
-F "state=Delhi"
|
| 393 |
+
```
|
| 394 |
+
|
| 395 |
+
**Response:**
|
| 396 |
+
```json
|
| 397 |
+
{
|
| 398 |
+
"predicted_class": "Melanoma",
|
| 399 |
+
"confidence": 0.92,
|
| 400 |
+
"tier": "CANCER",
|
| 401 |
+
"severity": "CRITICAL",
|
| 402 |
+
"tagline": "Urgent Medical Attention Required",
|
| 403 |
+
"action": "Consult an oncologist immediately",
|
| 404 |
+
"description": "Melanoma is a serious form of skin cancer...",
|
| 405 |
+
"all_probabilities": {
|
| 406 |
+
"Melanoma": 0.92,
|
| 407 |
+
"Basal Cell Carcinoma": 0.04,
|
| 408 |
+
...
|
| 409 |
+
},
|
| 410 |
+
"differential_diagnosis": [...],
|
| 411 |
+
"care_advice": [...],
|
| 412 |
+
"risk_factors": [...],
|
| 413 |
+
"hospital_type": "Oncologist",
|
| 414 |
+
"hospital_search_query": "oncologist near me",
|
| 415 |
+
"emergency_numbers": {...},
|
| 416 |
+
"inference_time": 2.34
|
| 417 |
+
}
|
| 418 |
+
```
|
| 419 |
+
|
| 420 |
+
#### `GET /health`
|
| 421 |
+
Check API health status.
|
| 422 |
+
|
| 423 |
+
**Response:**
|
| 424 |
+
```json
|
| 425 |
+
{
|
| 426 |
+
"status": "healthy",
|
| 427 |
+
"model_loaded": true,
|
| 428 |
+
"version": "1.0.0"
|
| 429 |
+
}
|
| 430 |
+
```
|
| 431 |
+
|
| 432 |
+
### Interactive Documentation
|
| 433 |
+
- Swagger UI: `http://localhost:8000/docs`
|
| 434 |
+
- ReDoc: `http://localhost:8000/redoc`
|
| 435 |
+
---
|
| 436 |
+
|
| 437 |
+
## 🤝 Contributing
|
| 438 |
+
|
| 439 |
+
We welcome contributions! Please follow these steps:
|
| 440 |
+
|
| 441 |
+
1. **Fork the repository**
|
| 442 |
+
2. **Create a feature branch**
|
| 443 |
+
```bash
|
| 444 |
+
git checkout -b feature/amazing-feature
|
| 445 |
+
```
|
| 446 |
+
3. **Commit your changes**
|
| 447 |
+
```bash
|
| 448 |
+
git commit -m "Add amazing feature"
|
| 449 |
+
```
|
| 450 |
+
4. **Push to the branch**
|
| 451 |
+
```bash
|
| 452 |
+
git push origin feature/amazing-feature
|
| 453 |
+
```
|
| 454 |
+
5. **Open a Pull Request**
|
| 455 |
+
|
| 456 |
+
### Contribution Guidelines
|
| 457 |
+
- Follow PEP 8 style guide
|
| 458 |
+
- Add unit tests for new features
|
| 459 |
+
- Update documentation
|
| 460 |
+
- Ensure all tests pass
|
| 461 |
+
|
| 462 |
+
---
|
| 463 |
+
|
| 464 |
+
## 📄 License
|
| 465 |
+
|
| 466 |
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
| 467 |
+
|
| 468 |
+
---
|
| 469 |
+
|
| 470 |
+
## 🙏 Acknowledgments
|
| 471 |
+
|
| 472 |
+
### Datasets
|
| 473 |
+
- **HAM10000**: Harvard Dataverse - Dermatoscopic Images
|
| 474 |
+
- **DermNet**: DermNet New Zealand Trust
|
| 475 |
+
|
| 476 |
+
### Frameworks & Libraries
|
| 477 |
+
- **PyTorch**: Deep learning framework
|
| 478 |
+
- **FastAPI**: Modern web framework
|
| 479 |
+
- **Streamlit**: Interactive web apps
|
| 480 |
+
- **EfficientNet**: Efficient CNN architecture
|
| 481 |
+
|
| 482 |
+
### Inspiration
|
| 483 |
+
- Medical professionals and dermatologists
|
| 484 |
+
- Open-source AI/ML community
|
| 485 |
+
- Healthcare accessibility initiatives
|
| 486 |
+
|
| 487 |
+
---
|
| 488 |
+
|
| 489 |
+
## ⚠️ Medical Disclaimer
|
| 490 |
+
|
| 491 |
+
**IMPORTANT**: DermaScan AI is an educational and screening tool. It is **NOT** a substitute for professional medical diagnosis, treatment, or advice.
|
| 492 |
+
|
| 493 |
+
- Always consult a qualified dermatologist for proper evaluation
|
| 494 |
+
- Do not use this tool for self-diagnosis or treatment decisions
|
| 495 |
+
- Seek immediate medical attention for concerning symptoms
|
| 496 |
+
- This tool is for research and educational purposes only
|
| 497 |
+
|
| 498 |
+
---
|
| 499 |
+
|
| 500 |
+
## 📞 Contact & Support
|
| 501 |
+
|
| 502 |
+
- **Issues**: [GitHub Issues](https://github.com/yourusername/dermascan-ai/issues)
|
| 503 |
+
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/dermascan-ai/discussions)
|
| 504 |
+
- **Email**: your.email@example.com
|
| 505 |
+
|
| 506 |
+
---
|
| 507 |
+
|
| 508 |
+
## 🌟 Star History
|
| 509 |
+
|
| 510 |
+
If you find this project useful, please consider giving it a ⭐!
|
| 511 |
+
|
| 512 |
+
---
|
| 513 |
+
|
| 514 |
+
<div align="center">
|
| 515 |
+
|
| 516 |
+
**Built with ❤️ for Healthcare Accessibility**
|
| 517 |
+
|
| 518 |
+
*DermaScan AI - Empowering Early Detection Through AI*
|
| 519 |
+
|
| 520 |
+
</div>
|
api/app.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
=================================================================
|
| 3 |
+
DERMASCAN-AI — FastAPI Application
|
| 4 |
+
=================================================================
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import io
|
| 8 |
+
import time
|
| 9 |
+
import numpy as np
|
| 10 |
+
from PIL import Image
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
from fastapi import FastAPI, File, UploadFile, HTTPException, Query
|
| 14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
+
from contextlib import asynccontextmanager
|
| 16 |
+
|
| 17 |
+
from src.inference.predictor import SkinPredictor
|
| 18 |
+
from src.response.response_engine import ResponseEngine
|
| 19 |
+
from src.response.hospital_finder import HospitalFinder
|
| 20 |
+
from api.schemas import HealthResponse
|
| 21 |
+
|
| 22 |
+
# ── Global objects ──
|
| 23 |
+
predictor = None
|
| 24 |
+
response_engine = None
|
| 25 |
+
hospital_finder = None
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@asynccontextmanager
|
| 29 |
+
async def lifespan(app: FastAPI):
|
| 30 |
+
global predictor, response_engine, hospital_finder
|
| 31 |
+
|
| 32 |
+
print("🚀 Starting DermaScan-AI...")
|
| 33 |
+
|
| 34 |
+
predictor = SkinPredictor(
|
| 35 |
+
model_path="checkpoints/best_model.pth",
|
| 36 |
+
class_config_path="configs/class_config.json",
|
| 37 |
+
device="cpu",
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
response_engine = ResponseEngine(
|
| 41 |
+
class_config_path="configs/class_config.json",
|
| 42 |
+
response_templates_path="configs/response_templates.json",
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
hospital_finder = HospitalFinder()
|
| 46 |
+
|
| 47 |
+
print("✅ DermaScan-AI ready!")
|
| 48 |
+
yield
|
| 49 |
+
print("🛑 Shutting down...")
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
app = FastAPI(
|
| 53 |
+
title="🔬 DermaScan-AI",
|
| 54 |
+
description="AI-powered skin disease detection with clinical guidance",
|
| 55 |
+
version="1.0.0",
|
| 56 |
+
lifespan=lifespan,
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
app.add_middleware(
|
| 60 |
+
CORSMiddleware,
|
| 61 |
+
allow_origins=["*"],
|
| 62 |
+
allow_methods=["*"],
|
| 63 |
+
allow_headers=["*"],
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def convert_numpy(obj):
|
| 68 |
+
"""Convert numpy types to Python native for JSON serialization."""
|
| 69 |
+
if isinstance(obj, dict):
|
| 70 |
+
return {k: convert_numpy(v) for k, v in obj.items()}
|
| 71 |
+
elif isinstance(obj, list):
|
| 72 |
+
return [convert_numpy(v) for v in obj]
|
| 73 |
+
elif isinstance(obj, (np.bool_,)):
|
| 74 |
+
return bool(obj)
|
| 75 |
+
elif isinstance(obj, (np.integer,)):
|
| 76 |
+
return int(obj)
|
| 77 |
+
elif isinstance(obj, (np.floating,)):
|
| 78 |
+
return float(obj)
|
| 79 |
+
elif isinstance(obj, np.ndarray):
|
| 80 |
+
return obj.tolist()
|
| 81 |
+
return obj
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@app.get("/health", response_model=HealthResponse)
|
| 85 |
+
async def health():
|
| 86 |
+
return HealthResponse(
|
| 87 |
+
status="healthy",
|
| 88 |
+
model_loaded=predictor is not None,
|
| 89 |
+
model_name="EfficientNet-B3",
|
| 90 |
+
version="1.0.0",
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
@app.post("/predict")
|
| 95 |
+
async def predict(
|
| 96 |
+
file: UploadFile = File(...),
|
| 97 |
+
city: str = Query("Delhi", description="City in India"),
|
| 98 |
+
state: str = Query("Delhi", description="State in India"),
|
| 99 |
+
):
|
| 100 |
+
if file.content_type not in ["image/jpeg", "image/png", "image/jpg"]:
|
| 101 |
+
raise HTTPException(400, "Only JPG/PNG images supported")
|
| 102 |
+
|
| 103 |
+
contents = await file.read()
|
| 104 |
+
if len(contents) > 10 * 1024 * 1024:
|
| 105 |
+
raise HTTPException(400, "File too large (max 10MB)")
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
image = Image.open(io.BytesIO(contents)).convert('RGB')
|
| 109 |
+
except Exception:
|
| 110 |
+
raise HTTPException(400, "Invalid image file")
|
| 111 |
+
|
| 112 |
+
start = time.time()
|
| 113 |
+
prediction = predictor.predict(image)
|
| 114 |
+
inference_time = time.time() - start
|
| 115 |
+
|
| 116 |
+
response = response_engine.generate_response(
|
| 117 |
+
predicted_class=prediction['predicted_class'],
|
| 118 |
+
confidence=prediction['confidence'],
|
| 119 |
+
all_probabilities=prediction['all_probabilities'],
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
hospital_result = hospital_finder.search(
|
| 123 |
+
query=response['hospital_search_query'],
|
| 124 |
+
city=city,
|
| 125 |
+
state=state,
|
| 126 |
+
)
|
| 127 |
+
response['maps_url'] = hospital_result['maps_url']
|
| 128 |
+
response['maps_embed_url'] = hospital_result['embed_url']
|
| 129 |
+
response['hospital_location'] = hospital_result['location']
|
| 130 |
+
response['inference_time'] = round(inference_time, 3)
|
| 131 |
+
response['emergency_numbers'] = hospital_finder.get_emergency_numbers()
|
| 132 |
+
|
| 133 |
+
return convert_numpy(response)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
if __name__ == "__main__":
|
| 137 |
+
import uvicorn
|
| 138 |
+
uvicorn.run("api.app:app", host="0.0.0.0", port=8000, reload=True)
|
api/schemas.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pydantic schemas for API."""
|
| 2 |
+
from pydantic import BaseModel, Field
|
| 3 |
+
from typing import List, Dict, Optional
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class PredictionResponse(BaseModel):
|
| 7 |
+
predicted_class: str
|
| 8 |
+
confidence: float
|
| 9 |
+
confidence_level: str
|
| 10 |
+
tier: str
|
| 11 |
+
severity: str
|
| 12 |
+
emoji: str
|
| 13 |
+
tagline: str
|
| 14 |
+
action: str
|
| 15 |
+
urgency_message: str
|
| 16 |
+
description: str
|
| 17 |
+
care_advice: List[str]
|
| 18 |
+
risk_factors: List[str]
|
| 19 |
+
hospital_search_query: str
|
| 20 |
+
hospital_type: str
|
| 21 |
+
differential_diagnosis: List[Dict]
|
| 22 |
+
cancer_alert: bool
|
| 23 |
+
cancer_warning: Optional[str] = None
|
| 24 |
+
all_probabilities: Dict[str, float]
|
| 25 |
+
disclaimer: str
|
| 26 |
+
maps_url: str
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class HealthResponse(BaseModel):
|
| 30 |
+
status: str
|
| 31 |
+
model_loaded: bool
|
| 32 |
+
model_name: str
|
| 33 |
+
version: str
|
configs/class_config.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"0": {
|
| 3 |
+
"name": "Melanoma",
|
| 4 |
+
"folder": "Melanoma",
|
| 5 |
+
"tier": "CANCER",
|
| 6 |
+
"severity": "CRITICAL",
|
| 7 |
+
"color": "#e74c3c"
|
| 8 |
+
},
|
| 9 |
+
"1": {
|
| 10 |
+
"name": "Basal Cell Carcinoma",
|
| 11 |
+
"folder": "Basal_Cell_Carcinoma",
|
| 12 |
+
"tier": "CANCER",
|
| 13 |
+
"severity": "HIGH",
|
| 14 |
+
"color": "#e67e22"
|
| 15 |
+
},
|
| 16 |
+
"2": {
|
| 17 |
+
"name": "Actinic Keratoses",
|
| 18 |
+
"folder": "Actinic_Keratoses",
|
| 19 |
+
"tier": "PRE-CANCER",
|
| 20 |
+
"severity": "MEDIUM",
|
| 21 |
+
"color": "#f39c12"
|
| 22 |
+
},
|
| 23 |
+
"3": {
|
| 24 |
+
"name": "Melanocytic Nevi",
|
| 25 |
+
"folder": "Melanocytic_Nevi",
|
| 26 |
+
"tier": "BENIGN",
|
| 27 |
+
"severity": "LOW",
|
| 28 |
+
"color": "#27ae60"
|
| 29 |
+
},
|
| 30 |
+
"4": {
|
| 31 |
+
"name": "Benign Keratosis",
|
| 32 |
+
"folder": "Benign_Keratosis",
|
| 33 |
+
"tier": "BENIGN",
|
| 34 |
+
"severity": "LOW",
|
| 35 |
+
"color": "#2ecc71"
|
| 36 |
+
},
|
| 37 |
+
"5": {
|
| 38 |
+
"name": "Dermatofibroma",
|
| 39 |
+
"folder": "Dermatofibroma",
|
| 40 |
+
"tier": "BENIGN",
|
| 41 |
+
"severity": "LOW",
|
| 42 |
+
"color": "#1abc9c"
|
| 43 |
+
},
|
| 44 |
+
"6": {
|
| 45 |
+
"name": "Vascular Lesions",
|
| 46 |
+
"folder": "Vascular_Lesions",
|
| 47 |
+
"tier": "BENIGN",
|
| 48 |
+
"severity": "LOW",
|
| 49 |
+
"color": "#16a085"
|
| 50 |
+
},
|
| 51 |
+
"7": {
|
| 52 |
+
"name": "Acne and Rosacea",
|
| 53 |
+
"folder": "Acne",
|
| 54 |
+
"tier": "DISEASE",
|
| 55 |
+
"severity": "LOW",
|
| 56 |
+
"color": "#3498db"
|
| 57 |
+
},
|
| 58 |
+
"8": {
|
| 59 |
+
"name": "Eczema",
|
| 60 |
+
"folder": "Eczema",
|
| 61 |
+
"tier": "DISEASE",
|
| 62 |
+
"severity": "LOW",
|
| 63 |
+
"color": "#2980b9"
|
| 64 |
+
},
|
| 65 |
+
"9": {
|
| 66 |
+
"name": "Psoriasis",
|
| 67 |
+
"folder": "Psoriasis",
|
| 68 |
+
"tier": "DISEASE",
|
| 69 |
+
"severity": "LOW",
|
| 70 |
+
"color": "#9b59b6"
|
| 71 |
+
},
|
| 72 |
+
"10": {
|
| 73 |
+
"name": "Fungal Infection",
|
| 74 |
+
"folder": "Fungal_Infection",
|
| 75 |
+
"tier": "DISEASE",
|
| 76 |
+
"severity": "LOW",
|
| 77 |
+
"color": "#8e44ad"
|
| 78 |
+
},
|
| 79 |
+
"11": {
|
| 80 |
+
"name": "Warts and Viral",
|
| 81 |
+
"folder": "Warts",
|
| 82 |
+
"tier": "DISEASE",
|
| 83 |
+
"severity": "LOW",
|
| 84 |
+
"color": "#34495e"
|
| 85 |
+
},
|
| 86 |
+
"12": {
|
| 87 |
+
"name": "Vitiligo",
|
| 88 |
+
"folder": "Vitiligo",
|
| 89 |
+
"tier": "DISEASE",
|
| 90 |
+
"severity": "LOW",
|
| 91 |
+
"color": "#7f8c8d"
|
| 92 |
+
}
|
| 93 |
+
}
|
configs/config.yaml
ADDED
|
File without changes
|
configs/india_cities.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Andhra Pradesh": ["Visakhapatnam", "Vijayawada", "Guntur", "Tirupati", "Nellore", "Kakinada", "Rajahmundry", "Kadapa", "Anantapur", "Kurnool"],
|
| 3 |
+
"Arunachal Pradesh": ["Itanagar", "Naharlagun", "Tawang", "Pasighat", "Ziro"],
|
| 4 |
+
"Assam": ["Guwahati", "Dibrugarh", "Silchar", "Jorhat", "Tezpur", "Nagaon", "Tinsukia"],
|
| 5 |
+
"Bihar": ["Patna", "Gaya", "Muzaffarpur", "Bhagalpur", "Darbhanga", "Purnia", "Ara", "Begusarai"],
|
| 6 |
+
"Chhattisgarh": ["Raipur", "Bhilai", "Bilaspur", "Korba", "Durg", "Rajnandgaon", "Jagdalpur"],
|
| 7 |
+
"Goa": ["Panaji", "Margao", "Vasco da Gama", "Mapusa", "Ponda"],
|
| 8 |
+
"Gujarat": ["Ahmedabad", "Surat", "Vadodara", "Rajkot", "Gandhinagar", "Bhavnagar", "Junagadh", "Jamnagar", "Anand", "Navsari", "Morbi", "Mehsana"],
|
| 9 |
+
"Haryana": ["Gurgaon", "Faridabad", "Panipat", "Ambala", "Karnal", "Hisar", "Rohtak", "Sonipat", "Yamunanagar", "Panchkula"],
|
| 10 |
+
"Himachal Pradesh": ["Shimla", "Manali", "Dharamshala", "Mandi", "Solan", "Kullu", "Hamirpur", "Una"],
|
| 11 |
+
"Jharkhand": ["Ranchi", "Jamshedpur", "Dhanbad", "Bokaro", "Hazaribagh", "Deoghar", "Giridih"],
|
| 12 |
+
"Karnataka": ["Bangalore", "Mysore", "Hubli", "Mangalore", "Belgaum", "Gulbarga", "Davangere", "Shimoga", "Tumkur", "Udupi"],
|
| 13 |
+
"Kerala": ["Thiruvananthapuram", "Kochi", "Kozhikode", "Thrissur", "Kannur", "Kollam", "Palakkad", "Alappuzha", "Malappuram"],
|
| 14 |
+
"Madhya Pradesh": ["Bhopal", "Indore", "Jabalpur", "Gwalior", "Ujjain", "Sagar", "Rewa", "Satna", "Dewas"],
|
| 15 |
+
"Maharashtra": ["Mumbai", "Pune", "Nagpur", "Thane", "Nashik", "Aurangabad", "Navi Mumbai", "Solapur", "Kolhapur", "Amravati", "Sangli", "Akola"],
|
| 16 |
+
"Manipur": ["Imphal", "Thoubal", "Bishnupur"],
|
| 17 |
+
"Meghalaya": ["Shillong", "Tura", "Jowai"],
|
| 18 |
+
"Mizoram": ["Aizawl", "Lunglei", "Champhai"],
|
| 19 |
+
"Nagaland": ["Kohima", "Dimapur", "Mokokchung", "Tuensang"],
|
| 20 |
+
"Odisha": ["Bhubaneswar", "Cuttack", "Rourkela", "Berhampur", "Sambalpur", "Puri", "Balasore"],
|
| 21 |
+
"Punjab": ["Chandigarh", "Ludhiana", "Amritsar", "Jalandhar", "Patiala", "Mohali", "Bathinda", "Pathankot"],
|
| 22 |
+
"Rajasthan": ["Jaipur", "Jodhpur", "Udaipur", "Kota", "Ajmer", "Bikaner", "Alwar", "Bharatpur", "Sikar", "Bhilwara"],
|
| 23 |
+
"Sikkim": ["Gangtok", "Namchi", "Pelling"],
|
| 24 |
+
"Tamil Nadu": ["Chennai", "Coimbatore", "Madurai", "Salem", "Trichy", "Vellore", "Tirunelveli", "Erode", "Thoothukudi", "Thanjavur"],
|
| 25 |
+
"Telangana": ["Hyderabad", "Warangal", "Nizamabad", "Karimnagar", "Secunderabad", "Khammam", "Mahbubnagar"],
|
| 26 |
+
"Tripura": ["Agartala", "Udaipur", "Dharmanagar"],
|
| 27 |
+
"Uttar Pradesh": ["Lucknow", "Noida", "Kanpur", "Agra", "Varanasi", "Ghaziabad", "Meerut", "Prayagraj", "Bareilly", "Aligarh", "Moradabad", "Gorakhpur", "Mathura"],
|
| 28 |
+
"Uttarakhand": ["Dehradun", "Haridwar", "Rishikesh", "Haldwani", "Nainital", "Roorkee", "Kashipur"],
|
| 29 |
+
"West Bengal": ["Kolkata", "Howrah", "Durgapur", "Siliguri", "Asansol", "Bardhaman", "Kharagpur", "Haldia"],
|
| 30 |
+
"Delhi": ["New Delhi", "Dwarka", "Rohini", "Saket", "Karol Bagh", "Lajpat Nagar", "Pitampura", "Janakpuri"],
|
| 31 |
+
"Chandigarh": ["Chandigarh"],
|
| 32 |
+
"Puducherry": ["Puducherry", "Karaikal"],
|
| 33 |
+
"Jammu and Kashmir": ["Srinagar", "Jammu", "Anantnag", "Baramulla", "Kathua", "Udhampur"],
|
| 34 |
+
"Ladakh": ["Leh", "Kargil"]
|
| 35 |
+
}
|
configs/response_templates.json
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Melanoma": {
|
| 3 |
+
"severity": "CRITICAL",
|
| 4 |
+
"tier": "CANCER",
|
| 5 |
+
"emoji": "🔴",
|
| 6 |
+
"tagline": "Possible Melanoma Detected",
|
| 7 |
+
"action": "SEEK IMMEDIATE MEDICAL ATTENTION",
|
| 8 |
+
"description": "Melanoma is the most serious type of skin cancer. It develops in the cells that give skin its color (melanocytes). Early detection is critical — when caught early, the 5-year survival rate is over 99%.",
|
| 9 |
+
"urgency_message": "Please consult a dermatologist or oncologist as soon as possible. Do NOT delay. Early treatment dramatically improves outcomes.",
|
| 10 |
+
"care_advice": [
|
| 11 |
+
"Do NOT try to treat this at home",
|
| 12 |
+
"Schedule an appointment with a dermatologist within this week",
|
| 13 |
+
"Take clear photos of the lesion for medical records",
|
| 14 |
+
"Note any recent changes in size, shape, color, or sensation",
|
| 15 |
+
"Avoid sun exposure on the affected area",
|
| 16 |
+
"Do not scratch, pick, or irritate the lesion"
|
| 17 |
+
],
|
| 18 |
+
"risk_factors": [
|
| 19 |
+
"History of sunburns or excessive UV exposure",
|
| 20 |
+
"Fair skin, light hair, or light eyes",
|
| 21 |
+
"Family history of melanoma",
|
| 22 |
+
"Large number of moles (50+)",
|
| 23 |
+
"Weakened immune system"
|
| 24 |
+
],
|
| 25 |
+
"hospital_search_query": "oncologist skin cancer specialist near me",
|
| 26 |
+
"hospital_type": "Cancer Hospital / Dermatology Specialist"
|
| 27 |
+
},
|
| 28 |
+
|
| 29 |
+
"Basal Cell Carcinoma": {
|
| 30 |
+
"severity": "HIGH",
|
| 31 |
+
"tier": "CANCER",
|
| 32 |
+
"emoji": "🟠",
|
| 33 |
+
"tagline": "Possible Basal Cell Carcinoma Detected",
|
| 34 |
+
"action": "CONSULT A DERMATOLOGIST PROMPTLY",
|
| 35 |
+
"description": "Basal Cell Carcinoma (BCC) is the most common form of skin cancer. While it rarely spreads to other parts of the body, it can cause significant local tissue damage if left untreated.",
|
| 36 |
+
"urgency_message": "Please schedule an appointment with a dermatologist within 2 weeks. BCC is highly treatable when caught early.",
|
| 37 |
+
"care_advice": [
|
| 38 |
+
"Schedule a dermatologist appointment within 2 weeks",
|
| 39 |
+
"Do not attempt to remove or treat the lesion yourself",
|
| 40 |
+
"Protect the area from sun exposure (SPF 50+)",
|
| 41 |
+
"Monitor for any changes in size or appearance",
|
| 42 |
+
"Take photos to track any changes before your appointment",
|
| 43 |
+
"Avoid tanning beds and prolonged sun exposure"
|
| 44 |
+
],
|
| 45 |
+
"risk_factors": [
|
| 46 |
+
"Chronic sun exposure over many years",
|
| 47 |
+
"History of sunburns",
|
| 48 |
+
"Fair skin",
|
| 49 |
+
"Age over 50",
|
| 50 |
+
"Previous history of skin cancer"
|
| 51 |
+
],
|
| 52 |
+
"hospital_search_query": "dermatologist skin cancer near me",
|
| 53 |
+
"hospital_type": "Dermatology Clinic / Skin Cancer Center"
|
| 54 |
+
},
|
| 55 |
+
|
| 56 |
+
"Actinic Keratoses": {
|
| 57 |
+
"severity": "MEDIUM",
|
| 58 |
+
"tier": "PRE-CANCER",
|
| 59 |
+
"emoji": "🟡",
|
| 60 |
+
"tagline": "Possible Pre-Cancerous Lesion Detected",
|
| 61 |
+
"action": "SCHEDULE A DERMATOLOGY APPOINTMENT",
|
| 62 |
+
"description": "Actinic Keratosis (AK) is a rough, scaly patch on the skin caused by years of sun exposure. It's considered pre-cancerous — about 5-10% of AKs can progress to squamous cell carcinoma if untreated.",
|
| 63 |
+
"urgency_message": "While not an emergency, please see a dermatologist within the next month. Early treatment prevents progression to cancer.",
|
| 64 |
+
"care_advice": [
|
| 65 |
+
"Schedule a dermatologist visit within 1 month",
|
| 66 |
+
"Apply broad-spectrum sunscreen (SPF 30+) daily",
|
| 67 |
+
"Wear protective clothing and hats outdoors",
|
| 68 |
+
"Avoid peak sun hours (10 AM – 4 PM)",
|
| 69 |
+
"Monitor the lesion for changes in size or texture",
|
| 70 |
+
"Do not pick or scratch the affected area",
|
| 71 |
+
"Keep the area moisturized"
|
| 72 |
+
],
|
| 73 |
+
"risk_factors": [
|
| 74 |
+
"Cumulative sun exposure",
|
| 75 |
+
"Fair skin",
|
| 76 |
+
"Age over 40",
|
| 77 |
+
"Living in sunny climates",
|
| 78 |
+
"Outdoor occupation"
|
| 79 |
+
],
|
| 80 |
+
"hospital_search_query": "dermatologist near me",
|
| 81 |
+
"hospital_type": "Dermatology Clinic"
|
| 82 |
+
},
|
| 83 |
+
|
| 84 |
+
"Melanocytic Nevi": {
|
| 85 |
+
"severity": "LOW",
|
| 86 |
+
"tier": "BENIGN",
|
| 87 |
+
"emoji": "🟢",
|
| 88 |
+
"tagline": "Common Mole (Melanocytic Nevus)",
|
| 89 |
+
"action": "MONITOR AT HOME",
|
| 90 |
+
"description": "This appears to be a common mole (melanocytic nevus). Moles are very common and usually harmless. Most adults have 10-40 moles. However, it's important to monitor moles for changes.",
|
| 91 |
+
"urgency_message": "No immediate medical attention needed. Continue regular self-examinations using the ABCDE rule.",
|
| 92 |
+
"care_advice": [
|
| 93 |
+
"Monitor monthly using the ABCDE rule:",
|
| 94 |
+
" A — Asymmetry: Is one half different from the other?",
|
| 95 |
+
" B — Border: Are edges irregular, ragged, or blurred?",
|
| 96 |
+
" C — Color: Is the color uneven or has it changed?",
|
| 97 |
+
" D — Diameter: Is it larger than 6mm (pencil eraser)?",
|
| 98 |
+
" E — Evolving: Has it changed in size, shape, or color?",
|
| 99 |
+
"Use sunscreen (SPF 30+) to protect moles from UV damage",
|
| 100 |
+
"See a dermatologist if any of the ABCDE signs appear",
|
| 101 |
+
"Annual skin check recommended for people with many moles"
|
| 102 |
+
],
|
| 103 |
+
"risk_factors": [],
|
| 104 |
+
"hospital_search_query": "dermatologist near me",
|
| 105 |
+
"hospital_type": "Dermatology Clinic"
|
| 106 |
+
},
|
| 107 |
+
|
| 108 |
+
"Benign Keratosis": {
|
| 109 |
+
"severity": "LOW",
|
| 110 |
+
"tier": "BENIGN",
|
| 111 |
+
"emoji": "🟢",
|
| 112 |
+
"tagline": "Benign Keratosis (Non-Cancerous Growth)",
|
| 113 |
+
"action": "NO TREATMENT USUALLY NEEDED",
|
| 114 |
+
"description": "This appears to be a benign keratosis, such as a seborrheic keratosis or solar lentigo. These are very common, harmless skin growths that typically appear with age. They are NOT cancerous.",
|
| 115 |
+
"urgency_message": "No medical attention needed unless the growth is bothersome, irritated, or you notice sudden changes.",
|
| 116 |
+
"care_advice": [
|
| 117 |
+
"No treatment is usually necessary",
|
| 118 |
+
"These growths are harmless and non-cancerous",
|
| 119 |
+
"See a doctor if the growth becomes irritated or bleeds",
|
| 120 |
+
"See a doctor if it changes rapidly in size or color",
|
| 121 |
+
"Can be removed for cosmetic reasons if desired",
|
| 122 |
+
"Protect skin from excessive sun exposure",
|
| 123 |
+
"Use moisturizer to keep the area comfortable"
|
| 124 |
+
],
|
| 125 |
+
"risk_factors": [],
|
| 126 |
+
"hospital_search_query": "dermatologist near me",
|
| 127 |
+
"hospital_type": "Dermatology Clinic"
|
| 128 |
+
},
|
| 129 |
+
|
| 130 |
+
"Dermatofibroma": {
|
| 131 |
+
"severity": "LOW",
|
| 132 |
+
"tier": "BENIGN",
|
| 133 |
+
"emoji": "🟢",
|
| 134 |
+
"tagline": "Dermatofibroma (Benign Skin Nodule)",
|
| 135 |
+
"action": "USUALLY NO TREATMENT NEEDED",
|
| 136 |
+
"description": "This appears to be a dermatofibroma — a common, harmless firm bump in the skin. They are benign and typically develop on the legs. They may result from minor injuries like insect bites.",
|
| 137 |
+
"urgency_message": "No medical attention needed. These are harmless. Consult a doctor only if it grows rapidly or becomes painful.",
|
| 138 |
+
"care_advice": [
|
| 139 |
+
"No treatment is usually necessary",
|
| 140 |
+
"Dermatofibromas are completely harmless",
|
| 141 |
+
"They may persist indefinitely but don't become cancerous",
|
| 142 |
+
"See a doctor if it grows rapidly or changes significantly",
|
| 143 |
+
"Surgical removal is possible if it's bothersome",
|
| 144 |
+
"Avoid repeated trauma to the area"
|
| 145 |
+
],
|
| 146 |
+
"risk_factors": [],
|
| 147 |
+
"hospital_search_query": "dermatologist near me",
|
| 148 |
+
"hospital_type": "Dermatology Clinic"
|
| 149 |
+
},
|
| 150 |
+
|
| 151 |
+
"Vascular Lesions": {
|
| 152 |
+
"severity": "LOW",
|
| 153 |
+
"tier": "BENIGN",
|
| 154 |
+
"emoji": "🟢",
|
| 155 |
+
"tagline": "Vascular Lesion (Blood Vessel Related)",
|
| 156 |
+
"action": "USUALLY HARMLESS — MONITOR",
|
| 157 |
+
"description": "This appears to be a vascular lesion, such as a cherry angioma or hemangioma. These are growths made up of blood vessels and are almost always benign. They are very common, especially after age 30.",
|
| 158 |
+
"urgency_message": "No immediate medical attention needed. These are cosmetic concerns only. See a doctor if it bleeds frequently or grows rapidly.",
|
| 159 |
+
"care_advice": [
|
| 160 |
+
"Vascular lesions are almost always harmless",
|
| 161 |
+
"No treatment needed unless cosmetically bothersome",
|
| 162 |
+
"See a doctor if it bleeds repeatedly or won't stop bleeding",
|
| 163 |
+
"See a doctor if it grows rapidly",
|
| 164 |
+
"Laser treatment or electrocautery can remove them if desired",
|
| 165 |
+
"Avoid picking or scratching the lesion"
|
| 166 |
+
],
|
| 167 |
+
"risk_factors": [],
|
| 168 |
+
"hospital_search_query": "dermatologist near me",
|
| 169 |
+
"hospital_type": "Dermatology Clinic"
|
| 170 |
+
},
|
| 171 |
+
|
| 172 |
+
"Acne and Rosacea": {
|
| 173 |
+
"severity": "LOW",
|
| 174 |
+
"tier": "DISEASE",
|
| 175 |
+
"emoji": "🔵",
|
| 176 |
+
"tagline": "Acne or Rosacea Detected",
|
| 177 |
+
"action": "MANAGEABLE WITH PROPER CARE",
|
| 178 |
+
"description": "This appears to be acne or rosacea — two of the most common skin conditions worldwide. Acne involves clogged pores and inflammation. Rosacea causes redness and visible blood vessels, usually on the face.",
|
| 179 |
+
"urgency_message": "Not urgent. Can be managed with proper skincare. See a dermatologist if over-the-counter treatments don't improve symptoms within 6-8 weeks.",
|
| 180 |
+
"care_advice": [
|
| 181 |
+
"Wash affected area gently twice daily with mild cleanser",
|
| 182 |
+
"Use non-comedogenic (won't clog pores) moisturizer and sunscreen",
|
| 183 |
+
"Avoid touching or picking at pimples — causes scarring",
|
| 184 |
+
"Over-the-counter options: benzoyl peroxide (2.5-5%) or salicylic acid (0.5-2%)",
|
| 185 |
+
"For rosacea: avoid triggers like spicy food, alcohol, hot drinks, extreme temperatures",
|
| 186 |
+
"Use lukewarm water — hot water worsens both conditions",
|
| 187 |
+
"Change pillowcases frequently",
|
| 188 |
+
"Consider seeing a dermatologist for persistent or severe cases",
|
| 189 |
+
"Prescription options (from doctor): retinoids, antibiotics, or azelaic acid"
|
| 190 |
+
],
|
| 191 |
+
"risk_factors": [
|
| 192 |
+
"Hormonal changes",
|
| 193 |
+
"Stress",
|
| 194 |
+
"Certain medications",
|
| 195 |
+
"Family history"
|
| 196 |
+
],
|
| 197 |
+
"hospital_search_query": "dermatologist acne treatment near me",
|
| 198 |
+
"hospital_type": "Dermatology Clinic"
|
| 199 |
+
},
|
| 200 |
+
|
| 201 |
+
"Eczema": {
|
| 202 |
+
"severity": "LOW",
|
| 203 |
+
"tier": "DISEASE",
|
| 204 |
+
"emoji": "🔵",
|
| 205 |
+
"tagline": "Eczema (Atopic Dermatitis) Detected",
|
| 206 |
+
"action": "MANAGEABLE WITH PROPER CARE",
|
| 207 |
+
"description": "This appears to be eczema (atopic dermatitis) — a chronic condition that causes the skin to become itchy, red, dry, and cracked. It's very common, affecting about 15-20% of children and 3% of adults.",
|
| 208 |
+
"urgency_message": "Not urgent. Can be managed with moisturizing and avoiding triggers. See a doctor if symptoms are severe, infected, or disrupting sleep.",
|
| 209 |
+
"care_advice": [
|
| 210 |
+
"Moisturize frequently — at least 2-3 times daily",
|
| 211 |
+
"Use thick, fragrance-free moisturizers (like CeraVe, Eucerin, or Vaseline)",
|
| 212 |
+
"Apply moisturizer within 3 minutes of bathing to lock in moisture",
|
| 213 |
+
"Take short, lukewarm baths/showers (not hot)",
|
| 214 |
+
"Use gentle, fragrance-free soap and laundry detergent",
|
| 215 |
+
"Wear soft, breathable cotton clothing",
|
| 216 |
+
"Avoid known triggers: harsh soaps, certain fabrics, stress, allergens",
|
| 217 |
+
"For flare-ups: OTC hydrocortisone cream (1%) for short periods (max 7 days)",
|
| 218 |
+
"Keep nails short to minimize damage from scratching",
|
| 219 |
+
"Use a humidifier in dry weather",
|
| 220 |
+
"See a doctor if: area becomes weepy, crusty, or shows signs of infection"
|
| 221 |
+
],
|
| 222 |
+
"risk_factors": [
|
| 223 |
+
"Family history of eczema, asthma, or allergies",
|
| 224 |
+
"Dry climate",
|
| 225 |
+
"Stress",
|
| 226 |
+
"Certain foods or allergens"
|
| 227 |
+
],
|
| 228 |
+
"hospital_search_query": "dermatologist eczema treatment near me",
|
| 229 |
+
"hospital_type": "Dermatology Clinic"
|
| 230 |
+
},
|
| 231 |
+
|
| 232 |
+
"Psoriasis": {
|
| 233 |
+
"severity": "LOW",
|
| 234 |
+
"tier": "DISEASE",
|
| 235 |
+
"emoji": "🔵",
|
| 236 |
+
"tagline": "Psoriasis Detected",
|
| 237 |
+
"action": "CHRONIC BUT MANAGEABLE",
|
| 238 |
+
"description": "This appears to be psoriasis — a chronic autoimmune condition that causes cells to build up rapidly on the skin surface, forming thick, silvery scales and itchy, dry, red patches. It affects about 2-3% of the population.",
|
| 239 |
+
"urgency_message": "Not an emergency. Psoriasis is a chronic condition that can be managed effectively. See a dermatologist for a treatment plan.",
|
| 240 |
+
"care_advice": [
|
| 241 |
+
"Keep skin well moisturized — apply thick cream after bathing",
|
| 242 |
+
"Take daily lukewarm baths with colloidal oatmeal or bath oil",
|
| 243 |
+
"Use OTC treatments: salicylic acid or coal tar products",
|
| 244 |
+
"Get moderate sun exposure (10-15 min) — helps many cases",
|
| 245 |
+
"But avoid sunburn — can trigger flare-ups (Koebner phenomenon)",
|
| 246 |
+
"Avoid skin injuries — cuts, scrapes can trigger new patches",
|
| 247 |
+
"Manage stress — meditation, exercise, adequate sleep",
|
| 248 |
+
"Avoid alcohol and smoking — both worsen psoriasis",
|
| 249 |
+
"Consider vitamin D supplements (consult doctor first)",
|
| 250 |
+
"See a dermatologist for prescription treatments if OTC doesn't help",
|
| 251 |
+
"Joint pain? Tell your doctor — psoriatic arthritis affects 30% of patients"
|
| 252 |
+
],
|
| 253 |
+
"risk_factors": [
|
| 254 |
+
"Family history of psoriasis",
|
| 255 |
+
"Stress",
|
| 256 |
+
"Smoking",
|
| 257 |
+
"Certain infections",
|
| 258 |
+
"Some medications (lithium, beta-blockers)"
|
| 259 |
+
],
|
| 260 |
+
"hospital_search_query": "dermatologist psoriasis treatment near me",
|
| 261 |
+
"hospital_type": "Dermatology Clinic"
|
| 262 |
+
},
|
| 263 |
+
|
| 264 |
+
"Fungal Infection": {
|
| 265 |
+
"severity": "LOW",
|
| 266 |
+
"tier": "DISEASE",
|
| 267 |
+
"emoji": "🔵",
|
| 268 |
+
"tagline": "Fungal Skin Infection Detected",
|
| 269 |
+
"action": "TREATABLE WITH ANTIFUNGAL CARE",
|
| 270 |
+
"description": "This appears to be a fungal skin infection such as ringworm (tinea), athlete's foot, or candidiasis. These are very common and highly treatable infections caused by fungi that thrive in warm, moist environments.",
|
| 271 |
+
"urgency_message": "Not urgent but should be treated to prevent spreading. Most cases resolve with over-the-counter antifungal treatment within 2-4 weeks.",
|
| 272 |
+
"care_advice": [
|
| 273 |
+
"Apply OTC antifungal cream/powder (clotrimazole, miconazole, or terbinafine)",
|
| 274 |
+
"Apply antifungal product to the affected area twice daily",
|
| 275 |
+
"Continue treatment for 1-2 weeks AFTER symptoms clear",
|
| 276 |
+
"Keep the affected area clean and DRY",
|
| 277 |
+
"Wash hands after touching the affected area",
|
| 278 |
+
"Don't share towels, clothing, or personal items",
|
| 279 |
+
"Wear loose, breathable clothing and cotton underwear",
|
| 280 |
+
"Change socks daily if feet are affected",
|
| 281 |
+
"Dry feet thoroughly after bathing, especially between toes",
|
| 282 |
+
"See a doctor if: no improvement after 2 weeks of OTC treatment",
|
| 283 |
+
"See a doctor if: infection covers a large area or affects scalp/nails"
|
| 284 |
+
],
|
| 285 |
+
"risk_factors": [
|
| 286 |
+
"Warm, humid environments",
|
| 287 |
+
"Tight clothing",
|
| 288 |
+
"Weakened immune system",
|
| 289 |
+
"Public showers/pools",
|
| 290 |
+
"Close contact with infected person or animal"
|
| 291 |
+
],
|
| 292 |
+
"hospital_search_query": "dermatologist fungal infection near me",
|
| 293 |
+
"hospital_type": "Dermatology Clinic / General Practitioner"
|
| 294 |
+
},
|
| 295 |
+
|
| 296 |
+
"Warts and Viral": {
|
| 297 |
+
"severity": "LOW",
|
| 298 |
+
"tier": "DISEASE",
|
| 299 |
+
"emoji": "🔵",
|
| 300 |
+
"tagline": "Wart or Viral Skin Infection Detected",
|
| 301 |
+
"action": "USUALLY RESOLVES — TREATMENT AVAILABLE",
|
| 302 |
+
"description": "This appears to be a wart or viral skin infection (such as molluscum contagiosum). Warts are caused by the Human Papillomavirus (HPV) and are very common, especially in children and young adults. They are usually harmless.",
|
| 303 |
+
"urgency_message": "Not urgent. Many warts resolve on their own within 1-2 years. Treatment can speed up removal if desired.",
|
| 304 |
+
"care_advice": [
|
| 305 |
+
"Many warts resolve on their own without treatment",
|
| 306 |
+
"OTC treatment: salicylic acid pads/solutions (apply daily for weeks)",
|
| 307 |
+
"OTC treatment: freeze-away products (cryotherapy kits)",
|
| 308 |
+
"Cover the wart with a bandage to prevent spreading",
|
| 309 |
+
"Don't pick, scratch, or bite warts — spreads the virus",
|
| 310 |
+
"Wash hands after touching warts",
|
| 311 |
+
"Don't share towels or personal items",
|
| 312 |
+
"Keep the area dry",
|
| 313 |
+
"See a doctor for: genital warts, facial warts, or painful warts",
|
| 314 |
+
"See a doctor if: warts multiply rapidly or don't respond to OTC treatment",
|
| 315 |
+
"Professional options: cryotherapy, laser, or prescription treatments"
|
| 316 |
+
],
|
| 317 |
+
"risk_factors": [
|
| 318 |
+
"Weakened immune system",
|
| 319 |
+
"Broken skin",
|
| 320 |
+
"Walking barefoot in public areas",
|
| 321 |
+
"Close contact with infected person"
|
| 322 |
+
],
|
| 323 |
+
"hospital_search_query": "dermatologist wart removal near me",
|
| 324 |
+
"hospital_type": "Dermatology Clinic / General Practitioner"
|
| 325 |
+
},
|
| 326 |
+
|
| 327 |
+
"Vitiligo": {
|
| 328 |
+
"severity": "LOW",
|
| 329 |
+
"tier": "DISEASE",
|
| 330 |
+
"emoji": "🔵",
|
| 331 |
+
"tagline": "Vitiligo (Pigmentation Disorder) Detected",
|
| 332 |
+
"action": "NOT DANGEROUS — TREATMENT AVAILABLE",
|
| 333 |
+
"description": "This appears to be vitiligo — a condition where the skin loses its pigment cells (melanocytes), resulting in white patches. Vitiligo is not contagious, not painful, and not medically dangerous, but it can have significant psychological impact.",
|
| 334 |
+
"urgency_message": "Not medically urgent. Vitiligo is not dangerous but can affect quality of life. See a dermatologist to discuss treatment options.",
|
| 335 |
+
"care_advice": [
|
| 336 |
+
"Vitiligo is NOT contagious and NOT dangerous",
|
| 337 |
+
"Protect depigmented areas from sunburn — use SPF 50+ sunscreen",
|
| 338 |
+
"Depigmented skin burns very easily",
|
| 339 |
+
"See a dermatologist for treatment options if desired",
|
| 340 |
+
"Treatment options include: topical corticosteroids, calcineurin inhibitors",
|
| 341 |
+
"Phototherapy (light therapy) can help repigmentation",
|
| 342 |
+
"Cosmetic options: camouflage makeup, self-tanners",
|
| 343 |
+
"Join a support group — psychological support is important",
|
| 344 |
+
"Manage stress — can worsen vitiligo",
|
| 345 |
+
"Eat a balanced diet rich in antioxidants",
|
| 346 |
+
"Some patients benefit from vitamin B12 and folic acid (consult doctor)"
|
| 347 |
+
],
|
| 348 |
+
"risk_factors": [
|
| 349 |
+
"Family history of vitiligo",
|
| 350 |
+
"Autoimmune conditions (thyroid disease, type 1 diabetes)",
|
| 351 |
+
"Stress",
|
| 352 |
+
"Skin trauma"
|
| 353 |
+
],
|
| 354 |
+
"hospital_search_query": "dermatologist vitiligo treatment near me",
|
| 355 |
+
"hospital_type": "Dermatology Clinic"
|
| 356 |
+
}
|
| 357 |
+
}
|
frontend/app.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
=================================================================
|
| 3 |
+
DERMASCAN-AI — Professional Medical UI
|
| 4 |
+
Production Grade Healthcare Interface
|
| 5 |
+
=================================================================
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import streamlit as st
|
| 10 |
+
import requests
|
| 11 |
+
from PIL import Image
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
# Import components
|
| 15 |
+
from components.header import render_header
|
| 16 |
+
from components.sidebar import render_sidebar
|
| 17 |
+
from components.result_card import render_severity_banner, render_metrics, TIER_ICONS
|
| 18 |
+
from components.confidence_chart import render_confidence_chart
|
| 19 |
+
from components.care_advice_card import render_care_advice
|
| 20 |
+
from components.hospital_map import render_hospital_map
|
| 21 |
+
|
| 22 |
+
# ═══════════════════════════════════════════════════════════
|
| 23 |
+
# PAGE CONFIG
|
| 24 |
+
# ═══════════════════════════════════════════════════════════
|
| 25 |
+
st.set_page_config(
|
| 26 |
+
page_title="DermaScan AI | Advanced Dermatology Analysis",
|
| 27 |
+
page_icon="🏥",
|
| 28 |
+
layout="wide",
|
| 29 |
+
initial_sidebar_state="expanded",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
API_URL = "http://localhost:8000"
|
| 33 |
+
|
| 34 |
+
# ═══════════════════════════════════════════════════════════
|
| 35 |
+
# LOAD EXTERNAL DATA & STYLES
|
| 36 |
+
# ═══════════════════════════════════════════════════════════
|
| 37 |
+
config_dir = Path(__file__).parent.parent / "configs"
|
| 38 |
+
|
| 39 |
+
with open(config_dir / "india_cities.json", "r", encoding="utf-8") as f:
|
| 40 |
+
STATE_CITIES = json.load(f)
|
| 41 |
+
|
| 42 |
+
# Load CSS
|
| 43 |
+
css_file = Path(__file__).parent / "assets" / "style.css"
|
| 44 |
+
with open(css_file, "r", encoding="utf-8") as f:
|
| 45 |
+
st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
|
| 46 |
+
|
| 47 |
+
# Remove tooltips with JavaScript
|
| 48 |
+
st.markdown("""
|
| 49 |
+
<script>
|
| 50 |
+
// Remove all title attributes that cause tooltips
|
| 51 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 52 |
+
function removeTooltips() {
|
| 53 |
+
const elements = document.querySelectorAll('[title]');
|
| 54 |
+
elements.forEach(el => {
|
| 55 |
+
if (el.getAttribute('title') === 'keyboard_double') {
|
| 56 |
+
el.removeAttribute('title');
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Run immediately
|
| 62 |
+
removeTooltips();
|
| 63 |
+
|
| 64 |
+
// Run periodically to catch dynamically added elements
|
| 65 |
+
setInterval(removeTooltips, 500);
|
| 66 |
+
|
| 67 |
+
// Also run on mutations
|
| 68 |
+
const observer = new MutationObserver(removeTooltips);
|
| 69 |
+
observer.observe(document.body, {
|
| 70 |
+
childList: true,
|
| 71 |
+
subtree: true,
|
| 72 |
+
attributes: true,
|
| 73 |
+
attributeFilter: ['title']
|
| 74 |
+
});
|
| 75 |
+
});
|
| 76 |
+
</script>
|
| 77 |
+
""", unsafe_allow_html=True)
|
| 78 |
+
|
| 79 |
+
# ═══════════════════════════════════════════════════════════
|
| 80 |
+
# SIDEBAR
|
| 81 |
+
# ═══════════════════════════════════════════════════════════
|
| 82 |
+
selected_state, selected_city = render_sidebar(STATE_CITIES)
|
| 83 |
+
|
| 84 |
+
# ═══════════════════════════════════════════════════════════
|
| 85 |
+
# HEADER
|
| 86 |
+
# ═══════════════════════════════════════════════════════════
|
| 87 |
+
render_header()
|
| 88 |
+
|
| 89 |
+
# ═══════════════════════════════════════════════════════════
|
| 90 |
+
# UPLOAD SECTION
|
| 91 |
+
# ═══════════════════════════════════════════════════════════
|
| 92 |
+
uploaded_file = st.file_uploader(
|
| 93 |
+
"Upload a skin image for analysis",
|
| 94 |
+
type=["jpg", "jpeg", "png"],
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
if uploaded_file:
|
| 98 |
+
img_col, action_col = st.columns([1, 2])
|
| 99 |
+
|
| 100 |
+
with img_col:
|
| 101 |
+
image = Image.open(uploaded_file)
|
| 102 |
+
st.markdown('<div class="image-container">', unsafe_allow_html=True)
|
| 103 |
+
st.image(image, caption="📸 Uploaded Image", width="stretch")
|
| 104 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 105 |
+
|
| 106 |
+
with action_col:
|
| 107 |
+
st.markdown(f"**📍 Location:** {selected_city}, {selected_state}")
|
| 108 |
+
st.markdown("")
|
| 109 |
+
|
| 110 |
+
analyze = st.button("🔬 Analyze Image", width="stretch")
|
| 111 |
+
|
| 112 |
+
if analyze:
|
| 113 |
+
with st.spinner("🔄 Analyzing your image with AI..."):
|
| 114 |
+
try:
|
| 115 |
+
files = {
|
| 116 |
+
"file": (
|
| 117 |
+
uploaded_file.name,
|
| 118 |
+
uploaded_file.getvalue(),
|
| 119 |
+
uploaded_file.type,
|
| 120 |
+
)
|
| 121 |
+
}
|
| 122 |
+
params = {"city": selected_city, "state": selected_state}
|
| 123 |
+
|
| 124 |
+
resp = requests.post(
|
| 125 |
+
f"{API_URL}/predict",
|
| 126 |
+
files=files,
|
| 127 |
+
params=params,
|
| 128 |
+
timeout=60,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
if resp.status_code == 200:
|
| 132 |
+
st.session_state["result"] = resp.json()
|
| 133 |
+
st.success("✅ Analysis complete!")
|
| 134 |
+
st.rerun()
|
| 135 |
+
else:
|
| 136 |
+
st.error(f"❌ Server error: {resp.text}")
|
| 137 |
+
|
| 138 |
+
except requests.exceptions.ConnectionError:
|
| 139 |
+
st.error(
|
| 140 |
+
"⚠️ Cannot connect to API server. "
|
| 141 |
+
"Open another terminal and run: `python -m api.app`"
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
st.markdown("""
|
| 145 |
+
<div class="pro-card">
|
| 146 |
+
<h3>💡 Tips for Best Results</h3>
|
| 147 |
+
<p>
|
| 148 |
+
✓ Use a clear, well-lit close-up photo<br>
|
| 149 |
+
✓ Center the affected area in the frame<br>
|
| 150 |
+
✓ Keep camera 10-15 cm from the skin<br>
|
| 151 |
+
✓ Avoid shadows and reflections<br>
|
| 152 |
+
✓ Use natural lighting when possible
|
| 153 |
+
</p>
|
| 154 |
+
</div>
|
| 155 |
+
""", unsafe_allow_html=True)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# ═══════════════════════════════════════════════════════════
|
| 159 |
+
# RESULTS SECTION
|
| 160 |
+
# ═══════════════════════════════════════════════════════════
|
| 161 |
+
if "result" in st.session_state:
|
| 162 |
+
result = st.session_state["result"]
|
| 163 |
+
|
| 164 |
+
st.markdown("---")
|
| 165 |
+
|
| 166 |
+
# Severity Banner
|
| 167 |
+
render_severity_banner(result)
|
| 168 |
+
|
| 169 |
+
# Key Metrics
|
| 170 |
+
render_metrics(result)
|
| 171 |
+
|
| 172 |
+
# Cancer Warning
|
| 173 |
+
cancer_warning = result.get("cancer_warning", "")
|
| 174 |
+
if cancer_warning:
|
| 175 |
+
st.markdown(
|
| 176 |
+
f'<div class="warning-box">'
|
| 177 |
+
f'<div class="warning-box-icon">⚠️</div>'
|
| 178 |
+
f"<div><strong>MEDICAL ALERT:</strong> {cancer_warning}</div>"
|
| 179 |
+
f"</div>",
|
| 180 |
+
unsafe_allow_html=True,
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
# Tabs
|
| 184 |
+
tab1, tab2, tab3, tab4 = st.tabs(
|
| 185 |
+
["📋 Diagnosis", "📊 Confidence Analysis", "💊 Care Advice", "🏥 Find Hospitals"]
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
# Tab 1: Diagnosis
|
| 189 |
+
with tab1:
|
| 190 |
+
st.markdown(
|
| 191 |
+
f'<div class="pro-card">'
|
| 192 |
+
f'<h3>🔬 {result["predicted_class"]}</h3>'
|
| 193 |
+
f'<p>{result.get("description", "")}</p>'
|
| 194 |
+
f'<p style="margin-top:1rem;padding:0.8rem;background:#334155;border-radius:8px;">'
|
| 195 |
+
f'<strong>⏰ {result.get("urgency_message", "")}</strong></p>'
|
| 196 |
+
f"</div>",
|
| 197 |
+
unsafe_allow_html=True,
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
st.markdown(
|
| 201 |
+
f'<div class="pro-card">'
|
| 202 |
+
f"<h3>🎯 AI Confidence Assessment</h3>"
|
| 203 |
+
f'<p>{result.get("confidence_message", "")}</p>'
|
| 204 |
+
f"</div>",
|
| 205 |
+
unsafe_allow_html=True,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
diff = result.get("differential_diagnosis", [])
|
| 209 |
+
if len(diff) > 1:
|
| 210 |
+
diff_html = '<div class="pro-card"><h3>🔍 Differential Diagnosis</h3>'
|
| 211 |
+
diff_html += '<p style="margin-bottom:1rem;color:#cbd5e1;">Other possible conditions to consider:</p>'
|
| 212 |
+
for d in diff:
|
| 213 |
+
d_icon = TIER_ICONS.get(d.get("tier", ""), "⚪")
|
| 214 |
+
d_prob = d.get("probability", 0)
|
| 215 |
+
diff_html += (
|
| 216 |
+
f'<div class="info-item">'
|
| 217 |
+
f'<div class="info-item-icon">{d_icon}</div>'
|
| 218 |
+
f'<div><strong>{d["class_name"]}</strong> — Probability: {d_prob:.1%}</div>'
|
| 219 |
+
f"</div>"
|
| 220 |
+
)
|
| 221 |
+
diff_html += "</div>"
|
| 222 |
+
st.markdown(diff_html, unsafe_allow_html=True)
|
| 223 |
+
|
| 224 |
+
# Tab 2: Confidence Chart
|
| 225 |
+
with tab2:
|
| 226 |
+
render_confidence_chart(result)
|
| 227 |
+
|
| 228 |
+
# Tab 3: Care Advice
|
| 229 |
+
with tab3:
|
| 230 |
+
render_care_advice(result)
|
| 231 |
+
|
| 232 |
+
# Tab 4: Hospitals
|
| 233 |
+
with tab4:
|
| 234 |
+
render_hospital_map(result, selected_city, selected_state)
|
| 235 |
+
|
| 236 |
+
# Disclaimer
|
| 237 |
+
disclaimer = result.get(
|
| 238 |
+
"disclaimer",
|
| 239 |
+
"This is an AI tool for educational purposes only. "
|
| 240 |
+
"Not a substitute for professional medical diagnosis.",
|
| 241 |
+
)
|
| 242 |
+
st.markdown(
|
| 243 |
+
f'<div class="disclaimer-box">'
|
| 244 |
+
f"<strong>⚕️ MEDICAL DISCLAIMER:</strong> {disclaimer}"
|
| 245 |
+
f"</div>",
|
| 246 |
+
unsafe_allow_html=True,
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
inf_t = result.get("inference_time", 0)
|
| 250 |
+
st.markdown(
|
| 251 |
+
f'<p style="text-align:center;color:#94a3b8;font-size:0.85rem;margin-top:1.5rem;">'
|
| 252 |
+
f'⚡ Analysis completed in {inf_t:.2f}s | 🧠 EfficientNet-B3 | 🏥 DermaScan AI v1.0'
|
| 253 |
+
f'</p>',
|
| 254 |
+
unsafe_allow_html=True
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
elif not uploaded_file:
|
| 258 |
+
st.markdown(
|
| 259 |
+
'<div class="upload-placeholder">'
|
| 260 |
+
'<div class="icon">📸</div>'
|
| 261 |
+
"<h3>Upload a Skin Image to Begin Analysis</h3>"
|
| 262 |
+
"<p>Our advanced AI system will analyze the image, identify potential conditions, "
|
| 263 |
+
"provide personalized care recommendations, and help you locate nearby medical facilities.</p>"
|
| 264 |
+
"</div>",
|
| 265 |
+
unsafe_allow_html=True,
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# ═══════════════════════════════════════════════════════════
|
| 269 |
+
# FOOTER
|
| 270 |
+
# ═══════════════════════════════════════════════════════════
|
| 271 |
+
st.markdown("---")
|
| 272 |
+
st.markdown(
|
| 273 |
+
"<p style='text-align:center;color:#94a3b8;font-size:0.8rem;line-height:1.8;'>"
|
| 274 |
+
"🏥 <strong>DermaScan AI</strong> | 🧠 EfficientNet-B3 Architecture | 📊 HAM10000 + DermNet Dataset<br>"
|
| 275 |
+
"🔬 13 Skin Conditions | 🎯 96% AUC-ROC Accuracy | ⚡ Real-time Analysis<br>"
|
| 276 |
+
"🛠️ Built with PyTorch • FastAPI • Streamlit<br>"
|
| 277 |
+
"<em>For educational and research purposes only. Not a substitute for professional medical advice.</em>"
|
| 278 |
+
"</p>",
|
| 279 |
+
unsafe_allow_html=True,
|
| 280 |
+
)
|
frontend/assets/style.css
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ═══════════════════════════════════════════════════════════
|
| 2 |
+
DERMASCAN AI - PROFESSIONAL MEDICAL UI STYLES
|
| 3 |
+
Dark Mode Compatible - Production Grade
|
| 4 |
+
═══════════════════════════════════════════════════════════ */
|
| 5 |
+
|
| 6 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Poppins:wght@400;500;600;700;800&display=swap');
|
| 7 |
+
|
| 8 |
+
/* ═══════════════════════════════════════════════════════════
|
| 9 |
+
CSS VARIABLES - DARK MODE THEME
|
| 10 |
+
═══════════════════════════════════════════════════════════ */
|
| 11 |
+
:root {
|
| 12 |
+
--primary: #3b82f6;
|
| 13 |
+
--primary-dark: #2563eb;
|
| 14 |
+
--primary-light: #60a5fa;
|
| 15 |
+
--secondary: #10b981;
|
| 16 |
+
--bg-main: #0f172a;
|
| 17 |
+
--bg-card: #1e293b;
|
| 18 |
+
--bg-elevated: #334155;
|
| 19 |
+
--border: #334155;
|
| 20 |
+
--border-light: #475569;
|
| 21 |
+
--text-primary: #f1f5f9;
|
| 22 |
+
--text-secondary: #cbd5e1;
|
| 23 |
+
--text-muted: #94a3b8;
|
| 24 |
+
--shadow-sm: 0 1px 2px 0 rgba(0,0,0,0.3);
|
| 25 |
+
--shadow: 0 4px 6px -1px rgba(0,0,0,0.4), 0 2px 4px -1px rgba(0,0,0,0.3);
|
| 26 |
+
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.5), 0 4px 6px -2px rgba(0,0,0,0.4);
|
| 27 |
+
--shadow-xl: 0 20px 25px -5px rgba(0,0,0,0.6), 0 10px 10px -5px rgba(0,0,0,0.5);
|
| 28 |
+
--red: #ef4444;
|
| 29 |
+
--red-light: #7f1d1d;
|
| 30 |
+
--orange: #f97316;
|
| 31 |
+
--orange-light: #7c2d12;
|
| 32 |
+
--yellow: #f59e0b;
|
| 33 |
+
--yellow-light: #78350f;
|
| 34 |
+
--green: #10b981;
|
| 35 |
+
--green-light: #064e3b;
|
| 36 |
+
--blue: #3b82f6;
|
| 37 |
+
--blue-light: #1e3a8a;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* ═══════════════════════════════════════════════════════════
|
| 41 |
+
GLOBAL STYLES
|
| 42 |
+
═══════════════════════════════════════════════════════════ */
|
| 43 |
+
body, p, h1, h2, h3, h4, h5, h6, div, span, label, button {
|
| 44 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif !important;
|
| 45 |
+
color: var(--text-primary) !important;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
h1, h2, h3 {
|
| 49 |
+
font-family: 'Poppins', 'Inter', sans-serif !important;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
#MainMenu, footer, header { visibility: hidden; }
|
| 53 |
+
|
| 54 |
+
.block-container {
|
| 55 |
+
padding-top: 2rem !important;
|
| 56 |
+
padding-bottom: 3rem !important;
|
| 57 |
+
max-width: 1200px !important;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* ═══════════════════════════════════════════════════════════
|
| 61 |
+
HEADER
|
| 62 |
+
═══════════════════════════════════════════════════════════ */
|
| 63 |
+
.medical-header {
|
| 64 |
+
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #3b82f6 100%);
|
| 65 |
+
border-radius: 20px;
|
| 66 |
+
padding: 2.5rem 2rem;
|
| 67 |
+
text-align: center;
|
| 68 |
+
margin-bottom: 2rem;
|
| 69 |
+
box-shadow: var(--shadow-xl);
|
| 70 |
+
position: relative;
|
| 71 |
+
overflow: hidden;
|
| 72 |
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
.medical-header::before {
|
| 76 |
+
content: '';
|
| 77 |
+
position: absolute;
|
| 78 |
+
top: 0;
|
| 79 |
+
left: 0;
|
| 80 |
+
right: 0;
|
| 81 |
+
bottom: 0;
|
| 82 |
+
background: url('data:image/svg+xml,<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"><defs><pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse"><path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="1"/></pattern></defs><rect width="100" height="100" fill="url(%23grid)"/></svg>');
|
| 83 |
+
opacity: 0.3;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.medical-header-content {
|
| 87 |
+
position: relative;
|
| 88 |
+
z-index: 1;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.medical-header h1 {
|
| 92 |
+
color: white !important;
|
| 93 |
+
font-size: 2.5rem;
|
| 94 |
+
font-weight: 800;
|
| 95 |
+
margin: 0;
|
| 96 |
+
letter-spacing: -0.5px;
|
| 97 |
+
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.medical-header .subtitle {
|
| 101 |
+
color: rgba(255,255,255,0.95) !important;
|
| 102 |
+
margin: 0.8rem 0 1.2rem;
|
| 103 |
+
font-size: 1.1rem;
|
| 104 |
+
font-weight: 500;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.medical-header .badges {
|
| 108 |
+
display: flex;
|
| 109 |
+
gap: 0.6rem;
|
| 110 |
+
justify-content: center;
|
| 111 |
+
flex-wrap: wrap;
|
| 112 |
+
margin-top: 1rem;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.badge {
|
| 116 |
+
background: rgba(255,255,255,0.15);
|
| 117 |
+
backdrop-filter: blur(10px);
|
| 118 |
+
border: 1px solid rgba(255,255,255,0.25);
|
| 119 |
+
color: white !important;
|
| 120 |
+
padding: 0.4rem 1rem;
|
| 121 |
+
border-radius: 50px;
|
| 122 |
+
font-size: 0.85rem;
|
| 123 |
+
font-weight: 600;
|
| 124 |
+
display: inline-flex;
|
| 125 |
+
align-items: center;
|
| 126 |
+
gap: 0.4rem;
|
| 127 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* ══════════════════════════════════════════��════════════════
|
| 131 |
+
CARDS
|
| 132 |
+
═══════════════════════════════════════════════════════════ */
|
| 133 |
+
.pro-card {
|
| 134 |
+
background: var(--bg-card);
|
| 135 |
+
border: 1px solid var(--border);
|
| 136 |
+
border-radius: 16px;
|
| 137 |
+
padding: 1.5rem;
|
| 138 |
+
margin-bottom: 1rem;
|
| 139 |
+
box-shadow: var(--shadow);
|
| 140 |
+
transition: all 0.3s ease;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.pro-card:hover {
|
| 144 |
+
box-shadow: var(--shadow-lg);
|
| 145 |
+
transform: translateY(-2px);
|
| 146 |
+
border-color: var(--border-light);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.pro-card h3 {
|
| 150 |
+
color: var(--text-primary) !important;
|
| 151 |
+
margin: 0 0 0.8rem;
|
| 152 |
+
font-size: 1.2rem;
|
| 153 |
+
font-weight: 700;
|
| 154 |
+
display: flex;
|
| 155 |
+
align-items: center;
|
| 156 |
+
gap: 0.5rem;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.pro-card p {
|
| 160 |
+
color: var(--text-secondary) !important;
|
| 161 |
+
margin: 0;
|
| 162 |
+
line-height: 1.7;
|
| 163 |
+
font-size: 0.95rem;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
/* ═══════════════════════════════════════════════════════════
|
| 167 |
+
SEVERITY BANNERS
|
| 168 |
+
═══════════════════════════════════════════════════════════ */
|
| 169 |
+
.severity-banner {
|
| 170 |
+
border-radius: 16px;
|
| 171 |
+
padding: 1.8rem 2rem;
|
| 172 |
+
margin-bottom: 1.5rem;
|
| 173 |
+
box-shadow: var(--shadow-lg);
|
| 174 |
+
border-left: 6px solid;
|
| 175 |
+
position: relative;
|
| 176 |
+
overflow: hidden;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.severity-banner h2 {
|
| 180 |
+
margin: 0;
|
| 181 |
+
font-size: 1.6rem;
|
| 182 |
+
font-weight: 800;
|
| 183 |
+
display: flex;
|
| 184 |
+
align-items: center;
|
| 185 |
+
gap: 0.6rem;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.severity-banner h3 {
|
| 189 |
+
margin: 0.6rem 0 0;
|
| 190 |
+
font-size: 1.05rem;
|
| 191 |
+
font-weight: 500;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.banner-critical {
|
| 195 |
+
background: linear-gradient(135deg, var(--red-light) 0%, #991b1b 100%);
|
| 196 |
+
border-left-color: var(--red);
|
| 197 |
+
}
|
| 198 |
+
.banner-critical h2, .banner-critical h3 { color: #fca5a5 !important; }
|
| 199 |
+
|
| 200 |
+
.banner-high {
|
| 201 |
+
background: linear-gradient(135deg, var(--orange-light) 0%, #9a3412 100%);
|
| 202 |
+
border-left-color: var(--orange);
|
| 203 |
+
}
|
| 204 |
+
.banner-high h2, .banner-high h3 { color: #fdba74 !important; }
|
| 205 |
+
|
| 206 |
+
.banner-medium {
|
| 207 |
+
background: linear-gradient(135deg, var(--yellow-light) 0%, #92400e 100%);
|
| 208 |
+
border-left-color: var(--yellow);
|
| 209 |
+
}
|
| 210 |
+
.banner-medium h2, .banner-medium h3 { color: #fcd34d !important; }
|
| 211 |
+
|
| 212 |
+
.banner-low {
|
| 213 |
+
background: linear-gradient(135deg, var(--green-light) 0%, #065f46 100%);
|
| 214 |
+
border-left-color: var(--green);
|
| 215 |
+
}
|
| 216 |
+
.banner-low h2, .banner-low h3 { color: #86efac !important; }
|
| 217 |
+
|
| 218 |
+
/* ═══════════════════════════════════════════════════════════
|
| 219 |
+
METRICS
|
| 220 |
+
═══════════════════════════════════════════════════════════ */
|
| 221 |
+
.metric-card {
|
| 222 |
+
background: var(--bg-card);
|
| 223 |
+
border: 2px solid var(--border);
|
| 224 |
+
border-radius: 16px;
|
| 225 |
+
padding: 1.5rem;
|
| 226 |
+
text-align: center;
|
| 227 |
+
min-height: 140px;
|
| 228 |
+
box-shadow: var(--shadow);
|
| 229 |
+
transition: all 0.3s ease;
|
| 230 |
+
display: flex;
|
| 231 |
+
flex-direction: column;
|
| 232 |
+
justify-content: center;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.metric-card:hover {
|
| 236 |
+
box-shadow: var(--shadow-lg);
|
| 237 |
+
transform: translateY(-4px);
|
| 238 |
+
border-color: var(--primary);
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.metric-card .label {
|
| 242 |
+
color: var(--text-muted) !important;
|
| 243 |
+
font-size: 0.75rem;
|
| 244 |
+
text-transform: uppercase;
|
| 245 |
+
letter-spacing: 1.5px;
|
| 246 |
+
font-weight: 700;
|
| 247 |
+
margin-bottom: 0.5rem;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.metric-card .value {
|
| 251 |
+
font-size: 2rem;
|
| 252 |
+
font-weight: 800;
|
| 253 |
+
margin: 0.5rem 0;
|
| 254 |
+
font-family: 'Poppins', sans-serif;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.metric-card .sublabel {
|
| 258 |
+
color: var(--text-muted) !important;
|
| 259 |
+
font-size: 0.8rem;
|
| 260 |
+
font-weight: 500;
|
| 261 |
+
margin-top: 0.3rem;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/* ═══════════════════════════════════════════════════════════
|
| 265 |
+
INFO ITEMS
|
| 266 |
+
═══════════════════════════════════════════════════════════ */
|
| 267 |
+
.info-item {
|
| 268 |
+
background: var(--bg-elevated);
|
| 269 |
+
border: 1px solid var(--border);
|
| 270 |
+
border-radius: 12px;
|
| 271 |
+
padding: 1rem 1.2rem;
|
| 272 |
+
margin-bottom: 0.6rem;
|
| 273 |
+
color: var(--text-secondary) !important;
|
| 274 |
+
font-size: 0.92rem;
|
| 275 |
+
line-height: 1.6;
|
| 276 |
+
display: flex;
|
| 277 |
+
align-items: flex-start;
|
| 278 |
+
gap: 0.8rem;
|
| 279 |
+
transition: all 0.2s ease;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.info-item:hover {
|
| 283 |
+
background: var(--bg-card);
|
| 284 |
+
box-shadow: var(--shadow-sm);
|
| 285 |
+
transform: translateX(4px);
|
| 286 |
+
border-color: var(--border-light);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.info-item-icon {
|
| 290 |
+
font-size: 1.2rem;
|
| 291 |
+
flex-shrink: 0;
|
| 292 |
+
margin-top: 0.1rem;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
/* ══════════════════════════════════════════���════════════════
|
| 296 |
+
WARNING & DISCLAIMER
|
| 297 |
+
═══════════════════════════════════════════════════════════ */
|
| 298 |
+
.warning-box {
|
| 299 |
+
background: linear-gradient(135deg, var(--red-light) 0%, #991b1b 100%);
|
| 300 |
+
border: 2px solid var(--red);
|
| 301 |
+
border-radius: 12px;
|
| 302 |
+
padding: 1.2rem 1.5rem;
|
| 303 |
+
color: #fca5a5 !important;
|
| 304 |
+
font-size: 0.95rem;
|
| 305 |
+
margin: 1rem 0;
|
| 306 |
+
box-shadow: var(--shadow);
|
| 307 |
+
display: flex;
|
| 308 |
+
align-items: flex-start;
|
| 309 |
+
gap: 1rem;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.warning-box-icon {
|
| 313 |
+
font-size: 1.5rem;
|
| 314 |
+
flex-shrink: 0;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
.disclaimer-box {
|
| 318 |
+
background: var(--yellow-light);
|
| 319 |
+
border: 2px solid var(--yellow);
|
| 320 |
+
border-radius: 12px;
|
| 321 |
+
padding: 1rem 1.5rem;
|
| 322 |
+
color: #fcd34d !important;
|
| 323 |
+
font-size: 0.85rem;
|
| 324 |
+
line-height: 1.6;
|
| 325 |
+
margin: 1.5rem 0;
|
| 326 |
+
box-shadow: var(--shadow-sm);
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
/* ═══════════════════════════════════════════════════════════
|
| 330 |
+
EMERGENCY CARD
|
| 331 |
+
═══════════════════════════════════════════════════════════ */
|
| 332 |
+
.emergency-card {
|
| 333 |
+
background: linear-gradient(135deg, var(--red-light) 0%, #991b1b 100%);
|
| 334 |
+
border: 2px solid var(--red);
|
| 335 |
+
border-radius: 12px;
|
| 336 |
+
padding: 1.2rem;
|
| 337 |
+
margin-top: 1rem;
|
| 338 |
+
box-shadow: var(--shadow);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.emergency-card h4 {
|
| 342 |
+
color: #fca5a5 !important;
|
| 343 |
+
margin: 0 0 0.8rem;
|
| 344 |
+
font-size: 1rem;
|
| 345 |
+
font-weight: 700;
|
| 346 |
+
display: flex;
|
| 347 |
+
align-items: center;
|
| 348 |
+
gap: 0.5rem;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.emergency-card p {
|
| 352 |
+
color: #fecaca !important;
|
| 353 |
+
margin: 0.4rem 0;
|
| 354 |
+
font-size: 0.9rem;
|
| 355 |
+
font-weight: 500;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
/* ═══════════════════════════════════════════════════════════
|
| 359 |
+
PLACEHOLDER
|
| 360 |
+
═══════════════════════════════════════════════════════════ */
|
| 361 |
+
.upload-placeholder {
|
| 362 |
+
background: var(--bg-card);
|
| 363 |
+
border: 3px dashed var(--border);
|
| 364 |
+
border-radius: 20px;
|
| 365 |
+
padding: 4rem 2rem;
|
| 366 |
+
text-align: center;
|
| 367 |
+
margin-top: 2rem;
|
| 368 |
+
transition: all 0.3s ease;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.upload-placeholder:hover {
|
| 372 |
+
border-color: var(--primary);
|
| 373 |
+
background: var(--bg-elevated);
|
| 374 |
+
box-shadow: var(--shadow-lg);
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.upload-placeholder .icon {
|
| 378 |
+
font-size: 4rem;
|
| 379 |
+
margin-bottom: 1rem;
|
| 380 |
+
opacity: 0.6;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.upload-placeholder h3 {
|
| 384 |
+
color: var(--text-primary) !important;
|
| 385 |
+
font-weight: 600;
|
| 386 |
+
margin-top: 1rem;
|
| 387 |
+
font-size: 1.3rem;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.upload-placeholder p {
|
| 391 |
+
color: var(--text-muted) !important;
|
| 392 |
+
font-size: 0.95rem;
|
| 393 |
+
margin-top: 0.8rem;
|
| 394 |
+
max-width: 500px;
|
| 395 |
+
margin-left: auto;
|
| 396 |
+
margin-right: auto;
|
| 397 |
+
line-height: 1.6;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
/* ═══════════════════════════════════════════════════════════
|
| 401 |
+
BUTTONS
|
| 402 |
+
═══════════════════════════════════════════════════════════ */
|
| 403 |
+
.stButton > button {
|
| 404 |
+
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%) !important;
|
| 405 |
+
color: white !important;
|
| 406 |
+
border: none !important;
|
| 407 |
+
padding: 0.8rem 2rem !important;
|
| 408 |
+
border-radius: 12px !important;
|
| 409 |
+
font-size: 1rem !important;
|
| 410 |
+
font-weight: 600 !important;
|
| 411 |
+
box-shadow: var(--shadow) !important;
|
| 412 |
+
transition: all 0.3s ease !important;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.stButton > button:hover {
|
| 416 |
+
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 100%) !important;
|
| 417 |
+
box-shadow: var(--shadow-lg) !important;
|
| 418 |
+
transform: translateY(-2px) !important;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.stLinkButton > a {
|
| 422 |
+
background: linear-gradient(135deg, var(--secondary) 0%, #059669 100%) !important;
|
| 423 |
+
color: white !important;
|
| 424 |
+
border: none !important;
|
| 425 |
+
border-radius: 12px !important;
|
| 426 |
+
font-weight: 600 !important;
|
| 427 |
+
text-decoration: none !important;
|
| 428 |
+
padding: 0.8rem 2rem !important;
|
| 429 |
+
box-shadow: var(--shadow) !important;
|
| 430 |
+
transition: all 0.3s ease !important;
|
| 431 |
+
display: inline-block !important;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.stLinkButton > a:hover {
|
| 435 |
+
background: linear-gradient(135deg, #059669 0%, var(--secondary) 100%) !important;
|
| 436 |
+
box-shadow: var(--shadow-lg) !important;
|
| 437 |
+
transform: translateY(-2px) !important;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
/* ═══════════════════════════════════════════════════════════
|
| 441 |
+
TABS
|
| 442 |
+
═════════════════════════════���═════════════════════════════ */
|
| 443 |
+
.stTabs [data-baseweb="tab-list"] {
|
| 444 |
+
gap: 8px;
|
| 445 |
+
background: var(--bg-elevated);
|
| 446 |
+
padding: 0.5rem;
|
| 447 |
+
border-radius: 12px;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.stTabs [data-baseweb="tab"] {
|
| 451 |
+
background: transparent !important;
|
| 452 |
+
border: none !important;
|
| 453 |
+
border-radius: 8px !important;
|
| 454 |
+
color: var(--text-muted) !important;
|
| 455 |
+
font-size: 0.9rem !important;
|
| 456 |
+
font-weight: 600 !important;
|
| 457 |
+
padding: 0.6rem 1.2rem !important;
|
| 458 |
+
transition: all 0.2s ease !important;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.stTabs [data-baseweb="tab"]:hover {
|
| 462 |
+
background: var(--bg-card) !important;
|
| 463 |
+
color: var(--primary) !important;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.stTabs [aria-selected="true"] {
|
| 467 |
+
background: var(--primary) !important;
|
| 468 |
+
color: white !important;
|
| 469 |
+
box-shadow: var(--shadow-sm) !important;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
/* ═══════════════════════════════════════════════════════════
|
| 473 |
+
SIDEBAR
|
| 474 |
+
═══════════════════════════════════════════════════════════ */
|
| 475 |
+
section[data-testid="stSidebar"] {
|
| 476 |
+
background: var(--bg-card) !important;
|
| 477 |
+
border-right: 1px solid var(--border) !important;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
section[data-testid="stSidebar"] > div {
|
| 481 |
+
padding-top: 2rem !important;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
section[data-testid="stSidebar"] h3 {
|
| 485 |
+
color: var(--text-primary) !important;
|
| 486 |
+
font-size: 0.9rem;
|
| 487 |
+
font-weight: 700;
|
| 488 |
+
text-transform: uppercase;
|
| 489 |
+
letter-spacing: 1px;
|
| 490 |
+
margin-bottom: 1rem;
|
| 491 |
+
padding: 0 0.5rem;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
section[data-testid="stSidebar"] .stSelectbox label {
|
| 495 |
+
color: var(--text-primary) !important;
|
| 496 |
+
font-weight: 600 !important;
|
| 497 |
+
font-size: 0.85rem !important;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
section[data-testid="stSidebar"] .stExpander {
|
| 501 |
+
background: var(--bg-elevated);
|
| 502 |
+
border: 1px solid var(--border);
|
| 503 |
+
border-radius: 10px;
|
| 504 |
+
margin-bottom: 0.5rem;
|
| 505 |
+
box-shadow: var(--shadow-sm);
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
section[data-testid="stSidebar"] .stExpander:hover {
|
| 509 |
+
box-shadow: var(--shadow);
|
| 510 |
+
border-color: var(--border-light);
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
/* Hide tooltips globally */
|
| 514 |
+
[data-testid="stTooltipIcon"] {
|
| 515 |
+
display: none !important;
|
| 516 |
+
visibility: hidden !important;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.stTooltipIcon {
|
| 520 |
+
display: none !important;
|
| 521 |
+
visibility: hidden !important;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
/* Hide all title tooltips */
|
| 525 |
+
[title] {
|
| 526 |
+
position: relative !important;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
[title]:hover::after,
|
| 530 |
+
[title]:hover::before {
|
| 531 |
+
display: none !important;
|
| 532 |
+
content: none !important;
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
/* Specifically target keyboard_double tooltip */
|
| 536 |
+
[title="keyboard_double"],
|
| 537 |
+
[aria-label="keyboard_double"],
|
| 538 |
+
button[title="keyboard_double"],
|
| 539 |
+
div[title="keyboard_double"],
|
| 540 |
+
span[title="keyboard_double"] {
|
| 541 |
+
pointer-events: auto !important;
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
[title="keyboard_double"]::after,
|
| 545 |
+
[title="keyboard_double"]::before {
|
| 546 |
+
display: none !important;
|
| 547 |
+
visibility: hidden !important;
|
| 548 |
+
content: none !important;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
/* Hide Streamlit's built-in tooltips */
|
| 552 |
+
.stTooltip {
|
| 553 |
+
display: none !important;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
div[data-baseweb="tooltip"] {
|
| 557 |
+
display: none !important;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
/* Remove title attribute display */
|
| 561 |
+
button::after,
|
| 562 |
+
div::after,
|
| 563 |
+
span::after,
|
| 564 |
+
section::after {
|
| 565 |
+
content: none !important;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
/* ═══════════════════════════════════════════════════════════
|
| 569 |
+
FILE UPLOADER
|
| 570 |
+
═══════════════════════════════════════════════════════════ */
|
| 571 |
+
.stFileUploader {
|
| 572 |
+
background: var(--bg-card);
|
| 573 |
+
border: 2px dashed var(--border);
|
| 574 |
+
border-radius: 12px;
|
| 575 |
+
padding: 1.5rem;
|
| 576 |
+
transition: all 0.3s ease;
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
.stFileUploader:hover {
|
| 580 |
+
border-color: var(--primary);
|
| 581 |
+
background: var(--bg-elevated);
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
/* ═══════════════════════════════════════════════════════════
|
| 585 |
+
MAP & IMAGE CONTAINERS
|
| 586 |
+
═══════════════════════════════════════════════════════════ */
|
| 587 |
+
.map-container {
|
| 588 |
+
border: 2px solid var(--border);
|
| 589 |
+
border-radius: 16px;
|
| 590 |
+
overflow: hidden;
|
| 591 |
+
box-shadow: var(--shadow-lg);
|
| 592 |
+
margin: 1rem 0;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
.image-container {
|
| 596 |
+
border: 2px solid var(--border);
|
| 597 |
+
border-radius: 16px;
|
| 598 |
+
overflow: hidden;
|
| 599 |
+
box-shadow: var(--shadow-lg);
|
| 600 |
+
background: var(--bg-elevated);
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
/* ═══════════════════════════════════════════════════════════
|
| 604 |
+
CHART LEGEND
|
| 605 |
+
══════════════════════════════════════════════��════════════ */
|
| 606 |
+
.chart-legend {
|
| 607 |
+
display: flex;
|
| 608 |
+
gap: 1.5rem;
|
| 609 |
+
justify-content: center;
|
| 610 |
+
flex-wrap: wrap;
|
| 611 |
+
font-size: 0.85rem;
|
| 612 |
+
margin-top: 1rem;
|
| 613 |
+
padding: 1rem;
|
| 614 |
+
background: var(--bg-elevated);
|
| 615 |
+
border-radius: 12px;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
.legend-item {
|
| 619 |
+
display: flex;
|
| 620 |
+
align-items: center;
|
| 621 |
+
gap: 0.5rem;
|
| 622 |
+
font-weight: 600;
|
| 623 |
+
color: var(--text-secondary) !important;
|
| 624 |
+
}
|
frontend/components/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
DermaScan AI - Frontend Components
|
| 3 |
+
Professional medical-grade UI components
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from .header import render_header
|
| 7 |
+
from .sidebar import render_sidebar
|
| 8 |
+
from .result_card import render_severity_banner, render_metrics, TIER_ICONS
|
| 9 |
+
from .confidence_chart import render_confidence_chart
|
| 10 |
+
from .care_advice_card import render_care_advice
|
| 11 |
+
from .hospital_map import render_hospital_map
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
'render_header',
|
| 15 |
+
'render_sidebar',
|
| 16 |
+
'render_severity_banner',
|
| 17 |
+
'render_metrics',
|
| 18 |
+
'render_confidence_chart',
|
| 19 |
+
'render_care_advice',
|
| 20 |
+
'render_hospital_map',
|
| 21 |
+
'TIER_ICONS',
|
| 22 |
+
]
|
frontend/components/care_advice_card.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Care Advice Card Component for DermaScan AI
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def render_care_advice(result):
|
| 8 |
+
"""
|
| 9 |
+
Render care advice and risk factors
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
result: Dictionary containing prediction results
|
| 13 |
+
"""
|
| 14 |
+
care = result.get("care_advice", [])
|
| 15 |
+
if care:
|
| 16 |
+
st.markdown('<div class="pro-card"><h3>💊 Recommended Care Steps</h3>', unsafe_allow_html=True)
|
| 17 |
+
for i, a in enumerate(care, 1):
|
| 18 |
+
icon = "✓" if not a.startswith(" ") else "→"
|
| 19 |
+
st.markdown(
|
| 20 |
+
f'<div class="info-item">'
|
| 21 |
+
f'<div class="info-item-icon">{icon}</div>'
|
| 22 |
+
f'<div>{a}</div>'
|
| 23 |
+
f"</div>",
|
| 24 |
+
unsafe_allow_html=True,
|
| 25 |
+
)
|
| 26 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 27 |
+
|
| 28 |
+
risks = result.get("risk_factors", [])
|
| 29 |
+
if risks:
|
| 30 |
+
st.markdown("")
|
| 31 |
+
risk_html = '<div class="pro-card"><h3>⚠️ Risk Factors</h3>'
|
| 32 |
+
risk_html += '<p style="margin-bottom:1rem;color:#cbd5e1;">Factors that may increase risk:</p>'
|
| 33 |
+
for r in risks:
|
| 34 |
+
risk_html += f'<div class="info-item"><div class="info-item-icon">⚡</div><div>{r}</div></div>'
|
| 35 |
+
risk_html += "</div>"
|
| 36 |
+
st.markdown(risk_html, unsafe_allow_html=True)
|
frontend/components/confidence_chart.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Confidence Chart Component for DermaScan AI
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
import plotly.graph_objects as go
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def render_confidence_chart(result):
|
| 9 |
+
"""
|
| 10 |
+
Render the confidence analysis chart
|
| 11 |
+
|
| 12 |
+
Args:
|
| 13 |
+
result: Dictionary containing prediction results
|
| 14 |
+
"""
|
| 15 |
+
probs = result.get("all_probabilities", {})
|
| 16 |
+
sorted_p = dict(sorted(probs.items(), key=lambda x: x[1], reverse=True))
|
| 17 |
+
names = list(sorted_p.keys())
|
| 18 |
+
vals = list(sorted_p.values())
|
| 19 |
+
|
| 20 |
+
cancer_set = {"Melanoma", "Basal Cell Carcinoma", "Actinic Keratoses"}
|
| 21 |
+
benign_set = {
|
| 22 |
+
"Melanocytic Nevi",
|
| 23 |
+
"Benign Keratosis",
|
| 24 |
+
"Dermatofibroma",
|
| 25 |
+
"Vascular Lesions",
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
colors = []
|
| 29 |
+
for n in names:
|
| 30 |
+
if n == result["predicted_class"]:
|
| 31 |
+
colors.append("#3b82f6")
|
| 32 |
+
elif n in cancer_set:
|
| 33 |
+
colors.append("#ef4444")
|
| 34 |
+
elif n in benign_set:
|
| 35 |
+
colors.append("#10b981")
|
| 36 |
+
else:
|
| 37 |
+
colors.append("#60a5fa")
|
| 38 |
+
|
| 39 |
+
fig = go.Figure(
|
| 40 |
+
go.Bar(
|
| 41 |
+
x=vals,
|
| 42 |
+
y=names,
|
| 43 |
+
orientation="h",
|
| 44 |
+
marker_color=colors,
|
| 45 |
+
text=[f"{v:.1%}" for v in vals],
|
| 46 |
+
textposition="outside",
|
| 47 |
+
textfont=dict(color="#f1f5f9", size=12, family="Inter"),
|
| 48 |
+
)
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
fig.update_layout(
|
| 52 |
+
height=450,
|
| 53 |
+
margin=dict(l=10, r=80, t=20, b=20),
|
| 54 |
+
xaxis=dict(
|
| 55 |
+
range=[0, min(1.0, max(vals) * 1.5)],
|
| 56 |
+
tickfont=dict(color="#94a3b8", family="Inter"),
|
| 57 |
+
gridcolor="#334155",
|
| 58 |
+
title=None,
|
| 59 |
+
showgrid=True,
|
| 60 |
+
),
|
| 61 |
+
yaxis=dict(
|
| 62 |
+
autorange="reversed",
|
| 63 |
+
tickfont=dict(color="#f1f5f9", size=12, family="Inter"),
|
| 64 |
+
),
|
| 65 |
+
plot_bgcolor="#1e293b",
|
| 66 |
+
paper_bgcolor="#0f172a",
|
| 67 |
+
font=dict(color="#f1f5f9", family="Inter"),
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
st.plotly_chart(fig, width="stretch")
|
| 71 |
+
|
| 72 |
+
st.markdown(
|
| 73 |
+
'<div class="chart-legend">'
|
| 74 |
+
'<span class="legend-item"><span style="color:#3b82f6;font-size:1.2rem;">●</span> Predicted</span>'
|
| 75 |
+
'<span class="legend-item"><span style="color:#ef4444;font-size:1.2rem;">●</span> Cancer</span>'
|
| 76 |
+
'<span class="legend-item"><span style="color:#10b981;font-size:1.2rem;">●</span> Benign</span>'
|
| 77 |
+
'<span class="legend-item"><span style="color:#60a5fa;font-size:1.2rem;">●</span> Disease</span>'
|
| 78 |
+
"</div>",
|
| 79 |
+
unsafe_allow_html=True,
|
| 80 |
+
)
|
frontend/components/header.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Header Component for DermaScan AI
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def render_header():
|
| 8 |
+
"""Render the professional medical header"""
|
| 9 |
+
st.markdown("""
|
| 10 |
+
<div class="medical-header">
|
| 11 |
+
<div class="medical-header-content">
|
| 12 |
+
<h1>🏥 DermaScan AI</h1>
|
| 13 |
+
<p class="subtitle">Advanced AI-Powered Dermatology Analysis System</p>
|
| 14 |
+
<div class="badges">
|
| 15 |
+
<span class="badge">🧠 EfficientNet-B3</span>
|
| 16 |
+
<span class="badge">📊 96% AUC-ROC</span>
|
| 17 |
+
<span class="badge">🔬 13 Conditions</span>
|
| 18 |
+
<span class="badge">🇮🇳 India Optimized</span>
|
| 19 |
+
<span class="badge">⚡ Real-time Analysis</span>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
""", unsafe_allow_html=True)
|
frontend/components/hospital_map.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Hospital Map Component for DermaScan AI
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def render_hospital_map(result, selected_city, selected_state):
|
| 8 |
+
"""
|
| 9 |
+
Render the hospital finder with embedded Google Maps
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
result: Dictionary containing prediction results
|
| 13 |
+
selected_city: Selected city name
|
| 14 |
+
selected_state: Selected state name
|
| 15 |
+
"""
|
| 16 |
+
hosp_type = result.get("hospital_type", "Dermatologist")
|
| 17 |
+
location = result.get("hospital_location", f"{selected_city}, {selected_state}")
|
| 18 |
+
|
| 19 |
+
st.markdown(
|
| 20 |
+
f'<div class="pro-card">'
|
| 21 |
+
f"<h3>🏥 Find {hosp_type}</h3>"
|
| 22 |
+
f"<p>📍 Searching in: <strong>{location}</strong></p>"
|
| 23 |
+
f"</div>",
|
| 24 |
+
unsafe_allow_html=True,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
search_query = result.get("hospital_search_query", "dermatologist near me")
|
| 28 |
+
full_query = f"{search_query} in {selected_city}, {selected_state}, India"
|
| 29 |
+
maps_url = "https://www.google.com/maps/search/" + full_query.replace(" ", "+")
|
| 30 |
+
|
| 31 |
+
# Embed Google Maps
|
| 32 |
+
maps_embed_url = f"https://www.google.com/maps/embed/v1/search?key=AIzaSyBFw0Qbyq9zTFTd-tUY6dZWTgaQzuU17R8&q={full_query.replace(' ', '+')}"
|
| 33 |
+
|
| 34 |
+
st.markdown(
|
| 35 |
+
f'<div class="map-container">'
|
| 36 |
+
f'<iframe width="100%" height="450" style="border:0;" '
|
| 37 |
+
f'src="{maps_embed_url}" allowfullscreen loading="lazy"></iframe>'
|
| 38 |
+
f'</div>',
|
| 39 |
+
unsafe_allow_html=True,
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
st.link_button(
|
| 43 |
+
f"🗺️ Open in Google Maps - {hosp_type}",
|
| 44 |
+
maps_url,
|
| 45 |
+
width="stretch",
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
st.markdown("")
|
| 49 |
+
|
| 50 |
+
emergency = result.get("emergency_numbers", {})
|
| 51 |
+
if emergency:
|
| 52 |
+
emer_html = '<div class="emergency-card"><h4>🚨 Emergency Contacts</h4>'
|
| 53 |
+
for label, num in emergency.items():
|
| 54 |
+
emer_html += f"<p>📞 {label}: <b>{num}</b></p>"
|
| 55 |
+
emer_html += "</div>"
|
| 56 |
+
st.markdown(emer_html, unsafe_allow_html=True)
|
frontend/components/result_card.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Result Card Components for DermaScan AI
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
|
| 6 |
+
TIER_ICONS = {
|
| 7 |
+
"CANCER": "🔴",
|
| 8 |
+
"PRE-CANCER": "🟡",
|
| 9 |
+
"BENIGN": "🟢",
|
| 10 |
+
"DISEASE": "🔵",
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def render_severity_banner(result):
|
| 15 |
+
"""Render the severity banner"""
|
| 16 |
+
severity = result.get("severity", "LOW").lower()
|
| 17 |
+
tagline = result.get("tagline", "Analysis Complete")
|
| 18 |
+
action = result.get("action", "Consult a doctor")
|
| 19 |
+
|
| 20 |
+
severity_emoji = {
|
| 21 |
+
"critical": "🚨",
|
| 22 |
+
"high": "⚠️",
|
| 23 |
+
"medium": "⚡",
|
| 24 |
+
"low": "✅"
|
| 25 |
+
}.get(severity, "ℹ️")
|
| 26 |
+
|
| 27 |
+
st.markdown(
|
| 28 |
+
f'<div class="severity-banner banner-{severity}">'
|
| 29 |
+
f"<h2>{severity_emoji} {tagline}</h2>"
|
| 30 |
+
f"<h3>📋 {action}</h3>"
|
| 31 |
+
f"</div>",
|
| 32 |
+
unsafe_allow_html=True,
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def render_metrics(result):
|
| 37 |
+
"""Render the key metrics cards"""
|
| 38 |
+
c1, c2, c3 = st.columns(3)
|
| 39 |
+
|
| 40 |
+
conf = result["confidence"]
|
| 41 |
+
conf_color = "#10b981" if conf > 0.7 else "#f59e0b" if conf > 0.4 else "#ef4444"
|
| 42 |
+
conf_emoji = "🎯" if conf > 0.7 else "⚡" if conf > 0.4 else "⚠️"
|
| 43 |
+
|
| 44 |
+
tier = result.get("tier", "UNKNOWN")
|
| 45 |
+
tier_icon = TIER_ICONS.get(tier, "⚪")
|
| 46 |
+
tier_color = {
|
| 47 |
+
"CANCER": "#ef4444",
|
| 48 |
+
"PRE-CANCER": "#f59e0b",
|
| 49 |
+
"BENIGN": "#10b981",
|
| 50 |
+
"DISEASE": "#3b82f6",
|
| 51 |
+
}.get(tier, "#94a3b8")
|
| 52 |
+
|
| 53 |
+
with c1:
|
| 54 |
+
st.markdown(
|
| 55 |
+
f'<div class="metric-card">'
|
| 56 |
+
f'<div class="label">Confidence Score</div>'
|
| 57 |
+
f'<div class="value" style="color:{conf_color};">{conf_emoji} {conf:.1%}</div>'
|
| 58 |
+
f'<div class="sublabel">{result.get("confidence_level", "")}</div>'
|
| 59 |
+
f"</div>",
|
| 60 |
+
unsafe_allow_html=True,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
with c2:
|
| 64 |
+
st.markdown(
|
| 65 |
+
f'<div class="metric-card">'
|
| 66 |
+
f'<div class="label">Classification</div>'
|
| 67 |
+
f'<div class="value" style="color:{tier_color};">{tier_icon} {tier}</div>'
|
| 68 |
+
f'<div class="sublabel">{result.get("severity", "")} Severity</div>'
|
| 69 |
+
f"</div>",
|
| 70 |
+
unsafe_allow_html=True,
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
with c3:
|
| 74 |
+
st.markdown(
|
| 75 |
+
f'<div class="metric-card">'
|
| 76 |
+
f'<div class="label">Diagnosis</div>'
|
| 77 |
+
f'<div class="value" style="font-size:1.3rem;color:#3b82f6;">🔬 {result["predicted_class"]}</div>'
|
| 78 |
+
f'<div class="sublabel">AI Prediction</div>'
|
| 79 |
+
f"</div>",
|
| 80 |
+
unsafe_allow_html=True,
|
| 81 |
+
)
|
frontend/components/sidebar.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Sidebar Component for DermaScan AI
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def render_sidebar(state_cities):
|
| 8 |
+
"""
|
| 9 |
+
Render the sidebar with location selection and information
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
state_cities: Dictionary of states and their cities
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
tuple: (selected_state, selected_city)
|
| 16 |
+
"""
|
| 17 |
+
with st.sidebar:
|
| 18 |
+
st.markdown("### 📍 Location")
|
| 19 |
+
selected_state = st.selectbox(
|
| 20 |
+
"State",
|
| 21 |
+
list(state_cities.keys()),
|
| 22 |
+
index=list(state_cities.keys()).index("Delhi")
|
| 23 |
+
)
|
| 24 |
+
cities = state_cities.get(selected_state, ["Other"])
|
| 25 |
+
selected_city = st.selectbox("City", cities, index=0)
|
| 26 |
+
|
| 27 |
+
st.markdown("---")
|
| 28 |
+
st.markdown("### 🔬 About DermaScan AI")
|
| 29 |
+
st.markdown("""
|
| 30 |
+
<div class="pro-card" style="font-size:0.85rem;">
|
| 31 |
+
<p style="margin-bottom:0.8rem;">
|
| 32 |
+
Advanced AI-powered dermatology analysis system using deep learning
|
| 33 |
+
to detect and classify skin conditions.
|
| 34 |
+
</p>
|
| 35 |
+
<p style="margin-bottom:0.5rem;"><strong>🧠 Technology</strong></p>
|
| 36 |
+
<p style="margin-bottom:0.8rem;color:#cbd5e1;">
|
| 37 |
+
• EfficientNet-B3 Architecture<br>
|
| 38 |
+
• 96% AUC-ROC Accuracy<br>
|
| 39 |
+
• Real-time Analysis
|
| 40 |
+
</p>
|
| 41 |
+
<p style="margin-bottom:0.5rem;"><strong>🔬 Detects 13 Conditions</strong></p>
|
| 42 |
+
<p style="margin-bottom:0.8rem;color:#cbd5e1;">
|
| 43 |
+
• 3 Cancer Types<br>
|
| 44 |
+
• 4 Benign Conditions<br>
|
| 45 |
+
• 6 Skin Diseases
|
| 46 |
+
</p>
|
| 47 |
+
<p style="margin-bottom:0.5rem;"><strong>📊 Training Data</strong></p>
|
| 48 |
+
<p style="color:#cbd5e1;">
|
| 49 |
+
• HAM10000 Dataset<br>
|
| 50 |
+
• DermNet Collection<br>
|
| 51 |
+
• 10,000+ Images
|
| 52 |
+
</p>
|
| 53 |
+
</div>
|
| 54 |
+
""", unsafe_allow_html=True)
|
| 55 |
+
|
| 56 |
+
st.markdown("---")
|
| 57 |
+
st.markdown("### 🚨 Emergency Contacts")
|
| 58 |
+
st.markdown("""
|
| 59 |
+
<div class="emergency-card">
|
| 60 |
+
<h4>🇮🇳 India Helplines</h4>
|
| 61 |
+
<p>🚨 Emergency: <b>112</b></p>
|
| 62 |
+
<p>🚑 Ambulance: <b>108</b></p>
|
| 63 |
+
<p>🏥 Health: <b>104</b></p>
|
| 64 |
+
<p>🎗️ Cancer: <b>1800-11-6006</b></p>
|
| 65 |
+
</div>
|
| 66 |
+
""", unsafe_allow_html=True)
|
| 67 |
+
|
| 68 |
+
st.markdown("---")
|
| 69 |
+
st.markdown("### ⚕️ Medical Disclaimer")
|
| 70 |
+
st.markdown("""
|
| 71 |
+
<div style="font-size:0.75rem;color:#94a3b8;line-height:1.5;padding:0.5rem;">
|
| 72 |
+
This AI tool is for educational and screening purposes only.
|
| 73 |
+
It is NOT a substitute for professional medical diagnosis.
|
| 74 |
+
Always consult a qualified dermatologist for proper evaluation.
|
| 75 |
+
</div>
|
| 76 |
+
""", unsafe_allow_html=True)
|
| 77 |
+
|
| 78 |
+
st.markdown("---")
|
| 79 |
+
st.markdown(
|
| 80 |
+
"<p style='text-align:center;color:#94a3b8;font-size:0.75rem;font-weight:600;'>"
|
| 81 |
+
"🏥 DermaScan AI v1.0<br>Medical Grade Analysis</p>",
|
| 82 |
+
unsafe_allow_html=True,
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
return selected_state, selected_city
|
requirements.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit>=1.35.0
|
| 2 |
+
fastapi>=0.110.0
|
| 3 |
+
uvicorn>=0.29.0
|
| 4 |
+
pydantic>=2.7.0
|
| 5 |
+
python-multipart>=0.0.9
|
| 6 |
+
|
| 7 |
+
torch>=2.2.0
|
| 8 |
+
torchvision>=0.17.0
|
| 9 |
+
timm>=0.9.16
|
| 10 |
+
|
| 11 |
+
albumentations>=1.4.8
|
| 12 |
+
opencv-python-headless>=4.9.0.80
|
| 13 |
+
Pillow>=10.3.0
|
| 14 |
+
|
| 15 |
+
numpy>=1.26.4
|
| 16 |
+
pandas>=2.2.2
|
| 17 |
+
scikit-learn>=1.4.2
|
| 18 |
+
plotly>=5.22.0
|
| 19 |
+
|
| 20 |
+
requests>=2.31.0
|
| 21 |
+
matplotlib>=3.8.4
|
| 22 |
+
seaborn>=0.13.2
|
src/inference/predictor.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
=================================================================
|
| 3 |
+
PREDICTOR — Single Image Inference Pipeline
|
| 4 |
+
=================================================================
|
| 5 |
+
"""
|
| 6 |
+
import pathlib
|
| 7 |
+
pathlib.PosixPath = pathlib.WindowsPath
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
import numpy as np
|
| 13 |
+
from PIL import Image
|
| 14 |
+
import albumentations as A
|
| 15 |
+
from albumentations.pytorch import ToTensorV2
|
| 16 |
+
from typing import Dict, Tuple
|
| 17 |
+
import json
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class SkinPredictor:
|
| 21 |
+
"""
|
| 22 |
+
Production inference pipeline.
|
| 23 |
+
Loads model once, predicts on any image.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(
|
| 27 |
+
self,
|
| 28 |
+
model_path: str = "checkpoints/best_model.pth",
|
| 29 |
+
class_config_path: str = "configs/class_config.json",
|
| 30 |
+
device: str = None,
|
| 31 |
+
img_size: int = 224,
|
| 32 |
+
):
|
| 33 |
+
# Device
|
| 34 |
+
if device:
|
| 35 |
+
self.device = torch.device(device)
|
| 36 |
+
elif torch.cuda.is_available():
|
| 37 |
+
self.device = torch.device('cuda')
|
| 38 |
+
else:
|
| 39 |
+
self.device = torch.device('cpu')
|
| 40 |
+
|
| 41 |
+
# Load class config
|
| 42 |
+
with open(class_config_path, 'r') as f:
|
| 43 |
+
self.class_config = json.load(f)
|
| 44 |
+
|
| 45 |
+
self.num_classes = len(self.class_config)
|
| 46 |
+
self.class_names = [self.class_config[str(i)]['name'] for i in range(self.num_classes)]
|
| 47 |
+
|
| 48 |
+
# Build model
|
| 49 |
+
self.model = self._build_model()
|
| 50 |
+
self._load_weights(model_path)
|
| 51 |
+
self.model.eval()
|
| 52 |
+
|
| 53 |
+
# Transform
|
| 54 |
+
self.transform = A.Compose([
|
| 55 |
+
A.Resize(img_size, img_size),
|
| 56 |
+
A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),
|
| 57 |
+
ToTensorV2(),
|
| 58 |
+
])
|
| 59 |
+
|
| 60 |
+
print(f"✅ Predictor ready on {self.device}")
|
| 61 |
+
|
| 62 |
+
def _build_model(self):
|
| 63 |
+
"""Build model architecture (must match training)."""
|
| 64 |
+
import timm
|
| 65 |
+
import torch.nn as nn
|
| 66 |
+
|
| 67 |
+
class DermaScanModel(nn.Module):
|
| 68 |
+
def __init__(self):
|
| 69 |
+
super().__init__()
|
| 70 |
+
self.backbone = timm.create_model(
|
| 71 |
+
'efficientnet_b3', pretrained=False,
|
| 72 |
+
num_classes=0, drop_rate=0.0,
|
| 73 |
+
)
|
| 74 |
+
self.feature_dim = self.backbone.num_features
|
| 75 |
+
self.head = nn.Sequential(
|
| 76 |
+
nn.Linear(self.feature_dim, 512),
|
| 77 |
+
nn.BatchNorm1d(512),
|
| 78 |
+
nn.SiLU(inplace=True),
|
| 79 |
+
nn.Dropout(0.3),
|
| 80 |
+
nn.Linear(512, 128),
|
| 81 |
+
nn.BatchNorm1d(128),
|
| 82 |
+
nn.SiLU(inplace=True),
|
| 83 |
+
nn.Dropout(0.15),
|
| 84 |
+
nn.Linear(128, 13),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
def forward(self, x):
|
| 88 |
+
return self.head(self.backbone(x))
|
| 89 |
+
|
| 90 |
+
return DermaScanModel().to(self.device)
|
| 91 |
+
|
| 92 |
+
def _load_weights(self, model_path: str):
|
| 93 |
+
"""Load trained weights."""
|
| 94 |
+
checkpoint = torch.load(model_path, map_location=self.device, weights_only=False)
|
| 95 |
+
|
| 96 |
+
if 'model_state_dict' in checkpoint:
|
| 97 |
+
self.model.load_state_dict(checkpoint['model_state_dict'])
|
| 98 |
+
else:
|
| 99 |
+
self.model.load_state_dict(checkpoint)
|
| 100 |
+
|
| 101 |
+
print(f" Weights loaded from {model_path}")
|
| 102 |
+
|
| 103 |
+
@torch.no_grad()
|
| 104 |
+
def predict(self, image) -> Dict:
|
| 105 |
+
"""
|
| 106 |
+
Predict on a single image.
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
image: PIL Image, numpy array, or file path
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
Dictionary with prediction results
|
| 113 |
+
"""
|
| 114 |
+
# Handle different input types
|
| 115 |
+
if isinstance(image, str):
|
| 116 |
+
image = Image.open(image).convert('RGB')
|
| 117 |
+
elif isinstance(image, Image.Image):
|
| 118 |
+
image = image.convert('RGB')
|
| 119 |
+
|
| 120 |
+
img_array = np.array(image)
|
| 121 |
+
|
| 122 |
+
# Transform
|
| 123 |
+
tensor = self.transform(image=img_array)['image'].unsqueeze(0)
|
| 124 |
+
tensor = tensor.to(self.device)
|
| 125 |
+
|
| 126 |
+
# Predict
|
| 127 |
+
logits = self.model(tensor)
|
| 128 |
+
probabilities = F.softmax(logits, dim=1)[0].cpu().numpy()
|
| 129 |
+
|
| 130 |
+
predicted_class = int(np.argmax(probabilities))
|
| 131 |
+
confidence = float(probabilities[predicted_class])
|
| 132 |
+
|
| 133 |
+
return {
|
| 134 |
+
"predicted_class": predicted_class,
|
| 135 |
+
"predicted_class_name": self.class_names[predicted_class],
|
| 136 |
+
"confidence": confidence,
|
| 137 |
+
"all_probabilities": probabilities,
|
| 138 |
+
}
|
src/response/hospital_finder.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
=================================================================
|
| 3 |
+
HOSPITAL FINDER — India-Specific
|
| 4 |
+
=================================================================
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
INDIAN_STATES = [
|
| 8 |
+
"Andhra Pradesh", "Arunachal Pradesh", "Assam", "Bihar",
|
| 9 |
+
"Chhattisgarh", "Goa", "Gujarat", "Haryana", "Himachal Pradesh",
|
| 10 |
+
"Jharkhand", "Karnataka", "Kerala", "Madhya Pradesh",
|
| 11 |
+
"Maharashtra", "Manipur", "Meghalaya", "Mizoram", "Nagaland",
|
| 12 |
+
"Odisha", "Punjab", "Rajasthan", "Sikkim", "Tamil Nadu",
|
| 13 |
+
"Telangana", "Tripura", "Uttar Pradesh", "Uttarakhand",
|
| 14 |
+
"West Bengal", "Delhi", "Chandigarh", "Puducherry",
|
| 15 |
+
"Jammu and Kashmir", "Ladakh",
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class HospitalFinder:
|
| 20 |
+
"""
|
| 21 |
+
Find nearby hospitals in India using Google Maps URL.
|
| 22 |
+
No API key needed — generates search URLs.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self):
|
| 26 |
+
self.country = "India"
|
| 27 |
+
|
| 28 |
+
def search(self, query: str, city: str, state: str) -> dict:
|
| 29 |
+
"""
|
| 30 |
+
Generate Google Maps search for hospitals in a specific city.
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
query: Search type (e.g., "skin cancer specialist")
|
| 34 |
+
city: City name
|
| 35 |
+
state: State name
|
| 36 |
+
|
| 37 |
+
Returns:
|
| 38 |
+
Dictionary with maps_url and search info
|
| 39 |
+
"""
|
| 40 |
+
location = f"{city}, {state}, India"
|
| 41 |
+
full_query = f"{query} in {location}"
|
| 42 |
+
encoded = full_query.replace(" ", "+")
|
| 43 |
+
|
| 44 |
+
maps_url = f"https://www.google.com/maps/search/{encoded}"
|
| 45 |
+
embed_url = f"https://maps.google.com/maps?q={encoded}&z=13&output=embed"
|
| 46 |
+
|
| 47 |
+
return {
|
| 48 |
+
"maps_url": maps_url,
|
| 49 |
+
"embed_url": embed_url,
|
| 50 |
+
"query": full_query,
|
| 51 |
+
"city": city,
|
| 52 |
+
"state": state,
|
| 53 |
+
"location": location,
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
def get_emergency_numbers(self) -> dict:
|
| 57 |
+
"""Indian emergency numbers."""
|
| 58 |
+
return {
|
| 59 |
+
"Emergency": "112",
|
| 60 |
+
"Ambulance": "108",
|
| 61 |
+
"Health Helpline": "104",
|
| 62 |
+
"AIIMS Delhi": "011-26588500",
|
| 63 |
+
"National Cancer Helpline": "1800-11-6006",
|
| 64 |
+
}
|
src/response/response_engine.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
=================================================================
|
| 3 |
+
RESPONSE ENGINE — The Brain of DermaScan-AI
|
| 4 |
+
=================================================================
|
| 5 |
+
Takes model prediction → generates complete clinical response:
|
| 6 |
+
- Diagnosis with confidence
|
| 7 |
+
- Severity tier (CANCER / PRE-CANCER / BENIGN / DISEASE)
|
| 8 |
+
- Care advice
|
| 9 |
+
- Hospital recommendation
|
| 10 |
+
- Urgency level
|
| 11 |
+
=================================================================
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from typing import Dict, Any, Optional
|
| 17 |
+
import numpy as np
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ResponseEngine:
|
| 21 |
+
"""
|
| 22 |
+
Generates structured clinical responses based on model predictions.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(
|
| 26 |
+
self,
|
| 27 |
+
class_config_path: str = "configs/class_config.json",
|
| 28 |
+
response_templates_path: str = "configs/response_templates.json",
|
| 29 |
+
):
|
| 30 |
+
# Load class config
|
| 31 |
+
with open(class_config_path, 'r') as f:
|
| 32 |
+
self.class_config = json.load(f)
|
| 33 |
+
|
| 34 |
+
# Load response templates
|
| 35 |
+
with open(response_templates_path, 'r') as f:
|
| 36 |
+
self.templates = json.load(f)
|
| 37 |
+
|
| 38 |
+
self.num_classes = len(self.class_config)
|
| 39 |
+
self.class_names = [self.class_config[str(i)]['name'] for i in range(self.num_classes)]
|
| 40 |
+
self.class_tiers = [self.class_config[str(i)]['tier'] for i in range(self.num_classes)]
|
| 41 |
+
|
| 42 |
+
# Confidence thresholds
|
| 43 |
+
self.HIGH_CONFIDENCE = 0.7
|
| 44 |
+
self.MEDIUM_CONFIDENCE = 0.4
|
| 45 |
+
self.LOW_CONFIDENCE = 0.2
|
| 46 |
+
|
| 47 |
+
def generate_response(
|
| 48 |
+
self,
|
| 49 |
+
predicted_class: int,
|
| 50 |
+
confidence: float,
|
| 51 |
+
all_probabilities: np.ndarray,
|
| 52 |
+
) -> Dict[str, Any]:
|
| 53 |
+
"""
|
| 54 |
+
Generate a complete response for a prediction.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
predicted_class: Index of predicted class
|
| 58 |
+
confidence: Confidence of top prediction (0-1)
|
| 59 |
+
all_probabilities: Array of probabilities for all 13 classes
|
| 60 |
+
|
| 61 |
+
Returns:
|
| 62 |
+
Complete response dictionary
|
| 63 |
+
"""
|
| 64 |
+
class_name = self.class_names[predicted_class]
|
| 65 |
+
template = self.templates.get(class_name, {})
|
| 66 |
+
class_info = self.class_config[str(predicted_class)]
|
| 67 |
+
|
| 68 |
+
# ── Confidence assessment ──
|
| 69 |
+
if confidence >= self.HIGH_CONFIDENCE:
|
| 70 |
+
confidence_level = "HIGH"
|
| 71 |
+
confidence_message = "The AI model has high confidence in this assessment."
|
| 72 |
+
elif confidence >= self.MEDIUM_CONFIDENCE:
|
| 73 |
+
confidence_level = "MEDIUM"
|
| 74 |
+
confidence_message = "The AI model has moderate confidence. Consider getting a professional opinion."
|
| 75 |
+
else:
|
| 76 |
+
confidence_level = "LOW"
|
| 77 |
+
confidence_message = "The AI model has low confidence. This result should be verified by a medical professional."
|
| 78 |
+
|
| 79 |
+
# ── Check for close second prediction ──
|
| 80 |
+
sorted_indices = np.argsort(all_probabilities)[::-1]
|
| 81 |
+
second_class = sorted_indices[1]
|
| 82 |
+
second_prob = all_probabilities[second_class]
|
| 83 |
+
second_name = self.class_names[second_class]
|
| 84 |
+
|
| 85 |
+
close_call = (confidence - second_prob) < 0.15
|
| 86 |
+
|
| 87 |
+
# ── Build differential diagnosis ──
|
| 88 |
+
differential = []
|
| 89 |
+
for idx in sorted_indices[:3]:
|
| 90 |
+
if all_probabilities[idx] > 0.05:
|
| 91 |
+
differential.append({
|
| 92 |
+
"class_name": self.class_names[idx],
|
| 93 |
+
"probability": round(float(all_probabilities[idx]), 4),
|
| 94 |
+
"tier": self.class_tiers[idx],
|
| 95 |
+
})
|
| 96 |
+
|
| 97 |
+
# ── Is any cancer class in top 3? ──
|
| 98 |
+
cancer_alert = False
|
| 99 |
+
cancer_in_top3 = []
|
| 100 |
+
for d in differential:
|
| 101 |
+
if d['tier'] in ['CANCER', 'PRE-CANCER']:
|
| 102 |
+
cancer_in_top3.append(d)
|
| 103 |
+
if d['probability'] > 0.1:
|
| 104 |
+
cancer_alert = True
|
| 105 |
+
|
| 106 |
+
# ── Build response ──
|
| 107 |
+
response = {
|
| 108 |
+
# Core prediction
|
| 109 |
+
"predicted_class": class_name,
|
| 110 |
+
"predicted_class_idx": int(predicted_class),
|
| 111 |
+
"confidence": round(float(confidence), 4),
|
| 112 |
+
"confidence_level": confidence_level,
|
| 113 |
+
"confidence_message": confidence_message,
|
| 114 |
+
|
| 115 |
+
# Classification
|
| 116 |
+
"tier": class_info['tier'],
|
| 117 |
+
"severity": template.get('severity', class_info['severity']),
|
| 118 |
+
"emoji": template.get('emoji', '❓'),
|
| 119 |
+
"tagline": template.get('tagline', f'{class_name} Detected'),
|
| 120 |
+
|
| 121 |
+
# Action
|
| 122 |
+
"action": template.get('action', 'CONSULT A DOCTOR'),
|
| 123 |
+
"urgency_message": template.get('urgency_message', ''),
|
| 124 |
+
"description": template.get('description', ''),
|
| 125 |
+
|
| 126 |
+
# Care advice
|
| 127 |
+
"care_advice": template.get('care_advice', []),
|
| 128 |
+
"risk_factors": template.get('risk_factors', []),
|
| 129 |
+
|
| 130 |
+
# Hospital
|
| 131 |
+
"hospital_search_query": template.get('hospital_search_query', 'dermatologist near me'),
|
| 132 |
+
"hospital_type": template.get('hospital_type', 'Dermatology Clinic'),
|
| 133 |
+
|
| 134 |
+
# Differential diagnosis
|
| 135 |
+
"differential_diagnosis": differential,
|
| 136 |
+
"close_call": close_call,
|
| 137 |
+
|
| 138 |
+
# Cancer alert
|
| 139 |
+
"cancer_alert": cancer_alert,
|
| 140 |
+
"cancer_in_differential": cancer_in_top3,
|
| 141 |
+
|
| 142 |
+
# All probabilities (for chart)
|
| 143 |
+
"all_probabilities": {
|
| 144 |
+
self.class_names[i]: round(float(all_probabilities[i]), 4)
|
| 145 |
+
for i in range(self.num_classes)
|
| 146 |
+
},
|
| 147 |
+
|
| 148 |
+
# Disclaimer
|
| 149 |
+
"disclaimer": (
|
| 150 |
+
"⚠️ IMPORTANT: This is an AI-assisted analysis tool for educational purposes only. "
|
| 151 |
+
"It is NOT a substitute for professional medical diagnosis. The accuracy of AI predictions "
|
| 152 |
+
"can vary. Always consult a qualified dermatologist or healthcare professional for "
|
| 153 |
+
"proper diagnosis and treatment. If you notice any suspicious changes in your skin, "
|
| 154 |
+
"please seek medical attention promptly."
|
| 155 |
+
),
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
# ── Override: If cancer probability is high, ALWAYS warn ──
|
| 159 |
+
if cancer_alert and class_info['tier'] not in ['CANCER', 'PRE-CANCER']:
|
| 160 |
+
highest_cancer = max(cancer_in_top3, key=lambda x: x['probability'])
|
| 161 |
+
response['cancer_warning'] = (
|
| 162 |
+
f"⚠️ Note: While the top prediction is {class_name}, "
|
| 163 |
+
f"the model also detected a {highest_cancer['probability']:.0%} probability of "
|
| 164 |
+
f"{highest_cancer['class_name']} ({highest_cancer['tier']}). "
|
| 165 |
+
f"We strongly recommend consulting a dermatologist to rule out any malignancy."
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
return response
|
| 169 |
+
|
| 170 |
+
def get_severity_color(self, severity: str) -> str:
|
| 171 |
+
"""Return color hex code for severity level."""
|
| 172 |
+
colors = {
|
| 173 |
+
"CRITICAL": "#e74c3c",
|
| 174 |
+
"HIGH": "#e67e22",
|
| 175 |
+
"MEDIUM": "#f39c12",
|
| 176 |
+
"LOW": "#27ae60",
|
| 177 |
+
}
|
| 178 |
+
return colors.get(severity, "#95a5a6")
|
| 179 |
+
|
| 180 |
+
def get_tier_color(self, tier: str) -> str:
|
| 181 |
+
"""Return color hex code for tier."""
|
| 182 |
+
colors = {
|
| 183 |
+
"CANCER": "#e74c3c",
|
| 184 |
+
"PRE-CANCER": "#f39c12",
|
| 185 |
+
"BENIGN": "#27ae60",
|
| 186 |
+
"DISEASE": "#3498db",
|
| 187 |
+
}
|
| 188 |
+
return colors.get(tier, "#95a5a6")
|