Spaces:
Sleeping
Sleeping
Add real KNN pipeline: train on your own polygon labels + spectral bands, evaluate against ground truth
Browse files- .gitattributes +7 -0
- app.py +162 -19
- data/bands/band_1_uv.tif +3 -0
- data/bands/band_2_blue.tif +3 -0
- data/bands/band_3_green.tif +3 -0
- data/bands/band_4_red.tif +3 -0
- data/bands/band_5_nir.tif +3 -0
- data/bands/band_6_swir1.tif +3 -0
- data/bands/band_7_swir2.tif +3 -0
.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 |
-
#
|
| 62 |
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 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=
|
| 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"
|
| 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 |
-
"🚀
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
data/bands/band_2_blue.tif
ADDED
|
|
Git LFS Details
|
data/bands/band_3_green.tif
ADDED
|
|
Git LFS Details
|
data/bands/band_4_red.tif
ADDED
|
|
Git LFS Details
|
data/bands/band_5_nir.tif
ADDED
|
|
Git LFS Details
|
data/bands/band_6_swir1.tif
ADDED
|
|
Git LFS Details
|
data/bands/band_7_swir2.tif
ADDED
|
|
Git LFS Details
|