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