lterriel commited on
Commit
e00397b
·
verified ·
1 Parent(s): 29c9366

Update README.md

Browse files
Files changed (1) hide show
  1. README.md +611 -110
README.md CHANGED
@@ -138,155 +138,520 @@ import onnxruntime as ort
138
  from PIL import Image
139
  from pathlib import Path
140
 
141
- run = Path("./mobilenet_v3_large")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
- cfg = json.loads((run / "inference_config.json").read_text())
144
- pre = json.loads((run / "preprocess.json").read_text())
145
 
146
- img = Image.open("page.jpg").convert("RGB").resize((pre["img_size"], pre["img_size"]))
147
- x = np.asarray(img).astype("float32") / 255.0
148
- x = (x - np.array(pre["mean"])) / np.array(pre["std"])
149
- x = x.transpose(2, 0, 1)[None].astype("float32")
150
 
151
- sess = ort.InferenceSession(str(run / "onnx/model.onnx"))
152
- logits = sess.run(None, {cfg["input_name"]: x})[0][0]
 
 
 
 
 
 
153
 
154
- probs = np.exp(logits - logits.max())
155
- probs = probs / probs.sum()
156
 
157
- p_illu = float(probs[cfg["positive_index"]])
158
- label = cfg["positive_label"] if p_illu >= cfg["threshold"] else "non_illumination"
159
 
160
- print(label, p_illu)
161
- ```
 
 
 
 
 
 
162
 
163
- ## Python — ONNX from Hugging Face
164
 
165
- ```bash
166
- pip install huggingface_hub onnxruntime pillow numpy
167
- ```
168
 
169
- ```python
170
- from huggingface_hub import snapshot_download
171
- from pathlib import Path
 
 
 
 
 
 
172
 
173
- repo = "lterriel/medieval-illumination-bin-classifier"
174
- run_name = "final_mobilenetv3_large"
175
- local_dir = Path(snapshot_download(
176
- repo_id=repo,
177
- allow_patterns=[
178
- f"{run_name}/onnx/model.onnx",
179
- f"{run_name}/preprocess.json",
180
- f"{run_name}/inference_config.json",
181
- ],
182
- )) / run_name
183
- ```
184
 
185
- Then use the same ONNX code as above, replacing:
186
 
187
- ```python
188
- run = Path("./mobilenet_v3_large")
189
- ```
190
 
191
- with:
192
 
193
- ```python
194
- run = local_dir
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  ```
196
 
197
- ## Python — PyTorch / non-ONNX local
198
 
199
  ```bash
200
- pip install torch torchvision pillow numpy
201
  ```
202
 
203
  ```python
204
  import json
205
- import torch
206
  import numpy as np
 
 
207
  from PIL import Image
208
  from pathlib import Path
209
- from torchvision import models
210
 
211
- run = Path("./mobilenet_v3_large")
 
212
 
213
- cfg = json.loads((run / "inference_config.json").read_text())
214
- pre = json.loads((run / "preprocess.json").read_text())
 
 
 
 
215
 
216
- model = models.mobilenet_v3_large(weights=None)
217
- model.classifier[-1] = torch.nn.Linear(model.classifier[-1].in_features, 2)
218
- model.load_state_dict(torch.load(run / "checkpoints/best.pt", map_location="cpu"))
219
- model.eval()
220
 
221
- img = Image.open("page.jpg").convert("RGB").resize((pre["img_size"], pre["img_size"]))
222
- x = np.asarray(img).astype("float32") / 255.0
223
- x = (x - np.array(pre["mean"])) / np.array(pre["std"])
224
- x = torch.tensor(x.transpose(2, 0, 1)[None]).float()
225
 
226
- with torch.no_grad():
227
- logits = model(x)
228
- probs = torch.softmax(logits, dim=1)[0]
229
 
230
- p_illu = float(probs[cfg["positive_index"]])
231
- label = cfg["positive_label"] if p_illu >= cfg["threshold"] else "non_illumination"
 
 
 
 
 
232
 
