CircleStar commited on
Commit
32e60fc
·
verified ·
1 Parent(s): 38c98c7

Add real KNN pipeline: train on your own polygon labels + spectral bands, evaluate against ground truth

Browse files
.gitattributes CHANGED
@@ -58,3 +58,10 @@ polygon_images/polygon_19.png filter=lfs diff=lfs merge=lfs -text
58
  polygon_images/polygon_20.png filter=lfs diff=lfs merge=lfs -text
59
  polygon_images/polygon_21.png filter=lfs diff=lfs merge=lfs -text
60
  polygon_images/polygon_23.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
58
  polygon_images/polygon_20.png filter=lfs diff=lfs merge=lfs -text
59
  polygon_images/polygon_21.png filter=lfs diff=lfs merge=lfs -text
60
  polygon_images/polygon_23.png filter=lfs diff=lfs merge=lfs -text
61
+ data/bands/band_1_uv.tif filter=lfs diff=lfs merge=lfs -text
62
+ data/bands/band_2_blue.tif filter=lfs diff=lfs merge=lfs -text
63
+ data/bands/band_3_green.tif filter=lfs diff=lfs merge=lfs -text
64
+ data/bands/band_4_red.tif filter=lfs diff=lfs merge=lfs -text
65
+ data/bands/band_5_nir.tif filter=lfs diff=lfs merge=lfs -text
66
+ data/bands/band_6_swir1.tif filter=lfs diff=lfs merge=lfs -text
67
+ data/bands/band_7_swir2.tif filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -6,11 +6,15 @@ import matplotlib.pyplot as plt
6
  import rasterio
7
  import json
8
  import os
 
 
9
 
10
  # ─────────────────────────────────────────────────────────────────────────────
11
  # Constants
12
  # ─────────────────────────────────────────────────────────────────────────────
13
  N_POLYGONS = 23
 
 
14
 
