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 • 26.25 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
object_list_content = document.getElementById("object-types-content")
20
object_list_filter = document.getElementById("filter-object-types")
21
22
confirm_button.style.display = "none"
23
cancel_button.style.display = "none"
24
backspace_button.style.display = "none"
25
delete_button.style.display = "none"
26
previous_button.style.display = "none"
27
next_button.style.display = "none"
28
shape_type = ""
29
bbox_pos = None
30
new_shape = None
31
selected_shape = None
32
33
34
def make_shape_container():
35
shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
36
shape.setAttribute("width", "100%")
37
shape.setAttribute("height", "100%")
38
shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
39
shape.classList.add("shape-container")
40
41
return shape
42
43
44
async def get_all_objects():
45
response = await pyfetch("/api/object-types")
46
if response.ok:
47
return await response.json()
48
49
50
def follow_cursor(event):
51
rect = zone.getBoundingClientRect()
52
x = event.clientX - rect.left
53
y = event.clientY - rect.top
54
vertical_ruler.style.left = str(x) + "px"
55
horizontal_ruler.style.top = str(y) + "px"
56
57
58
def change_object_type(event):
59
global selected_shape
60
if selected_shape is None:
61
return
62
selected_shape.setAttribute("data-object-type", event.currentTarget.value)
63
64
65
change_object_type_proxy = create_proxy(change_object_type)
66
67
68
def list_shapes():
69
shapes = list(zone.getElementsByClassName("shape"))
70
json_shapes = []
71
for shape in shapes:
72
shape_dict = {}
73
if shape.tagName == "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
elif shape.tagName == "polygon" or shape.tagName == "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
elif shape.tagName == "circle" and 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
else:
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
delete_shape_key_proxy = create_proxy(lambda event: event.key == "Delete" and delete_shape_proxy(None))
178
next_shape_key_proxy = create_proxy(lambda event: event.key == "ArrowRight" and next_shape_proxy(None))
179
previous_shape_key_proxy = create_proxy(lambda event: event.key == "ArrowLeft" and previous_shape_proxy(None))
180
181
182
def get_centre(shape):
183
if shape.tagName == "rect":
184
x = float(shape.getAttribute("x")) + float(shape.getAttribute("width")) / 2
185
y = float(shape.getAttribute("y")) + float(shape.getAttribute("height")) / 2
186
elif shape.tagName == "polygon":
187
points = shape.getAttribute("points").split(" ")
188
# Average of the extreme points
189
sorted_x = sorted([float(point.split(",")[0]) for point in points])
190
sorted_y = sorted([float(point.split(",")[1]) for point in points])
191
top = sorted_y[0]
192
bottom = sorted_y[-1]
193
left = sorted_x[0]
194
right = sorted_x[-1]
195
196
x = (left + right) / 2
197
y = (top + bottom) / 2
198
elif shape.tagName == "polyline":
199
points = shape.getAttribute("points").split(" ")
200
# Median point
201
x = float(points[len(points) // 2].split(",")[0])
202
y = float(points[len(points) // 2].split(",")[1])
203
elif shape.tagName == "circle" and shape.classList.contains("shape-point"):
204
x = float(shape.getAttribute("cx"))
205
y = float(shape.getAttribute("cy"))
206
else:
207
return None
208
209
return x, y
210
211
212
async def update_object_list_filter(event):
213
filter_text = event.currentTarget.value
214
for label in object_list_content.children:
215
if label.innerText.lower().find(filter_text.lower()) == -1:
216
label.style.display = "none"
217
else:
218
label.style.display = "flex"
219
220
221
async def focus_shape(shape):
222
global selected_shape
223
224
if shape_type != "select":
225
return
226
if selected_shape is not None:
227
selected_shape.classList.remove("selected")
228
229
selected_shape = shape
230
231
selected_shape.classList.add("selected")
232
233
objects = await get_all_objects()
234
235
delete_button.style.display = "flex"
236
next_button.style.display = "flex"
237
previous_button.style.display = "flex"
238
document.addEventListener("keydown", delete_shape_key_proxy)
239
document.addEventListener("keydown", next_shape_key_proxy)
240
document.addEventListener("keydown", previous_shape_key_proxy)
241
242
object_list_content.innerHTML = ""
243
object_list_filter.value = ""
244
new_radio = document.createElement("input")
245
new_radio.setAttribute("type", "radio")
246
new_radio.setAttribute("name", "object-type")
247
new_radio.setAttribute("tabindex", "1") # Focus first
248
new_radio.setAttribute("value", "")
249
new_label = document.createElement("label")
250
new_label.appendChild(new_radio)
251
new_label.append("Undefined")
252
object_list_content.appendChild(new_label)
253
new_radio.addEventListener("change", change_object_type_proxy)
254
255
selected_object = selected_shape.getAttribute("data-object-type")
256
if not selected_object:
257
new_radio.setAttribute("checked", "")
258
259
for object, description in objects.items():
260
new_radio = document.createElement("input")
261
new_radio.setAttribute("type", "radio")
262
new_radio.setAttribute("name", "object-type")
263
new_radio.setAttribute("value", object)
264
if selected_object == object:
265
new_radio.setAttribute("checked", "")
266
new_label = document.createElement("label")
267
new_label.appendChild(new_radio)
268
new_label.append(object)
269
object_list_content.appendChild(new_label)
270
new_radio.addEventListener("change", change_object_type_proxy)
271
272
object_list.style.display = "flex"
273
object_list_filter.focus()
274
object_list_filter.addEventListener("input", update_object_list_filter)
275
object_list.style.left = str(get_centre(shape)[0] / image.naturalWidth * 100) + "%"
276
object_list.style.top = str(get_centre(shape)[1] / image.naturalHeight * 100) + "%"
277
object_list.style.right = "auto"
278
object_list.style.bottom = "auto"
279
object_list.style.maxHeight = str(image.height - get_centre(shape)[1] / image.naturalHeight * image.height) + "px"
280
object_list.style.maxWidth = str(image.width - get_centre(shape)[0] / image.naturalWidth * image.width) + "px"
281
282
if get_centre(shape)[0] / image.naturalWidth * image.width + object_list.offsetWidth > image.width:
283
object_list.style.left = "auto"
284
object_list.style.right = str((image.naturalWidth - get_centre(shape)[0]) / image.naturalWidth * 100) + "%"
285
object_list.style.maxWidth = str(get_centre(shape)[0] / image.naturalWidth * image.width) + "px"
286
287
if get_centre(shape)[1] / image.naturalHeight * image.height + object_list.offsetHeight > image.height:
288
object_list.style.top = "auto"
289
object_list.style.bottom = str((image.naturalHeight - get_centre(shape)[1]) / image.naturalHeight * 100) + "%"
290
object_list.style.maxHeight = str(get_centre(shape)[1] / image.naturalHeight * image.height) + "px"
291
292
293
async def select_shape(event):
294
await focus_shape(event.target)
295
296
297
async def next_shape(event):
298
global selected_shape
299
if selected_shape is None:
300
return
301
302
selected_svg = selected_shape.parentNode
303
304
while selected_svg is not None:
305
next_sibling = selected_svg.nextElementSibling
306
if next_sibling and next_sibling.classList.contains("shape-container"):
307
selected_svg = next_sibling
308
break
309
elif next_sibling is None:
310
# If no more siblings, loop back to the first child
311
selected_svg = selected_svg.parentNode.firstElementChild
312
while selected_svg is not None and not selected_svg.classList.contains(
313
"shape-container"):
314
selected_svg = selected_svg.nextElementSibling
315
break
316
else:
317
selected_svg = next_sibling
318
319
if selected_svg:
320
shape = selected_svg.firstElementChild
321
await focus_shape(shape)
322
323
event.preventDefault()
324
325
326
async def previous_shape(event):
327
global selected_shape
328
if selected_shape is None:
329
return
330
331
selected_svg = selected_shape.parentNode
332
333
while selected_svg is not None:
334
next_sibling = selected_svg.previousElementSibling
335
if next_sibling and next_sibling.classList.contains("shape-container"):
336
selected_svg = next_sibling
337
break
338
elif next_sibling is None:
339
# If no more siblings, loop back to the last child
340
selected_svg = selected_svg.parentNode.lastElementChild
341
while selected_svg is not None and not selected_svg.classList.contains(
342
"shape-container"):
343
selected_svg = selected_svg.previousElementSibling
344
break
345
else:
346
selected_svg = next_sibling
347
348
if selected_svg:
349
shape = selected_svg.firstElementChild
350
await focus_shape(shape)
351
352
event.preventDefault()
353
354
355
def unselect_shape(event):
356
global selected_shape
357
358
if selected_shape is not None:
359
selected_shape.classList.remove("selected")
360
selected_shape = None
361
362
object_list_content.innerHTML = ""
363
object_list.style.display = "none"
364
delete_button.style.display = "none"
365
next_button.style.display = "none"
366
previous_button.style.display = "none"
367
document.removeEventListener("keydown", delete_shape_key_proxy)
368
document.removeEventListener("keydown", next_shape_key_proxy)
369
document.removeEventListener("keydown", previous_shape_key_proxy)
370
371
372
def delete_shape(event):
373
global selected_shape
374
if selected_shape is None:
375
return
376
# Shape is SVG shape inside SVG so we need to remove the parent SVG
377
selected_shape.parentNode.remove()
378
selected_shape = None
379
object_list_content.innerHTML = ""
380
object_list.style.display = "none"
381
delete_button.style.display = "none"
382
next_button.style.display = "none"
383
previous_button.style.display = "none"
384
document.removeEventListener("keydown", delete_shape_key_proxy)
385
document.removeEventListener("keydown", next_shape_key_proxy)
386
document.removeEventListener("keydown", previous_shape_key_proxy)
387
388
389
select_shape_proxy = create_proxy(select_shape)
390
unselect_shape_proxy = create_proxy(unselect_shape)
391
delete_shape_proxy = create_proxy(delete_shape)
392
next_shape_proxy = create_proxy(next_shape)
393
previous_shape_proxy = create_proxy(previous_shape)
394
395
delete_button.addEventListener("click", delete_shape_proxy)
396
next_button.addEventListener("click", next_shape_proxy)
397
previous_button.addEventListener("click", previous_shape_proxy)
398
399
# These are functions usable in JS
400
cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event))
401
make_bbox_proxy = create_proxy(lambda event: make_bbox(event))
402
make_polygon_proxy = create_proxy(lambda event: make_polygon(event))
403
follow_cursor_proxy = create_proxy(follow_cursor)
404
405
406
def switch_shape(event):
407
global shape_type
408
object_list_content.innerHTML = ""
409
unselect_shape(None)
410
shape = event.currentTarget.id
411
shape_type = shape
412
for button in list(document.getElementById("shape-selector").children):
413
if not button.classList.contains("button-flat"):
414
button.classList.add("button-flat")
415
event.currentTarget.classList.remove("button-flat")
416
if shape_type == "select":
417
# Add event listeners to existing shapes
418
print(len(list(document.getElementsByClassName("shape"))), "shapes found")
419
for shape in document.getElementsByClassName("shape"):
420
print("Adding event listener to shape:", shape)
421
shape.addEventListener("click", select_shape_proxy)
422
image.addEventListener("click", unselect_shape_proxy)
423
helper_message.innerText = "Click on a shape to select"
424
# Cancel the current shape creation
425
if shape_type == "shape-bbox":
426
cancel_bbox(None)
427
elif shape_type == "shape-polygon":
428
cancel_polygon(None)
429
elif shape_type == "shape-polyline":
430
cancel_polygon(None)
431
else:
432
# Remove event listeners for selection
433
for shape in document.getElementsByClassName("shape"):
434
print("Removing event listener from shape:", shape)
435
shape.removeEventListener("click", select_shape_proxy)
436
image.removeEventListener("click", unselect_shape_proxy)
437
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
438
print("Shape is now of type:", shape)
439
440
441
def select_mode():
442
global shape_type
443
document.getElementById("select").click()
444
445
446
vertical_ruler = document.getElementById("annotation-ruler-vertical")
447
horizontal_ruler = document.getElementById("annotation-ruler-horizontal")
448
vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary")
449
horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary")
450
helper_message = document.getElementById("annotation-helper-message")
451
452
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
453
454
455
def cancel_bbox(event):
456
global bbox_pos, new_shape
457
458
# Key must be ESCAPE
459
if event is not None and hasattr(event, "key") and event.key != "Escape":
460
return
461
462
if new_shape is not None and event is not None:
463
# Require event so the shape is kept when it ends normally
464
new_shape.remove()
465
zone.removeEventListener("click", make_bbox_proxy)
466
document.removeEventListener("keydown", cancel_bbox_proxy)
467
cancel_button.removeEventListener("click", cancel_bbox_proxy)
468
469
bbox_pos = None
470
vertical_ruler.style.display = "none"
471
horizontal_ruler.style.display = "none"
472
vertical_ruler_2.style.display = "none"
473
horizontal_ruler_2.style.display = "none"
474
zone.style.cursor = "auto"
475
cancel_button.style.display = "none"
476
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
477
new_shape = None
478
479
480
def make_bbox(event):
481
global new_shape, bbox_pos
482
zone_rect = zone.getBoundingClientRect()
483
484
if bbox_pos is None:
485
helper_message.innerText = "Now define the second point"
486
487
bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width,
488
(event.clientY - zone_rect.top) / zone_rect.height]
489
vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%"
490
horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%"
491
vertical_ruler_2.style.display = "block"
492
horizontal_ruler_2.style.display = "block"
493
494
else:
495
x0, y0 = bbox_pos.copy()
496
x1 = (event.clientX - zone_rect.left) / zone_rect.width
497
y1 = (event.clientY - zone_rect.top) / zone_rect.height
498
499
rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect")
500
501
new_shape = make_shape_container()
502
zone_rect = zone.getBoundingClientRect()
503
504
new_shape.appendChild(rectangle)
505
zone.appendChild(new_shape)
506
507
minx = min(x0, x1)
508
miny = min(y0, y1)
509
maxx = max(x0, x1)
510
maxy = max(y0, y1)
511
512
rectangle.setAttribute("x", str(minx * image.naturalWidth))
513
rectangle.setAttribute("y", str(miny * image.naturalHeight))
514
rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth))
515
rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight))
516
rectangle.setAttribute("fill", "none")
517
rectangle.setAttribute("data-object-type", "")
518
rectangle.classList.add("shape-bbox")
519
rectangle.classList.add("shape")
520
521
# Add event listeners to the new shape
522
rectangle.addEventListener("click", select_shape_proxy)
523
524
cancel_bbox(None)
525
526
527
polygon_points = []
528
529
530
def make_polygon(event):
531
global new_shape, polygon_points
532
533
polygon = new_shape.children[0]
534
535
zone_rect = zone.getBoundingClientRect()
536
537
polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width,
538
(event.clientY - zone_rect.top) / zone_rect.height))
539
540
# Update the polygon
541
polygon.setAttribute("points", " ".join(
542
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
543
polygon_points]))
544
545
546
def reset_polygon():
547
global new_shape, polygon_points
548
549
zone.removeEventListener("click", make_polygon_proxy)
550
document.removeEventListener("keydown", close_polygon_proxy)
551
document.removeEventListener("keydown", cancel_polygon_proxy)
552
document.removeEventListener("keydown", backspace_polygon_proxy)
553
confirm_button.style.display = "none"
554
cancel_button.style.display = "none"
555
backspace_button.style.display = "none"
556
confirm_button.removeEventListener("click", close_polygon_proxy)
557
cancel_button.removeEventListener("click", cancel_polygon_proxy)
558
backspace_button.removeEventListener("click", backspace_polygon_proxy)
559
polygon_points.clear()
560
561
zone.style.cursor = "auto"
562
new_shape = None
563
564
565
def close_polygon(event):
566
if event is not None and hasattr(event, "key") and event.key != "Enter":
567
return
568
# Polygon is already there, but we need to remove the events
569
reset_polygon()
570
571
572
def cancel_polygon(event):
573
if event is not None and hasattr(event, "key") and event.key != "Escape":
574
return
575
# Delete the polygon
576
new_shape.remove()
577
reset_polygon()
578
579
580
def backspace_polygon(event):
581
if event is not None and hasattr(event, "key") and event.key != "Backspace":
582
return
583
if not polygon_points:
584
return
585
polygon_points.pop()
586
polygon = new_shape.children[0]
587
polygon.setAttribute("points", " ".join(
588
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
589
polygon_points]))
590
591
592
close_polygon_proxy = create_proxy(close_polygon)
593
cancel_polygon_proxy = create_proxy(cancel_polygon)
594
backspace_polygon_proxy = create_proxy(backspace_polygon)
595
596
597
def open_shape(event):
598
global new_shape, bbox_pos
599
if bbox_pos or shape_type == "select":
600
return
601
print("Creating a new shape of type:", shape_type)
602
603
if shape_type == "shape-bbox":
604
helper_message.innerText = ("Define the first point at the intersection of the lines "
605
"by clicking on the image, or click the cross to cancel")
606
607
cancel_button.addEventListener("click", cancel_bbox_proxy)
608
document.addEventListener("keydown", cancel_bbox_proxy)
609
cancel_button.style.display = "flex"
610
bbox_pos = None
611
zone.addEventListener("click", make_bbox_proxy)
612
vertical_ruler.style.display = "block"
613
horizontal_ruler.style.display = "block"
614
zone.style.cursor = "crosshair"
615
elif shape_type == "shape-polygon" or shape_type == "shape-polyline":
616
if shape_type == "shape-polygon":
617
helper_message.innerText = ("Click on the image to define the points of the polygon, "
618
"press escape to cancel, enter to close, or backspace to "
619
"remove the last point")
620
elif shape_type == "shape-polyline":
621
helper_message.innerText = ("Click on the image to define the points of the polyline, "
622
"press escape to cancel, enter to finish, or backspace to "
623
"remove the last point")
624
625
if not polygon_points and not new_shape:
626
new_shape = make_shape_container()
627
zone_rect = zone.getBoundingClientRect()
628
629
if not polygon_points and int(new_shape.children.length) == 0:
630
zone.addEventListener("click", make_polygon_proxy)
631
document.addEventListener("keydown", close_polygon_proxy)
632
document.addEventListener("keydown", cancel_polygon_proxy)
633
document.addEventListener("keydown", backspace_polygon_proxy)
634
cancel_button.addEventListener("click", cancel_polygon_proxy)
635
cancel_button.style.display = "flex"
636
confirm_button.addEventListener("click", close_polygon_proxy)
637
confirm_button.style.display = "flex"
638
backspace_button.addEventListener("click", backspace_polygon_proxy)
639
backspace_button.style.display = "flex"
640
if shape_type == "shape-polygon":
641
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
642
polygon.classList.add("shape-polygon")
643
elif shape_type == "shape-polyline":
644
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polyline")
645
polygon.classList.add("shape-polyline")
646
polygon.setAttribute("fill", "none")
647
polygon.setAttribute("data-object-type", "")
648
polygon.classList.add("shape")
649
new_shape.appendChild(polygon)
650
zone.appendChild(new_shape)
651
zone.style.cursor = "crosshair"
652
elif shape_type == "shape-point":
653
point = document.createElementNS("http://www.w3.org/2000/svg", "circle")
654
zone_rect = zone.getBoundingClientRect()
655
point.setAttribute("cx", str((event.clientX - zone_rect.left) / zone_rect.width * image.naturalWidth))
656
point.setAttribute("cy", str((event.clientY - zone_rect.top) / zone_rect.height * image.naturalHeight))
657
point.setAttribute("r", "0")
658
point.classList.add("shape-point")
659
point.classList.add("shape")
660
point.setAttribute("data-object-type", "")
661
662
new_shape = make_shape_container()
663
zone_rect = zone.getBoundingClientRect()
664
665
new_shape.appendChild(point)
666
zone.appendChild(new_shape)
667
668
new_shape = None
669
670
671
672
for button in list(document.getElementById("shape-selector").children):
673
button.addEventListener("click", create_proxy(switch_shape))
674
print("Shape", button.id, "is available")
675
676
zone.addEventListener("mousemove", follow_cursor_proxy)
677
zone.addEventListener("click", create_proxy(open_shape))
678
679
# Load existing annotations, if any
680
put_shapes(await load_shapes())
681
print("Ready!")
682