Web platform for sharing free data for ML and research

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