233
- print(label, p_illu)
234
- ```
 
 
 
 
235
 
236
- For another torchvision architecture, replace the model constructor:
 
 
 
237
 
238
- - mobilenetV2
239
 
240
- ```
241
- model = models.mobilenet_v2(weights=None)
242
- model.classifier[-1] = torch.nn.Linear(model.classifier[-1].in_features, 2)
243
- ```
244
 
245
- - mobilenetV2 (small)
 
246
 
247
- ```
248
- model = models.mobilenet_v3_small(weights=None)
249
- model.classifier[-1] = torch.nn.Linear(model.classifier[-1].in_features, 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  ```
251
 
252
- ## Python — PyTorch / non-ONNX from Hugging Face
253
 
254
  ```bash
255
- pip install huggingface_hub torch torchvision pillow numpy
256
  ```
 
257
 
258
  ```python
259
- from huggingface_hub import snapshot_download
260
- from pathlib import Path
261
 
262
- repo = "lterriel/medieval-illumination-bin-classifier"
263
- run_name = "final_mobilenetv3_large"
264
-
265
- run = Path(snapshot_download(
266
- repo_id=repo,
267
- allow_patterns=[
268
- f"{run_name}/checkpoints/best.pt",
269
- f"{run_name}/preprocess.json",
270
- f"{run_name}/inference_config.json",
271
- ],
272
- )) / run_name
273
- ```
274
 
275
- Then use the same PyTorch code as above.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
 
277
  ## JS (HF - ONNX)
278
 
279
  ```javascript
280
  <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>
 
 
 
 
 
 
 
 
 
 
 
281
  <input type="file" id="file" accept="image/*">
282
- <pre id="out"></pre>
 
283
 
284
  <script type="module">
285
- const run = "https://huggingface.co/lterriel/medieval-illumination-bin-classifier/resolve/main/final_mobilenetv3_large";
 
 
 
 
 
286
 
287
- const cfg = await fetch(`${run}/inference_config.json`).then(r => r.json());
288
- const pre = await fetch(`${run}/preprocess.json`).then(r => r.json());
289
- const sess = await ort.InferenceSession.create(`${run}/onnx/model.onnx`);
290
 
291
  function softmax(a) {
292
  const m = Math.max(...a);
@@ -295,12 +660,80 @@ function softmax(a) {
295
  return e.map(x => x / s);
296
  }
297
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  async function imageToTensor(file) {
299
  const img = new Image();
300
  img.src = URL.createObjectURL(file);
301
  await img.decode();
302
 
303
- const size = pre.img_size;
 
304
  const canvas = document.createElement("canvas");
305
  canvas.width = size;
306
  canvas.height = size;
@@ -311,32 +744,100 @@ async function imageToTensor(file) {
311
  const data = ctx.getImageData(0, 0, size, size).data;
312
  const x = new Float32Array(1 * 3 * size * size);
313
 
 
 
 
314
  for (let i = 0, p = 0; i < data.length; i += 4, p++) {
315
- x[p] = (data[i] / 255 - pre.mean[0]) / pre.std[0];
316
- x[size * size + p] = (data[i + 1] / 255 - pre.mean[1]) / pre.std[1];
317
- x[2 * size * size + p] = (data[i + 2] / 255 - pre.mean[2]) / pre.std[2];
318
  }
319
 
 
 
320
  return new ort.Tensor("float32", x, [1, 3, size, size]);
321
  }
322
 
323
- document.querySelector("#file").onchange = async (e) => {
324
- const tensor = await imageToTensor(e.target.files[0]);
325
- const res = await sess.run({ [cfg.input_name]: tensor });
 
 
 
 
 
 
 
 
 
 
326
 
327
- const logits = Array.from(res[cfg.output_name].data);
328
  const probs = softmax(logits);
329
 
330
- const pIllu = probs[cfg.positive_index];
331
- const label = pIllu >= cfg.threshold ? cfg.positive_label : "non_illumination";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
- document.querySelector("#out").textContent = JSON.stringify({
334
- label,
335
- p_illumination: pIllu,
336
- probs
337
- }, null, 2);
 
 
338
  };
339
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  ```
341
 
342
  # Training tools
 
138
  from PIL import Image
139
  from pathlib import Path
140
 
141
+ # Model list to test.
142
+ # Check if the models are stored
143
+ # in your expected directory structure
144
+ # (e.g. './Artefacts/{model_name}/onnx/model.onnx')
145
+ MODELS = [
146
+ "mobilenetv2",
147
+ "mobilenetv3_large",
148
+ "mobilenetv3_small",
149
+ "mobilevitv2",
150
+ ]
151
+
152
+ ARTEFACTS_DIR = Path("./Artefacts")
153
+
154
+ # Test images
155
+ IMAGE_PATHS = {
156
+ "illumination": Path("./dataset/test/illustration/gahom_0020__fdf0ee350c94.jpg"),
157
+ "non_illumination": Path("./dataset/test/non_illustration/CREMMA-Medieval-LAT_00007.jpg"),
158
+ }
159
 
 
 
160
 
161
+ def softmax(logits: np.ndarray) -> np.ndarray:
162
+ """Softmax function to convert logits to probabilities.
 
 
163
 
164
+ :param logits: logits array
165
+ :type logits: np.ndarray
166
+ :return: probabilities array
167
+ :rtype: np.ndarray
168
+ """
169
+ logits = logits.astype(np.float32)
170
+ exp = np.exp(logits - logits.max())
171
+ return exp / exp.sum()
172
 
 
 
173
 
174
+ def load_json(path: Path) -> dict:
175
+ """Load a JSON file and return its content as a dictionary.
176
 
177
+ :param path: path to the JSON file
178
+ :type path: Path
179
+ :return: content of the JSON file as a dictionary
180
+ :rtype: dict
181
+ """
182
+ if not path.exists():
183
+ raise FileNotFoundError(f"File not founded: {path}")
184
+ return json.loads(path.read_text())
185
 
 
186
 
187
+ def preprocess_image(image_path: Path, pre: dict) -> np.ndarray:
188
+ """Preprocess the image according to the provided configuration.
 
189
 
190
+ :param image_path: path to the image file
191
+ :type image_path: Path
192
+ :param pre: preprocessing configuration (expects keys 'img_size', 'mean', 'std
193
+ :type pre: dict
194
+ :return: preprocessed image as a numpy array ready for model input
195
+ :rtype: np.ndarray
196
+ """
197
+ if not image_path.exists():
198
+ raise FileNotFoundError(f"Image not founded: {image_path}")
199
 
200
+ img_size = pre["img_size"]
201
+ mean = np.array(pre["mean"], dtype=np.float32)
202
+ std = np.array(pre["std"], dtype=np.float32)
 
 
 
 
 
 
 
 
203
 
204
+ img = Image.open(image_path).convert("RGB").resize((img_size, img_size))
205
 
206
+ x = np.asarray(img).astype(np.float32) / 255.0
207
+ x = (x - mean) / std
208
+ x = x.transpose(2, 0, 1)[None].astype(np.float32)
209
 
210
+ return x
211
 
212
+
213
+ def predict(model_name: str, image_path: Path) -> dict:
214
+ """Run inference on a single model and return the results.
215
+
216
+ :param model_name: name of the model to test
217
+ :type model_name: str
218
+ :param image_path: path to the image file to test
219
+ :type image_path: Path
220
+ :return: dictionary containing the prediction results and probabilities
221
+ :rtype: dict
222
+ """
223
+ run = ARTEFACTS_DIR / model_name
224
+
225
+ cfg = load_json(run / "inference_config.json")
226
+ pre = load_json(run / "preprocess.json")
227
+
228
+ model_path = run / "onnx" / "model.onnx"
229
+ if not model_path.exists():
230
+ raise FileNotFoundError(f"ONNX model not founded: {model_path}")
231
+
232
+ x = preprocess_image(image_path, pre)
233
+
234
+ sess = ort.InferenceSession(str(model_path))
235
+
236
+ input_name = cfg.get("input_name")
237
+ if input_name is None:
238
+ input_name = sess.get_inputs()[0].name
239
+
240
+ output = sess.run(None, {input_name: x})[0]
241
+
242
+ # Cas standard : shape (1, 2)
243
+ logits = output[0]
244
+
245
+ probs = softmax(logits)
246
+
247
+ p_illu = float(probs[1])
248
+
249
+ positive_label = cfg.get("positive_label", "illumination")
250
+ negative_label = cfg.get("negative_label", "non_illumination")
251
+ threshold = float(cfg.get("threshold", 0.5))
252
+
253
+ label = positive_label if p_illu >= threshold else negative_label
254
+
255
+ return {
256
+ "model": model_name,
257
+ "image": str(image_path),
258
+ "label": label,
259
+ "p_illustration": p_illu,
260
+ "probs": probs.tolist(),
261
+ "threshold": threshold,
262
+ }
263
+
264
+
265
+ def main():
266
+ """Main function to run the tests on all models and images."""
267
+ for image_type, image_path in IMAGE_PATHS.items():
268
+ print("=" * 80)
269
+ print(f"Image expected: {image_type}")
270
+ print(f"Image: {image_path}")
271
+ print("=" * 80)
272
+
273
+ for model_name in MODELS:
274
+ try:
275
+ result = predict(model_name, image_path)
276
+
277
+ print(
278
+ f"{result['model']:<20} "
279
+ f"=> {result['label']:<18} "
280
+ f"p_illu={result['p_illustration']:.4f} "
281
+ f"probs={result['probs']}"
282
+ )
283
+
284
+ except Exception as e:
285
+ print(f"{model_name:<20} → ERROR: {e}")
286
+
287
+ print()
288
+
289
+
290
+ if __name__ == "__main__":
291
+ main()
292
  ```
293
 
294
+ ## Python — ONNX from Hugging Face
295
 
296
  ```bash
297
+ pip install huggingface_hub onnxruntime pillow numpy
298
  ```
299
 
300
  ```python
301
  import json
 
302
  import numpy as np
303
+ import onnxruntime as ort
304
+
305
  from PIL import Image
306
  from pathlib import Path
307
+ from huggingface_hub import snapshot_download
308
 
309
+ # Repository HF that contains the ONNX models and their configs
310
+ REPO_ID = "ENC-PSL/BSICLE"
311
 
312
+ MODELS = [
313
+ "mobilenetv2",
314
+ "mobilenetv3_large",
315
+ "mobilenetv3_small",
316
+ "mobilevitv2",
317
+ ]
318
 
319
+ IMAGE_PATHS = {
320
+ "illumination": Path("./dataset/test/illustration/gahom_0020__fdf0ee350c94.jpg"),
321
+ "non_illumination": Path("./dataset/test/non_illustration/CREMMA-Medieval-LAT_00007.jpg"),
322
+ }
323
 
 
 
 
 
324
 
325
+ def download_models(repo_id: str, model_names: list[str]) -> Path:
326
+ """download models and their configs from HF Hub, and return the local path to the snapshot
 
327
 
328
+ :param repo_id: repository id on HF Hub
329
+ :type repo_id: str
330
+ :param model_names: list of model names to download (e.g. ["mobilenetv2", "mobilenetv3_large"])
331
+ :return: local path to the snapshot containing the models and their configs
332
+ :rtype: Path
333
+ """
334
+ allow_patterns = []
335
 
336
+ for model_name in model_names:
337
+ allow_patterns.extend([
338
+ f"{model_name}/onnx/model.onnx",
339
+ f"{model_name}/preprocess.json",
340
+ f"{model_name}/inference_config.json",
341
+ ])
342
 
343
+ snapshot_path = snapshot_download(
344
+ repo_id=repo_id,
345
+ allow_patterns=allow_patterns,
346
+ )
347
 
348
+ return Path(snapshot_path)
349
 
 
 
 
 
350
 
351
+ def load_json(path: Path) -> dict:
352
+ """Load a JSON file and return its content as a dictionary.
353
 
354
+ :param path: path to the JSON file
355
+ :type path: Path
356
+ :return: content of the JSON file as a dictionary
357
+ :rtype: dict
358
+ """
359
+ if not path.exists():
360
+ raise FileNotFoundError(f"Fichier introuvable : {path}")
361
+
362
+ return json.loads(path.read_text())
363
+
364
+
365
+ def softmax(logits: np.ndarray) -> np.ndarray:
366
+ """Softmax function to convert logits to probabilities.
367
+
368
+ :param logits: logits array
369
+ :type logits: np.ndarray
370
+ :return: probabilities array
371
+ :rtype: np.ndarray
372
+ """
373
+ logits = logits.astype(np.float32)
374
+ exp = np.exp(logits - logits.max())
375
+
376
+ return exp / exp.sum()
377
+
378
+
379
+ def preprocess_image(image_path: Path, pre: dict) -> np.ndarray:
380
+ """Preprocess the image according to the provided configuration.
381
+
382
+ :param image_path: path to the image file
383
+ :type image_path: Path
384
+ :param pre: preprocessing configuration (expects keys 'img_size', 'mean', 'std
385
+ :type pre: dict
386
+ :return: preprocessed image as a numpy array ready for model input
387
+ :rtype: np.ndarray
388
+ """
389
+ if not image_path.exists():
390
+ raise FileNotFoundError(f"Image not founded: {image_path}")
391
+
392
+ img_size = int(pre["img_size"])
393
+ mean = np.array(pre["mean"], dtype=np.float32)
394
+ std = np.array(pre["std"], dtype=np.float32)
395
+
396
+ img = Image.open(image_path).convert("RGB").resize((img_size, img_size))
397
+
398
+ x = np.asarray(img).astype(np.float32) / 255.0
399
+ x = (x - mean) / std
400
+ x = x.transpose(2, 0, 1)[None].astype(np.float32)
401
+
402
+ return x
403
+
404
+
405
+ def get_labels(cfg: dict) -> list[str]:
406
+ """Get the list of class labels from the configuration dictionary.
407
+
408
+ :param cfg: configuration dictionary that may contain class labels in different keys
409
+ :type cfg: dict
410
+ :return: list of class labels
411
+ :rtype: list[str]
412
+ """
413
+ if "class_names" in cfg:
414
+ return cfg["class_names"]
415
+
416
+ if "labels" in cfg:
417
+ return cfg["labels"]
418
+
419
+ if "id2label" in cfg:
420
+ id2label = cfg["id2label"]
421
+ return [
422
+ id2label[str(i)] if str(i) in id2label else id2label[i]
423
+ for i in range(len(id2label))
424
+ ]
425
+
426
+ return [
427
+ cfg.get("negative_label", "non_illumination"),
428
+ cfg.get("positive_label", "illumination"),
429
+ ]
430
+
431
+
432
+ def get_positive_index(labels: list[str], positive_label: str) -> int:
433
+ """Get the index of the positive label in the labels list.
434
+
435
+ :param labels: list of class labels
436
+ :type labels: list[str]
437
+ :param positive_label: name of the positive label
438
+ :type positive_label: str
439
+ :return: index of the positive label
440
+ :rtype: int
441
+ """
442
+ if positive_label in labels:
443
+ return labels.index(positive_label)
444
+
445
+ if len(labels) > 1:
446
+ return 1
447
+
448
+ raise ValueError(
449
+ f"Cannot determine positive index: positive_label={positive_label!r} not in labels={labels}"
450
+ )
451
+
452
+
453
+ def predict(model_dir: Path, image_path: Path) -> dict:
454
+ """Run inference on a single model and return the results.
455
+
456
+ :param model_dir: path to the model directory
457
+ :type model_dir: Path
458
+ :param image_path: path to the image file to test
459
+ :type image_path: Path
460
+ :return: dictionary containing the prediction results and probabilities
461
+ :rtype: dict
462
+ """
463
+ cfg = load_json(model_dir / "inference_config.json")
464
+ pre = load_json(model_dir / "preprocess.json")
465
+
466
+ model_path = model_dir / "onnx" / "model.onnx"
467
+ if not model_path.exists():
468
+ raise FileNotFoundError(f"ONNX model not founded: {model_path}")
469
+
470
+ x = preprocess_image(image_path, pre)
471
+
472
+ sess = ort.InferenceSession(str(model_path))
473
+
474
+ input_name = cfg.get("input_name")
475
+ if input_name is None:
476
+ input_name = sess.get_inputs()[0].name
477
+
478
+ output = sess.run(None, {input_name: x})[0]
479
+
480
+ logits = output[0]
481
+ probs = softmax(logits)
482
+
483
+ labels = get_labels(cfg)
484
+
485
+ positive_label = cfg.get("positive_label", "illumination")
486
+ negative_label = cfg.get("negative_label", "non_illumination")
487
+ threshold = float(cfg.get("threshold", 0.5))
488
+
489
+ positive_idx = get_positive_index(labels, positive_label)
490
+
491
+ argmax_idx = int(np.argmax(probs))
492
+ argmax_label = labels[argmax_idx]
493
+ argmax_score = float(probs[argmax_idx])
494
+
495
+ p_illumination = float(probs[positive_idx])
496
+ threshold_label = positive_label if p_illumination >= threshold else negative_label
497
+
498
+ probs_by_label = {
499
+ labels[i]: float(probs[i])
500
+ for i in range(len(labels))
501
+ }
502
+
503
+ return {
504
+ "label_threshold": threshold_label,
505
+ "p_illumination": p_illumination,
506
+ "threshold": threshold,
507
+ "positive_idx": positive_idx,
508
+ "argmax_idx": argmax_idx,
509
+ "argmax_label": argmax_label,
510
+ "score_argmax": argmax_score,
511
+ "labels": labels,
512
+ "probs": probs.tolist(),
513
+ "probs_by_label": probs_by_label,
514
+ }
515
+
516
+
517
+ def main():
518
+ """Main function to run the tests on all models and images."""
519
+ snapshot_root = download_models(REPO_ID, MODELS)
520
+
521
+ print(f"Model downloaded in: {snapshot_root}")
522
+ print()
523
+
524
+ for image_type, image_path in IMAGE_PATHS.items():
525
+ print("=" * 100)
526
+ print(f"Image expected: {image_type}")
527
+ print(f"Image: {image_path}")
528
+ print("=" * 100)
529
+
530
+ for model_name in MODELS:
531
+ model_dir = snapshot_root / model_name
532
+
533
+ try:
534
+ result = predict(model_dir, image_path)
535
+
536
+ print(
537
+ f"{model_name:<20} "
538
+ f"=> predicted={result['label_threshold']:<18} "
539
+ f"p_illumination={result['p_illumination']:.4f} "
540
+ f"argmax={result['argmax_idx']}:{result['argmax_label']:<18} "
541
+ f"score={result['score_argmax']:.4f} "
542
+ f"probs={result['probs_by_label']}"
543
+ )
544
+
545
+ except Exception as e:
546
+ print(f"{model_name:<20} => ERROR : {e}")
547
+
548
+ print()
549
+
550
+
551
+ if __name__ == "__main__":
552
+ main()
553
  ```
554
 
555
+ ## Python — PyTorch / non-ONNX local & Hugging Face
556
 
557
  ```bash
558
+ pip install torch torchvision pillow numpy timm
559
  ```
560
+ Use the same code as above, just change the function `load_model`.
561
 
562
  ```python
563
+ def load_model(run: Path) -> torch.nn.Module:
564
+ """Load a PyTorch model from a checkpoint.
565
 
566
+ :param run: path to the model run directory
567
+ :type run: Path
568
+ :return: loaded PyTorch model
569
+ :rtype: torch.nn.Module
570
+ """
571
+ checkpoint_path = run / "checkpoints" / "best.pt"
572
+
573
+ if not checkpoint_path.exists():
574
+ raise FileNotFoundError(f"Checkpoint introuvable : {checkpoint_path}")
 
 
 
575
 
576
+ model_name = run.name
577
+
578
+ if model_name in {"mobilenetv2", "mobilenet_v2"}:
579
+ model = models.mobilenet_v2(weights=None)
580
+ model.classifier[-1] = torch.nn.Linear(model.classifier[-1].in_features, 2)
581
+
582
+ elif model_name in {"mobilenetv3_large", "mobilenet_v3_large"}:
583
+ model = models.mobilenet_v3_large(weights=None)
584
+ model.classifier[-1] = torch.nn.Linear(model.classifier[-1].in_features, 2)
585
+
586
+ elif model_name in {"mobilenetv3_small", "mobilenet_v3_small"}:
587
+ model = models.mobilenet_v3_small(weights=None)
588
+ model.classifier[-1] = torch.nn.Linear(model.classifier[-1].in_features, 2)
589
+
590
+ elif model_name in {"mobilevitv2", "mobilevit_v2"}:
591
+ import timm
592
+
593
+ model = timm.create_model(
594
+ "mobilevitv2_050",
595
+ pretrained=False,
596
+ num_classes=2,
597
+ )
598
+
599
+ else:
600
+ raise ValueError(
601
+ f"Architecture non supportée : {model_name}. "
602
+ f"Architectures disponibles : mobilenetv2, mobilenetv3_large, "
603
+ f"mobilenetv3_small, mobilevitv2"
604
+ )
605
+
606
+ state = torch.load(checkpoint_path, map_location="cpu")
607
+
608
+ if isinstance(state, dict) and "state_dict" in state:
609
+ state = state["state_dict"]
610
+
611
+ if isinstance(state, dict) and "model_state_dict" in state:
612
+ state = state["model_state_dict"]
613
+
614
+ state = {
615
+ key.replace("module.", ""): value
616
+ for key, value in state.items()
617
+ }
618
+
619
+ model.load_state_dict(state)
620
+ model.eval()
621
+
622
+ return model
623
+ ```
624
 
625
  ## JS (HF - ONNX)
626
 
627
  ```javascript
628
  <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>
629
+
630
+ <label for="model">Model:</label>
631
+ <select id="model">
632
+ <option value="mobilenetv2">mobilenetv2</option>
633
+ <option value="mobilenetv3_large">mobilenetv3_large</option>
634
+ <option value="mobilenetv3_small">mobilenetv3_small</option>
635
+ <option value="mobilevitv2">mobilevitv2</option>
636
+ </select>
637
+
638
+ <br><br>
639
+
640
  <input type="file" id="file" accept="image/*">
641
+
642
+ <pre id="out">loading...</pre>
643
 
644
  <script type="module">
645
+ const REPO_BASE = "https://huggingface.co/ENC-PSL/BSICLE/resolve/main";
646
+
647
+ let cfg = null;
648
+ let pre = null;
649
+ let sess = null;
650
+ let currentModel = null;
651
 
652
+ const out = document.querySelector("#out");
653
+ const fileInput = document.querySelector("#file");
654
+ const modelSelect = document.querySelector("#model");
655
 
656
  function softmax(a) {
657
  const m = Math.max(...a);
 
660
  return e.map(x => x / s);
661
  }
662
 
663
+ function getLabels(cfg) {
664
+ if (cfg.class_names) {
665
+ return cfg.class_names;
666
+ }
667
+
668
+ if (cfg.labels) {
669
+ return cfg.labels;
670
+ }
671
+
672
+ if (cfg.id2label) {
673
+ return Object.keys(cfg.id2label)
674
+ .sort((a, b) => Number(a) - Number(b))
675
+ .map(k => cfg.id2label[k]);
676
+ }
677
+
678
+ return [
679
+ cfg.negative_label ?? "non_illumination",
680
+ cfg.positive_label ?? "illumination",
681
+ ];
682
+ }
683
+
684
+ function getPositiveIndex(labels, cfg) {
685
+ if (cfg.positive_index !== undefined) {
686
+ return Number(cfg.positive_index);
687
+ }
688
+
689
+ const positiveLabel = cfg.positive_label ?? "illumination";
690
+
691
+ if (labels.includes(positiveLabel)) {
692
+ return labels.indexOf(positiveLabel);
693
+ }
694
+
695
+ if (labels.length > 1) {
696
+ return 1;
697
+ }
698
+
699
+ throw new Error(
700
+ `Impossible de trouver l'index positif pour positive_label=${positiveLabel}`
701
+ );
702
+ }
703
+
704
+ async function loadModel(modelName) {
705
+ currentModel = modelName;
706
+
707
+ const run = `${REPO_BASE}/${modelName}`;
708
+
709
+ out.textContent = `Chargement du modèle ${modelName}...`;
710
+
711
+ cfg = await fetch(`${run}/inference_config.json`).then(r => {
712
+ if (!r.ok) {
713
+ throw new Error(`Impossible de charger inference_config.json pour ${modelName}`);
714
+ }
715
+ return r.json();
716
+ });
717
+
718
+ pre = await fetch(`${run}/preprocess.json`).then(r => {
719
+ if (!r.ok) {
720
+ throw new Error(`Impossible de charger preprocess.json pour ${modelName}`);
721
+ }
722
+ return r.json();
723
+ });
724
+
725
+ sess = await ort.InferenceSession.create(`${run}/onnx/model.onnx`);
726
+
727
+ out.textContent = `Loaded model: ${modelName}`;
728
+ }
729
+
730
  async function imageToTensor(file) {
731
  const img = new Image();
732
  img.src = URL.createObjectURL(file);
733
  await img.decode();
734
 
735
+ const size = Number(pre.img_size);
736
+
737
  const canvas = document.createElement("canvas");
738
  canvas.width = size;
739
  canvas.height = size;
 
744
  const data = ctx.getImageData(0, 0, size, size).data;
745
  const x = new Float32Array(1 * 3 * size * size);
746
 
747
+ const mean = pre.mean;
748
+ const std = pre.std;
749
+
750
  for (let i = 0, p = 0; i < data.length; i += 4, p++) {
751
+ x[p] = (data[i] / 255 - mean[0]) / std[0];
752
+ x[size * size + p] = (data[i + 1] / 255 - mean[1]) / std[1];
753
+ x[2 * size * size + p] = (data[i + 2] / 255 - mean[2]) / std[2];
754
  }
755
 
756
+ URL.revokeObjectURL(img.src);
757
+
758
  return new ort.Tensor("float32", x, [1, 3, size, size]);
759
  }
760
 
761
+ async function predict(file) {
762
+ if (!sess || !cfg || !pre) {
763
+ throw new Error("No model loaded.");
764
+ }
765
+
766
+ const tensor = await imageToTensor(file);
767
+
768
+ const inputName = cfg.input_name ?? sess.inputNames[0];
769
+ const outputName = cfg.output_name ?? sess.outputNames[0];
770
+
771
+ const res = await sess.run({
772
+ [inputName]: tensor,
773
+ });
774
 
775
+ const logits = Array.from(res[outputName].data);
776
  const probs = softmax(logits);
777
 
778
+ const labels = getLabels(cfg);
779
+
780
+ const positiveLabel = cfg.positive_label ?? "illumination";
781
+ const negativeLabel = cfg.negative_label ?? "non_illumination";
782
+ const threshold = Number(cfg.threshold ?? 0.5);
783
+
784
+ const positiveIndex = getPositiveIndex(labels, cfg);
785
+
786
+ const pIllumination = probs[positiveIndex];
787
+ const labelThreshold = pIllumination >= threshold ? positiveLabel : negativeLabel;
788
+
789
+ const argmaxIdx = probs.indexOf(Math.max(...probs));
790
+ const argmaxLabel = labels[argmaxIdx];
791
+ const argmaxScore = probs[argmaxIdx];
792
+
793
+ const probsByLabel = {};
794
+ labels.forEach((label, i) => {
795
+ probsByLabel[label] = probs[i];
796
+ });
797
+
798
+ return {
799
+ model: currentModel,
800
+ predicted: labelThreshold,
801
+ p_illumination: pIllumination,
802
+ threshold,
803
+ positive_index: positiveIndex,
804
+ argmax: `${argmaxIdx}:${argmaxLabel}`,
805
+ argmax_score: argmaxScore,
806
+ labels,
807
+ probs,
808
+ probs_by_label: probsByLabel,
809
+ };
810
+ }
811
+
812
+ modelSelect.onchange = async () => {
813
+ try {
814
+ await loadModel(modelSelect.value);
815
 
816
+ if (fileInput.files.length > 0) {
817
+ const result = await predict(fileInput.files[0]);
818
+ out.textContent = JSON.stringify(result, null, 2);
819
+ }
820
+ } catch (err) {
821
+ out.textContent = String(err);
822
+ }
823
  };
824
+
825
+ fileInput.onchange = async (e) => {
826
+ try {
827
+ const file = e.target.files[0];
828
+
829
+ if (!file) {
830
+ return;
831
+ }
832
+
833
+ const result = await predict(file);
834
+ out.textContent = JSON.stringify(result, null, 2);
835
+ } catch (err) {
836
+ out.textContent = String(err);
837
+ }
838
+ };
839
+
840
+ await loadModel(modelSelect.value);
841
  ```
842
 
843
  # Training tools