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