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 • 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