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