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