danielle2035 commited on
Commit
d32e728
·
1 Parent(s): d5ec07c
Files changed (47) hide show
  1. .gitignore +31 -0
  2. README.md +284 -0
  3. backend/.env.example +3 -0
  4. backend/Dockerfile +12 -0
  5. backend/api/api/__init__.py +0 -0
  6. backend/api/api/asgi.py +16 -0
  7. backend/api/api/settings.py +112 -0
  8. backend/api/api/urls.py +30 -0
  9. backend/api/api/wsgi.py +16 -0
  10. backend/api/app.log +0 -0
  11. backend/api/manage.py +22 -0
  12. backend/api/models/README.md +7 -0
  13. backend/api/models/pytorch_model.pth +3 -0
  14. backend/api/notifications/__init__.py +0 -0
  15. backend/api/notifications/admin.py +3 -0
  16. backend/api/notifications/api_views.py +218 -0
  17. backend/api/notifications/apps.py +7 -0
  18. backend/api/notifications/models.py +1 -0
  19. backend/api/notifications/serializers.py +22 -0
  20. backend/api/notifications/tests.py +3 -0
  21. backend/api/notifications/urls.py +6 -0
  22. backend/api/notifications/views.py +3 -0
  23. backend/requirements.txt +14 -0
  24. docker-compose.yml +28 -0
  25. frontend/Dockerfile +12 -0
  26. frontend/package-lock.json +0 -0
  27. frontend/package.json +32 -0
  28. frontend/public/favicon.ico +0 -0
  29. frontend/public/index.html +43 -0
  30. frontend/public/manifest.json +25 -0
  31. frontend/public/robots.txt +3 -0
  32. frontend/src/App.css +0 -0
  33. frontend/src/App.js +470 -0
  34. frontend/src/index.css +6 -0
  35. frontend/src/index.js +28 -0
  36. frontend/src/reportWebVitals.js +13 -0
  37. frontend/src/setupTests.js +5 -0
  38. frontend/src/store/index.js +35 -0
  39. frontend/src/theme.js +63 -0
  40. ml/data/README.md +22 -0
  41. ml/models/__init__.py +0 -0
  42. ml/models/cnn_pytorch.py +42 -0
  43. ml/models/cnn_tensorflow.py +32 -0
  44. ml/models/train.py +154 -0
  45. ml/requirements.txt +13 -0
  46. ml/utils/__init__.py +0 -0
  47. 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