Skip to content

Commit 01cc020

Browse files
committed
Upload evaluation scripts
1 parent 1ca175a commit 01cc020

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

evaluation/eval.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import random
5+
from glob import glob
6+
7+
from tqdm import tqdm
8+
9+
import eval_utils
10+
import yolo_utils
11+
12+
CLASSES = ["system_measures", "stave_measures", "staves", "systems", "grand_staff"]
13+
14+
15+
def yolo_pred_gt_job(model_path: str, val_dir: str, count: int, seed: int = 42) -> tuple[
16+
list[list[tuple[list[float], float, int]]], list[list[tuple[list[float], float, int]]]
17+
]:
18+
model = yolo_utils.load_model(model_path)
19+
images = list(glob(val_dir + "/*.png"))
20+
21+
if count is not None:
22+
random.Random(seed).shuffle(images)
23+
images = images[:count]
24+
25+
ground_truths = []
26+
predictions = []
27+
28+
for image in tqdm(images):
29+
im, (width, height) = yolo_utils.prepare_image(image)
30+
31+
prediction = model.predict(source=im)[0]
32+
33+
predictions.append(yolo_utils.prepare_prediction(prediction.boxes))
34+
ground_truths.append(yolo_utils.prepare_ground_truth(yolo_utils.get_gt_path(image), width, height))
35+
36+
return ground_truths, predictions
37+
38+
39+
if __name__ == "__main__":
40+
parser = argparse.ArgumentParser(description="Evaluate TensorFlow object detection model on a validation dataset.")
41+
parser.add_argument("model_path", type=str, help="Path to model.")
42+
parser.add_argument("dataset_dir", type=str, help="Path to validation dataset.")
43+
parser.add_argument("-c", "--count", type=int, help="How many images the model will be tested on.")
44+
parser.add_argument("-s", "--seed", type=int, default=42, help="Seed for dataset shuffling.")
45+
args = parser.parse_args()
46+
47+
GROUND_TRUTH, PREDICTIONS = yolo_pred_gt_job(args.model_path, args.dataset_dir, args.count, seed=int(args.seed))
48+
49+
eval_utils.evaluate_metrics(GROUND_TRUTH, PREDICTIONS, CLASSES)

