Web platform for sharing free image data for ML and research

Homepage: https://datasets.roundabout-host.com

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