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 • 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