Skip to content

Commit 3573f7a

Browse files
committed
Simplify livebook bounding box machinery
1 parent 70f64c1 commit 3573f7a

2 files changed

Lines changed: 42 additions & 58 deletions

File tree

lib/detection.ex

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ if ImageVision.ortex_configured?() do
5454
@num_classes 80
5555
@default_min_score 0.5
5656

57+
# 10-colour high-contrast palette for `draw_bbox_with_labels/3`.
58+
@default_palette ~w(
59+
#e6194b #3cb44b #4363d8 #f58231 #911eb4
60+
#42d4f4 #f032e6 #bfef45 #469990 #9a6324
61+
)
62+
5763
# Standard 80-class COCO labels in the order produced by the
5864
# HuggingFace RT-DETR `id2label` map. Baked at compile time so
5965
# the module works without `priv/` lookups.
@@ -155,8 +161,23 @@ if ImageVision.ortex_configured?() do
155161
156162
* `image` is the image upon which detection was run.
157163
158-
* `options` is a keyword list of options. Currently unused;
159-
accepted for forward compatibility.
164+
* `options` is a keyword list of options.
165+
166+
### Options
167+
168+
* `:opacity` is the opacity of the label background, a float in
169+
`[0.0, 1.0]`. The default is `0.85`. Use `1.0` for fully
170+
opaque label backgrounds.
171+
172+
* `:stroke_width` is the bounding box stroke width in pixels.
173+
The default is `2`.
174+
175+
* `:font_size` is the label text size in pixels. The default
176+
is `13`.
177+
178+
* `:palette` is a list of CSS colour strings used to assign
179+
colours to labels. Cycles if there are more labels than
180+
colours. The default is a 10-colour high-contrast palette.
160181
161182
### Returns
162183
@@ -174,14 +195,17 @@ if ImageVision.ortex_configured?() do
174195
175196
"""
176197
@spec draw_bbox_with_labels([detection()], Vimage.t(), Keyword.t()) :: Vimage.t()
177-
def draw_bbox_with_labels(detections, %Vimage{} = image, _options \\ []) do
198+
def draw_bbox_with_labels(detections, %Vimage{} = image, options \\ []) do
199+
opacity = Keyword.get(options, :opacity, 0.85)
200+
stroke_width = Keyword.get(options, :stroke_width, 2)
201+
font_size = Keyword.get(options, :font_size, 13)
202+
palette = Keyword.get(options, :palette, @default_palette)
203+
178204
width = Image.width(image)
179205
height = Image.height(image)
180206

181-
palette = ~w(
182-
#e6194b #3cb44b #4363d8 #f58231 #911eb4
183-
#42d4f4 #f032e6 #bfef45 #469990 #9a6324
184-
)
207+
label_height = font_size + 5
208+
text_baseline = font_size + 1
185209

186210
label_colors =
187211
detections
@@ -194,15 +218,16 @@ if ImageVision.ortex_configured?() do
194218
Enum.map(detections, fn %{label: label, score: score, box: {x, y, w, h}} ->
195219
color = Map.fetch!(label_colors, label)
196220
text = "#{label} #{Float.round(score * 100, 1)}%"
197-
label_y = max(0, y - 20)
198-
label_w = String.length(text) * 8 + 8
221+
label_y = max(0, y - label_height)
222+
label_w = round(String.length(text) * font_size * 0.55) + 8
199223

200224
"""
201225
<rect x="#{x}" y="#{y}" width="#{w}" height="#{h}"
202-
fill="none" stroke="#{color}" stroke-width="2"/>
203-
<rect x="#{x}" y="#{label_y}" width="#{label_w}" height="20" fill="#{color}"/>
204-
<text x="#{x + 4}" y="#{label_y + 14}"
205-
font-family="sans-serif" font-size="13" font-weight="bold" fill="white">#{text}</text>
226+
fill="none" stroke="#{color}" stroke-width="#{stroke_width}"/>
227+
<rect x="#{x}" y="#{label_y}" width="#{label_w}" height="#{label_height}"
228+
fill="#{color}" opacity="#{opacity}"/>
229+
<text x="#{x + 4}" y="#{label_y + text_baseline}"
230+
font-family="sans-serif" font-size="#{font_size}" font-weight="bold" fill="white">#{text}</text>
206231
"""
207232
end)
208233
|> Enum.join()

livebooks/image_vision.livemd

Lines changed: 4 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -81,51 +81,10 @@ detections = Image.Detection.detect(image)
8181
```
8282
8383
```elixir
84-
# Assign a consistent colour to each label so the overlay is easy to read.
85-
colours = [
86-
"#e6194b", "#3cb44b", "#4363d8", "#f58231", "#911eb4",
87-
"#42d4f4", "#f032e6", "#bfef45", "#fabed4", "#469990"
88-
]
89-
90-
label_colours =
91-
detections
92-
|> Enum.map(& &1.label)
93-
|> Enum.uniq()
94-
|> Enum.zip(Stream.cycle(colours))
95-
|> Map.new()
96-
97-
width = Image.width(image)
98-
height = Image.height(image)
99-
100-
# Build an SVG overlay: one rect + label per detection.
101-
boxes_svg =
102-
Enum.map(detections, fn %{label: label, score: score, box: {x, y, w, h}} ->
103-
colour = Map.get(label_colours, label, "#ffffff")
104-
pct = Float.round(score * 100, 1)
105-
text = "#{label} #{pct}%"
106-
# Label background sits above the box; clamp to image top.
107-
label_y = max(0, trunc(y) - 18)
108-
109-
"""
110-
<rect x="#{trunc(x)}" y="#{trunc(y)}" width="#{trunc(w)}" height="#{trunc(h)}"
111-
fill="none" stroke="#{colour}" stroke-width="2"/>
112-
<rect x="#{trunc(x)}" y="#{label_y}" width="#{String.length(text) * 7 + 6}" height="18"
113-
fill="#{colour}" opacity="0.85"/>
114-
<text x="#{trunc(x) + 3}" y="#{label_y + 13}"
115-
font-family="sans-serif" font-size="13" font-weight="bold" fill="white">#{text}</text>
116-
"""
117-
end)
118-
|> Enum.join("\n")
119-
120-
svg = """
121-
<svg xmlns="http://www.w3.org/2000/svg" width="#{width}" height="#{height}">
122-
#{boxes_svg}
123-
</svg>
124-
"""
125-
126-
{:ok, overlay} = Image.open(svg, access: :sequential)
127-
{:ok, result} = Image.compose(image, overlay)
128-
result
84+
# Draw bounding boxes and labels using the library helper.
85+
# See Image.Detection.draw_bbox_with_labels/3 for tunable options
86+
# (`:opacity`, `:stroke_width`, `:font_size`, `:palette`).
87+
Image.Detection.draw_bbox_with_labels(detections, image)
12988
```
13089
13190
```elixir

0 commit comments

Comments
 (0)