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