evaluation/eval_utils.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from __future__ import annotations
2+
from pycocotools.coco import COCO
3+
from pycocotools.cocoeval import COCOeval
4+
5+
from PIL import Image
6+
from PIL import Image, ImageDraw, ImageFont
7+
8+
import os
9+
import cv2
10+
11+
font_path = os.path.join(cv2.__path__[0], 'qt', 'fonts', 'DejaVuSans.ttf')
12+
13+
14+
def box_to_coco_format(box):
15+
"""
16+
Convert (top, left, bottom, right) to (left, top, width, height)
17+
"""
18+
top, left, bottom, right = box
19+
width = right - left
20+
height = bottom - top
21+
return [left, top, width, height]
22+
23+
24+
def prepare_coco_format(gt_data: list[list[tuple[tuple[float, float, float, float], float, int]]],
25+
pred_data: list[list[tuple[tuple[float, float, float, float], float, int]]],
26+
classes: list[str]):
27+
"""
28+
Input data format: ((left, top, width, height), certainty, class), coordinates are absolute.
29+
"""
30+
coco_ground_truth = {
31+
"images": [{"id": img_id} for img_id in range(len(gt_data))], # Image IDs for all images
32+
"annotations": [],
33+
"categories": [{"id": cls_id, "name": cls_name} for cls_id, cls_name in enumerate(classes, 1)],
34+
}
35+
36+
coco_predictions = []
37+
annotation_id = 0
38+
39+
# Add ground truth annotations for each image
40+
for img_id, gt_boxes in enumerate(gt_data):
41+
for box, certainty, clss in gt_boxes:
42+
coco_ground_truth["annotations"].append({
43+
"id": annotation_id,
44+
"image_id": img_id, # assign image ID
45+
"category_id": clss, # multiple classes
46+
"bbox": box,
47+
"area": box[2] * box[3], # area of the bbox
48+
"iscrowd": 0
49+
})
50+
annotation_id += 1
51+
52+
# Add predictions for each image
53+
for img_id, pred_boxes in enumerate(pred_data):
54+
for box, certainty, clss in pred_boxes:
55+
coco_predictions.append({
56+
"image_id": img_id, # Assign image ID
57+
"category_id": clss, # Multiple classes
58+
"bbox": box,
59+
"score": certainty
60+
})
61+
62+
return coco_ground_truth, coco_predictions
63+
64+
65+
def evaluate_metrics(gt_data, pred_data, classes):
66+
coco_gt_data, coco_pred_data = prepare_coco_format(gt_data, pred_data, classes)
67+
68+
# Create COCO ground truth object
69+
coco_gt = COCO()
70+
coco_gt.dataset = coco_gt_data
71+
coco_gt.createIndex()
72+
73+
# Create COCO predictions object (simulates the result file)
74+
coco_dt = coco_gt.loadRes(coco_pred_data)
75+
76+
# Evaluate using COCOeval
77+
coco_eval = COCOeval(coco_gt, coco_dt, 'bbox')
78+
coco_eval.params.imgIds = list(range(len(gt_data))) # Evaluate all image IDs
79+
coco_eval.evaluate()
80+
coco_eval.accumulate()
81+
print(f"=== Total Metrics (all images and classes combined) ===")
82+
coco_eval.summarize()
83+
84+
# Per-class metrics
85+
for class_id, class_name in enumerate(classes, 1):
86+
coco_eval.params.catIds = [class_id] # Filter by class
87+
coco_eval.evaluate()
88+
coco_eval.accumulate()
89+
print(f"\n=== Metrics for class: {class_name} ===")
90+
coco_eval.summarize()
91+
92+
93+
def draw_rectangles(image_path, coordinates: list[tuple[tuple[float, float, float, float], float, int]], classes: int,
94+
threshold: float = 0.5):
95+
"""
96+
Draws rectangles on the image based on absolute coordinates. Expects COCO format (left, top, width, height).
97+
98+
Mainly used for debugging.
99+
"""
100+
for current_cls in range(classes):
101+
img = Image.open(image_path)
102+
draw = ImageDraw.Draw(img)
103+
for dat in coordinates:
104+
if dat[2] - 1 == current_cls and dat[1] > threshold:
105+
(left, top, widht, height) = dat[0]
106+
# Scale the coordinates based on the image size
107+
top_pixel = int(top)
108+
left_pixel = int(left)
109+
bottom_pixel = int(top + height)
110+
right_pixel = int(left + widht)
111+
112+
# Draw a rectangle (outline only)
113+
draw.rectangle([left_pixel, top_pixel, right_pixel, bottom_pixel], outline="red", width=2)
114+
115+
print(f"saving {current_cls}")
116+
img.save(f"eval_tests/{current_cls}.png")
117+
118+
119+
def draw_rectangles_with_conf(image_path, coordinates: list[tuple[tuple[float, float, float, float], float, int]],
120+
classes: int, threshold: float = 0.5):
121+
"""
122+
Draws rectangles on the image based on absolute coordinates and displays confidence.
123+
Expects coordinates in COCO format (left, top, width, height), with confidence and class info.
124+
"""
125+
126+
# Load a font (optional, you can specify a font path if desired)
127+
# font = ImageFont.load_default()
128+
font = ImageFont.truetype(font_path, size=25)
129+
130+
for current_cls in range(classes):
131+
img = Image.open(image_path)
132+
draw = ImageDraw.Draw(img)
133+
134+
for dat in coordinates:
135+
# dat[0]: (left, top, width, height)
136+
# dat[1]: confidence score
137+
# dat[2]: class label (subtract 1 as per the existing code)
138+
139+
if dat[2] - 1 == current_cls and dat[1] > threshold:
140+
(left, top, width, height) = dat[0]
141+
confidence = dat[1]
142+
143+
# Scale the coordinates based on the image size
144+
top_pixel = int(top)
145+
left_pixel = int(left)
146+
bottom_pixel = int(top + height)
147+
right_pixel = int(left + width)
148+
149+
# Draw the main rectangle (outline only)
150+
draw.rectangle([left_pixel, top_pixel, right_pixel, bottom_pixel], outline="red", width=2)
151+
152+
# Create a filled rectangle for the confidence label at the top-left of the main rectangle
153+
label_height = 30 # Fixed height for the label box
154+
label_width = 80 # Width of the label box for the confidence text
155+
draw.rectangle([left_pixel, top_pixel, left_pixel + label_width, top_pixel + label_height], fill="red")
156+
157+
# Draw the confidence score inside the label box
158+
draw.text((left_pixel + 5, top_pixel), f"{confidence:.3f}", fill="white", font=font)
159+
160+
print(f"saving {current_cls}")
161+
img.save(f"{image_path}_{current_cls}.png")

evaluation/yolo_utils.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import numpy as np
2+
from PIL import Image
3+
from ultralytics import YOLO
4+
5+
6+
def load_model(model_path: str):
7+
return YOLO(model_path)
8+
9+
10+
def prepare_image(image_path: str):
11+
image = Image.open(image_path)
12+
return image, image.size
13+
14+
15+
def prepare_prediction(prediction) -> list[tuple[list[float], float, int]]:
16+
pred = []
17+
for i in range(len(prediction.cls)):
18+
coord = list([float(prediction.xyxy[i][j]) for j in range(len(prediction.xywh[0]))])
19+
pred.append((
20+
[coord[0], coord[1], coord[2] - coord[0], coord[3] - coord[1]],
21+
float(prediction.conf[i]),
22+
int(prediction.cls[i]) + 1
23+
))
24+
return pred
25+
26+
27+
def get_gt_path(img_path: str) -> str:
28+
return img_path.replace("images", "labels").replace(".png", ".txt")
29+
30+
31+
def prepare_ground_truth(gt_path: str, width: int, height: int) -> list[tuple[list[float], float, int]]:
32+
parsed_data = []
33+
with open(gt_path, 'r') as file:
34+
for line in file:
35+
values = line.strip().split()
36+
37+
# Extract the class (first value)
38+
class_value = int(values[0])
39+
x = float(values[1])
40+
y = float(values[2])
41+
w = float(values[3])
42+
h = float(values[4])
43+
44+
parsed_data.append(([int((x - w / 2) * width), int((y - h / 2) * height), int(w * width), int(h * height)],
45+
1.0, class_value + 1))
46+
47+
return parsed_data

0 commit comments

Comments
 (0)