Web platform for sharing free image data for ML and research

Homepage: https://datasets.roundabout-host.com

By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 picture-annotation.py

View raw Download
text/x-script.python • 18.06 kiB
Python script, ASCII text executable
        
            
1
from pyscript import document
2
from pyodide.ffi import create_proxy
3
from pyodide.http import pyfetch
4
import asyncio
5
6
document.getElementById("shape-options").style.display = "flex"
7
8
image = document.getElementById("annotation-image")
9
zone = document.getElementById("annotation-zone")
10
confirm_button = document.getElementById("annotation-confirm")
11
cancel_button = document.getElementById("annotation-cancel")
12
backspace_button = document.getElementById("annotation-backspace")
13
delete_button = document.getElementById("annotation-delete")
14
previous_button = document.getElementById("annotation-previous")
15
next_button = document.getElementById("annotation-next")
16
17
object_list = document.getElementById("object-types")
18
19
confirm_button.style.display = "none"
20
cancel_button.style.display = "none"
21
backspace_button.style.display = "none"
22
delete_button.style.display = "none"
23
previous_button.style.display = "none"
24
next_button.style.display = "none"
25
shape_type = ""
26
bbox_pos = None
27
new_shape = None
28
selected_shape = None
29
30
31
async def get_all_objects():
32
response = await pyfetch("/api/object-types")
33
if response.ok:
34
return await response.json()
35
36
37
def follow_cursor(event):
38
rect = zone.getBoundingClientRect()
39
x = event.clientX - rect.left
40
y = event.clientY - rect.top
41
vertical_ruler.style.left = str(x) + "px"
42
horizontal_ruler.style.top = str(y) + "px"
43
44
45
def change_object_type(event):
46
global selected_shape
47
if selected_shape is None:
48
return
49
selected_shape.setAttribute("data-object-type", event.currentTarget.value)
50
51
52
change_object_type_proxy = create_proxy(change_object_type)
53
54
55
async def focus_shape(shape):
56
global selected_shape
57
58
if shape_type != "select":
59
return
60
if selected_shape is not None:
61
selected_shape.classList.remove("selected")
62
63
selected_shape = shape
64
65
selected_shape.classList.add("selected")
66
67
objects = await get_all_objects()
68
69
delete_button.style.display = "block"
70
next_button.style.display = "block"
71
previous_button.style.display = "block"
72
73
object_list.innerHTML = ""
74
75
new_radio = document.createElement("input")
76
new_radio.setAttribute("type", "radio")
77
new_radio.setAttribute("name", "object-type")
78
new_radio.setAttribute("value", "")
79
new_label = document.createElement("label")
80
new_label.appendChild(new_radio)
81
new_label.append("Undefined")
82
object_list.appendChild(new_label)
83
new_radio.addEventListener("change", change_object_type_proxy)
84
85
selected_object = selected_shape.getAttribute("data-object-type")
86
if not selected_object:
87
new_radio.setAttribute("checked", "")
88
89
for object, description in objects.items():
90
new_radio = document.createElement("input")
91
new_radio.setAttribute("type", "radio")
92
new_radio.setAttribute("name", "object-type")
93
new_radio.setAttribute("value", object)
94
if selected_object == object:
95
new_radio.setAttribute("checked", "")
96
new_label = document.createElement("label")
97
new_label.appendChild(new_radio)
98
new_label.append(object)
99
object_list.appendChild(new_label)
100
new_radio.addEventListener("change", change_object_type_proxy)
101
102
103
async def select_shape(event):
104
await focus_shape(event.target)
105
106
107
async def next_shape(event):
108
global selected_shape
109
if selected_shape is None:
110
return
111
112
selected_svg = selected_shape.parentNode
113
114
while selected_svg is not None:
115
next_sibling = selected_svg.nextElementSibling
116
if next_sibling and next_sibling.classList.contains("shape-container"):
117
selected_svg = next_sibling
118
break
119
elif next_sibling is None:
120
# If no more siblings, loop back to the first child
121
selected_svg = selected_svg.parentNode.firstElementChild
122
while selected_svg is not None and not selected_svg.classList.contains(
123
"shape-container"):
124
selected_svg = selected_svg.nextElementSibling
125
break
126
else:
127
selected_svg = next_sibling
128
129
if selected_svg:
130
shape = selected_svg.firstElementChild
131
await focus_shape(shape)
132
133
134
async def previous_shape(event):
135
global selected_shape
136
if selected_shape is None:
137
return
138
139
selected_svg = selected_shape.parentNode
140
141
while selected_svg is not None:
142
next_sibling = selected_svg.previousElementSibling
143
if next_sibling and next_sibling.classList.contains("shape-container"):
144
selected_svg = next_sibling
145
break
146
elif next_sibling is None:
147
# If no more siblings, loop back to the last child
148
selected_svg = selected_svg.parentNode.lastElementChild
149
while selected_svg is not None and not selected_svg.classList.contains(
150
"shape-container"):
151
selected_svg = selected_svg.previousElementSibling
152
break
153
else:
154
selected_svg = next_sibling
155
156
if selected_svg:
157
shape = selected_svg.firstElementChild
158
await focus_shape(shape)
159
160
161
def unselect_shape(event):
162
global selected_shape
163
164
if selected_shape is not None:
165
selected_shape.classList.remove("selected")
166
selected_shape = None
167
168
object_list.innerHTML = ""
169
delete_button.style.display = "none"
170
next_button.style.display = "none"
171
previous_button.style.display = "none"
172
173
174
def delete_shape(event):
175
global selected_shape
176
if selected_shape is None:
177
return
178
# Shape is SVG shape inside SVG so we need to remove the parent SVG
179
selected_shape.parentNode.remove()
180
selected_shape = None
181
object_list.innerHTML = ""
182
delete_button.style.display = "none"
183
next_button.style.display = "none"
184
previous_button.style.display = "none"
185
186
187
select_shape_proxy = create_proxy(select_shape)
188
unselect_shape_proxy = create_proxy(unselect_shape)
189
delete_shape_proxy = create_proxy(delete_shape)
190
next_shape_proxy = create_proxy(next_shape)
191
previous_shape_proxy = create_proxy(previous_shape)
192
193
delete_button.addEventListener("click", delete_shape_proxy)
194
next_button.addEventListener("click", next_shape_proxy)
195
previous_button.addEventListener("click", previous_shape_proxy)
196
197
# These are functions usable in JS
198
cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event))
199
make_bbox_proxy = create_proxy(lambda event: make_bbox(event))
200
make_polygon_proxy = create_proxy(lambda event: make_polygon(event))
201
follow_cursor_proxy = create_proxy(follow_cursor)
202
203
204
def switch_shape(event):
205
global shape_type
206
object_list.innerHTML = ""
207
unselect_shape(None)
208
shape = event.currentTarget.id
209
shape_type = shape
210
if shape_type == "select":
211
# Add event listeners to existing shapes
212
print(len(list(document.getElementsByClassName("shape"))), "shapes found")
213
for shape in document.getElementsByClassName("shape"):
214
print("Adding event listener to shape:", shape)
215
shape.addEventListener("click", select_shape_proxy)
216
image.addEventListener("click", unselect_shape_proxy)
217
helper_message.innerText = "Click on a shape to select"
218
# Cancel the current shape creation
219
match shape_type:
220
case "shape-bbox":
221
cancel_bbox(None)
222
case "shape-polygon":
223
cancel_polygon(None)
224
case "shape-polyline":
225
cancel_polygon(None)
226
else:
227
# Remove event listeners for selection
228
for shape in document.getElementsByClassName("shape"):
229
print("Removing event listener from shape:", shape)
230
shape.removeEventListener("click", select_shape_proxy)
231
image.removeEventListener("click", unselect_shape_proxy)
232
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
233
print("Shape is now of type:", shape)
234
235
236
vertical_ruler = document.getElementById("annotation-ruler-vertical")
237
horizontal_ruler = document.getElementById("annotation-ruler-horizontal")
238
vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary")
239
horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary")
240
helper_message = document.getElementById("annotation-helper-message")
241
242
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
243
244
245
def cancel_bbox(event):
246
global bbox_pos, new_shape
247
248
# Key must be ESCAPE
249
if event is not None and hasattr(event, "key") and event.key != "Escape":
250
return
251
252
if new_shape is not None and event is not None:
253
# Require event so the shape is kept when it ends normally
254
new_shape.remove()
255
zone.removeEventListener("click", make_bbox_proxy)
256
document.removeEventListener("keydown", cancel_bbox_proxy)
257
cancel_button.removeEventListener("click", cancel_bbox_proxy)
258
259
bbox_pos = None
260
vertical_ruler.style.display = "none"
261
horizontal_ruler.style.display = "none"
262
vertical_ruler_2.style.display = "none"
263
horizontal_ruler_2.style.display = "none"
264
zone.style.cursor = "auto"
265
cancel_button.style.display = "none"
266
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
267
new_shape = None
268
269
270
def make_bbox(event):
271
global new_shape, bbox_pos
272
zone_rect = zone.getBoundingClientRect()
273
274
if bbox_pos is None:
275
helper_message.innerText = "Now define the second point"
276
277
bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width,
278
(event.clientY - zone_rect.top) / zone_rect.height]
279
vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%"
280
horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%"
281
vertical_ruler_2.style.display = "block"
282
horizontal_ruler_2.style.display = "block"
283
284
else:
285
x0, y0 = bbox_pos.copy()
286
x1 = (event.clientX - zone_rect.left) / zone_rect.width
287
y1 = (event.clientY - zone_rect.top) / zone_rect.height
288
289
rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect")
290
291
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
292
new_shape.setAttribute("width", "100%")
293
new_shape.setAttribute("height", "100%")
294
zone_rect = zone.getBoundingClientRect()
295
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
296
new_shape.classList.add("shape-container")
297
298
new_shape.appendChild(rectangle)
299
zone.appendChild(new_shape)
300
301
minx = min(x0, x1)
302
miny = min(y0, y1)
303
maxx = max(x0, x1)
304
maxy = max(y0, y1)
305
306
rectangle.setAttribute("x", str(minx * image.naturalWidth))
307
rectangle.setAttribute("y", str(miny * image.naturalHeight))
308
rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth))
309
rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight))
310
rectangle.setAttribute("fill", "none")
311
rectangle.setAttribute("data-object-type", "")
312
rectangle.classList.add("shape-bbox")
313
rectangle.classList.add("shape")
314
315
# Add event listeners to the new shape
316
rectangle.addEventListener("click", select_shape_proxy)
317
318
cancel_bbox(None)
319
320
321
polygon_points = []
322
323
324
def make_polygon(event):
325
global new_shape, polygon_points
326
327
polygon = new_shape.children[0]
328
329
zone_rect = zone.getBoundingClientRect()
330
331
polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width,
332
(event.clientY - zone_rect.top) / zone_rect.height))
333
334
# Update the polygon
335
polygon.setAttribute("points", " ".join(
336
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
337
polygon_points]))
338
339
340
def reset_polygon():
341
global new_shape, polygon_points
342
343
zone.removeEventListener("click", make_polygon_proxy)
344
document.removeEventListener("keydown", close_polygon_proxy)
345
document.removeEventListener("keydown", cancel_polygon_proxy)
346
document.removeEventListener("keydown", backspace_polygon_proxy)
347
confirm_button.style.display = "none"
348
cancel_button.style.display = "none"
349
backspace_button.style.display = "none"
350
confirm_button.removeEventListener("click", close_polygon_proxy)
351
cancel_button.removeEventListener("click", cancel_polygon_proxy)
352
backspace_button.removeEventListener("click", backspace_polygon_proxy)
353
polygon_points.clear()
354
355
zone.style.cursor = "auto"
356
new_shape = None
357
358
359
def close_polygon(event):
360
if event is not None and hasattr(event, "key") and event.key != "Enter":
361
return
362
# Polygon is already there, but we need to remove the events
363
reset_polygon()
364
365
366
def cancel_polygon(event):
367
if event is not None and hasattr(event, "key") and event.key != "Escape":
368
return
369
# Delete the polygon
370
new_shape.remove()
371
reset_polygon()
372
373
374
def backspace_polygon(event):
375
if event is not None and hasattr(event, "key") and event.key != "Backspace":
376
return
377
if not polygon_points:
378
return
379
polygon_points.pop()
380
polygon = new_shape.children[0]
381
polygon.setAttribute("points", " ".join(
382
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
383
polygon_points]))
384
385
386
close_polygon_proxy = create_proxy(close_polygon)
387
cancel_polygon_proxy = create_proxy(cancel_polygon)
388
backspace_polygon_proxy = create_proxy(backspace_polygon)
389
390
391
def open_shape(event):
392
global new_shape, bbox_pos
393
if bbox_pos or shape_type == "select":
394
return
395
print("Creating a new shape of type:", shape_type)
396
397
match shape_type:
398
case "shape-bbox":
399
helper_message.innerText = ("Define the first point at the intersection of the lines "
400
"by clicking on the image, or click the cross to cancel")
401
402
cancel_button.addEventListener("click", cancel_bbox_proxy)
403
document.addEventListener("keydown", cancel_bbox_proxy)
404
cancel_button.style.display = "block"
405
bbox_pos = None
406
zone.addEventListener("click", make_bbox_proxy)
407
vertical_ruler.style.display = "block"
408
horizontal_ruler.style.display = "block"
409
zone.style.cursor = "crosshair"
410
case "shape-polygon" | "shape-polyline":
411
match shape_type:
412
case "shape-polygon":
413
helper_message.innerText = ("Click on the image to define the points of the polygon, "
414
"press escape to cancel, enter to close, or backspace to "
415
"remove the last point")
416
case "shape-polyline":
417
helper_message.innerText = ("Click on the image to define the points of the polyline, "
418
"press escape to cancel, enter to finish, or backspace to "
419
"remove the last point")
420
421
if not polygon_points and not new_shape:
422
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
423
new_shape.setAttribute("width", "100%")
424
new_shape.setAttribute("height", "100%")
425
zone_rect = zone.getBoundingClientRect()
426
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
427
new_shape.classList.add("shape-container")
428
429
if not polygon_points and int(len(new_shape.children)) == 0:
430
zone.addEventListener("click", make_polygon_proxy)
431
document.addEventListener("keydown", close_polygon_proxy)
432
document.addEventListener("keydown", cancel_polygon_proxy)
433
document.addEventListener("keydown", backspace_polygon_proxy)
434
cancel_button.addEventListener("click", cancel_polygon_proxy)
435
cancel_button.style.display = "block"
436
confirm_button.addEventListener("click", close_polygon_proxy)
437
confirm_button.style.display = "block"
438
backspace_button.addEventListener("click", backspace_polygon_proxy)
439
backspace_button.style.display = "block"
440
match shape_type:
441
case "shape-polygon":
442
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
443
polygon.classList.add("shape-polygon")
444
case "shape-polyline":
445
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polyline")
446
polygon.classList.add("shape-polyline")
447
polygon.setAttribute("fill", "none")
448
polygon.setAttribute("data-object-type", "")
449
polygon.classList.add("shape")
450
new_shape.appendChild(polygon)
451
zone.appendChild(new_shape)
452
zone.style.cursor = "crosshair"
453
case "shape-point":
454
point = document.createElementNS("http://www.w3.org/2000/svg", "circle")
455
zone_rect = zone.getBoundingClientRect()
456
point.setAttribute("cx", str((event.clientX - zone_rect.left) / zone_rect.width * image.naturalWidth))
457
point.setAttribute("cy", str((event.clientY - zone_rect.top) / zone_rect.height * image.naturalHeight))
458
point.setAttribute("r", "0")
459
point.classList.add("shape-point")
460
point.classList.add("shape")
461
point.setAttribute("data-object-type", "")
462
463
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
464
new_shape.setAttribute("width", "100%")
465
new_shape.setAttribute("height", "100%")
466
zone_rect = zone.getBoundingClientRect()
467
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
468
new_shape.classList.add("shape-container")
469
470
new_shape.appendChild(point)
471
zone.appendChild(new_shape)
472
473
new_shape = None
474
475
476
477
for button in list(document.getElementById("shape-selector").children):
478
button.addEventListener("click", create_proxy(switch_shape))
479
print("Shape", button.id, "is available")
480
481
zone.addEventListener("mousemove", follow_cursor_proxy)
482
zone.addEventListener("click", create_proxy(open_shape))
483