15
  CLASSES = {
16
  1: "Eau",
@@ -24,6 +28,17 @@ CLASSES = {
24
 
25
  CLASS_CHOICES = [f"{k} - {v}" for k, v in CLASSES.items()]
26
 
 
 
 
 
 
 
 
 
 
 
 
27
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
28
 
29
  # ─────────────────────────────────────────────────────────────────────────────
@@ -37,11 +52,16 @@ def load_data():
37
 
38
  ground_truth = read_tif('ground_truth.tif')
39
  knn_result = read_tif('knn_result.tif')
 
 
 
 
 
40
 
41
  with open(os.path.join(BASE_DIR, 'data', 'polygon_teacher_classes.json')) as f:
42
  polygon_teacher = {int(k): v for k, v in json.load(f).items()}
43
 
44
- # Pre-compute KNN confusion matrix (pixels where GT > 0)
45
  gt_flat = ground_truth.flatten()
46
  knn_flat = knn_result.flatten()
47
  valid = gt_flat > 0
@@ -49,29 +69,80 @@ def load_data():
49
  np.add.at(knn_matrix, (gt_flat[valid] - 1, knn_flat[valid] - 1), 1)
50
  knn_oa = knn_matrix.diagonal().sum() / knn_matrix.sum()
51
 
 
 
 
52
  return dict(
53
  polygon_teacher= polygon_teacher,
 
 
 
 
54
  knn_matrix = knn_matrix,
55
  knn_oa = knn_oa,
 
56
  )
57
 
58
  DATA = load_data()
59
 
60
  # ─────────────────────────────────────────────────────────────────────────────
61
- # Visualization helpers
62
  # ─────────────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
- def fig_knn_matrix():
65
- matrix = DATA['knn_matrix']
66
- oa = DATA['knn_oa']
67
 
 
 
68
  short = ["Eau", "Vergers", "Δ-Cult.", "Bâti", "Irr.-Dés.", "Non-Irr.", "Sable"]
69
  fig, ax = plt.subplots(figsize=(9, 7))
70
 
71
  row_tot = matrix.sum(axis=1, keepdims=True)
72
  pct = np.where(row_tot > 0, matrix / row_tot * 100, 0)
73
 
74
- im = ax.imshow(pct, cmap='Blues', vmin=0, vmax=100)
75
  plt.colorbar(im, ax=ax, label="% de la classe réelle", shrink=0.8)
76
 
77
  ax.set_xticks(range(7)); ax.set_yticks(range(7))
@@ -79,8 +150,7 @@ def fig_knn_matrix():
79
  ax.set_yticklabels([f"C{i+1}\n{short[i]}" for i in range(7)], fontsize=8)
80
  ax.set_xlabel("Classe prédite (KNN)", fontsize=11, labelpad=8)
81
  ax.set_ylabel("Classe réelle (vérité terrain)", fontsize=11, labelpad=8)
82
- ax.set_title(f"Matrice de confusion – KNN vs Vérité terrain\n"
83
- f"Précision globale = {oa*100:.1f}%",
84
  fontsize=12, fontweight='bold', pad=12)
85
 
86
  for r in range(7):
@@ -93,6 +163,38 @@ def fig_knn_matrix():
93
  plt.tight_layout()
94
  return fig
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  def fig_student_matrix(student_labels):
97
  """7×7 confusion matrix: student labels vs teacher labels."""
98
  matrix = np.zeros((7, 7), dtype=int)
@@ -222,8 +324,19 @@ puis soumettez vos réponses pour générer la carte et la matrice de confusion.
222
  )
223
  gr.Markdown("---")
224
  progress_bar = gr.Markdown("**Progression : 0 / 23**")
 
 
 
 
 
 
 
 
 
 
 
225
  btn_submit = gr.Button(
226
- "🚀 Soumettre toutes mes réponses",
227
  variant="primary",
228
  visible=False,
229
  size="lg",
@@ -234,6 +347,7 @@ puis soumettez vos réponses pour générer la carte et la matrice de confusion.
234
  results_placeholder = gr.Markdown(
235
  "*(Les résultats apparaîtront ici après soumission)*"
236
  )
 
237
  accuracy_md = gr.Markdown(visible=False)
238
  results_tbl = gr.Dataframe(
239
  headers=["Polygone", "Votre réponse", "Réponse enseignant", "Résultat"],
@@ -243,7 +357,14 @@ puis soumettez vos réponses pour générer la carte et la matrice de confusion.
243
  with gr.Row(visible=False) as row_plots_student:
244
  student_matrix_plot = gr.Plot(label="Votre interprétation vs Enseignant")
245
 
246
- knn_plot = gr.Plot(label="Matrice de confusion KNN vs Vérité terrain", visible=False)
 
 
 
 
 
 
 
247
 
248
  # ─────────────────────────────────────────────────────────────────────
249
  # Event handlers
@@ -287,9 +408,11 @@ puis soumettez vos réponses pour générer la carte et la matrice de confusion.
287
  gr.update(value=overall),
288
  gr.update(visible=show_submit))
289
 
290
- def on_submit(current_labels):
291
- """Compute and display all results."""
292
- # Accuracy vs teacher
 
 
293
  correct = sum(
294
  1 for pid in range(1, 24)
295
  if current_labels[pid-1] is not None
@@ -299,23 +422,39 @@ puis soumettez vos réponses pour générer la carte et la matrice de confusion.
299
  pct = correct / total * 100 if total > 0 else 0
300
 
301
  acc_text = (
302
- f"## 🎯 Résultat de votre interprétation\n\n"
303
  f"**{correct} / {total} polygones correctement identifiés ({pct:.0f}%)**\n\n"
304
  f"*(Comparaison avec la légende fournie par l'enseignant)*"
305
  )
306
 
307
  table = build_results_table(current_labels)
308
-
309
- knn_fig = fig_knn_matrix()
310
  student_fig = fig_student_matrix(current_labels)
311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  return (
313
  gr.update(value=""),
314
  gr.update(value=acc_text, visible=True),
315
  gr.update(value=table, visible=True),
316
  gr.update(visible=True),
317
  gr.update(value=student_fig),
318
- gr.update(value=knn_fig, visible=True),
 
 
 
 
319
  )
320
 
321
  # Wire up navigation
@@ -337,17 +476,21 @@ puis soumettez vos réponses pour générer la carte et la matrice de confusion.
337
  outputs=[labels, progress_bar, progress_md, btn_submit],
338
  )
339
 
340
- # Wire up submit
341
  btn_submit.click(
342
  on_submit,
343
- inputs=[labels],
344
  outputs=[
345
  results_placeholder,
346
  accuracy_md,
347
  results_tbl,
348
  row_plots_student,
349
  student_matrix_plot,
 
 
 
350
  knn_plot,
 
351
  ],
352
  )
353
 
 
6
  import rasterio
7
  import json
8
  import os
9
+ from sklearn.neighbors import KNeighborsClassifier
10
+ from sklearn.preprocessing import StandardScaler
11
 
12
  # ─────────────────────────────────────────────────────────────────────────────
13
  # Constants
14
  # ─────────────────────────────────────────────────────────────────────────────
15
  N_POLYGONS = 23
16
+ K_MIN, K_MAX, K_DEFAULT = 1, 15, 5
17
+ MAP_MAX_DIM = 500 # downsample target for map visualizations (speed)
18
 
19
  CLASSES = {
20
  1: "Eau",
 
28
 
29
  CLASS_CHOICES = [f"{k} - {v}" for k, v in CLASSES.items()]
30
 
31
+ # index 0 = fond / non classé, 1..7 = classes ci-dessus
32
+ COLORS_RGB = np.array([
33
+ [20, 20, 20], [0, 100, 220], [0, 160, 60], [120, 220, 100],
34
+ [220, 50, 50], [255, 165, 0], [160, 90, 30], [240, 230, 140],
35
+ ], dtype=np.uint8)
36
+
37
+ BAND_FILES = [
38
+ 'band_1_uv.tif', 'band_2_blue.tif', 'band_3_green.tif', 'band_4_red.tif',
39
+ 'band_5_nir.tif', 'band_6_swir1.tif', 'band_7_swir2.tif',
40
+ ]
41
+
42
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
43
 
44
  # ─────────────────────────────────────────────────────────────────────────────
 
52
 
53
  ground_truth = read_tif('ground_truth.tif')
54
  knn_result = read_tif('knn_result.tif')
55
+ training_ids = read_tif('training_polygons.tif')
56
+
57
+ bands = np.stack(
58
+ [read_tif(os.path.join('bands', f)) for f in BAND_FILES], axis=-1
59
+ ).astype(np.float32)
60
 
61
  with open(os.path.join(BASE_DIR, 'data', 'polygon_teacher_classes.json')) as f:
62
  polygon_teacher = {int(k): v for k, v in json.load(f).items()}
63
 
64
+ # Pre-compute reference KNN confusion matrix (teacher labels, pixels where GT > 0)
65
  gt_flat = ground_truth.flatten()
66
  knn_flat = knn_result.flatten()
67
  valid = gt_flat > 0
 
69
  np.add.at(knn_matrix, (gt_flat[valid] - 1, knn_flat[valid] - 1), 1)
70
  knn_oa = knn_matrix.diagonal().sum() / knn_matrix.sum()
71
 
72
+ h, w = ground_truth.shape
73
+ stride = max(1, max(h, w) // MAP_MAX_DIM)
74
+
75
  return dict(
76
  polygon_teacher= polygon_teacher,
77
+ ground_truth = ground_truth,
78
+ knn_result = knn_result,
79
+ training_ids = training_ids,
80
+ bands = bands,
81
  knn_matrix = knn_matrix,
82
  knn_oa = knn_oa,
83
+ stride = stride,
84
  )
85
 
86
  DATA = load_data()
87
 
88
  # ─────────────────────────────────────────────────────────────────────────────
89
+ # Model training (from the student's own labels + real spectral bands)
90
  # ─────────────────────────────────────────────────────────────────────────────
91
+ def train_and_predict(student_labels, k):
92
+ """Train a KNN classifier on the student's 23 labeled polygons (using the
93
+ real spectral bands), then evaluate it against Ground Truth and produce a
94
+ downsampled classification map for display."""
95
+ bands = DATA['bands']
96
+ training_ids = DATA['training_ids']
97
+ ground_truth = DATA['ground_truth']
98
+ knn_result = DATA['knn_result']
99
+ stride = DATA['stride']
100
+
101
+ train_mask = training_ids > 0
102
+ X_train = bands[train_mask]
103
+ y_train = np.array([int(student_labels[pid - 1]) for pid in training_ids[train_mask]])
104
+
105
+ scaler = StandardScaler().fit(X_train)
106
+ clf = KNeighborsClassifier(n_neighbors=int(k), algorithm='kd_tree', n_jobs=-1)
107
+ clf.fit(scaler.transform(X_train), y_train)
108
+
109
+ # Accuracy / confusion matrix on the real Ground Truth pixels
110
+ gt_mask = ground_truth > 0
111
+ X_gt = bands[gt_mask]
112
+ pred_gt = clf.predict(scaler.transform(X_gt))
113
+ y_gt = ground_truth[gt_mask]
114
+
115
+ model_matrix = np.zeros((7, 7), dtype=np.int64)
116
+ np.add.at(model_matrix, (y_gt - 1, pred_gt - 1), 1)
117
+ model_oa = model_matrix.diagonal().sum() / model_matrix.sum()
118
+
119
+ # Downsampled full-image map for visualization (fast: predict only on the
120
+ # decimated grid, not the full 4.6M pixels)
121
+ bands_small = bands[::stride, ::stride]
122
+ hs, ws, _ = bands_small.shape
123
+ pred_small = clf.predict(scaler.transform(bands_small.reshape(-1, 7)))
124
+ student_map = pred_small.reshape(hs, ws)
125
+
126
+ gt_small = ground_truth[::stride, ::stride]
127
+ knn_small = knn_result[::stride, ::stride]
128
+ # zero out predictions outside the study area so the map matches GT/KNN extent
129
+ student_map = np.where(knn_small > 0, student_map, 0)
130
+
131
+ return model_oa, model_matrix, student_map, knn_small, gt_small
132
 
133
+ # ─────────────────────────────────────────────────────────────────────────────
134
+ # Visualization helpers
135
+ # ─────────────────────────────────────────────────────────────────────────────
136
 
137
+ def fig_confusion_matrix(matrix, oa, title, cmap='Blues'):
138
+ """Generic 7×7 confusion matrix plot: predicted classes vs Ground Truth."""
139
  short = ["Eau", "Vergers", "Δ-Cult.", "Bâti", "Irr.-Dés.", "Non-Irr.", "Sable"]
140
  fig, ax = plt.subplots(figsize=(9, 7))
141
 
142
  row_tot = matrix.sum(axis=1, keepdims=True)
143
  pct = np.where(row_tot > 0, matrix / row_tot * 100, 0)
144
 
145
+ im = ax.imshow(pct, cmap=cmap, vmin=0, vmax=100)
146
  plt.colorbar(im, ax=ax, label="% de la classe réelle", shrink=0.8)
147
 
148
  ax.set_xticks(range(7)); ax.set_yticks(range(7))
 
150
  ax.set_yticklabels([f"C{i+1}\n{short[i]}" for i in range(7)], fontsize=8)
151
  ax.set_xlabel("Classe prédite (KNN)", fontsize=11, labelpad=8)
152
  ax.set_ylabel("Classe réelle (vérité terrain)", fontsize=11, labelpad=8)
153
+ ax.set_title(f"{title}\nPrécision globale = {oa*100:.1f}%",
 
154
  fontsize=12, fontweight='bold', pad=12)
155
 
156
  for r in range(7):
 
163
  plt.tight_layout()
164
  return fig
165
 
166
+ def fig_knn_matrix():
167
+ """Reference matrix: pre-computed KNN (trained on teacher labels) vs Ground Truth."""
168
+ return fig_confusion_matrix(
169
+ DATA['knn_matrix'], DATA['knn_oa'],
170
+ "Matrice de confusion – Modèle de référence (enseignant) vs Vérité terrain",
171
+ cmap='Blues',
172
+ )
173
+
174
+ def fig_model_matrix(model_matrix, model_oa):
175
+ """Student's own trained KNN model vs Ground Truth."""
176
+ return fig_confusion_matrix(
177
+ model_matrix, model_oa,
178
+ "Matrice de confusion – VOTRE modèle KNN vs Vérité terrain",
179
+ cmap='Purples',
180
+ )
181
+
182
+ def fig_three_panel_map(student_map, knn_map, gt_map):
183
+ """Side-by-side classification maps: student's trained model / teacher
184
+ reference KNN / Ground Truth."""
185
+ fig, axes = plt.subplots(1, 3, figsize=(15, 5))
186
+ for ax, rast, title in zip(
187
+ axes,
188
+ [student_map, knn_map, gt_map],
189
+ ["Votre modèle KNN", "Modèle de référence (enseignant)", "Vérité terrain"],
190
+ ):
191
+ rgb = COLORS_RGB[rast]
192
+ ax.imshow(rgb, interpolation='nearest')
193
+ ax.set_title(title, fontsize=11, fontweight='bold')
194
+ ax.axis('off')
195
+ plt.tight_layout()
196
+ return fig
197
+
198
  def fig_student_matrix(student_labels):
199
  """7×7 confusion matrix: student labels vs teacher labels."""
200
  matrix = np.zeros((7, 7), dtype=int)
 
324
  )
325
  gr.Markdown("---")
326
  progress_bar = gr.Markdown("**Progression : 0 / 23**")
327
+ gr.Markdown("---")
328
+ gr.Markdown("### Entraînement du modèle KNN")
329
+ gr.Markdown(
330
+ "*(Une fois les 23 polygones étiquetés, ce modèle sera "
331
+ "entraîné sur VOS réponses + les vraies valeurs spectrales "
332
+ "des 7 bandes satellite, puis évalué sur la vérité terrain)*"
333
+ )
334
+ k_slider = gr.Slider(
335
+ minimum=K_MIN, maximum=K_MAX, value=K_DEFAULT, step=1,
336
+ label="k (nombre de voisins)",
337
+ )
338
  btn_submit = gr.Button(
339
+ "🚀 Entraîner mon modèle KNN et voir les résultats",
340
  variant="primary",
341
  visible=False,
342
  size="lg",
 
347
  results_placeholder = gr.Markdown(
348
  "*(Les résultats apparaîtront ici après soumission)*"
349
  )
350
+ gr.Markdown("## 1️⃣ Qualité de votre interprétation visuelle")
351
  accuracy_md = gr.Markdown(visible=False)
352
  results_tbl = gr.Dataframe(
353
  headers=["Polygone", "Votre réponse", "Réponse enseignant", "Résultat"],
 
357
  with gr.Row(visible=False) as row_plots_student:
358
  student_matrix_plot = gr.Plot(label="Votre interprétation vs Enseignant")
359
 
360
+ gr.Markdown("## 2️⃣ Votre modèle KNN entraîné sur vos étiquettes")
361
+ model_accuracy_md = gr.Markdown(visible=False)
362
+ with gr.Row(visible=False) as row_plots_model:
363
+ model_matrix_plot = gr.Plot(label="Votre modèle KNN vs Vérité terrain")
364
+ knn_plot = gr.Plot(label="Modèle de référence (enseignant) vs Vérité terrain")
365
+ three_panel_plot = gr.Plot(
366
+ label="Cartes de classification", visible=False,
367
+ )
368
 
369
  # ─────────────────────────────────────────────────────────────────────
370
  # Event handlers
 
408
  gr.update(value=overall),
409
  gr.update(visible=show_submit))
410
 
411
+ def on_submit(current_labels, k):
412
+ """Compute and display all results: labeling accuracy vs teacher, then
413
+ train a real KNN model on the student's labels + spectral bands and
414
+ evaluate it against Ground Truth."""
415
+ # 1) Accuracy of the visual interpretation vs teacher's answer key
416
  correct = sum(
417
  1 for pid in range(1, 24)
418
  if current_labels[pid-1] is not None
 
422
  pct = correct / total * 100 if total > 0 else 0
423
 
424
  acc_text = (
 
425
  f"**{correct} / {total} polygones correctement identifiés ({pct:.0f}%)**\n\n"
426
  f"*(Comparaison avec la légende fournie par l'enseignant)*"
427
  )
428
 
429
  table = build_results_table(current_labels)
 
 
430
  student_fig = fig_student_matrix(current_labels)
431
 
432
+ # 2) Train a KNN model on the student's own labels + real spectral bands,
433
+ # then evaluate it against Ground Truth
434
+ model_oa, model_matrix, student_map, knn_map, gt_map = train_and_predict(
435
+ current_labels, k
436
+ )
437
+ model_acc_text = (
438
+ f"**Précision globale = {model_oa*100:.1f}%** "
439
+ f"(évaluée sur les {int((DATA['ground_truth'] > 0).sum())} pixels de vérité terrain, k={int(k)})\n\n"
440
+ f"*(C'est la précision réelle d'un modèle KNN entraîné uniquement sur VOS 23 polygones étiquetés — "
441
+ f"comparez-la à celle du modèle de référence de l'enseignant ci-dessous)*"
442
+ )
443
+ model_fig = fig_model_matrix(model_matrix, model_oa)
444
+ knn_fig = fig_knn_matrix()
445
+ map_fig = fig_three_panel_map(student_map, knn_map, gt_map)
446
+
447
  return (
448
  gr.update(value=""),
449
  gr.update(value=acc_text, visible=True),
450
  gr.update(value=table, visible=True),
451
  gr.update(visible=True),
452
  gr.update(value=student_fig),
453
+ gr.update(value=model_acc_text, visible=True),
454
+ gr.update(visible=True),
455
+ gr.update(value=model_fig),
456
+ gr.update(value=knn_fig),
457
+ gr.update(value=map_fig, visible=True),
458
  )
459
 
460
  # Wire up navigation
 
476
  outputs=[labels, progress_bar, progress_md, btn_submit],
477
  )
478
 
479
+ # Wire up submit (labeling accuracy + model training/evaluation)
480
  btn_submit.click(
481
  on_submit,
482
+ inputs=[labels, k_slider],
483
  outputs=[
484
  results_placeholder,
485
  accuracy_md,
486
  results_tbl,
487
  row_plots_student,
488
  student_matrix_plot,
489
+ model_accuracy_md,
490
+ row_plots_model,
491
+ model_matrix_plot,
492
  knn_plot,
493
+ three_panel_plot,
494
  ],
495
  )
496
 
data/bands/band_1_uv.tif ADDED

Git LFS Details

  • SHA256: 0c6ab26d58f8b43a9cd8a155179521924212e971ad2a92104b9999eede180a1e
  • Pointer size: 132 Bytes
  • Size of remote file: 9.21 MB
data/bands/band_2_blue.tif ADDED

Git LFS Details

  • SHA256: e0382308d0ba125742fb0e717f2c1401495dd8a80996c552a199fddc49174196
  • Pointer size: 132 Bytes
  • Size of remote file: 9.21 MB
data/bands/band_3_green.tif ADDED

Git LFS Details

  • SHA256: c4e890c147e7c66e792d862bcf8d79d084d90b179c127e45b336d1958391a681
  • Pointer size: 132 Bytes
  • Size of remote file: 9.21 MB
data/bands/band_4_red.tif ADDED

Git LFS Details

  • SHA256: 6925d1714c8b9517ef1ecaf775ab712de277deb7e4059ee4718b9a84ffa05930
  • Pointer size: 132 Bytes
  • Size of remote file: 9.21 MB
data/bands/band_5_nir.tif ADDED

Git LFS Details

  • SHA256: c00cb894ee07e0ff1a9e008c270611c54610f28155ed95c6b26c61b4983af865
  • Pointer size: 133 Bytes
  • Size of remote file: 18.4 MB
data/bands/band_6_swir1.tif ADDED

Git LFS Details

  • SHA256: 687f914a411c11b2418839394d33e8bccd77802856d83ab0b42c571f7af89898
  • Pointer size: 133 Bytes
  • Size of remote file: 18.4 MB
data/bands/band_7_swir2.tif ADDED

Git LFS Details

  • SHA256: caadf9ba9655be858ae92e1ae7a389c62bdbdc6af81b3dca46f3a76f5f0d7386
  • Pointer size: 133 Bytes
  • Size of remote file: 18.4 MB