Jules Musquin commited on
Commit ·
ef8cc33
1
Parent(s): 5bffa8b
[ADD] adding a IoU.py, a script to calculate the IoU metric between 2 yolo models.
Browse files- .gitignore +2 -1
- IoU_yolo.py +110 -0
- inference.py +43 -22
.gitignore
CHANGED
|
@@ -31,4 +31,5 @@ lib64/
|
|
| 31 |
generated_html/
|
| 32 |
annotations/
|
| 33 |
dataset/
|
| 34 |
-
test/
|
|
|
|
|
|
| 31 |
generated_html/
|
| 32 |
annotations/
|
| 33 |
dataset/
|
| 34 |
+
test/
|
| 35 |
+
yolov5/
|
IoU_yolo.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
GT_DIR = "test_images/wit396_pdf545/labels"
|
| 5 |
+
PRED_DIR = ""
|
| 6 |
+
|
| 7 |
+
def yolo_to_corners(box):
|
| 8 |
+
"""Convertit (x_center, y_center, w, h) en (x1, y1, x2, y2)."""
|
| 9 |
+
x_c, y_c, w, h = box
|
| 10 |
+
x1 = x_c - w / 2
|
| 11 |
+
y1 = y_c - h / 2
|
| 12 |
+
x2 = x_c + w / 2
|
| 13 |
+
y2 = y_c + h / 2
|
| 14 |
+
return np.array([x1, y1, x2, y2])
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def get_iou(box_a, box_b):
|
| 18 |
+
"""Calcule l'IoU entre deux boxes au format YOLO (x_center, y_center, w, h)."""
|
| 19 |
+
a = yolo_to_corners(box_a)
|
| 20 |
+
b = yolo_to_corners(box_b)
|
| 21 |
+
|
| 22 |
+
ix1 = np.maximum(a[0], b[0])
|
| 23 |
+
iy1 = np.maximum(a[1], b[1])
|
| 24 |
+
ix2 = np.minimum(a[2], b[2])
|
| 25 |
+
iy2 = np.minimum(a[3], b[3])
|
| 26 |
+
|
| 27 |
+
i_width = np.maximum(ix2 - ix1, 0.0)
|
| 28 |
+
i_height = np.maximum(iy2 - iy1, 0.0)
|
| 29 |
+
area_of_intersection = i_width * i_height
|
| 30 |
+
|
| 31 |
+
area_a = max(a[2] - a[0], 0.0) * max(a[3] - a[1], 0.0)
|
| 32 |
+
area_b = max(b[2] - b[0], 0.0) * max(b[3] - b[1], 0.0)
|
| 33 |
+
|
| 34 |
+
area_of_union = area_a + area_b - area_of_intersection
|
| 35 |
+
if area_of_union <= 0:
|
| 36 |
+
return 0.0
|
| 37 |
+
|
| 38 |
+
return float(area_of_intersection / area_of_union)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def parse_yolo_file(filepath):
|
| 42 |
+
"""Lit un .txt YOLO et retourne une liste de {class_id, box}."""
|
| 43 |
+
boxes = []
|
| 44 |
+
if not os.path.exists(filepath):
|
| 45 |
+
return boxes
|
| 46 |
+
|
| 47 |
+
with open(filepath, "r") as f:
|
| 48 |
+
for line in f:
|
| 49 |
+
parts = line.strip().split()
|
| 50 |
+
if not parts:
|
| 51 |
+
continue
|
| 52 |
+
class_id = int(float(parts[0]))
|
| 53 |
+
xc, yc, w, h = (float(p) for p in parts[1:5])
|
| 54 |
+
boxes.append({"class_id": class_id, "box": [xc, yc, w, h]})
|
| 55 |
+
return boxes
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def match_boxes(gt_boxes, pred_boxes):
|
| 59 |
+
"""Associe GT et prédictions par classe, en priorisant le meilleur IoU."""
|
| 60 |
+
matches = []
|
| 61 |
+
unmatched_gt = list(gt_boxes)
|
| 62 |
+
unmatched_pred = list(pred_boxes)
|
| 63 |
+
|
| 64 |
+
classes = set(b["class_id"] for b in gt_boxes) | set(b["class_id"] for b in pred_boxes)
|
| 65 |
+
|
| 66 |
+
for cls in classes:
|
| 67 |
+
gt_cls = [b for b in unmatched_gt if b["class_id"] == cls]
|
| 68 |
+
pred_cls = [b for b in unmatched_pred if b["class_id"] == cls]
|
| 69 |
+
|
| 70 |
+
pairs = []
|
| 71 |
+
for gt_b in gt_cls:
|
| 72 |
+
for pred_b in pred_cls:
|
| 73 |
+
iou = get_iou(gt_b["box"], pred_b["box"])
|
| 74 |
+
pairs.append((iou, gt_b, pred_b))
|
| 75 |
+
|
| 76 |
+
pairs.sort(key=lambda x: x[0], reverse=True)
|
| 77 |
+
used_gt, used_pred = set(), set()
|
| 78 |
+
for iou, gt_b, pred_b in pairs:
|
| 79 |
+
if id(gt_b) in used_gt or id(pred_b) in used_pred:
|
| 80 |
+
continue
|
| 81 |
+
matches.append((gt_b, pred_b, iou))
|
| 82 |
+
used_gt.add(id(gt_b))
|
| 83 |
+
used_pred.add(id(pred_b))
|
| 84 |
+
unmatched_gt.remove(gt_b)
|
| 85 |
+
unmatched_pred.remove(pred_b)
|
| 86 |
+
|
| 87 |
+
return matches, unmatched_gt, unmatched_pred
|
| 88 |
+
|
| 89 |
+
gt_files = sorted(f for f in os.listdir(GT_DIR) if f.endswith(".txt"))
|
| 90 |
+
|
| 91 |
+
all_ious = []
|
| 92 |
+
|
| 93 |
+
for filename in gt_files:
|
| 94 |
+
gt_path = os.path.join(GT_DIR, filename)
|
| 95 |
+
pred_path = os.path.join(PRED_DIR, filename)
|
| 96 |
+
|
| 97 |
+
gt_boxes = parse_yolo_file(gt_path)
|
| 98 |
+
pred_boxes = parse_yolo_file(pred_path)
|
| 99 |
+
|
| 100 |
+
matches, unmatched_gt, unmatched_pred = match_boxes(gt_boxes, pred_boxes)
|
| 101 |
+
ious = [m[2] for m in matches]
|
| 102 |
+
all_ious.extend(ious)
|
| 103 |
+
|
| 104 |
+
mean_iou = np.mean(ious) if ious else 0.0
|
| 105 |
+
print(f"{filename}: {len(matches)} paires, IoU moyen = {mean_iou:.4f}, "
|
| 106 |
+
f"GT manqués = {len(unmatched_gt)}, faux positifs = {len(unmatched_pred)}")
|
| 107 |
+
|
| 108 |
+
print(f"Fichiers traités : {len(gt_files)}")
|
| 109 |
+
print(f"Paires appariées : {len(all_ious)}")
|
| 110 |
+
print(f"IoU moyen global : {np.mean(all_ious) if all_ious else 0.0:.4f}")
|
inference.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
import argparse
|
| 2 |
import glob
|
| 3 |
import os
|
| 4 |
-
import matplotlib.pyplot as plt
|
| 5 |
-
|
| 6 |
from ultralytics import YOLO
|
| 7 |
|
| 8 |
-
# uv run predict.py
|
|
|
|
|
|
|
| 9 |
def parse_args() -> argparse.Namespace:
|
| 10 |
parser = argparse.ArgumentParser(
|
| 11 |
-
description="Prédiction YOLO sur un dossier d'images avec sauvegarde des résultats annotés."
|
| 12 |
)
|
| 13 |
|
| 14 |
parser.add_argument(
|
|
@@ -24,41 +24,62 @@ def parse_args() -> argparse.Namespace:
|
|
| 24 |
parser.add_argument(
|
| 25 |
"output_path",
|
| 26 |
type=str,
|
| 27 |
-
help="Dossier de sortie pour les images annotées",
|
| 28 |
)
|
| 29 |
|
| 30 |
return parser.parse_args()
|
| 31 |
|
| 32 |
|
| 33 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
model = YOLO(model_path)
|
| 35 |
images = glob.glob(os.path.join(images_path, "*.jpg"))
|
| 36 |
selection = images[:25]
|
| 37 |
-
print(selection)
|
|
|
|
| 38 |
results = model(selection)
|
| 39 |
-
name_model = model_path.split('/')
|
| 40 |
|
| 41 |
-
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
|
| 45 |
-
os.
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
im_array = result.plot() # retourne un array numpy BGR avec les boxes dessinées
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
| 55 |
|
| 56 |
-
result.save(filename=os.path.join(output_dir, model_dir, f"result_{i}.jpg")) # sauvegarde avec nom unique
|
| 57 |
|
| 58 |
def main() -> None:
|
| 59 |
args = parse_args()
|
| 60 |
predict(args.model_path, args.images_path, args.output_path)
|
| 61 |
|
|
|
|
| 62 |
if __name__ == "__main__":
|
| 63 |
-
main()
|
| 64 |
-
|
|
|
|
| 1 |
import argparse
|
| 2 |
import glob
|
| 3 |
import os
|
|
|
|
|
|
|
| 4 |
from ultralytics import YOLO
|
| 5 |
|
| 6 |
+
# uv run predict.py path_to_model.pt path_to_images ./test
|
| 7 |
+
|
| 8 |
+
|
| 9 |
def parse_args() -> argparse.Namespace:
|
| 10 |
parser = argparse.ArgumentParser(
|
| 11 |
+
description="Prédiction YOLO sur un dossier d'images avec sauvegarde des résultats annotés et des labels .txt"
|
| 12 |
)
|
| 13 |
|
| 14 |
parser.add_argument(
|
|
|
|
| 24 |
parser.add_argument(
|
| 25 |
"output_path",
|
| 26 |
type=str,
|
| 27 |
+
help="Dossier de sortie pour les images annotées et les labels",
|
| 28 |
)
|
| 29 |
|
| 30 |
return parser.parse_args()
|
| 31 |
|
| 32 |
|
| 33 |
+
def save_yolo_txt(result, txt_path):
|
| 34 |
+
"""
|
| 35 |
+
Sauvegarde les prédictions d'un résultat Ultralytics au format YOLO :
|
| 36 |
+
<class_id> <x_center> <y_center> <width> <height> <confidence>
|
| 37 |
+
(coordonnées normalisées entre 0 et 1)
|
| 38 |
+
"""
|
| 39 |
+
boxes = result.boxes
|
| 40 |
+
with open(txt_path, "w") as f:
|
| 41 |
+
for box in boxes:
|
| 42 |
+
class_id = int(box.cls[0])
|
| 43 |
+
conf = float(box.conf[0])
|
| 44 |
+
x_center, y_center, width, height = box.xywhn[0].tolist()
|
| 45 |
+
f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f} {conf:.6f}\n")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def predict(model_path: str, images_path: str, output_path: str):
|
| 49 |
model = YOLO(model_path)
|
| 50 |
images = glob.glob(os.path.join(images_path, "*.jpg"))
|
| 51 |
selection = images[:25]
|
| 52 |
+
print(f"{len(selection)} images sélectionnées pour la prédiction")
|
| 53 |
+
|
| 54 |
results = model(selection)
|
|
|
|
| 55 |
|
| 56 |
+
# Nom du modèle (sans extension) pour organiser les résultats par modèle testé
|
| 57 |
+
model_name = model_path.split('/')
|
| 58 |
+
model_name = model_name[0]
|
| 59 |
+
|
| 60 |
+
images_dir = os.path.join(output_path, model_name, "images")
|
| 61 |
+
labels_dir = os.path.join(output_path, model_name, "labels")
|
| 62 |
+
os.makedirs(images_dir, exist_ok=True)
|
| 63 |
+
os.makedirs(labels_dir, exist_ok=True)
|
| 64 |
|
| 65 |
+
for image_path, result in zip(selection, results):
|
| 66 |
+
image_name = os.path.splitext(os.path.basename(image_path))[0]
|
| 67 |
|
| 68 |
+
# Sauvegarde de l'image annotée
|
| 69 |
+
result.save(filename=os.path.join(images_dir, f"{image_name}.jpg"))
|
|
|
|
| 70 |
|
| 71 |
+
# Sauvegarde des prédictions au format YOLO .txt
|
| 72 |
+
save_yolo_txt(result, os.path.join(labels_dir, f"{image_name}.txt"))
|
| 73 |
+
|
| 74 |
+
print(f"Modèle testé : {model_name}")
|
| 75 |
+
print(f"Images annotées sauvegardées dans : {images_dir}")
|
| 76 |
+
print(f"Labels YOLO sauvegardés dans : {labels_dir}")
|
| 77 |
|
|
|
|
| 78 |
|
| 79 |
def main() -> None:
|
| 80 |
args = parse_args()
|
| 81 |
predict(args.model_path, args.images_path, args.output_path)
|
| 82 |
|
| 83 |
+
|
| 84 |
if __name__ == "__main__":
|
| 85 |
+
main()
|
|
|