Mingze commited on
Commit
6cc84ea
·
1 Parent(s): c46c3d9

Improve detection label placement

Browse files
detection_utils.py CHANGED
@@ -48,6 +48,48 @@ def detection_color(class_id: int) -> tuple[int, int, int]:
48
  )
49
 
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  def render_detection(
52
  image: Image.Image,
53
  detections: Iterable[dict[str, object]],
@@ -58,8 +100,13 @@ def render_detection(
58
  short_side = min(rendered.size)
59
  line_width = max(2, round(short_side / 320))
60
  font = _load_font(max(13, min(24, round(short_side / 55))))
 
61
 
62
- for detection in detections:
 
 
 
 
63
  class_id = int(detection["class_id"])
64
  color = detection_color(class_id)
65
  box = [
@@ -73,13 +120,24 @@ def render_detection(
73
  f"{float(detection['confidence']):.2f}"
74
  )
75
  draw.rectangle(box, outline=color, width=line_width)
76
- text_box = draw.textbbox((box[0], box[1]), label, font=font)
77
  text_height = text_box[3] - text_box[1]
78
  text_width = text_box[2] - text_box[0]
79
- text_y = max(0.0, box[1] - text_height - 8)
80
- background = [box[0], text_y, box[0] + text_width + 8, text_y + text_height + 8]
 
 
 
 
 
 
81
  draw.rectangle(background, fill=color)
82
- draw.text((box[0] + 4, text_y + 4), label, fill="white", font=font)
 
 
 
 
 
83
 
84
  return rendered
85
 
 
48
  )
49
 
50
 
51
+ def _boxes_overlap(
52
+ first: tuple[float, float, float, float],
53
+ second: tuple[float, float, float, float],
54
+ ) -> bool:
55
+ return not (
56
+ first[2] <= second[0]
57
+ or second[2] <= first[0]
58
+ or first[3] <= second[1]
59
+ or second[3] <= first[1]
60
+ )
61
+
62
+
63
+ def _label_box(
64
+ object_box: list[float],
65
+ label_width: float,
66
+ label_height: float,
67
+ image_size: tuple[int, int],
68
+ occupied: list[tuple[float, float, float, float]],
69
+ ) -> tuple[float, float, float, float]:
70
+ """Place a label near its object while avoiding earlier labels."""
71
+ image_width, image_height = image_size
72
+ center_x = (object_box[0] + object_box[2]) / 2
73
+ x = max(0.0, min(center_x - label_width / 2, image_width - label_width))
74
+
75
+ candidates: list[tuple[float, float, float, float]] = []
76
+ for slot in range(7):
77
+ bottom = object_box[1] - 4 - slot * (label_height + 4)
78
+ top = bottom - label_height
79
+ if top >= 0:
80
+ candidates.append((x, top, x + label_width, bottom))
81
+ for slot in range(4):
82
+ top = object_box[3] + 4 + slot * (label_height + 4)
83
+ bottom = top + label_height
84
+ if bottom <= image_height:
85
+ candidates.append((x, top, x + label_width, bottom))
86
+
87
+ for candidate in candidates:
88
+ if not any(_boxes_overlap(candidate, used) for used in occupied):
89
+ return candidate
90
+ return candidates[0] if candidates else (x, 0.0, x + label_width, label_height)
91
+
92
+
93
  def render_detection(
94
  image: Image.Image,
95
  detections: Iterable[dict[str, object]],
 
100
  short_side = min(rendered.size)
101
  line_width = max(2, round(short_side / 320))
102
  font = _load_font(max(13, min(24, round(short_side / 55))))
103
+ occupied_labels: list[tuple[float, float, float, float]] = []
104
 
105
+ ordered_detections = sorted(
106
+ detections,
107
+ key=lambda item: (float(item["x1"]), float(item["y1"])),
108
+ )
109
+ for detection in ordered_detections:
110
  class_id = int(detection["class_id"])
111
  color = detection_color(class_id)
112
  box = [
 
120
  f"{float(detection['confidence']):.2f}"
121
  )
122
  draw.rectangle(box, outline=color, width=line_width)
123
+ text_box = draw.textbbox((0, 0), label, font=font)
124
  text_height = text_box[3] - text_box[1]
125
  text_width = text_box[2] - text_box[0]
126
+ background = _label_box(
127
+ box,
128
+ text_width + 8,
129
+ text_height + 8,
130
+ rendered.size,
131
+ occupied_labels,
132
+ )
133
+ occupied_labels.append(background)
134
  draw.rectangle(background, fill=color)
135
+ draw.text(
136
+ (background[0] + 4, background[1] + 4),
137
+ label,
138
+ fill="white",
139
+ font=font,
140
+ )
141
 
142
  return rendered
143
 
tests/test_detection_utils.py CHANGED
@@ -55,3 +55,9 @@ def test_detection_overlay_matches_input_size():
55
  rendered = utils.render_detection(image, DETECTIONS)
56
  assert rendered.size == image.size
57
  assert rendered.getpixel((1, 2)) != (255, 255, 255)
 
 
 
 
 
 
 
55
  rendered = utils.render_detection(image, DETECTIONS)
56
  assert rendered.size == image.size
57
  assert rendered.getpixel((1, 2)) != (255, 255, 255)
58
+
59
+
60
+ def test_label_boxes_avoid_existing_labels():
61
+ first = utils._label_box([20, 30, 30, 40], 20, 10, (100, 100), [])
62
+ second = utils._label_box([22, 30, 32, 40], 20, 10, (100, 100), [first])
63
+ assert not utils._boxes_overlap(first, second)