Spaces:
Sleeping
Sleeping
Commit ·
d32e728
1
Parent(s): d5ec07c
Add file
Browse files- .gitignore +31 -0
- README.md +284 -0
- backend/.env.example +3 -0
- backend/Dockerfile +12 -0
- backend/api/api/__init__.py +0 -0
- backend/api/api/asgi.py +16 -0
- backend/api/api/settings.py +112 -0
- backend/api/api/urls.py +30 -0
- backend/api/api/wsgi.py +16 -0
- backend/api/app.log +0 -0
- backend/api/manage.py +22 -0
- backend/api/models/README.md +7 -0
- backend/api/models/pytorch_model.pth +3 -0
- backend/api/notifications/__init__.py +0 -0
- backend/api/notifications/admin.py +3 -0
- backend/api/notifications/api_views.py +218 -0
- backend/api/notifications/apps.py +7 -0
- backend/api/notifications/models.py +1 -0
- backend/api/notifications/serializers.py +22 -0
- backend/api/notifications/tests.py +3 -0
- backend/api/notifications/urls.py +6 -0
- backend/api/notifications/views.py +3 -0
- backend/requirements.txt +14 -0
- docker-compose.yml +28 -0
- frontend/Dockerfile +12 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +32 -0
- frontend/public/favicon.ico +0 -0
- frontend/public/index.html +43 -0
- frontend/public/manifest.json +25 -0
- frontend/public/robots.txt +3 -0
- frontend/src/App.css +0 -0
- frontend/src/App.js +470 -0
- frontend/src/index.css +6 -0
- frontend/src/index.js +28 -0
- frontend/src/reportWebVitals.js +13 -0
- frontend/src/setupTests.js +5 -0
- frontend/src/store/index.js +35 -0
- frontend/src/theme.js +63 -0
- ml/data/README.md +22 -0
- ml/models/__init__.py +0 -0
- ml/models/cnn_pytorch.py +42 -0
- ml/models/cnn_tensorflow.py +32 -0
- ml/models/train.py +154 -0
- ml/requirements.txt +13 -0
- ml/utils/__init__.py +0 -0
- ml/utils/prep.py +174 -0
.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Node
|
| 2 |
+
node_modules/
|
| 3 |
+
npm-debug.log*
|
| 4 |
+
yarn-debug.log*
|
| 5 |
+
yarn-error.log*
|
| 6 |
+
|
| 7 |
+
# React build
|
| 8 |
+
frontend/build/
|
| 9 |
+
|
| 10 |
+
# Python
|
| 11 |
+
__pycache__/
|
| 12 |
+
*.pyc
|
| 13 |
+
*.pyo
|
| 14 |
+
*.pyd
|
| 15 |
+
.Python
|
| 16 |
+
|
| 17 |
+
# Virtual env
|
| 18 |
+
venv/
|
| 19 |
+
env/
|
| 20 |
+
.env
|
| 21 |
+
|
| 22 |
+
# Django
|
| 23 |
+
db.sqlite3
|
| 24 |
+
|
| 25 |
+
# OS
|
| 26 |
+
.DS_Store
|
| 27 |
+
Thumbs.db
|
| 28 |
+
|
| 29 |
+
# IDE
|
| 30 |
+
.vscode/
|
| 31 |
+
.idea/
|
README.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
---
|
| 2 |
title: Intel Classification
|
| 3 |
emoji: 🌖
|
|
@@ -9,3 +10,286 @@ short_description: This Project is about classification the natural scenes
|
|
| 9 |
---
|
| 10 |
|
| 11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<<<<<<< HEAD
|
| 2 |
---
|
| 3 |
title: Intel Classification
|
| 4 |
emoji: 🌖
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 13 |
+
=======
|
| 14 |
+
# Intel Image Classifier
|
| 15 |
+
|
| 16 |
+
Application web full-stack pour la classification de scènes naturelles (Intel Image Classification dataset) avec deux modèles CNN — PyTorch et TensorFlow.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## Structure du projet
|
| 21 |
+
|
| 22 |
+
```
|
| 23 |
+
intel-classifier/
|
| 24 |
+
├── backend/ # API Django REST
|
| 25 |
+
│ ├── api/
|
| 26 |
+
│ │ ├── api/
|
| 27 |
+
│ │ │ ├── settings.py
|
| 28 |
+
│ │ │ ├── urls.py
|
| 29 |
+
│ │ │ ├── wsgi.py
|
| 30 |
+
│ │ │ └── asgi.py
|
| 31 |
+
│ │ ├── notifications/
|
| 32 |
+
│ │ │ ├── api_views.py # Endpoint de classification
|
| 33 |
+
│ │ │ ├── serializers.py
|
| 34 |
+
│ │ │ └── urls.py
|
| 35 |
+
│ │ ├── models/
|
| 36 |
+
│ │ │ ├── danielle_model.pth ← À placer ici (PyTorch)
|
| 37 |
+
│ │ │ └── danielle_model.keras ← À placer ici (TensorFlow)
|
| 38 |
+
│ │ └── manage.py
|
| 39 |
+
│ ├── requirements.txt
|
| 40 |
+
│ └── Dockerfile
|
| 41 |
+
│
|
| 42 |
+
├── frontend/ # Interface React + MUI
|
| 43 |
+
│ ├── src/
|
| 44 |
+
│ │ ├── App.js # Composant principal
|
| 45 |
+
│ │ ├── theme.js
|
| 46 |
+
│ │ └── store/index.js
|
| 47 |
+
│ ├── public/
|
| 48 |
+
│ ├── package.json
|
| 49 |
+
│ └── Dockerfile
|
| 50 |
+
│
|
| 51 |
+
├── ml/ # Code d'entraînement
|
| 52 |
+
│ ├── models/
|
| 53 |
+
│ │ ├── cnn_pytorch.py # Architecture CNN PyTorch
|
| 54 |
+
│ │ ├── cnn_tensorflow.py # Architecture CNN TensorFlow
|
| 55 |
+
│ │ └── train.py # Trainer class
|
| 56 |
+
│ ├── utils/
|
| 57 |
+
│ │ └── prep.py # Data loaders + CLASSES
|
| 58 |
+
│ ├── train_kaggle.py # Script d'entraînement Kaggle
|
| 59 |
+
│ └── data/
|
| 60 |
+
│ ├── seg_train/ ← Dataset train à placer ici
|
| 61 |
+
│ └── seg_test/ ← Dataset test à placer ici
|
| 62 |
+
│
|
| 63 |
+
└── docker-compose.yml
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## Classes
|
| 69 |
+
|
| 70 |
+
| Classe | FR | Wolof |
|
| 71 |
+
|------------|-------------|---------------|
|
| 72 |
+
| buildings | Bâtiments | Kër yi |
|
| 73 |
+
| forest | Forêt | Géej bu wees |
|
| 74 |
+
| glacier | Glacier | Dëkk bu sedd |
|
| 75 |
+
| mountain | Montagne | Tund bi |
|
| 76 |
+
| sea | Mer | Géej bi |
|
| 77 |
+
| street | Rue | Yoon bi |
|
| 78 |
+
|
| 79 |
+
---
|
| 80 |
+
|
| 81 |
+
## Prérequis
|
| 82 |
+
|
| 83 |
+
- Python 3.10+
|
| 84 |
+
- Node.js 18+
|
| 85 |
+
- pip
|
| 86 |
+
- npm
|
| 87 |
+
- (Optionnel) Docker + Docker Compose
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## Installation et lancement (sans Docker)
|
| 92 |
+
|
| 93 |
+
### 1. Placer les modèles entraînés
|
| 94 |
+
|
| 95 |
+
Copie tes fichiers modèles dans le dossier backend :
|
| 96 |
+
|
| 97 |
+
```
|
| 98 |
+
backend/api/models/danielle_model.pth ← modèle PyTorch
|
| 99 |
+
backend/api/models/danielle_model.keras ← modèle TensorFlow
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
> Si tu n'as qu'un seul modèle, l'autre sera simplement indisponible à la sélection.
|
| 103 |
+
|
| 104 |
+
---
|
| 105 |
+
|
| 106 |
+
### 2. Backend (Django)
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
# Aller dans le dossier backend
|
| 110 |
+
cd backend
|
| 111 |
+
|
| 112 |
+
# Créer un environnement virtuel
|
| 113 |
+
python -m venv venv
|
| 114 |
+
|
| 115 |
+
# Activer l'environnement virtuel
|
| 116 |
+
# Windows :
|
| 117 |
+
venv\Scripts\activate
|
| 118 |
+
# Mac/Linux :
|
| 119 |
+
source venv/bin/activate
|
| 120 |
+
|
| 121 |
+
# Installer les dépendances
|
| 122 |
+
pip install -r requirements.txt
|
| 123 |
+
|
| 124 |
+
# Aller dans le dossier Django
|
| 125 |
+
cd api
|
| 126 |
+
|
| 127 |
+
# Appliquer les migrations
|
| 128 |
+
python manage.py migrate
|
| 129 |
+
|
| 130 |
+
# Lancer le serveur
|
| 131 |
+
python manage.py runserver
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
Le backend sera disponible sur : **http://127.0.0.1:8000**
|
| 135 |
+
|
| 136 |
+
API Swagger disponible sur : **http://127.0.0.1:8000/swagger/**
|
| 137 |
+
|
| 138 |
+
---
|
| 139 |
+
|
| 140 |
+
### 3. Frontend (React)
|
| 141 |
+
|
| 142 |
+
Ouvre un **nouveau terminal** :
|
| 143 |
+
|
| 144 |
+
```bash
|
| 145 |
+
# Aller dans le dossier frontend
|
| 146 |
+
cd frontend
|
| 147 |
+
|
| 148 |
+
# Installer les dépendances
|
| 149 |
+
npm install
|
| 150 |
+
|
| 151 |
+
# Lancer l'application
|
| 152 |
+
npm start
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
Le frontend sera disponible sur : **http://localhost:3000**
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## Installation et lancement (avec Docker)
|
| 160 |
+
|
| 161 |
+
```bash
|
| 162 |
+
# À la racine du projet
|
| 163 |
+
docker-compose up --build
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
- Frontend : http://localhost:3000
|
| 167 |
+
- Backend : http://localhost:8000
|
| 168 |
+
- Swagger : http://localhost:8000/swagger/
|
| 169 |
+
|
| 170 |
+
---
|
| 171 |
+
|
| 172 |
+
## Entraînement des modèles
|
| 173 |
+
|
| 174 |
+
### Sur Kaggle (recommandé)
|
| 175 |
+
|
| 176 |
+
1. Upload `ml/train_kaggle.py` dans un notebook Kaggle
|
| 177 |
+
2. Active le GPU (Settings → Accelerator → GPU T4)
|
| 178 |
+
3. Ajoute le dataset `puneet6060/intel-image-classification`
|
| 179 |
+
4. Exécute le notebook
|
| 180 |
+
5. Télécharge `danielle_model.pth` et `danielle_model.keras`
|
| 181 |
+
6. Place-les dans `backend/api/models/`
|
| 182 |
+
|
| 183 |
+
### En local
|
| 184 |
+
|
| 185 |
+
```bash
|
| 186 |
+
cd ml
|
| 187 |
+
|
| 188 |
+
# Installer les dépendances ML
|
| 189 |
+
pip install torch torchvision tensorflow numpy matplotlib seaborn scikit-learn tqdm pillow
|
| 190 |
+
|
| 191 |
+
# Entraîner le modèle PyTorch
|
| 192 |
+
python -m models.train --model pytorch --epochs 25 --data data
|
| 193 |
+
|
| 194 |
+
# Entraîner le modèle TensorFlow
|
| 195 |
+
python -m models.train --model tensorflow --epochs 25 --data data
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
Structure du dataset attendue :
|
| 199 |
+
```
|
| 200 |
+
ml/data/
|
| 201 |
+
├── seg_train/
|
| 202 |
+
│ └── seg_train/
|
| 203 |
+
│ ├── buildings/
|
| 204 |
+
│ ├── forest/
|
| 205 |
+
│ ├── glacier/
|
| 206 |
+
│ ├── mountain/
|
| 207 |
+
│ ├── sea/
|
| 208 |
+
│ └── street/
|
| 209 |
+
└── seg_test/
|
| 210 |
+
└── seg_test/
|
| 211 |
+
├── buildings/
|
| 212 |
+
├── forest/
|
| 213 |
+
...
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
---
|
| 217 |
+
|
| 218 |
+
## API — Endpoint de classification
|
| 219 |
+
|
| 220 |
+
### `POST /api/classify/`
|
| 221 |
+
|
| 222 |
+
**Paramètres (form-data) :**
|
| 223 |
+
|
| 224 |
+
| Paramètre | Type | Requis | Description |
|
| 225 |
+
|------------|--------|--------|------------------------------------|
|
| 226 |
+
| image | file | Non* | Fichier image (jpg, png...) |
|
| 227 |
+
| image_url | string | Non* | URL d'une image en ligne |
|
| 228 |
+
| model | string | Non | `pytorch` (défaut) ou `tensorflow` |
|
| 229 |
+
|
| 230 |
+
*Au moins `image` ou `image_url` est requis.
|
| 231 |
+
|
| 232 |
+
**Réponse (200 OK) :**
|
| 233 |
+
|
| 234 |
+
```json
|
| 235 |
+
{
|
| 236 |
+
"predicted_class": "forest",
|
| 237 |
+
"confidence": 0.97,
|
| 238 |
+
"model_used": "pytorch",
|
| 239 |
+
"all_probabilities": [
|
| 240 |
+
{ "class": "forest", "probability": 0.97 },
|
| 241 |
+
{ "class": "mountain", "probability": 0.01 },
|
| 242 |
+
{ "class": "glacier", "probability": 0.01 },
|
| 243 |
+
{ "class": "sea", "probability": 0.01 },
|
| 244 |
+
{ "class": "buildings", "probability": 0.00 },
|
| 245 |
+
{ "class": "street", "probability": 0.00 }
|
| 246 |
+
]
|
| 247 |
+
}
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
---
|
| 251 |
+
|
| 252 |
+
## Fonctionnalités de l'interface
|
| 253 |
+
|
| 254 |
+
- Upload d'image depuis le disque
|
| 255 |
+
- Chargement d'image depuis une URL
|
| 256 |
+
- Sélection du modèle (PyTorch CNN / TensorFlow CNN)
|
| 257 |
+
- Affichage du résultat avec barre de confiance
|
| 258 |
+
- Affichage de toutes les probabilités par classe
|
| 259 |
+
- Interface multilingue : **Anglais / Français / Wolof**
|
| 260 |
+
- Design responsive avec Material UI
|
| 261 |
+
|
| 262 |
+
---
|
| 263 |
+
|
| 264 |
+
## Technologies utilisées
|
| 265 |
+
|
| 266 |
+
| Couche | Technologies |
|
| 267 |
+
|------------|-------------------------------------------------|
|
| 268 |
+
| Frontend | React 19, Material UI 6, Axios |
|
| 269 |
+
| Backend | Django 5, Django REST Framework, drf-yasg |
|
| 270 |
+
| ML | PyTorch 2.2, TensorFlow 2.16, torchvision, timm |
|
| 271 |
+
| Données | Intel Image Classification (Kaggle) |
|
| 272 |
+
| DevOps | Docker, Docker Compose |
|
| 273 |
+
|
| 274 |
+
---
|
| 275 |
+
|
| 276 |
+
## Problèmes fréquents
|
| 277 |
+
|
| 278 |
+
**Erreur CORS au démarrage du frontend**
|
| 279 |
+
→ Vérifie que le backend tourne sur `http://127.0.0.1:8000`
|
| 280 |
+
|
| 281 |
+
**`Model file not found`**
|
| 282 |
+
→ Vérifie que `danielle_model.pth` et/ou `danielle_model.keras` sont bien dans `backend/api/models/`
|
| 283 |
+
|
| 284 |
+
**`ModuleNotFoundError: No module named 'notifications'`**
|
| 285 |
+
→ Lance `manage.py` depuis le dossier `backend/api/` et non depuis `backend/`
|
| 286 |
+
|
| 287 |
+
**TensorFlow lent au premier chargement**
|
| 288 |
+
→ Normal, le modèle est chargé en mémoire à la première requête (lazy loading)
|
| 289 |
+
|
| 290 |
+
---
|
| 291 |
+
|
| 292 |
+
## Auteurs
|
| 293 |
+
|
| 294 |
+
Projet universitaire — Classification d'images Intel — 2026
|
| 295 |
+
>>>>>>> b102039 (Initial commit - Intel Image Classifier)
|
backend/.env.example
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DEBUG=True
|
| 2 |
+
SECRET_KEY=change-this-in-production
|
| 3 |
+
ALLOWED_HOSTS=*
|
backend/Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY requirements.txt .
|
| 6 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 8000
|
| 11 |
+
|
| 12 |
+
CMD ["python", "api/manage.py", "runserver", "0.0.0.0:8000"]
|
backend/api/api/__init__.py
ADDED
|
File without changes
|
backend/api/api/asgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ASGI config for api project.
|
| 3 |
+
|
| 4 |
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.asgi import get_asgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api.settings')
|
| 15 |
+
|
| 16 |
+
application = get_asgi_application()
|
backend/api/api/settings.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 5 |
+
|
| 6 |
+
SECRET_KEY = 'django-insecure-intel-image-classifier-change-in-production'
|
| 7 |
+
|
| 8 |
+
DEBUG = True
|
| 9 |
+
|
| 10 |
+
ALLOWED_HOSTS = ['*']
|
| 11 |
+
|
| 12 |
+
INSTALLED_APPS = [
|
| 13 |
+
'django.contrib.admin',
|
| 14 |
+
'django.contrib.auth',
|
| 15 |
+
'django.contrib.contenttypes',
|
| 16 |
+
'django.contrib.sessions',
|
| 17 |
+
'django.contrib.messages',
|
| 18 |
+
'django.contrib.staticfiles',
|
| 19 |
+
'rest_framework',
|
| 20 |
+
'rest_framework.authtoken',
|
| 21 |
+
'corsheaders',
|
| 22 |
+
'django_filters',
|
| 23 |
+
'drf_yasg',
|
| 24 |
+
'notifications',
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
MIDDLEWARE = [
|
| 28 |
+
'corsheaders.middleware.CorsMiddleware',
|
| 29 |
+
'django.middleware.security.SecurityMiddleware',
|
| 30 |
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
| 31 |
+
'django.middleware.common.CommonMiddleware',
|
| 32 |
+
'django.middleware.csrf.CsrfViewMiddleware',
|
| 33 |
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
| 34 |
+
'django.contrib.messages.middleware.MessageMiddleware',
|
| 35 |
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
ROOT_URLCONF = 'api.urls'
|
| 39 |
+
|
| 40 |
+
TEMPLATES = [
|
| 41 |
+
{
|
| 42 |
+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
| 43 |
+
'DIRS': [os.path.join(BASE_DIR, 'templates'), 'templates'],
|
| 44 |
+
'APP_DIRS': True,
|
| 45 |
+
'OPTIONS': {
|
| 46 |
+
'context_processors': [
|
| 47 |
+
'django.template.context_processors.debug',
|
| 48 |
+
'django.template.context_processors.request',
|
| 49 |
+
'django.contrib.auth.context_processors.auth',
|
| 50 |
+
'django.contrib.messages.context_processors.messages',
|
| 51 |
+
],
|
| 52 |
+
},
|
| 53 |
+
},
|
| 54 |
+
]
|
| 55 |
+
|
| 56 |
+
WSGI_APPLICATION = 'api.wsgi.application'
|
| 57 |
+
ASGI_APPLICATION = 'api.asgi.application'
|
| 58 |
+
|
| 59 |
+
DATABASES = {
|
| 60 |
+
'default': {
|
| 61 |
+
'ENGINE': 'django.db.backends.sqlite3',
|
| 62 |
+
'NAME': BASE_DIR / 'db.sqlite3',
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
REST_FRAMEWORK = {
|
| 67 |
+
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
| 68 |
+
'PAGE_SIZE': 10,
|
| 69 |
+
'DEFAULT_PERMISSION_CLASSES': [],
|
| 70 |
+
'DEFAULT_RENDERER_CLASSES': [
|
| 71 |
+
'rest_framework.renderers.JSONRenderer',
|
| 72 |
+
],
|
| 73 |
+
'DEFAULT_AUTHENTICATION_CLASSES': (
|
| 74 |
+
'rest_framework.authentication.SessionAuthentication',
|
| 75 |
+
'rest_framework.authentication.TokenAuthentication',
|
| 76 |
+
),
|
| 77 |
+
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
USE_X_FORWARDED_HOST = True
|
| 81 |
+
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
| 82 |
+
|
| 83 |
+
CORS_ALLOW_ALL_ORIGINS = True
|
| 84 |
+
|
| 85 |
+
CSRF_TRUSTED_ORIGINS = [
|
| 86 |
+
'http://localhost:3000',
|
| 87 |
+
'http://127.0.0.1:3000',
|
| 88 |
+
'http://localhost',
|
| 89 |
+
'http://127.0.0.1',
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
LANGUAGE_CODE = 'en-us'
|
| 93 |
+
TIME_ZONE = 'UTC'
|
| 94 |
+
USE_I18N = True
|
| 95 |
+
USE_TZ = True
|
| 96 |
+
|
| 97 |
+
STATIC_URL = 'static/intel-classifier/'
|
| 98 |
+
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
| 99 |
+
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
| 100 |
+
MEDIA_URL = '/media/intel-classifier/'
|
| 101 |
+
|
| 102 |
+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
| 103 |
+
|
| 104 |
+
LOGGING = {
|
| 105 |
+
'version': 1,
|
| 106 |
+
'disable_existing_loggers': False,
|
| 107 |
+
'handlers': {
|
| 108 |
+
'console': {'level': 'DEBUG', 'class': 'logging.StreamHandler'},
|
| 109 |
+
'file': {'level': 'ERROR', 'class': 'logging.FileHandler', 'filename': 'app.log'},
|
| 110 |
+
},
|
| 111 |
+
'loggers': {},
|
| 112 |
+
}
|
backend/api/api/urls.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from django.urls import path, include, re_path
|
| 3 |
+
from django.conf.urls.static import static
|
| 4 |
+
from django.conf import settings
|
| 5 |
+
from rest_framework import permissions
|
| 6 |
+
from drf_yasg.views import get_schema_view
|
| 7 |
+
from drf_yasg import openapi
|
| 8 |
+
|
| 9 |
+
schema_view = get_schema_view(
|
| 10 |
+
openapi.Info(
|
| 11 |
+
title="Intel Image Classifier API",
|
| 12 |
+
default_version='v1',
|
| 13 |
+
description="API for classifying natural scene images (buildings, forest, glacier, mountain, sea, street)",
|
| 14 |
+
contact=openapi.Contact(email="contact@example.com"),
|
| 15 |
+
license=openapi.License(name="MIT"),
|
| 16 |
+
),
|
| 17 |
+
public=True,
|
| 18 |
+
permission_classes=(permissions.AllowAny,),
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
urlpatterns = [
|
| 22 |
+
path('admin/', admin.site.urls),
|
| 23 |
+
path('api/', include('notifications.urls')),
|
| 24 |
+
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
|
| 25 |
+
path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
| 26 |
+
path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
| 30 |
+
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
backend/api/api/wsgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WSGI config for api project.
|
| 3 |
+
|
| 4 |
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.wsgi import get_wsgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api.settings')
|
| 15 |
+
|
| 16 |
+
application = get_wsgi_application()
|
backend/api/app.log
ADDED
|
File without changes
|
backend/api/manage.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Django's command-line utility for administrative tasks."""
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def main():
|
| 8 |
+
"""Run administrative tasks."""
|
| 9 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'api.settings')
|
| 10 |
+
try:
|
| 11 |
+
from django.core.management import execute_from_command_line
|
| 12 |
+
except ImportError as exc:
|
| 13 |
+
raise ImportError(
|
| 14 |
+
"Couldn't import Django. Are you sure it's installed and "
|
| 15 |
+
"available on your PYTHONPATH environment variable? Did you "
|
| 16 |
+
"forget to activate a virtual environment?"
|
| 17 |
+
) from exc
|
| 18 |
+
execute_from_command_line(sys.argv)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == '__main__':
|
| 22 |
+
main()
|
backend/api/models/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Models
|
| 2 |
+
|
| 3 |
+
Place your trained model files here:
|
| 4 |
+
|
| 5 |
+
- `pytorch_model.pth` — PyTorch CNN model
|
| 6 |
+
- `tensorflow_model.keras` — TensorFlow CNN model
|
| 7 |
+
|
backend/api/models/pytorch_model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:13d195dc6d7c62d86cc3a039a39f7eef1a119f6e67c1960fd161219635ac31c2
|
| 3 |
+
size 4859453
|
backend/api/notifications/__init__.py
ADDED
|
File without changes
|
backend/api/notifications/admin.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
# Register your models here.
|
| 3 |
+
|
backend/api/notifications/api_views.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import numpy as np
|
| 4 |
+
import requests
|
| 5 |
+
from io import BytesIO
|
| 6 |
+
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
from rest_framework.views import APIView
|
| 10 |
+
from rest_framework.response import Response
|
| 11 |
+
from rest_framework import status
|
| 12 |
+
|
| 13 |
+
from notifications.serializers import ImageClassifierSerializer, ClassificationResultSerializer
|
| 14 |
+
|
| 15 |
+
import torch
|
| 16 |
+
import torch.nn as nn
|
| 17 |
+
from torchvision import transforms
|
| 18 |
+
|
| 19 |
+
import tensorflow as tf
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# PATHS
|
| 23 |
+
|
| 24 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 25 |
+
PYTORCH_PATH = BASE_DIR / "models" / "pytorch_model.pth"
|
| 26 |
+
TENSORFLOW_PATH = BASE_DIR / "models" / "model_best.keras"
|
| 27 |
+
|
| 28 |
+
CLASSES = ["buildings", "forest", "glacier", "mountain", "sea", "street"]
|
| 29 |
+
|
| 30 |
+
CONFIDENCE_THRESHOLD = 0.6
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# PYTORCH MODEL
|
| 34 |
+
|
| 35 |
+
class CNN(nn.Module):
|
| 36 |
+
def __init__(self, num_classes=6):
|
| 37 |
+
super().__init__()
|
| 38 |
+
|
| 39 |
+
self.block1 = self._block(3, 32)
|
| 40 |
+
self.block2 = self._block(32, 64)
|
| 41 |
+
self.block3 = self._block(64, 128)
|
| 42 |
+
self.block4 = self._block(128, 256)
|
| 43 |
+
|
| 44 |
+
self.gap = nn.AdaptiveAvgPool2d(1)
|
| 45 |
+
self.fc1 = nn.Linear(256, 128)
|
| 46 |
+
self.fc2 = nn.Linear(128, num_classes)
|
| 47 |
+
self.dropout = nn.Dropout(0.5)
|
| 48 |
+
|
| 49 |
+
def _block(self, in_c, out_c):
|
| 50 |
+
return nn.Sequential(
|
| 51 |
+
nn.Conv2d(in_c, out_c, 3, padding=1),
|
| 52 |
+
nn.BatchNorm2d(out_c),
|
| 53 |
+
nn.ReLU(),
|
| 54 |
+
nn.Conv2d(out_c, out_c, 3, padding=1),
|
| 55 |
+
nn.BatchNorm2d(out_c),
|
| 56 |
+
nn.ReLU(),
|
| 57 |
+
nn.MaxPool2d(2)
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
def forward(self, x):
|
| 61 |
+
x = self.block1(x)
|
| 62 |
+
x = self.block2(x)
|
| 63 |
+
x = self.block3(x)
|
| 64 |
+
x = self.block4(x)
|
| 65 |
+
|
| 66 |
+
x = self.gap(x)
|
| 67 |
+
x = x.view(x.size(0), -1)
|
| 68 |
+
|
| 69 |
+
x = self.dropout(torch.relu(self.fc1(x)))
|
| 70 |
+
x = self.fc2(x)
|
| 71 |
+
return x
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# TRANSFORM
|
| 75 |
+
|
| 76 |
+
pytorch_transform = transforms.Compose([
|
| 77 |
+
transforms.Resize((150, 150)),
|
| 78 |
+
transforms.ToTensor(),
|
| 79 |
+
transforms.Normalize([0.485, 0.456, 0.406],
|
| 80 |
+
[0.229, 0.224, 0.225])
|
| 81 |
+
])
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
_pytorch_model = None
|
| 87 |
+
_tensorflow_model = None
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def get_pytorch_model():
|
| 91 |
+
global _pytorch_model
|
| 92 |
+
if _pytorch_model is None:
|
| 93 |
+
model = CNN(num_classes=len(CLASSES))
|
| 94 |
+
model.load_state_dict(torch.load(str(PYTORCH_PATH), map_location="cpu"))
|
| 95 |
+
model.eval()
|
| 96 |
+
_pytorch_model = model
|
| 97 |
+
return _pytorch_model
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def get_tensorflow_model():
|
| 101 |
+
global _tensorflow_model
|
| 102 |
+
if _tensorflow_model is None:
|
| 103 |
+
_tensorflow_model = tf.keras.models.load_model(str(TENSORFLOW_PATH), compile=False)
|
| 104 |
+
return _tensorflow_model
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class ImageClassifierAPIView(APIView):
|
| 109 |
+
|
| 110 |
+
def post(self, request):
|
| 111 |
+
serializer = ImageClassifierSerializer(data=request.data)
|
| 112 |
+
|
| 113 |
+
if not serializer.is_valid():
|
| 114 |
+
return Response(serializer.errors, status=400)
|
| 115 |
+
|
| 116 |
+
try:
|
| 117 |
+
|
| 118 |
+
if "image" in serializer.validated_data:
|
| 119 |
+
image = Image.open(serializer.validated_data["image"]).convert("RGB")
|
| 120 |
+
|
| 121 |
+
else:
|
| 122 |
+
image_url = serializer.validated_data["image_url"]
|
| 123 |
+
|
| 124 |
+
if not image_url.startswith("http"):
|
| 125 |
+
return Response({"error": "Invalid URL"}, status=400)
|
| 126 |
+
|
| 127 |
+
resp = requests.get(image_url, timeout=10)
|
| 128 |
+
resp.raise_for_status()
|
| 129 |
+
|
| 130 |
+
image = Image.open(BytesIO(resp.content)).convert("RGB")
|
| 131 |
+
|
| 132 |
+
model_name = serializer.validated_data.get("model", "pytorch")
|
| 133 |
+
|
| 134 |
+
if model_name == "pytorch":
|
| 135 |
+
result = self._predict_pytorch(image)
|
| 136 |
+
else:
|
| 137 |
+
result = self._predict_tensorflow(image)
|
| 138 |
+
|
| 139 |
+
result["model_used"] = model_name
|
| 140 |
+
|
| 141 |
+
return Response(result, status=200)
|
| 142 |
+
|
| 143 |
+
except Exception as e:
|
| 144 |
+
print("ERROR:", e)
|
| 145 |
+
return Response({"error": str(e)}, status=400)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def _predict_pytorch(self, image):
|
| 150 |
+
model = get_pytorch_model()
|
| 151 |
+
tensor = pytorch_transform(image).unsqueeze(0)
|
| 152 |
+
|
| 153 |
+
with torch.no_grad():
|
| 154 |
+
outputs = model(tensor)
|
| 155 |
+
probs = torch.nn.functional.softmax(outputs, dim=1)
|
| 156 |
+
|
| 157 |
+
values, indices = torch.topk(probs, k=len(CLASSES), dim=1)
|
| 158 |
+
|
| 159 |
+
values = values.squeeze().cpu().numpy()
|
| 160 |
+
indices = indices.squeeze().cpu().numpy()
|
| 161 |
+
|
| 162 |
+
confidence = float(values[0])
|
| 163 |
+
|
| 164 |
+
all_probs = [
|
| 165 |
+
{"class": CLASSES[indices[i]], "probability": float(values[i])}
|
| 166 |
+
for i in range(len(CLASSES))
|
| 167 |
+
]
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
if confidence < CONFIDENCE_THRESHOLD:
|
| 171 |
+
return {
|
| 172 |
+
"predicted_class": "unknown",
|
| 173 |
+
"confidence": confidence,
|
| 174 |
+
"all_probabilities": all_probs
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
return {
|
| 178 |
+
"predicted_class": CLASSES[indices[0]],
|
| 179 |
+
"confidence": confidence,
|
| 180 |
+
"all_probabilities": all_probs
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
# TENSORFLOW
|
| 184 |
+
|
| 185 |
+
def _predict_tensorflow(self, image):
|
| 186 |
+
|
| 187 |
+
model = get_tensorflow_model()
|
| 188 |
+
|
| 189 |
+
img = image.resize((130, 130))
|
| 190 |
+
|
| 191 |
+
arr = np.array(img, dtype=np.float32)
|
| 192 |
+
arr = arr[:, :, ::-1]
|
| 193 |
+
arr = np.expand_dims(arr, 0)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
preds = model.predict(arr, verbose=0)[0]
|
| 197 |
+
sorted_idx = np.argsort(preds)[::-1]
|
| 198 |
+
|
| 199 |
+
confidence = float(preds[sorted_idx[0]])
|
| 200 |
+
|
| 201 |
+
all_probs = [
|
| 202 |
+
{"class": CLASSES[i], "probability": float(preds[i])}
|
| 203 |
+
for i in sorted_idx
|
| 204 |
+
]
|
| 205 |
+
|
| 206 |
+
if confidence < CONFIDENCE_THRESHOLD:
|
| 207 |
+
|
| 208 |
+
return {
|
| 209 |
+
"predicted_class": "unknown",
|
| 210 |
+
"confidence": confidence,
|
| 211 |
+
"all_probabilities": all_probs
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
return {
|
| 215 |
+
"predicted_class": CLASSES[sorted_idx[0]],
|
| 216 |
+
"confidence": confidence,
|
| 217 |
+
"all_probabilities": all_probs
|
| 218 |
+
}
|
backend/api/notifications/apps.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class NotificationsConfig(AppConfig):
|
| 5 |
+
default_auto_field = 'django.db.models.BigAutoField'
|
| 6 |
+
name = 'notifications'
|
| 7 |
+
|
backend/api/notifications/models.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from django.db import models
|
backend/api/notifications/serializers.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework import serializers
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class ImageClassifierSerializer(serializers.Serializer):
|
| 5 |
+
image = serializers.ImageField(required=False)
|
| 6 |
+
image_url = serializers.URLField(required=False)
|
| 7 |
+
model = serializers.ChoiceField(
|
| 8 |
+
choices=['pytorch', 'tensorflow'],
|
| 9 |
+
default='pytorch'
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
def validate(self, data):
|
| 13 |
+
if not data.get('image') and not data.get('image_url'):
|
| 14 |
+
raise serializers.ValidationError("Provide either 'image' or 'image_url'.")
|
| 15 |
+
return data
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ClassificationResultSerializer(serializers.Serializer):
|
| 19 |
+
predicted_class = serializers.CharField()
|
| 20 |
+
model_used = serializers.CharField()
|
| 21 |
+
confidence = serializers.FloatField()
|
| 22 |
+
all_probabilities = serializers.ListField(child=serializers.DictField())
|
backend/api/notifications/tests.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.test import TestCase
|
| 2 |
+
|
| 3 |
+
# Create your tests here.
|
backend/api/notifications/urls.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path
|
| 2 |
+
from notifications.api_views import ImageClassifierAPIView
|
| 3 |
+
|
| 4 |
+
urlpatterns = [
|
| 5 |
+
path('classify/', ImageClassifierAPIView.as_view(), name='api_classify'),
|
| 6 |
+
]
|
backend/api/notifications/views.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.shortcuts import render
|
| 2 |
+
|
| 3 |
+
# Create your views here.
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Django>=5.0
|
| 2 |
+
djangorestframework>=3.14.0
|
| 3 |
+
django-environ
|
| 4 |
+
django-filter
|
| 5 |
+
django-cors-headers
|
| 6 |
+
python-dotenv
|
| 7 |
+
Pillow
|
| 8 |
+
requests
|
| 9 |
+
drf_yasg
|
| 10 |
+
torch>=2.2.0
|
| 11 |
+
torchvision>=0.17.0
|
| 12 |
+
tensorflow>=2.16.0
|
| 13 |
+
numpy>=1.26.0
|
| 14 |
+
timm
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: "3.9"
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
|
| 5 |
+
backend:
|
| 6 |
+
build:
|
| 7 |
+
context: ./backend
|
| 8 |
+
dockerfile: Dockerfile
|
| 9 |
+
ports:
|
| 10 |
+
- "8000:8000"
|
| 11 |
+
volumes:
|
| 12 |
+
- ./backend:/app
|
| 13 |
+
- ./ml/models:/app/api/models # mount trained models
|
| 14 |
+
environment:
|
| 15 |
+
- DEBUG=True
|
| 16 |
+
restart: unless-stopped
|
| 17 |
+
|
| 18 |
+
frontend:
|
| 19 |
+
build:
|
| 20 |
+
context: ./frontend
|
| 21 |
+
dockerfile: Dockerfile
|
| 22 |
+
ports:
|
| 23 |
+
- "3000:3000"
|
| 24 |
+
depends_on:
|
| 25 |
+
- backend
|
| 26 |
+
environment:
|
| 27 |
+
- REACT_APP_API_URL=http://localhost:8000
|
| 28 |
+
restart: unless-stopped
|
frontend/Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-alpine
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY package.json .
|
| 6 |
+
RUN npm install
|
| 7 |
+
|
| 8 |
+
COPY . .
|
| 9 |
+
|
| 10 |
+
EXPOSE 3000
|
| 11 |
+
|
| 12 |
+
CMD ["npm", "start"]
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"proxy": "http://127.0.0.1:5000",
|
| 3 |
+
"name": "intel-image-classifier",
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"private": true,
|
| 6 |
+
"dependencies": {
|
| 7 |
+
"@emotion/react": "^11.14.0",
|
| 8 |
+
"@emotion/styled": "^11.14.0",
|
| 9 |
+
"@mui/icons-material": "^6.4.0",
|
| 10 |
+
"@mui/material": "^6.4.0",
|
| 11 |
+
"@reduxjs/toolkit": "^2.5.0",
|
| 12 |
+
"axios": "^1.15.0",
|
| 13 |
+
"react": "^19.0.0",
|
| 14 |
+
"react-dom": "^19.0.0",
|
| 15 |
+
"react-redux": "^9.2.0",
|
| 16 |
+
"react-router-dom": "^7.1.1",
|
| 17 |
+
"react-scripts": "^5.0.1"
|
| 18 |
+
},
|
| 19 |
+
"scripts": {
|
| 20 |
+
"start": "react-scripts start",
|
| 21 |
+
"build": "react-scripts build",
|
| 22 |
+
"test": "react-scripts test",
|
| 23 |
+
"eject": "react-scripts eject"
|
| 24 |
+
},
|
| 25 |
+
"eslintConfig": {
|
| 26 |
+
"extends": ["react-app", "react-app/jest"]
|
| 27 |
+
},
|
| 28 |
+
"browserslist": {
|
| 29 |
+
"production": [">0.2%", "not dead", "not op_mini all"],
|
| 30 |
+
"development": ["last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
|
| 31 |
+
}
|
| 32 |
+
}
|
frontend/public/favicon.ico
ADDED
|
|
frontend/public/index.html
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 7 |
+
<meta name="theme-color" content="#000000" />
|
| 8 |
+
<meta
|
| 9 |
+
name="description"
|
| 10 |
+
content="Web site created using create-react-app"
|
| 11 |
+
/>
|
| 12 |
+
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
| 13 |
+
<!--
|
| 14 |
+
manifest.json provides metadata used when your web app is installed on a
|
| 15 |
+
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
| 16 |
+
-->
|
| 17 |
+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
| 18 |
+
<!--
|
| 19 |
+
Notice the use of %PUBLIC_URL% in the tags above.
|
| 20 |
+
It will be replaced with the URL of the `public` folder during the build.
|
| 21 |
+
Only files inside the `public` folder can be referenced from the HTML.
|
| 22 |
+
|
| 23 |
+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
| 24 |
+
work correctly both with client-side routing and a non-root public URL.
|
| 25 |
+
Learn how to configure a non-root public URL by running `npm run build`.
|
| 26 |
+
-->
|
| 27 |
+
<title>DANIELLE</title>
|
| 28 |
+
</head>
|
| 29 |
+
<body>
|
| 30 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
| 31 |
+
<div id="root"></div>
|
| 32 |
+
<!--
|
| 33 |
+
This HTML file is a template.
|
| 34 |
+
If you open it directly in the browser, you will see an empty page.
|
| 35 |
+
|
| 36 |
+
You can add webfonts, meta tags, or analytics to this file.
|
| 37 |
+
The build step will place the bundled scripts into the <body> tag.
|
| 38 |
+
|
| 39 |
+
To begin the development, run `npm start` or `yarn start`.
|
| 40 |
+
To create a production bundle, use `npm run build` or `yarn build`.
|
| 41 |
+
-->
|
| 42 |
+
</body>
|
| 43 |
+
</html>
|
frontend/public/manifest.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"short_name": "React App",
|
| 3 |
+
"name": "Create React App Sample",
|
| 4 |
+
"icons": [
|
| 5 |
+
{
|
| 6 |
+
"src": "favicon.ico",
|
| 7 |
+
"sizes": "64x64 32x32 24x24 16x16",
|
| 8 |
+
"type": "image/x-icon"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"src": "logo192.png",
|
| 12 |
+
"type": "image/png",
|
| 13 |
+
"sizes": "192x192"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"src": "logo512.png",
|
| 17 |
+
"type": "image/png",
|
| 18 |
+
"sizes": "512x512"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"start_url": ".",
|
| 22 |
+
"display": "standalone",
|
| 23 |
+
"theme_color": "#000000",
|
| 24 |
+
"background_color": "#ffffff"
|
| 25 |
+
}
|
frontend/public/robots.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# https://www.robotstxt.org/robotstxt.html
|
| 2 |
+
User-agent: *
|
| 3 |
+
Disallow:
|
frontend/src/App.css
ADDED
|
File without changes
|
frontend/src/App.js
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useMemo } from "react";
|
| 2 |
+
import axios from "axios";
|
| 3 |
+
|
| 4 |
+
import {
|
| 5 |
+
Box,
|
| 6 |
+
Button,
|
| 7 |
+
Container,
|
| 8 |
+
Typography,
|
| 9 |
+
Paper,
|
| 10 |
+
ThemeProvider,
|
| 11 |
+
createTheme,
|
| 12 |
+
IconButton,
|
| 13 |
+
Card,
|
| 14 |
+
CardMedia,
|
| 15 |
+
Chip,
|
| 16 |
+
AppBar,
|
| 17 |
+
Toolbar,
|
| 18 |
+
TextField,
|
| 19 |
+
Select,
|
| 20 |
+
FormControl,
|
| 21 |
+
InputLabel,
|
| 22 |
+
MenuItem,
|
| 23 |
+
LinearProgress,
|
| 24 |
+
Collapse,
|
| 25 |
+
Tooltip,
|
| 26 |
+
Divider
|
| 27 |
+
} from "@mui/material";
|
| 28 |
+
|
| 29 |
+
import {
|
| 30 |
+
CloudUpload as CloudUploadIcon,
|
| 31 |
+
Landscape as LandscapeIcon,
|
| 32 |
+
DarkMode as DarkModeIcon,
|
| 33 |
+
LightMode as LightModeIcon,
|
| 34 |
+
ExpandMore as ExpandMoreIcon,
|
| 35 |
+
ExpandLess as ExpandLessIcon,
|
| 36 |
+
GitHub as GitHubIcon,
|
| 37 |
+
LinkedIn as LinkedInIcon
|
| 38 |
+
} from "@mui/icons-material";
|
| 39 |
+
|
| 40 |
+
const LANGUAGES = [
|
| 41 |
+
{ code: "en", flag: "en", label: "English" },
|
| 42 |
+
{ code: "fr", flag: "fr", label: "Français" },
|
| 43 |
+
{ code: "wo", flag: "wo", label: "Wolof" },
|
| 44 |
+
];
|
| 45 |
+
|
| 46 |
+
function App() {
|
| 47 |
+
|
| 48 |
+
const [mode, setMode] = useState("light");
|
| 49 |
+
|
| 50 |
+
const theme = useMemo(() =>
|
| 51 |
+
createTheme({
|
| 52 |
+
palette: {
|
| 53 |
+
mode,
|
| 54 |
+
primary: { main: "#2e7d32" },
|
| 55 |
+
},
|
| 56 |
+
shape: { borderRadius: 16 },
|
| 57 |
+
components: {
|
| 58 |
+
MuiPaper: {
|
| 59 |
+
styleOverrides: {
|
| 60 |
+
root: {
|
| 61 |
+
borderRadius: 20,
|
| 62 |
+
boxShadow: mode === "dark"
|
| 63 |
+
? "0 8px 32px rgba(0,0,0,0.6)"
|
| 64 |
+
: "0 8px 32px rgba(0,0,0,0.12)",
|
| 65 |
+
transition: "transform 0.25s ease, box-shadow 0.25s ease",
|
| 66 |
+
"&:hover": {
|
| 67 |
+
transform: "translateY(-4px)",
|
| 68 |
+
boxShadow: mode === "dark"
|
| 69 |
+
? "0 16px 48px rgba(0,0,0,0.7)"
|
| 70 |
+
: "0 16px 48px rgba(0,0,0,0.18)",
|
| 71 |
+
},
|
| 72 |
+
},
|
| 73 |
+
},
|
| 74 |
+
},
|
| 75 |
+
MuiCard: {
|
| 76 |
+
styleOverrides: {
|
| 77 |
+
root: {
|
| 78 |
+
borderRadius: 20,
|
| 79 |
+
boxShadow: mode === "dark"
|
| 80 |
+
? "0 8px 32px rgba(0,0,0,0.6)"
|
| 81 |
+
: "0 8px 32px rgba(0,0,0,0.12)",
|
| 82 |
+
transition: "transform 0.25s ease, box-shadow 0.25s ease",
|
| 83 |
+
"&:hover": {
|
| 84 |
+
transform: "translateY(-4px)",
|
| 85 |
+
boxShadow: mode === "dark"
|
| 86 |
+
? "0 16px 48px rgba(0,0,0,0.7)"
|
| 87 |
+
: "0 16px 48px rgba(0,0,0,0.18)",
|
| 88 |
+
},
|
| 89 |
+
},
|
| 90 |
+
},
|
| 91 |
+
},
|
| 92 |
+
MuiButton: {
|
| 93 |
+
styleOverrides: {
|
| 94 |
+
root: { borderRadius: 10, textTransform: "none", fontWeight: 600 },
|
| 95 |
+
},
|
| 96 |
+
},
|
| 97 |
+
},
|
| 98 |
+
}),
|
| 99 |
+
[mode]
|
| 100 |
+
);
|
| 101 |
+
|
| 102 |
+
const [language, setLanguage] = useState("en");
|
| 103 |
+
|
| 104 |
+
const texts = {
|
| 105 |
+
en: {
|
| 106 |
+
title: "Intel Image Classifier",
|
| 107 |
+
subtitle: "Classify natural scenes",
|
| 108 |
+
upload: "Upload Image",
|
| 109 |
+
urlBtn: "Load image from URL",
|
| 110 |
+
classify: "Classify",
|
| 111 |
+
result: "Result",
|
| 112 |
+
confidence: "Confidence",
|
| 113 |
+
selectModel: "Select Model",
|
| 114 |
+
processing: "Processing...",
|
| 115 |
+
selectImage: "Please provide an image",
|
| 116 |
+
classes: "Possible Classes",
|
| 117 |
+
reset: "Reset",
|
| 118 |
+
details: "Details",
|
| 119 |
+
hideDetails: "Hide",
|
| 120 |
+
unknown: "Image not recognized",
|
| 121 |
+
},
|
| 122 |
+
fr: {
|
| 123 |
+
title: "Classificateur Intel",
|
| 124 |
+
subtitle: "Classifiez des scènes naturelles avec le deep learning",
|
| 125 |
+
upload: "Télécharger Image",
|
| 126 |
+
urlBtn: "Charger image depuis URL",
|
| 127 |
+
classify: "Classer",
|
| 128 |
+
result: "Résultat",
|
| 129 |
+
confidence: "Confiance",
|
| 130 |
+
selectModel: "Choisir modèle",
|
| 131 |
+
processing: "Traitement...",
|
| 132 |
+
selectImage: "Veuillez fournir une image",
|
| 133 |
+
classes: "Classes possibles",
|
| 134 |
+
reset: "Réinitialiser",
|
| 135 |
+
details: "Détails",
|
| 136 |
+
hideDetails: "Masquer",
|
| 137 |
+
unknown: "Image non reconnue",
|
| 138 |
+
},
|
| 139 |
+
wo: {
|
| 140 |
+
title: "Intel Xët-Nataal (IA)",
|
| 141 |
+
subtitle: "Jëfandikoo IA ngir xool nataal yi",
|
| 142 |
+
upload: "Yeb Nataal bi",
|
| 143 |
+
urlBtn: "Yeb nataal ci URL",
|
| 144 |
+
classify: "Wone",
|
| 145 |
+
result: "Njëg",
|
| 146 |
+
confidence: "Loo xam ne",
|
| 147 |
+
selectModel: "Tànn modil",
|
| 148 |
+
processing: "Di liggéey...",
|
| 149 |
+
selectImage: "Tànnal ab nataal",
|
| 150 |
+
classes: "Yëgël yi",
|
| 151 |
+
reset: "Tàkku",
|
| 152 |
+
details: "Xam ci kanam",
|
| 153 |
+
hideDetails: "Planque",
|
| 154 |
+
unknown: "Nataal xamul",
|
| 155 |
+
}
|
| 156 |
+
};
|
| 157 |
+
|
| 158 |
+
const t = texts[language];
|
| 159 |
+
|
| 160 |
+
const CLASS_LABELS = {
|
| 161 |
+
buildings: { en: "Buildings", fr: "Bâtiments", wo: "Kër yi" },
|
| 162 |
+
forest: { en: "Forest", fr: "Forêt", wo: "Géej bu wees" },
|
| 163 |
+
glacier: { en: "Glacier", fr: "Glacier", wo: "Dëkk bu sedd" },
|
| 164 |
+
mountain: { en: "Mountain", fr: "Montagne", wo: "Tund bi" },
|
| 165 |
+
sea: { en: "Sea", fr: "Mer", wo: "Géej bi" },
|
| 166 |
+
street: { en: "Street", fr: "Rue", wo: "Yoon bi" },
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
const getLabel = (cls) => CLASS_LABELS[cls]?.[language] ?? cls;
|
| 170 |
+
|
| 171 |
+
const [selectedImage, setSelectedImage] = useState(null);
|
| 172 |
+
const [imageUrl, setImageUrl] = useState("");
|
| 173 |
+
const [preview, setPreview] = useState(null);
|
| 174 |
+
const [showUrlInput, setShowUrlInput] = useState(false);
|
| 175 |
+
const [model, setModel] = useState("pytorch");
|
| 176 |
+
const [result, setResult] = useState(null);
|
| 177 |
+
const [isProcessing, setIsProcessing] = useState(false);
|
| 178 |
+
const [error, setError] = useState(null);
|
| 179 |
+
const [showDetails, setShowDetails] = useState(false);
|
| 180 |
+
|
| 181 |
+
const handleImageUpload = (event) => {
|
| 182 |
+
const file = event.target.files[0];
|
| 183 |
+
if (!file) return;
|
| 184 |
+
setSelectedImage(file);
|
| 185 |
+
setPreview(URL.createObjectURL(file));
|
| 186 |
+
setImageUrl("");
|
| 187 |
+
setResult(null);
|
| 188 |
+
setError(null);
|
| 189 |
+
setShowDetails(false);
|
| 190 |
+
};
|
| 191 |
+
|
| 192 |
+
const resetAll = () => {
|
| 193 |
+
setSelectedImage(null);
|
| 194 |
+
setImageUrl("");
|
| 195 |
+
setPreview(null);
|
| 196 |
+
setResult(null);
|
| 197 |
+
setError(null);
|
| 198 |
+
setShowDetails(false);
|
| 199 |
+
};
|
| 200 |
+
|
| 201 |
+
const processImage = async () => {
|
| 202 |
+
if (!selectedImage && !imageUrl) {
|
| 203 |
+
setError(t.selectImage);
|
| 204 |
+
return;
|
| 205 |
+
}
|
| 206 |
+
setIsProcessing(true);
|
| 207 |
+
setError(null);
|
| 208 |
+
|
| 209 |
+
try {
|
| 210 |
+
const formData = new FormData();
|
| 211 |
+
if (selectedImage) {
|
| 212 |
+
formData.append("image", selectedImage);
|
| 213 |
+
} else {
|
| 214 |
+
formData.append("image_url", imageUrl);
|
| 215 |
+
}
|
| 216 |
+
formData.append("model", model);
|
| 217 |
+
|
| 218 |
+
const response = await axios.post(
|
| 219 |
+
"http://127.0.0.1:8000/api/classify/",
|
| 220 |
+
formData,
|
| 221 |
+
{ headers: { "Content-Type": "multipart/form-data" } }
|
| 222 |
+
);
|
| 223 |
+
|
| 224 |
+
const data = response.data;
|
| 225 |
+
const conf = Math.round((parseFloat(data.confidence) || 0) * 100);
|
| 226 |
+
const allProbs = (data.all_probabilities || []).map(item => ({
|
| 227 |
+
class: item.class,
|
| 228 |
+
probability: parseFloat(item.probability) || 0
|
| 229 |
+
}));
|
| 230 |
+
|
| 231 |
+
setResult({
|
| 232 |
+
predictedClass: conf < 50 ? "unknown" : (data.predicted_class || "unknown"),
|
| 233 |
+
confidence: conf,
|
| 234 |
+
allProbabilities: allProbs,
|
| 235 |
+
modelUsed: data.model_used || model
|
| 236 |
+
});
|
| 237 |
+
|
| 238 |
+
} catch (err) {
|
| 239 |
+
console.error(err);
|
| 240 |
+
setError("Classification error. Please try again.");
|
| 241 |
+
} finally {
|
| 242 |
+
setIsProcessing(false);
|
| 243 |
+
}
|
| 244 |
+
};
|
| 245 |
+
|
| 246 |
+
return (
|
| 247 |
+
<ThemeProvider theme={theme}>
|
| 248 |
+
<Box sx={{
|
| 249 |
+
minHeight: "100vh",
|
| 250 |
+
display: "flex",
|
| 251 |
+
flexDirection: "column",
|
| 252 |
+
bgcolor: "background.default",
|
| 253 |
+
color: "text.primary"
|
| 254 |
+
}}>
|
| 255 |
+
|
| 256 |
+
{/* HEADER */}
|
| 257 |
+
<AppBar position="static" color="transparent" elevation={0}
|
| 258 |
+
sx={{ borderBottom: "1px solid", borderColor: "divider" }}>
|
| 259 |
+
<Toolbar>
|
| 260 |
+
<LandscapeIcon sx={{ mr: 2, color: "primary.main" }} />
|
| 261 |
+
<Typography variant="h6" sx={{ flexGrow: 1, fontWeight: 800 }}>
|
| 262 |
+
{t.title}
|
| 263 |
+
</Typography>
|
| 264 |
+
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mr: 1 }}>
|
| 265 |
+
{LANGUAGES.map((lang) => (
|
| 266 |
+
<Tooltip key={lang.code} title={lang.label}>
|
| 267 |
+
<IconButton
|
| 268 |
+
onClick={() => setLanguage(lang.code)}
|
| 269 |
+
size="small"
|
| 270 |
+
sx={{
|
| 271 |
+
fontSize: "1.4rem",
|
| 272 |
+
opacity: language === lang.code ? 1 : 0.35,
|
| 273 |
+
transition: "opacity 0.2s",
|
| 274 |
+
p: "4px",
|
| 275 |
+
"&:hover": { opacity: 0.8 }
|
| 276 |
+
}}
|
| 277 |
+
>
|
| 278 |
+
{lang.flag}
|
| 279 |
+
</IconButton>
|
| 280 |
+
</Tooltip>
|
| 281 |
+
))}
|
| 282 |
+
</Box>
|
| 283 |
+
<Tooltip title={mode === "light" ? "Dark mode" : "Light mode"}>
|
| 284 |
+
<IconButton onClick={() => setMode(mode === "light" ? "dark" : "light")}>
|
| 285 |
+
{mode === "light" ? <DarkModeIcon /> : <LightModeIcon />}
|
| 286 |
+
</IconButton>
|
| 287 |
+
</Tooltip>
|
| 288 |
+
</Toolbar>
|
| 289 |
+
</AppBar>
|
| 290 |
+
|
| 291 |
+
{/* MAIN */}
|
| 292 |
+
<Container maxWidth="md" sx={{ mt: 4, flex: 1 }}>
|
| 293 |
+
|
| 294 |
+
{/* INPUT BOX */}
|
| 295 |
+
<Paper sx={{ p: 4 }} elevation={3}>
|
| 296 |
+
<Typography variant="h4" align="center"
|
| 297 |
+
sx={{ fontWeight: 800, mb: 1 }}>
|
| 298 |
+
{t.title}
|
| 299 |
+
</Typography>
|
| 300 |
+
<Typography align="center" color="text.secondary" sx={{ mb: 3 }}>
|
| 301 |
+
{t.subtitle}
|
| 302 |
+
</Typography>
|
| 303 |
+
|
| 304 |
+
<FormControl fullWidth sx={{ mt: 1 }}>
|
| 305 |
+
<InputLabel>{t.selectModel}</InputLabel>
|
| 306 |
+
<Select
|
| 307 |
+
value={model}
|
| 308 |
+
label={t.selectModel}
|
| 309 |
+
onChange={(e) => setModel(e.target.value)}
|
| 310 |
+
>
|
| 311 |
+
<MenuItem value="pytorch">PyTorch CNN</MenuItem>
|
| 312 |
+
<MenuItem value="tensorflow">TensorFlow CNN</MenuItem>
|
| 313 |
+
</Select>
|
| 314 |
+
</FormControl>
|
| 315 |
+
|
| 316 |
+
<Box sx={{ mt: 3, display: "flex", gap: 3, flexWrap: "wrap" }}>
|
| 317 |
+
<Box sx={{ flex: 1, minWidth: "250px" }}>
|
| 318 |
+
<Typography gutterBottom>{t.upload}</Typography>
|
| 319 |
+
<Button fullWidth variant="outlined"
|
| 320 |
+
startIcon={<CloudUploadIcon />} component="label">
|
| 321 |
+
{t.upload}
|
| 322 |
+
<input type="file" hidden accept="image/*" onChange={handleImageUpload} />
|
| 323 |
+
</Button>
|
| 324 |
+
</Box>
|
| 325 |
+
<Box sx={{ flex: 1, minWidth: "250px" }}>
|
| 326 |
+
<Typography gutterBottom>{t.urlBtn}</Typography>
|
| 327 |
+
<Button fullWidth variant="outlined" color="primary"
|
| 328 |
+
startIcon={<CloudUploadIcon />}
|
| 329 |
+
onClick={() => setShowUrlInput(!showUrlInput)}>
|
| 330 |
+
{t.urlBtn}
|
| 331 |
+
</Button>
|
| 332 |
+
{showUrlInput && (
|
| 333 |
+
<TextField fullWidth label="URL" value={imageUrl}
|
| 334 |
+
onChange={(e) => {
|
| 335 |
+
setImageUrl(e.target.value);
|
| 336 |
+
setSelectedImage(null);
|
| 337 |
+
setPreview(e.target.value);
|
| 338 |
+
}}
|
| 339 |
+
sx={{ mt: 2 }} />
|
| 340 |
+
)}
|
| 341 |
+
</Box>
|
| 342 |
+
</Box>
|
| 343 |
+
|
| 344 |
+
<Box sx={{ mt: 3 }}>
|
| 345 |
+
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
| 346 |
+
{t.classes}:
|
| 347 |
+
</Typography>
|
| 348 |
+
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 1 }}>
|
| 349 |
+
{Object.keys(CLASS_LABELS).map((cls) => (
|
| 350 |
+
<Chip key={cls} label={getLabel(cls)} size="small" variant="outlined" />
|
| 351 |
+
))}
|
| 352 |
+
</Box>
|
| 353 |
+
</Box>
|
| 354 |
+
</Paper>
|
| 355 |
+
|
| 356 |
+
{/* IMAGE PREVIEW */}
|
| 357 |
+
{preview && (
|
| 358 |
+
<Card sx={{ mt: 3, p: 2, textAlign: "center" }}>
|
| 359 |
+
<CardMedia component="img" height="300" image={preview}
|
| 360 |
+
sx={{ objectFit: "contain", borderRadius: 2 }} />
|
| 361 |
+
{isProcessing && <LinearProgress sx={{ mt: 1 }} color="primary" />}
|
| 362 |
+
<Box sx={{ mt: 2 }}>
|
| 363 |
+
<Button variant="contained" color="primary"
|
| 364 |
+
onClick={processImage} disabled={isProcessing} size="large">
|
| 365 |
+
{isProcessing ? t.processing : t.classify}
|
| 366 |
+
</Button>
|
| 367 |
+
</Box>
|
| 368 |
+
</Card>
|
| 369 |
+
)}
|
| 370 |
+
|
| 371 |
+
{/* ERROR */}
|
| 372 |
+
{error && (
|
| 373 |
+
<Paper sx={{ mt: 2, p: 2, backgroundColor: "#ffebee" }}>
|
| 374 |
+
<Typography color="error">{error}</Typography>
|
| 375 |
+
</Paper>
|
| 376 |
+
)}
|
| 377 |
+
|
| 378 |
+
{/* RESULT */}
|
| 379 |
+
{result && (
|
| 380 |
+
<Paper sx={{ mt: 3, p: 3, textAlign: "center" }}>
|
| 381 |
+
<Typography variant="h6" fontWeight={700} gutterBottom>
|
| 382 |
+
{t.result}
|
| 383 |
+
</Typography>
|
| 384 |
+
|
| 385 |
+
<Chip
|
| 386 |
+
label={result.predictedClass === "unknown"
|
| 387 |
+
? t.unknown
|
| 388 |
+
: getLabel(result.predictedClass)}
|
| 389 |
+
color={result.predictedClass === "unknown" ? "error" : "success"}
|
| 390 |
+
sx={{ fontSize: "1.1rem", px: 2, py: 2.5, mt: 1, fontWeight: 700 }}
|
| 391 |
+
/>
|
| 392 |
+
|
| 393 |
+
<Typography sx={{ mt: 2 }}>
|
| 394 |
+
{t.confidence}: <strong>{result.confidence}%</strong>
|
| 395 |
+
</Typography>
|
| 396 |
+
<Typography variant="caption" color="text.secondary">
|
| 397 |
+
Model: {result.modelUsed}
|
| 398 |
+
</Typography>
|
| 399 |
+
|
| 400 |
+
<Collapse in={showDetails}>
|
| 401 |
+
<Box sx={{ mt: 2, textAlign: "left" }}>
|
| 402 |
+
{result.allProbabilities.map((item) => (
|
| 403 |
+
<Box key={item.class} sx={{ mb: 1.5 }}>
|
| 404 |
+
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 0.5 }}>
|
| 405 |
+
<Typography variant="body2" fontWeight={600}>
|
| 406 |
+
{getLabel(item.class)}
|
| 407 |
+
</Typography>
|
| 408 |
+
<Typography variant="body2">
|
| 409 |
+
{Math.round(item.probability * 100)}%
|
| 410 |
+
</Typography>
|
| 411 |
+
</Box>
|
| 412 |
+
<LinearProgress variant="determinate"
|
| 413 |
+
value={Math.round(item.probability * 100)}
|
| 414 |
+
sx={{ height: 8, borderRadius: 4 }} color="primary" />
|
| 415 |
+
</Box>
|
| 416 |
+
))}
|
| 417 |
+
</Box>
|
| 418 |
+
</Collapse>
|
| 419 |
+
|
| 420 |
+
<Box sx={{ mt: 3, display: "flex", justifyContent: "center", gap: 2 }}>
|
| 421 |
+
<Button variant="outlined" onClick={resetAll}>{t.reset}</Button>
|
| 422 |
+
<Button variant="outlined" color="primary"
|
| 423 |
+
onClick={() => setShowDetails(!showDetails)}
|
| 424 |
+
endIcon={showDetails ? <ExpandLessIcon /> : <ExpandMoreIcon />}>
|
| 425 |
+
{showDetails ? t.hideDetails : t.details}
|
| 426 |
+
</Button>
|
| 427 |
+
</Box>
|
| 428 |
+
</Paper>
|
| 429 |
+
)}
|
| 430 |
+
|
| 431 |
+
</Container>
|
| 432 |
+
|
| 433 |
+
{/* FOOTER */}
|
| 434 |
+
<Box sx={{ mt: "auto" }}>
|
| 435 |
+
<Divider />
|
| 436 |
+
<Box sx={{
|
| 437 |
+
py: 1.5, px: 4,
|
| 438 |
+
display: "flex",
|
| 439 |
+
alignItems: "center",
|
| 440 |
+
justifyContent: "space-between",
|
| 441 |
+
flexWrap: "wrap",
|
| 442 |
+
gap: 1
|
| 443 |
+
}}>
|
| 444 |
+
<Typography variant="body2" color="text.secondary">
|
| 445 |
+
© 2026 Intel Image Classifier By <strong>Tsemo Danielle</strong>
|
| 446 |
+
</Typography>
|
| 447 |
+
<Box sx={{ display: "flex", gap: 1 }}>
|
| 448 |
+
<Tooltip title="GitHub">
|
| 449 |
+
<IconButton size="small"
|
| 450 |
+
onClick={() => window.open("https://github.com/nguemtchuengdanielle/")}>
|
| 451 |
+
<GitHubIcon fontSize="small" />
|
| 452 |
+
</IconButton>
|
| 453 |
+
</Tooltip>
|
| 454 |
+
<Tooltip title="LinkedIn">
|
| 455 |
+
<IconButton size="small"
|
| 456 |
+
onClick={() => window.open("https://linkedin.com/in/danielle-tsemo3")}
|
| 457 |
+
sx={{ color: "#0077b5" }}>
|
| 458 |
+
<LinkedInIcon fontSize="small" />
|
| 459 |
+
</IconButton>
|
| 460 |
+
</Tooltip>
|
| 461 |
+
</Box>
|
| 462 |
+
</Box>
|
| 463 |
+
</Box>
|
| 464 |
+
|
| 465 |
+
</Box>
|
| 466 |
+
</ThemeProvider>
|
| 467 |
+
);
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
export default App;
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import url('https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400..700&display=swap');
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=Dancing+Script:wght@400..700&family=Quicksand:wght@300..700&display=swap');
|
| 3 |
+
*{
|
| 4 |
+
margin: 0px;
|
| 5 |
+
padding: 0px;
|
| 6 |
+
}
|
frontend/src/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import ReactDOM from 'react-dom/client';
|
| 3 |
+
import './index.css';
|
| 4 |
+
import App from './App';
|
| 5 |
+
import {BrowserRouter} from "react-router-dom";
|
| 6 |
+
import axios from 'axios';
|
| 7 |
+
import {Provider} from 'react-redux'
|
| 8 |
+
|
| 9 |
+
import { store } from './store';
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
axios.defaults.baseURL = process.env.REACT_APP_API_URL || "http://localhost:8081";
|
| 13 |
+
|
| 14 |
+
document.title = "Classification de Prunes";
|
| 15 |
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
| 16 |
+
root.render(
|
| 17 |
+
<React.StrictMode>
|
| 18 |
+
<BrowserRouter>
|
| 19 |
+
<Provider store={store} >
|
| 20 |
+
<App />
|
| 21 |
+
</Provider>
|
| 22 |
+
|
| 23 |
+
</BrowserRouter>
|
| 24 |
+
|
| 25 |
+
</React.StrictMode>
|
| 26 |
+
);
|
| 27 |
+
|
| 28 |
+
|
frontend/src/reportWebVitals.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const reportWebVitals = onPerfEntry => {
|
| 2 |
+
if (onPerfEntry && onPerfEntry instanceof Function) {
|
| 3 |
+
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
| 4 |
+
getCLS(onPerfEntry);
|
| 5 |
+
getFID(onPerfEntry);
|
| 6 |
+
getFCP(onPerfEntry);
|
| 7 |
+
getLCP(onPerfEntry);
|
| 8 |
+
getTTFB(onPerfEntry);
|
| 9 |
+
});
|
| 10 |
+
}
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
export default reportWebVitals;
|
frontend/src/setupTests.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
| 2 |
+
// allows you to do things like:
|
| 3 |
+
// expect(element).toHaveTextContent(/react/i)
|
| 4 |
+
// learn more: https://github.com/testing-library/jest-dom
|
| 5 |
+
import '@testing-library/jest-dom';
|
frontend/src/store/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {createSlice, configureStore} from '@reduxjs/toolkit';
|
| 2 |
+
|
| 3 |
+
const authSlice = createSlice({
|
| 4 |
+
name: "auth",
|
| 5 |
+
initialState: {
|
| 6 |
+
isLoggedIn: false,
|
| 7 |
+
isAdmin: false,
|
| 8 |
+
openSnackbar: false,
|
| 9 |
+
msg: "",
|
| 10 |
+
snackbarId: 0 ,
|
| 11 |
+
},
|
| 12 |
+
reducers: {
|
| 13 |
+
login(state) {
|
| 14 |
+
state.isLoggedIn = true;
|
| 15 |
+
},
|
| 16 |
+
setAdmin(state){
|
| 17 |
+
state.isAdmin = true;
|
| 18 |
+
},
|
| 19 |
+
logout(state) {
|
| 20 |
+
state.isLoggedIn = false;
|
| 21 |
+
state.isAdmin = false;
|
| 22 |
+
},
|
| 23 |
+
|
| 24 |
+
resetSnackbar(state) {
|
| 25 |
+
state.openSnackbar = false;
|
| 26 |
+
state.msg = "";
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
export const authActions = authSlice.actions;
|
| 32 |
+
|
| 33 |
+
export const store = configureStore({
|
| 34 |
+
reducer: authSlice.reducer
|
| 35 |
+
});
|
frontend/src/theme.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createTheme } from '@mui/material';
|
| 2 |
+
|
| 3 |
+
// Custom theme with fruit-inspired colors
|
| 4 |
+
const theme = createTheme({
|
| 5 |
+
palette: {
|
| 6 |
+
primary: {
|
| 7 |
+
main: '#4caf50',
|
| 8 |
+
light: '#80e27e',
|
| 9 |
+
dark: '#087f23'
|
| 10 |
+
},
|
| 11 |
+
secondary: {
|
| 12 |
+
main: '#ff9800',
|
| 13 |
+
dark: '#c66900'
|
| 14 |
+
},
|
| 15 |
+
error: {
|
| 16 |
+
main: '#f44336',
|
| 17 |
+
light: '#ff7961',
|
| 18 |
+
dark: '#ba000d'
|
| 19 |
+
},
|
| 20 |
+
background: {
|
| 21 |
+
default: '#f5f5f5',
|
| 22 |
+
paper: '#ffffff'
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
typography: {
|
| 26 |
+
fontFamily: '"Poppins", "Roboto", "Helvetica", "Arial", sans-serif',
|
| 27 |
+
h4: {
|
| 28 |
+
fontWeight: 600
|
| 29 |
+
}
|
| 30 |
+
},
|
| 31 |
+
shape: {
|
| 32 |
+
borderRadius: 12
|
| 33 |
+
},
|
| 34 |
+
components: {
|
| 35 |
+
MuiButton: {
|
| 36 |
+
styleOverrides: {
|
| 37 |
+
root: {
|
| 38 |
+
borderRadius: 30,
|
| 39 |
+
textTransform: 'none',
|
| 40 |
+
fontWeight: 600,
|
| 41 |
+
padding: '10px 20px'
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
},
|
| 45 |
+
MuiPaper: {
|
| 46 |
+
styleOverrides: {
|
| 47 |
+
root: {
|
| 48 |
+
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.12)'
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
MuiChip: {
|
| 53 |
+
styleOverrides: {
|
| 54 |
+
root: {
|
| 55 |
+
fontWeight: 600,
|
| 56 |
+
fontSize: '0.9rem'
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
export default theme;
|
ml/data/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dataset
|
| 2 |
+
|
| 3 |
+
Download the Intel Image Classification dataset from Kaggle:
|
| 4 |
+
https://www.kaggle.com/datasets/puneet6060/intel-image-classification
|
| 5 |
+
|
| 6 |
+
Extract and place in this structure:
|
| 7 |
+
|
| 8 |
+
```
|
| 9 |
+
ml/data/
|
| 10 |
+
├── seg_train/
|
| 11 |
+
│ └── seg_train/
|
| 12 |
+
│ ├── buildings/
|
| 13 |
+
│ ├── forest/
|
| 14 |
+
│ ├── glacier/
|
| 15 |
+
│ ├── mountain/
|
| 16 |
+
│ ├── sea/
|
| 17 |
+
│ └── street/
|
| 18 |
+
└── seg_test/
|
| 19 |
+
└── seg_test/
|
| 20 |
+
├── buildings/
|
| 21 |
+
...
|
| 22 |
+
```
|
ml/models/__init__.py
ADDED
|
File without changes
|
ml/models/cnn_pytorch.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch.nn as nn
|
| 2 |
+
|
| 3 |
+
class CNN(nn.Module):
|
| 4 |
+
def __init__(self, num_classes=6):
|
| 5 |
+
super(CNN, self).__init__()
|
| 6 |
+
|
| 7 |
+
self.block1 = self._block(3, 32)
|
| 8 |
+
self.block2 = self._block(32, 64)
|
| 9 |
+
self.block3 = self._block(64, 128)
|
| 10 |
+
self.block4 = self._block(128, 256)
|
| 11 |
+
|
| 12 |
+
self.gap = nn.AdaptiveAvgPool2d(1)
|
| 13 |
+
|
| 14 |
+
self.fc1 = nn.Linear(256, 128)
|
| 15 |
+
self.fc2 = nn.Linear(128, num_classes)
|
| 16 |
+
|
| 17 |
+
self.relu = nn.ReLU()
|
| 18 |
+
self.dropout = nn.Dropout(0.5)
|
| 19 |
+
|
| 20 |
+
def _block(self, in_c, out_c):
|
| 21 |
+
return nn.Sequential(
|
| 22 |
+
nn.Conv2d(in_c, out_c, kernel_size=3, padding=1),
|
| 23 |
+
nn.BatchNorm2d(out_c),
|
| 24 |
+
nn.ReLU(),
|
| 25 |
+
nn.MaxPool2d(2)
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
def forward(self, x):
|
| 29 |
+
x = self.block1(x)
|
| 30 |
+
x = self.block2(x)
|
| 31 |
+
x = self.block3(x)
|
| 32 |
+
x = self.block4(x)
|
| 33 |
+
|
| 34 |
+
x = self.gap(x)
|
| 35 |
+
x = x.view(x.size(0), -1)
|
| 36 |
+
|
| 37 |
+
x = self.dropout(x)
|
| 38 |
+
x = self.relu(self.fc1(x))
|
| 39 |
+
x = self.dropout(x)
|
| 40 |
+
x = self.fc2(x)
|
| 41 |
+
|
| 42 |
+
return x
|
ml/models/cnn_tensorflow.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tensorflow as tf
|
| 2 |
+
from tensorflow.keras import layers, models
|
| 3 |
+
|
| 4 |
+
def build_model(num_classes=6, input_shape=(150, 150, 3)):
|
| 5 |
+
inputs = layers.Input(shape=input_shape)
|
| 6 |
+
|
| 7 |
+
def block(x, filters):
|
| 8 |
+
x = layers.Conv2D(filters, 3, padding="same", activation=None)(x)
|
| 9 |
+
x = layers.BatchNormalization()(x)
|
| 10 |
+
x = layers.ReLU()(x)
|
| 11 |
+
x = layers.MaxPooling2D()(x)
|
| 12 |
+
return x
|
| 13 |
+
|
| 14 |
+
x = block(inputs, 32)
|
| 15 |
+
x = block(x, 64)
|
| 16 |
+
x = block(x, 128)
|
| 17 |
+
x = block(x, 256)
|
| 18 |
+
|
| 19 |
+
x = layers.GlobalAveragePooling2D()(x)
|
| 20 |
+
x = layers.Dropout(0.5)(x)
|
| 21 |
+
|
| 22 |
+
outputs = layers.Dense(num_classes)(x)
|
| 23 |
+
|
| 24 |
+
model = models.Model(inputs, outputs)
|
| 25 |
+
|
| 26 |
+
model.compile(
|
| 27 |
+
optimizer="adam",
|
| 28 |
+
loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
|
| 29 |
+
metrics=["accuracy"]
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
return model
|
ml/models/train.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import torch
|
| 3 |
+
import tensorflow as tf
|
| 4 |
+
|
| 5 |
+
from torch.utils.data import DataLoader, random_split
|
| 6 |
+
from torchvision import datasets, transforms
|
| 7 |
+
|
| 8 |
+
from models.cnn_pytorch import CNN
|
| 9 |
+
from models.cnn_tensorflow import build_model
|
| 10 |
+
from utils.prep import CLASSES
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def get_data_loaders(data_path, batch_size=32):
|
| 14 |
+
|
| 15 |
+
transform = transforms.Compose([
|
| 16 |
+
transforms.Resize((150, 150)),
|
| 17 |
+
transforms.ToTensor()
|
| 18 |
+
])
|
| 19 |
+
|
| 20 |
+
train_data = datasets.ImageFolder(
|
| 21 |
+
f"{data_path}/seg_train/seg_train",
|
| 22 |
+
transform=transform
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
test_data = datasets.ImageFolder(
|
| 26 |
+
f"{data_path}/seg_test/seg_test",
|
| 27 |
+
transform=transform
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
val_size = int(0.2 * len(train_data))
|
| 31 |
+
train_size = len(train_data) - val_size
|
| 32 |
+
|
| 33 |
+
train_data, val_data = random_split(train_data, [train_size, val_size])
|
| 34 |
+
|
| 35 |
+
train_loader = DataLoader(train_data, batch_size=batch_size, shuffle=True)
|
| 36 |
+
val_loader = DataLoader(val_data, batch_size=batch_size, shuffle=False)
|
| 37 |
+
test_loader = DataLoader(test_data, batch_size=batch_size, shuffle=False)
|
| 38 |
+
|
| 39 |
+
return train_loader, val_loader, test_loader
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def train_pytorch(model, train_loader, val_loader, epochs, device):
|
| 44 |
+
|
| 45 |
+
model.to(device)
|
| 46 |
+
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
|
| 47 |
+
criterion = torch.nn.CrossEntropyLoss()
|
| 48 |
+
|
| 49 |
+
best_loss = float("inf")
|
| 50 |
+
|
| 51 |
+
for epoch in range(epochs):
|
| 52 |
+
|
| 53 |
+
# TRAIN
|
| 54 |
+
model.train()
|
| 55 |
+
total_loss, correct, total = 0, 0, 0
|
| 56 |
+
|
| 57 |
+
for x, y in train_loader:
|
| 58 |
+
x, y = x.to(device), y.to(device)
|
| 59 |
+
|
| 60 |
+
optimizer.zero_grad()
|
| 61 |
+
outputs = model(x)
|
| 62 |
+
loss = criterion(outputs, y)
|
| 63 |
+
loss.backward()
|
| 64 |
+
optimizer.step()
|
| 65 |
+
|
| 66 |
+
total_loss += loss.item() * y.size(0)
|
| 67 |
+
|
| 68 |
+
preds = outputs.argmax(1)
|
| 69 |
+
correct += (preds == y).sum().item()
|
| 70 |
+
total += y.size(0)
|
| 71 |
+
|
| 72 |
+
train_acc = 100 * correct / total
|
| 73 |
+
train_loss = total_loss / total
|
| 74 |
+
|
| 75 |
+
# VALIDATION
|
| 76 |
+
model.eval()
|
| 77 |
+
val_loss, val_correct, val_total = 0, 0, 0
|
| 78 |
+
|
| 79 |
+
with torch.no_grad():
|
| 80 |
+
for x, y in val_loader:
|
| 81 |
+
x, y = x.to(device), y.to(device)
|
| 82 |
+
outputs = model(x)
|
| 83 |
+
loss = criterion(outputs, y)
|
| 84 |
+
|
| 85 |
+
val_loss += loss.item() * y.size(0)
|
| 86 |
+
|
| 87 |
+
preds = outputs.argmax(1)
|
| 88 |
+
val_correct += (preds == y).sum().item()
|
| 89 |
+
val_total += y.size(0)
|
| 90 |
+
|
| 91 |
+
val_acc = 100 * val_correct / val_total
|
| 92 |
+
val_loss = val_loss / val_total
|
| 93 |
+
|
| 94 |
+
print(f"Epoch {epoch+1}/{epochs} | "
|
| 95 |
+
f"Train Loss {train_loss:.4f} Acc {train_acc:.2f}% | "
|
| 96 |
+
f"Val Loss {val_loss:.4f} Acc {val_acc:.2f}%")
|
| 97 |
+
|
| 98 |
+
# save best model
|
| 99 |
+
if val_loss < best_loss:
|
| 100 |
+
best_loss = val_loss
|
| 101 |
+
torch.save(model.state_dict(), "pytorch_model.pth")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def main():
|
| 107 |
+
|
| 108 |
+
parser = argparse.ArgumentParser()
|
| 109 |
+
|
| 110 |
+
parser.add_argument("--model", required=True, choices=["pytorch", "tensorflow"])
|
| 111 |
+
parser.add_argument("--epochs", type=int, default=25)
|
| 112 |
+
parser.add_argument("--data", type=str, required=True)
|
| 113 |
+
|
| 114 |
+
args = parser.parse_args()
|
| 115 |
+
|
| 116 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 117 |
+
|
| 118 |
+
# PYTORCH
|
| 119 |
+
if args.model == "pytorch":
|
| 120 |
+
|
| 121 |
+
train_loader, val_loader, test_loader = get_data_loaders(args.data)
|
| 122 |
+
|
| 123 |
+
model = CNN(num_classes=len(CLASSES))
|
| 124 |
+
|
| 125 |
+
train_pytorch(model, train_loader, val_loader, args.epochs, device)
|
| 126 |
+
|
| 127 |
+
# TENSORFLOW
|
| 128 |
+
else:
|
| 129 |
+
|
| 130 |
+
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
|
| 131 |
+
f"{args.data}/seg_train/seg_train",
|
| 132 |
+
image_size=(150, 150),
|
| 133 |
+
batch_size=32
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
|
| 137 |
+
f"{args.data}/seg_test/seg_test",
|
| 138 |
+
image_size=(150, 150),
|
| 139 |
+
batch_size=32
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
model = build_model(num_classes=len(CLASSES))
|
| 143 |
+
|
| 144 |
+
model.fit(
|
| 145 |
+
train_ds,
|
| 146 |
+
validation_data=val_ds,
|
| 147 |
+
epochs=args.epochs
|
| 148 |
+
)
|
| 149 |
+
|
| 150 |
+
model.save("tensorflow_model.keras")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
if __name__ == "__main__":
|
| 154 |
+
main()
|
ml/requirements.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
torch
|
| 2 |
+
torchvision
|
| 3 |
+
tensorflow
|
| 4 |
+
numpy
|
| 5 |
+
matplotlib
|
| 6 |
+
seaborn
|
| 7 |
+
scikit-learn
|
| 8 |
+
tqdm
|
| 9 |
+
pillow
|
| 10 |
+
|
| 11 |
+
#cd ml
|
| 12 |
+
#pip install -r requirements.txt
|
| 13 |
+
#python -m models.train --model pytorch --epochs 25 --data data
|
ml/utils/__init__.py
ADDED
|
File without changes
|
ml/utils/prep.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import tensorflow as tf
|
| 3 |
+
from torchvision import datasets, transforms
|
| 4 |
+
from torch.utils.data import DataLoader
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
CLASSES = ["buildings", "forest", "glacier", "mountain", "sea", "street"]
|
| 11 |
+
IMAGE_SIZE = (150, 150)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# PYTORCH DATA LOADER
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def get_pytorch_data(data_dir="data", batch_size=64):
|
| 18 |
+
|
| 19 |
+
data_dir = Path(data_dir)
|
| 20 |
+
|
| 21 |
+
train_path = data_dir / "seg_train"
|
| 22 |
+
test_path = data_dir / "seg_test"
|
| 23 |
+
|
| 24 |
+
train_transform = transforms.Compose([
|
| 25 |
+
transforms.Resize(IMAGE_SIZE),
|
| 26 |
+
transforms.RandomHorizontalFlip(),
|
| 27 |
+
transforms.RandomVerticalFlip(p=0.1),
|
| 28 |
+
transforms.RandomRotation(15),
|
| 29 |
+
transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2),
|
| 30 |
+
transforms.ToTensor(),
|
| 31 |
+
transforms.Normalize(
|
| 32 |
+
mean=[0.485, 0.456, 0.406],
|
| 33 |
+
std=[0.229, 0.224, 0.225]
|
| 34 |
+
)
|
| 35 |
+
])
|
| 36 |
+
|
| 37 |
+
test_transform = transforms.Compose([
|
| 38 |
+
transforms.Resize(IMAGE_SIZE),
|
| 39 |
+
transforms.ToTensor(),
|
| 40 |
+
transforms.Normalize(
|
| 41 |
+
mean=[0.485, 0.456, 0.406],
|
| 42 |
+
std=[0.229, 0.224, 0.225]
|
| 43 |
+
)
|
| 44 |
+
])
|
| 45 |
+
|
| 46 |
+
full_train = datasets.ImageFolder(
|
| 47 |
+
str(train_path),
|
| 48 |
+
transform=train_transform
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
test_dataset = datasets.ImageFolder(
|
| 52 |
+
str(test_path),
|
| 53 |
+
transform=test_transform
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
print("Class mapping:", full_train.class_to_idx)
|
| 58 |
+
|
| 59 |
+
# Split train / validation
|
| 60 |
+
val_size = int(0.2 * len(full_train))
|
| 61 |
+
train_size = len(full_train) - val_size
|
| 62 |
+
|
| 63 |
+
train_dataset, val_dataset = torch.utils.data.random_split(
|
| 64 |
+
full_train,
|
| 65 |
+
[train_size, val_size],
|
| 66 |
+
generator=torch.Generator().manual_seed(42)
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
train_loader = DataLoader(
|
| 70 |
+
train_dataset,
|
| 71 |
+
batch_size=batch_size,
|
| 72 |
+
shuffle=True,
|
| 73 |
+
num_workers=2
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
val_loader = DataLoader(
|
| 77 |
+
val_dataset,
|
| 78 |
+
batch_size=batch_size,
|
| 79 |
+
shuffle=False,
|
| 80 |
+
num_workers=2
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
test_loader = DataLoader(
|
| 84 |
+
test_dataset,
|
| 85 |
+
batch_size=batch_size,
|
| 86 |
+
shuffle=False,
|
| 87 |
+
num_workers=2
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
return train_loader, val_loader, test_loader
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# TENSORFLOW DATA PIPELINE
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def get_tensorflow_data(data_dir="data", batch_size=64):
|
| 98 |
+
|
| 99 |
+
data_dir = Path(data_dir)
|
| 100 |
+
|
| 101 |
+
train_dir = data_dir / "seg_train"
|
| 102 |
+
test_dir = data_dir / "seg_test"
|
| 103 |
+
|
| 104 |
+
train_ds = tf.keras.utils.image_dataset_from_directory(
|
| 105 |
+
str(train_dir),
|
| 106 |
+
image_size=IMAGE_SIZE,
|
| 107 |
+
batch_size=batch_size,
|
| 108 |
+
shuffle=True,
|
| 109 |
+
seed=42,
|
| 110 |
+
validation_split=0.2,
|
| 111 |
+
subset="training",
|
| 112 |
+
label_mode="int",
|
| 113 |
+
class_names=CLASSES
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
val_ds = tf.keras.utils.image_dataset_from_directory(
|
| 117 |
+
str(train_dir),
|
| 118 |
+
image_size=IMAGE_SIZE,
|
| 119 |
+
batch_size=batch_size,
|
| 120 |
+
shuffle=True,
|
| 121 |
+
seed=42,
|
| 122 |
+
validation_split=0.2,
|
| 123 |
+
subset="validation",
|
| 124 |
+
label_mode="int",
|
| 125 |
+
class_names=CLASSES
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
test_ds = tf.keras.utils.image_dataset_from_directory(
|
| 129 |
+
str(test_dir),
|
| 130 |
+
image_size=IMAGE_SIZE,
|
| 131 |
+
batch_size=batch_size,
|
| 132 |
+
shuffle=False,
|
| 133 |
+
label_mode="int",
|
| 134 |
+
class_names=CLASSES
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# AUGMENTATION
|
| 138 |
+
augmentation = tf.keras.Sequential([
|
| 139 |
+
tf.keras.layers.RandomFlip("horizontal"),
|
| 140 |
+
tf.keras.layers.RandomRotation(0.1),
|
| 141 |
+
tf.keras.layers.RandomZoom(0.1),
|
| 142 |
+
tf.keras.layers.RandomContrast(0.2),
|
| 143 |
+
])
|
| 144 |
+
|
| 145 |
+
normalization = tf.keras.layers.Rescaling(1.0 / 255)
|
| 146 |
+
|
| 147 |
+
train_ds = (
|
| 148 |
+
train_ds
|
| 149 |
+
.map(lambda x, y: (augmentation(x, training=True), y),
|
| 150 |
+
num_parallel_calls=tf.data.AUTOTUNE)
|
| 151 |
+
.map(lambda x, y: (normalization(x), y),
|
| 152 |
+
num_parallel_calls=tf.data.AUTOTUNE)
|
| 153 |
+
.cache()
|
| 154 |
+
.shuffle(1000)
|
| 155 |
+
.prefetch(tf.data.AUTOTUNE)
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
val_ds = (
|
| 159 |
+
val_ds
|
| 160 |
+
.map(lambda x, y: (normalization(x), y),
|
| 161 |
+
num_parallel_calls=tf.data.AUTOTUNE)
|
| 162 |
+
.cache()
|
| 163 |
+
.prefetch(tf.data.AUTOTUNE)
|
| 164 |
+
)
|
| 165 |
+
|
| 166 |
+
test_ds = (
|
| 167 |
+
test_ds
|
| 168 |
+
.map(lambda x, y: (normalization(x), y),
|
| 169 |
+
num_parallel_calls=tf.data.AUTOTUNE)
|
| 170 |
+
.cache()
|
| 171 |
+
.prefetch(tf.data.AUTOTUNE)
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
return train_ds, val_ds, test_ds
|