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