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.37 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
if shape_type == "select":
396
# Add event listeners to existing shapes
397
print(len(list(document.getElementsByClassName("shape"))), "shapes found")
398
for shape in document.getElementsByClassName("shape"):
399
print("Adding event listener to shape:", shape)
400
shape.addEventListener("click", select_shape_proxy)
401
image.addEventListener("click", unselect_shape_proxy)
402
helper_message.innerText = "Click on a shape to select"
403
# Cancel the current shape creation
404
if shape_type == "shape-bbox":
405
cancel_bbox(None)
406
elif shape_type == "shape-polygon":
407
cancel_polygon(None)
408
elif shape_type == "shape-polyline":
409
cancel_polygon(None)
410
else:
411
# Remove event listeners for selection
412
for shape in document.getElementsByClassName("shape"):
413
print("Removing event listener from shape:", shape)
414
shape.removeEventListener("click", select_shape_proxy)
415
image.removeEventListener("click", unselect_shape_proxy)
416
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
417
print("Shape is now of type:", shape)
418
419
420
def select_mode():
421
global shape_type
422
shape_type = "select"
423
document.getElementById("select").click()
424
425
426
vertical_ruler = document.getElementById("annotation-ruler-vertical")
427
horizontal_ruler = document.getElementById("annotation-ruler-horizontal")
428
vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary")
429
horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary")
430
helper_message = document.getElementById("annotation-helper-message")
431
432
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
433
434
435
def cancel_bbox(event):
436
global bbox_pos, new_shape
437
438
# Key must be ESCAPE
439
if event is not None and hasattr(event, "key") and event.key != "Escape":
440
return
441
442
if new_shape is not None and event is not None:
443
# Require event so the shape is kept when it ends normally
444
new_shape.remove()
445
zone.removeEventListener("click", make_bbox_proxy)
446
document.removeEventListener("keydown", cancel_bbox_proxy)
447
cancel_button.removeEventListener("click", cancel_bbox_proxy)
448
449
bbox_pos = None
450
vertical_ruler.style.display = "none"
451
horizontal_ruler.style.display = "none"
452
vertical_ruler_2.style.display = "none"
453
horizontal_ruler_2.style.display = "none"
454
zone.style.cursor = "auto"
455
cancel_button.style.display = "none"
456
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
457
new_shape = None
458
459
460
def make_bbox(event):
461
global new_shape, bbox_pos
462
zone_rect = zone.getBoundingClientRect()
463
464
if bbox_pos is None:
465
helper_message.innerText = "Now define the second point"
466
467
bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width,
468
(event.clientY - zone_rect.top) / zone_rect.height]
469
vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%"
470
horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%"
471
vertical_ruler_2.style.display = "block"
472
horizontal_ruler_2.style.display = "block"
473
474
else:
475
x0, y0 = bbox_pos.copy()
476
x1 = (event.clientX - zone_rect.left) / zone_rect.width
477
y1 = (event.clientY - zone_rect.top) / zone_rect.height
478
479
rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect")
480
481
new_shape = make_shape_container()
482
zone_rect = zone.getBoundingClientRect()
483
484
new_shape.appendChild(rectangle)
485
zone.appendChild(new_shape)
486
487
minx = min(x0, x1)
488
miny = min(y0, y1)
489
maxx = max(x0, x1)
490
maxy = max(y0, y1)
491
492
rectangle.setAttribute("x", str(minx * image.naturalWidth))
493
rectangle.setAttribute("y", str(miny * image.naturalHeight))
494
rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth))
495
rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight))
496
rectangle.setAttribute("fill", "none")
497
rectangle.setAttribute("data-object-type", "")
498
rectangle.classList.add("shape-bbox")
499
rectangle.classList.add("shape")
500
501
# Add event listeners to the new shape
502
rectangle.addEventListener("click", select_shape_proxy)
503
504
cancel_bbox(None)
505
506
507
polygon_points = []
508
509
510
def make_polygon(event):
511
global new_shape, polygon_points
512
513
polygon = new_shape.children[0]
514
515
zone_rect = zone.getBoundingClientRect()
516
517
polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width,
518
(event.clientY - zone_rect.top) / zone_rect.height))
519
520
# Update the polygon
521
polygon.setAttribute("points", " ".join(
522
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
523
polygon_points]))
524
525
526
def reset_polygon():
527
global new_shape, polygon_points
528
529
zone.removeEventListener("click", make_polygon_proxy)
530
document.removeEventListener("keydown", close_polygon_proxy)
531
document.removeEventListener("keydown", cancel_polygon_proxy)
532
document.removeEventListener("keydown", backspace_polygon_proxy)
533
confirm_button.style.display = "none"
534
cancel_button.style.display = "none"
535
backspace_button.style.display = "none"
536
confirm_button.removeEventListener("click", close_polygon_proxy)
537
cancel_button.removeEventListener("click", cancel_polygon_proxy)
538
backspace_button.removeEventListener("click", backspace_polygon_proxy)
539
polygon_points.clear()
540
541
zone.style.cursor = "auto"
542
new_shape = None
543
544
545
def close_polygon(event):
546
if event is not None and hasattr(event, "key") and event.key != "Enter":
547
return
548
# Polygon is already there, but we need to remove the events
549
reset_polygon()
550
551
552
def cancel_polygon(event):
553
if event is not None and hasattr(event, "key") and event.key != "Escape":
554
return
555
# Delete the polygon
556
new_shape.remove()
557
reset_polygon()
558
559
560
def backspace_polygon(event):
561
if event is not None and hasattr(event, "key") and event.key != "Backspace":
562
return
563
if not polygon_points:
564
return
565
polygon_points.pop()
566
polygon = new_shape.children[0]
567
polygon.setAttribute("points", " ".join(
568
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
569
polygon_points]))
570
571
572
close_polygon_proxy = create_proxy(close_polygon)
573
cancel_polygon_proxy = create_proxy(cancel_polygon)
574
backspace_polygon_proxy = create_proxy(backspace_polygon)
575
576
577
def open_shape(event):
578
global new_shape, bbox_pos
579
if bbox_pos or shape_type == "select":
580
return
581
print("Creating a new shape of type:", shape_type)
582
583
if shape_type == "shape-bbox":
584
helper_message.innerText = ("Define the first point at the intersection of the lines "
585
"by clicking on the image, or click the cross to cancel")
586
587
cancel_button.addEventListener("click", cancel_bbox_proxy)
588
document.addEventListener("keydown", cancel_bbox_proxy)
589
cancel_button.style.display = "flex"
590
bbox_pos = None
591
zone.addEventListener("click", make_bbox_proxy)
592
vertical_ruler.style.display = "block"
593
horizontal_ruler.style.display = "block"
594
zone.style.cursor = "crosshair"
595
elif shape_type == "shape-polygon" or shape_type == "shape-polyline":
596
if shape_type == "shape-polygon":
597
helper_message.innerText = ("Click on the image to define the points of the polygon, "
598
"press escape to cancel, enter to close, or backspace to "
599
"remove the last point")
600
elif shape_type == "shape-polyline":
601
helper_message.innerText = ("Click on the image to define the points of the polyline, "
602
"press escape to cancel, enter to finish, or backspace to "
603
"remove the last point")
604
605
if not polygon_points and not new_shape:
606
new_shape = make_shape_container()
607
zone_rect = zone.getBoundingClientRect()
608
609
if not polygon_points and int(new_shape.children.length) == 0:
610
zone.addEventListener("click", make_polygon_proxy)
611
document.addEventListener("keydown", close_polygon_proxy)
612
document.addEventListener("keydown", cancel_polygon_proxy)
613
document.addEventListener("keydown", backspace_polygon_proxy)
614
cancel_button.addEventListener("click", cancel_polygon_proxy)
615
cancel_button.style.display = "flex"
616
confirm_button.addEventListener("click", close_polygon_proxy)
617
confirm_button.style.display = "flex"
618
backspace_button.addEventListener("click", backspace_polygon_proxy)
619
backspace_button.style.display = "flex"
620
if shape_type == "shape-polygon":
621
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
622
polygon.classList.add("shape-polygon")
623
elif shape_type == "shape-polyline":
624
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polyline")
625
polygon.classList.add("shape-polyline")
626
polygon.setAttribute("fill", "none")
627
polygon.setAttribute("data-object-type", "")
628
polygon.classList.add("shape")
629
new_shape.appendChild(polygon)
630
zone.appendChild(new_shape)
631
zone.style.cursor = "crosshair"
632
elif shape_type == "shape-point":
633
point = document.createElementNS("http://www.w3.org/2000/svg", "circle")
634
zone_rect = zone.getBoundingClientRect()
635
point.setAttribute("cx", str((event.clientX - zone_rect.left) / zone_rect.width * image.naturalWidth))
636
point.setAttribute("cy", str((event.clientY - zone_rect.top) / zone_rect.height * image.naturalHeight))
637
point.setAttribute("r", "0")
638
point.classList.add("shape-point")
639
point.classList.add("shape")
640
point.setAttribute("data-object-type", "")
641
642
new_shape = make_shape_container()
643
zone_rect = zone.getBoundingClientRect()
644
645
new_shape.appendChild(point)
646
zone.appendChild(new_shape)
647
648
new_shape = None
649
650
651
652
for button in list(document.getElementById("shape-selector").children):
653
button.addEventListener("click", create_proxy(switch_shape))
654
print("Shape", button.id, "is available")
655
656
zone.addEventListener("mousemove", follow_cursor_proxy)
657
zone.addEventListener("click", create_proxy(open_shape))
658
659
# Load existing annotations, if any
660
put_shapes(await load_shapes())
661
print("Ready!")
662