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 • 21.88 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
176
177
async def focus_shape(shape):
178
global selected_shape
179
180
if shape_type != "select":
181
return
182
if selected_shape is not None:
183
selected_shape.classList.remove("selected")
184
185
selected_shape = shape
186
187
selected_shape.classList.add("selected")
188
189
objects = await get_all_objects()
190
191
delete_button.style.display = "block"
192
next_button.style.display = "block"
193
previous_button.style.display = "block"
194
195
object_list.innerHTML = ""
196
197
new_radio = document.createElement("input")
198
new_radio.setAttribute("type", "radio")
199
new_radio.setAttribute("name", "object-type")
200
new_radio.setAttribute("value", "")
201
new_label = document.createElement("label")
202
new_label.appendChild(new_radio)
203
new_label.append("Undefined")
204
object_list.appendChild(new_label)
205
new_radio.addEventListener("change", change_object_type_proxy)
206
207
selected_object = selected_shape.getAttribute("data-object-type")
208
if not selected_object:
209
new_radio.setAttribute("checked", "")
210
211
for object, description in objects.items():
212
new_radio = document.createElement("input")
213
new_radio.setAttribute("type", "radio")
214
new_radio.setAttribute("name", "object-type")
215
new_radio.setAttribute("value", object)
216
if selected_object == object:
217
new_radio.setAttribute("checked", "")
218
new_label = document.createElement("label")
219
new_label.appendChild(new_radio)
220
new_label.append(object)
221
object_list.appendChild(new_label)
222
new_radio.addEventListener("change", change_object_type_proxy)
223
224
225
async def select_shape(event):
226
await focus_shape(event.target)
227
228
229
async def next_shape(event):
230
global selected_shape
231
if selected_shape is None:
232
return
233
234
selected_svg = selected_shape.parentNode
235
236
while selected_svg is not None:
237
next_sibling = selected_svg.nextElementSibling
238
if next_sibling and next_sibling.classList.contains("shape-container"):
239
selected_svg = next_sibling
240
break
241
elif next_sibling is None:
242
# If no more siblings, loop back to the first child
243
selected_svg = selected_svg.parentNode.firstElementChild
244
while selected_svg is not None and not selected_svg.classList.contains(
245
"shape-container"):
246
selected_svg = selected_svg.nextElementSibling
247
break
248
else:
249
selected_svg = next_sibling
250
251
if selected_svg:
252
shape = selected_svg.firstElementChild
253
await focus_shape(shape)
254
255
256
async def previous_shape(event):
257
global selected_shape
258
if selected_shape is None:
259
return
260
261
selected_svg = selected_shape.parentNode
262
263
while selected_svg is not None:
264
next_sibling = selected_svg.previousElementSibling
265
if next_sibling and next_sibling.classList.contains("shape-container"):
266
selected_svg = next_sibling
267
break
268
elif next_sibling is None:
269
# If no more siblings, loop back to the last child
270
selected_svg = selected_svg.parentNode.lastElementChild
271
while selected_svg is not None and not selected_svg.classList.contains(
272
"shape-container"):
273
selected_svg = selected_svg.previousElementSibling
274
break
275
else:
276
selected_svg = next_sibling
277
278
if selected_svg:
279
shape = selected_svg.firstElementChild
280
await focus_shape(shape)
281
282
283
def unselect_shape(event):
284
global selected_shape
285
286
if selected_shape is not None:
287
selected_shape.classList.remove("selected")
288
selected_shape = None
289
290
object_list.innerHTML = ""
291
delete_button.style.display = "none"
292
next_button.style.display = "none"
293
previous_button.style.display = "none"
294
295
296
def delete_shape(event):
297
global selected_shape
298
if selected_shape is None:
299
return
300
# Shape is SVG shape inside SVG so we need to remove the parent SVG
301
selected_shape.parentNode.remove()
302
selected_shape = None
303
object_list.innerHTML = ""
304
delete_button.style.display = "none"
305
next_button.style.display = "none"
306
previous_button.style.display = "none"
307
308
309
select_shape_proxy = create_proxy(select_shape)
310
unselect_shape_proxy = create_proxy(unselect_shape)
311
delete_shape_proxy = create_proxy(delete_shape)
312
next_shape_proxy = create_proxy(next_shape)
313
previous_shape_proxy = create_proxy(previous_shape)
314
315
delete_button.addEventListener("click", delete_shape_proxy)
316
next_button.addEventListener("click", next_shape_proxy)
317
previous_button.addEventListener("click", previous_shape_proxy)
318
319
# These are functions usable in JS
320
cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event))
321
make_bbox_proxy = create_proxy(lambda event: make_bbox(event))
322
make_polygon_proxy = create_proxy(lambda event: make_polygon(event))
323
follow_cursor_proxy = create_proxy(follow_cursor)
324
325
326
def switch_shape(event):
327
global shape_type
328
object_list.innerHTML = ""
329
unselect_shape(None)
330
shape = event.currentTarget.id
331
shape_type = shape
332
if shape_type == "select":
333
# Add event listeners to existing shapes
334
print(len(list(document.getElementsByClassName("shape"))), "shapes found")
335
for shape in document.getElementsByClassName("shape"):
336
print("Adding event listener to shape:", shape)
337
shape.addEventListener("click", select_shape_proxy)
338
image.addEventListener("click", unselect_shape_proxy)
339
helper_message.innerText = "Click on a shape to select"
340
# Cancel the current shape creation
341
if shape_type == "shape-bbox":
342
cancel_bbox(None)
343
elif shape_type == "shape-polygon":
344
cancel_polygon(None)
345
elif shape_type == "shape-polyline":
346
cancel_polygon(None)
347
else:
348
# Remove event listeners for selection
349
for shape in document.getElementsByClassName("shape"):
350
print("Removing event listener from shape:", shape)
351
shape.removeEventListener("click", select_shape_proxy)
352
image.removeEventListener("click", unselect_shape_proxy)
353
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
354
print("Shape is now of type:", shape)
355
356
357
vertical_ruler = document.getElementById("annotation-ruler-vertical")
358
horizontal_ruler = document.getElementById("annotation-ruler-horizontal")
359
vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary")
360
horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary")
361
helper_message = document.getElementById("annotation-helper-message")
362
363
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
364
365
366
def cancel_bbox(event):
367
global bbox_pos, new_shape
368
369
# Key must be ESCAPE
370
if event is not None and hasattr(event, "key") and event.key != "Escape":
371
return
372
373
if new_shape is not None and event is not None:
374
# Require event so the shape is kept when it ends normally
375
new_shape.remove()
376
zone.removeEventListener("click", make_bbox_proxy)
377
document.removeEventListener("keydown", cancel_bbox_proxy)
378
cancel_button.removeEventListener("click", cancel_bbox_proxy)
379
380
bbox_pos = None
381
vertical_ruler.style.display = "none"
382
horizontal_ruler.style.display = "none"
383
vertical_ruler_2.style.display = "none"
384
horizontal_ruler_2.style.display = "none"
385
zone.style.cursor = "auto"
386
cancel_button.style.display = "none"
387
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
388
new_shape = None
389
390
391
def make_bbox(event):
392
global new_shape, bbox_pos
393
zone_rect = zone.getBoundingClientRect()
394
395
if bbox_pos is None:
396
helper_message.innerText = "Now define the second point"
397
398
bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width,
399
(event.clientY - zone_rect.top) / zone_rect.height]
400
vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%"
401
horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%"
402
vertical_ruler_2.style.display = "block"
403
horizontal_ruler_2.style.display = "block"
404
405
else:
406
x0, y0 = bbox_pos.copy()
407
x1 = (event.clientX - zone_rect.left) / zone_rect.width
408
y1 = (event.clientY - zone_rect.top) / zone_rect.height
409
410
rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect")
411
412
new_shape = make_shape_container()
413
zone_rect = zone.getBoundingClientRect()
414
415
new_shape.appendChild(rectangle)
416
zone.appendChild(new_shape)
417
418
minx = min(x0, x1)
419
miny = min(y0, y1)
420
maxx = max(x0, x1)
421
maxy = max(y0, y1)
422
423
rectangle.setAttribute("x", str(minx * image.naturalWidth))
424
rectangle.setAttribute("y", str(miny * image.naturalHeight))
425
rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth))
426
rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight))
427
rectangle.setAttribute("fill", "none")
428
rectangle.setAttribute("data-object-type", "")
429
rectangle.classList.add("shape-bbox")
430
rectangle.classList.add("shape")
431
432
# Add event listeners to the new shape
433
rectangle.addEventListener("click", select_shape_proxy)
434
435
cancel_bbox(None)
436
437
438
polygon_points = []
439
440
441
def make_polygon(event):
442
global new_shape, polygon_points
443
444
polygon = new_shape.children[0]
445
446
zone_rect = zone.getBoundingClientRect()
447
448
polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width,
449
(event.clientY - zone_rect.top) / zone_rect.height))
450
451
# Update the polygon
452
polygon.setAttribute("points", " ".join(
453
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
454
polygon_points]))
455
456
457
def reset_polygon():
458
global new_shape, polygon_points
459
460
zone.removeEventListener("click", make_polygon_proxy)
461
document.removeEventListener("keydown", close_polygon_proxy)
462
document.removeEventListener("keydown", cancel_polygon_proxy)
463
document.removeEventListener("keydown", backspace_polygon_proxy)
464
confirm_button.style.display = "none"
465
cancel_button.style.display = "none"
466
backspace_button.style.display = "none"
467
confirm_button.removeEventListener("click", close_polygon_proxy)
468
cancel_button.removeEventListener("click", cancel_polygon_proxy)
469
backspace_button.removeEventListener("click", backspace_polygon_proxy)
470
polygon_points.clear()
471
472
zone.style.cursor = "auto"
473
new_shape = None
474
475
476
def close_polygon(event):
477
if event is not None and hasattr(event, "key") and event.key != "Enter":
478
return
479
# Polygon is already there, but we need to remove the events
480
reset_polygon()
481
482
483
def cancel_polygon(event):
484
if event is not None and hasattr(event, "key") and event.key != "Escape":
485
return
486
# Delete the polygon
487
new_shape.remove()
488
reset_polygon()
489
490
491
def backspace_polygon(event):
492
if event is not None and hasattr(event, "key") and event.key != "Backspace":
493
return
494
if not polygon_points:
495
return
496
polygon_points.pop()
497
polygon = new_shape.children[0]
498
polygon.setAttribute("points", " ".join(
499
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
500
polygon_points]))
501
502
503
close_polygon_proxy = create_proxy(close_polygon)
504
cancel_polygon_proxy = create_proxy(cancel_polygon)
505
backspace_polygon_proxy = create_proxy(backspace_polygon)
506
507
508
def open_shape(event):
509
global new_shape, bbox_pos
510
if bbox_pos or shape_type == "select":
511
return
512
print("Creating a new shape of type:", shape_type)
513
514
if shape_type == "shape-bbox":
515
helper_message.innerText = ("Define the first point at the intersection of the lines "
516
"by clicking on the image, or click the cross to cancel")
517
518
cancel_button.addEventListener("click", cancel_bbox_proxy)
519
document.addEventListener("keydown", cancel_bbox_proxy)
520
cancel_button.style.display = "block"
521
bbox_pos = None
522
zone.addEventListener("click", make_bbox_proxy)
523
vertical_ruler.style.display = "block"
524
horizontal_ruler.style.display = "block"
525
zone.style.cursor = "crosshair"
526
elif shape_type == "shape-polygon" or shape_type == "shape-polyline":
527
if shape_type == "shape-polygon":
528
helper_message.innerText = ("Click on the image to define the points of the polygon, "
529
"press escape to cancel, enter to close, or backspace to "
530
"remove the last point")
531
elif shape_type == "shape-polyline":
532
helper_message.innerText = ("Click on the image to define the points of the polyline, "
533
"press escape to cancel, enter to finish, or backspace to "
534
"remove the last point")
535
536
if not polygon_points and not new_shape:
537
new_shape = make_shape_container()
538
zone_rect = zone.getBoundingClientRect()
539
540
if not polygon_points and int(new_shape.children.length) == 0:
541
zone.addEventListener("click", make_polygon_proxy)
542
document.addEventListener("keydown", close_polygon_proxy)
543
document.addEventListener("keydown", cancel_polygon_proxy)
544
document.addEventListener("keydown", backspace_polygon_proxy)
545
cancel_button.addEventListener("click", cancel_polygon_proxy)
546
cancel_button.style.display = "block"
547
confirm_button.addEventListener("click", close_polygon_proxy)
548
confirm_button.style.display = "block"
549
backspace_button.addEventListener("click", backspace_polygon_proxy)
550
backspace_button.style.display = "block"
551
if shape_type == "shape-polygon":
552
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
553
polygon.classList.add("shape-polygon")
554
elif shape_type == "shape-polyline":
555
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polyline")
556
polygon.classList.add("shape-polyline")
557
polygon.setAttribute("fill", "none")
558
polygon.setAttribute("data-object-type", "")
559
polygon.classList.add("shape")
560
new_shape.appendChild(polygon)
561
zone.appendChild(new_shape)
562
zone.style.cursor = "crosshair"
563
elif shape_type == "shape-point":
564
point = document.createElementNS("http://www.w3.org/2000/svg", "circle")
565
zone_rect = zone.getBoundingClientRect()
566
point.setAttribute("cx", str((event.clientX - zone_rect.left) / zone_rect.width * image.naturalWidth))
567
point.setAttribute("cy", str((event.clientY - zone_rect.top) / zone_rect.height * image.naturalHeight))
568
point.setAttribute("r", "0")
569
point.classList.add("shape-point")
570
point.classList.add("shape")
571
point.setAttribute("data-object-type", "")
572
573
new_shape = make_shape_container()
574
zone_rect = zone.getBoundingClientRect()
575
576
new_shape.appendChild(point)
577
zone.appendChild(new_shape)
578
579
new_shape = None
580
581
582
583
for button in list(document.getElementById("shape-selector").children):
584
button.addEventListener("click", create_proxy(switch_shape))
585
print("Shape", button.id, "is available")
586
587
zone.addEventListener("mousemove", follow_cursor_proxy)
588
zone.addEventListener("click", create_proxy(open_shape))
589
590
# Load existing annotations, if any
591
put_shapes(await load_shapes())
592
print("Ready!")
593