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 • 29.98 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) / scale
53
y = (event.clientY - rect.top) / scale
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
transform_origin = ["top", "left"]
282
283
if get_centre(shape)[0] / image.naturalWidth * image.width + object_list.offsetWidth > image.width:
284
object_list.style.left = "auto"
285
object_list.style.right = str((image.naturalWidth - get_centre(shape)[0]) / image.naturalWidth * 100) + "%"
286
object_list.style.maxWidth = str(get_centre(shape)[0] / image.naturalWidth * image.width) + "px"
287
transform_origin[1] = "right"
288
289
if get_centre(shape)[1] / image.naturalHeight * image.height + object_list.offsetHeight > image.height:
290
object_list.style.top = "auto"
291
object_list.style.bottom = str((image.naturalHeight - get_centre(shape)[1]) / image.naturalHeight * 100) + "%"
292
object_list.style.maxHeight = str(get_centre(shape)[1] / image.naturalHeight * image.height) + "px"
293
transform_origin[0] = "bottom"
294
295
object_list.style.transformOrigin = " ".join(transform_origin)
296
297
298
async def select_shape(event):
299
await focus_shape(event.target)
300
301
302
async def next_shape(event):
303
global selected_shape
304
if selected_shape is None:
305
return
306
307
selected_svg = selected_shape.parentNode
308
309
while selected_svg is not None:
310
next_sibling = selected_svg.nextElementSibling
311
if next_sibling and next_sibling.classList.contains("shape-container"):
312
selected_svg = next_sibling
313
break
314
elif next_sibling is None:
315
# If no more siblings, loop back to the first child
316
selected_svg = selected_svg.parentNode.firstElementChild
317
while selected_svg is not None and not selected_svg.classList.contains(
318
"shape-container"):
319
selected_svg = selected_svg.nextElementSibling
320
break
321
else:
322
selected_svg = next_sibling
323
324
if selected_svg:
325
shape = selected_svg.firstElementChild
326
await focus_shape(shape)
327
328
event.preventDefault()
329
330
331
async def previous_shape(event):
332
global selected_shape
333
if selected_shape is None:
334
return
335
336
selected_svg = selected_shape.parentNode
337
338
while selected_svg is not None:
339
next_sibling = selected_svg.previousElementSibling
340
if next_sibling and next_sibling.classList.contains("shape-container"):
341
selected_svg = next_sibling
342
break
343
elif next_sibling is None:
344
# If no more siblings, loop back to the last child
345
selected_svg = selected_svg.parentNode.lastElementChild
346
while selected_svg is not None and not selected_svg.classList.contains(
347
"shape-container"):
348
selected_svg = selected_svg.previousElementSibling
349
break
350
else:
351
selected_svg = next_sibling
352
353
if selected_svg:
354
shape = selected_svg.firstElementChild
355
await focus_shape(shape)
356
357
event.preventDefault()
358
359
360
def unselect_shape(event):
361
global selected_shape
362
363
if selected_shape is not None:
364
selected_shape.classList.remove("selected")
365
selected_shape = None
366
367
object_list_content.innerHTML = ""
368
object_list.style.display = "none"
369
delete_button.style.display = "none"
370
next_button.style.display = "none"
371
previous_button.style.display = "none"
372
document.removeEventListener("keydown", delete_shape_key_proxy)
373
document.removeEventListener("keydown", next_shape_key_proxy)
374
document.removeEventListener("keydown", previous_shape_key_proxy)
375
376
377
def delete_shape(event):
378
global selected_shape
379
if selected_shape is None:
380
return
381
# Shape is SVG shape inside SVG so we need to remove the parent SVG
382
selected_shape.parentNode.remove()
383
selected_shape = None
384
object_list_content.innerHTML = ""
385
object_list.style.display = "none"
386
delete_button.style.display = "none"
387
next_button.style.display = "none"
388
previous_button.style.display = "none"
389
document.removeEventListener("keydown", delete_shape_key_proxy)
390
document.removeEventListener("keydown", next_shape_key_proxy)
391
document.removeEventListener("keydown", previous_shape_key_proxy)
392
393
394
select_shape_proxy = create_proxy(select_shape)
395
unselect_shape_proxy = create_proxy(unselect_shape)
396
delete_shape_proxy = create_proxy(delete_shape)
397
next_shape_proxy = create_proxy(next_shape)
398
previous_shape_proxy = create_proxy(previous_shape)
399
400
delete_button.addEventListener("click", delete_shape_proxy)
401
next_button.addEventListener("click", next_shape_proxy)
402
previous_button.addEventListener("click", previous_shape_proxy)
403
404
# These are functions usable in JS
405
cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event))
406
make_bbox_proxy = create_proxy(lambda event: make_bbox(event))
407
make_polygon_proxy = create_proxy(lambda event: make_polygon(event))
408
follow_cursor_proxy = create_proxy(follow_cursor)
409
410
411
def switch_shape(event):
412
global shape_type
413
object_list_content.innerHTML = ""
414
unselect_shape(None)
415
shape = event.currentTarget.id
416
shape_type = shape
417
for button in list(document.getElementById("shape-selector").children):
418
if not button.classList.contains("button-flat"):
419
button.classList.add("button-flat")
420
event.currentTarget.classList.remove("button-flat")
421
if shape_type == "select":
422
# Add event listeners to existing shapes
423
print(len(list(document.getElementsByClassName("shape"))), "shapes found")
424
for shape in document.getElementsByClassName("shape"):
425
print("Adding event listener to shape:", shape)
426
shape.addEventListener("click", select_shape_proxy)
427
image.addEventListener("click", unselect_shape_proxy)
428
helper_message.innerText = "Click on a shape to select"
429
# Cancel the current shape creation
430
if shape_type == "shape-bbox":
431
cancel_bbox(None)
432
elif shape_type == "shape-polygon":
433
cancel_polygon(None)
434
elif shape_type == "shape-polyline":
435
cancel_polygon(None)
436
else:
437
# Remove event listeners for selection
438
for shape in document.getElementsByClassName("shape"):
439
print("Removing event listener from shape:", shape)
440
shape.removeEventListener("click", select_shape_proxy)
441
image.removeEventListener("click", unselect_shape_proxy)
442
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
443
print("Shape is now of type:", shape)
444
445
446
def select_mode():
447
global shape_type
448
document.getElementById("select").click()
449
450
451
vertical_ruler = document.getElementById("annotation-ruler-vertical")
452
horizontal_ruler = document.getElementById("annotation-ruler-horizontal")
453
vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary")
454
horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary")
455
helper_message = document.getElementById("annotation-helper-message")
456
457
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
458
459
460
def cancel_bbox(event):
461
global bbox_pos, new_shape
462
463
# Key must be ESCAPE
464
if event is not None and hasattr(event, "key") and event.key != "Escape":
465
return
466
467
if new_shape is not None and event is not None:
468
# Require event so the shape is kept when it ends normally
469
new_shape.remove()
470
zone.removeEventListener("click", make_bbox_proxy)
471
document.removeEventListener("keydown", cancel_bbox_proxy)
472
cancel_button.removeEventListener("click", cancel_bbox_proxy)
473
474
bbox_pos = None
475
vertical_ruler.style.display = "none"
476
horizontal_ruler.style.display = "none"
477
vertical_ruler_2.style.display = "none"
478
horizontal_ruler_2.style.display = "none"
479
zone.style.cursor = "auto"
480
cancel_button.style.display = "none"
481
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
482
new_shape = None
483
update_transform()
484
485
486
def make_bbox(event):
487
global new_shape, bbox_pos
488
zone_rect = zone.getBoundingClientRect()
489
490
if bbox_pos is None:
491
helper_message.innerText = "Now define the second point"
492
493
bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width,
494
(event.clientY - zone_rect.top) / zone_rect.height]
495
vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%"
496
horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%"
497
vertical_ruler_2.style.display = "block"
498
horizontal_ruler_2.style.display = "block"
499
500
else:
501
x0, y0 = bbox_pos.copy()
502
x1 = (event.clientX - zone_rect.left) / zone_rect.width
503
y1 = (event.clientY - zone_rect.top) / zone_rect.height
504
505
rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect")
506
507
new_shape = make_shape_container()
508
zone_rect = zone.getBoundingClientRect()
509
510
new_shape.appendChild(rectangle)
511
zone.appendChild(new_shape)
512
513
minx = min(x0, x1)
514
miny = min(y0, y1)
515
maxx = max(x0, x1)
516
maxy = max(y0, y1)
517
518
rectangle.setAttribute("x", str(minx * image.naturalWidth))
519
rectangle.setAttribute("y", str(miny * image.naturalHeight))
520
rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth))
521
rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight))
522
rectangle.setAttribute("fill", "none")
523
rectangle.setAttribute("data-object-type", "")
524
rectangle.classList.add("shape-bbox")
525
rectangle.classList.add("shape")
526
527
# Add event listeners to the new shape
528
rectangle.addEventListener("click", select_shape_proxy)
529
530
cancel_bbox(None)
531
532
533
polygon_points = []
534
535
536
def make_polygon(event):
537
global new_shape, polygon_points
538
539
polygon = new_shape.children[0]
540
541
zone_rect = zone.getBoundingClientRect()
542
543
polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width,
544
(event.clientY - zone_rect.top) / zone_rect.height))
545
546
# Update the polygon
547
polygon.setAttribute("points", " ".join(
548
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
549
polygon_points]))
550
551
552
def reset_polygon():
553
global new_shape, polygon_points
554
555
zone.removeEventListener("click", make_polygon_proxy)
556
document.removeEventListener("keydown", close_polygon_proxy)
557
document.removeEventListener("keydown", cancel_polygon_proxy)
558
document.removeEventListener("keydown", backspace_polygon_proxy)
559
confirm_button.style.display = "none"
560
cancel_button.style.display = "none"
561
backspace_button.style.display = "none"
562
confirm_button.removeEventListener("click", close_polygon_proxy)
563
cancel_button.removeEventListener("click", cancel_polygon_proxy)
564
backspace_button.removeEventListener("click", backspace_polygon_proxy)
565
polygon_points.clear()
566
567
zone.style.cursor = "auto"
568
new_shape = None
569
570
update_transform()
571
572
573
def close_polygon(event):
574
if event is not None and hasattr(event, "key") and event.key != "Enter":
575
return
576
# Polygon is already there, but we need to remove the events
577
reset_polygon()
578
579
580
def cancel_polygon(event):
581
if event is not None and hasattr(event, "key") and event.key != "Escape":
582
return
583
# Delete the polygon
584
new_shape.remove()
585
reset_polygon()
586
587
588
def backspace_polygon(event):
589
if event is not None and hasattr(event, "key") and event.key != "Backspace":
590
return
591
if not polygon_points:
592
return
593
polygon_points.pop()
594
polygon = new_shape.children[0]
595
polygon.setAttribute("points", " ".join(
596
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
597
polygon_points]))
598
599
600
close_polygon_proxy = create_proxy(close_polygon)
601
cancel_polygon_proxy = create_proxy(cancel_polygon)
602
backspace_polygon_proxy = create_proxy(backspace_polygon)
603
604
605
def open_shape(event):
606
global new_shape, bbox_pos
607
if bbox_pos or shape_type == "select":
608
return
609
print("Creating a new shape of type:", shape_type)
610
611
if shape_type == "shape-bbox":
612
helper_message.innerText = ("Define the first point at the intersection of the lines "
613
"by clicking on the image, or click the cross to cancel")
614
615
cancel_button.addEventListener("click", cancel_bbox_proxy)
616
document.addEventListener("keydown", cancel_bbox_proxy)
617
cancel_button.style.display = "flex"
618
bbox_pos = None
619
zone.addEventListener("click", make_bbox_proxy)
620
vertical_ruler.style.display = "block"
621
horizontal_ruler.style.display = "block"
622
zone.style.cursor = "crosshair"
623
elif shape_type == "shape-polygon" or shape_type == "shape-polyline":
624
if shape_type == "shape-polygon":
625
helper_message.innerText = ("Click on the image to define the points of the polygon, "
626
"press escape to cancel, enter to close, or backspace to "
627
"remove the last point")
628
elif shape_type == "shape-polyline":
629
helper_message.innerText = ("Click on the image to define the points of the polyline, "
630
"press escape to cancel, enter to finish, or backspace to "
631
"remove the last point")
632
633
if not polygon_points and not new_shape:
634
new_shape = make_shape_container()
635
zone_rect = zone.getBoundingClientRect()
636
637
if not polygon_points and int(new_shape.children.length) == 0:
638
zone.addEventListener("click", make_polygon_proxy)
639
document.addEventListener("keydown", close_polygon_proxy)
640
document.addEventListener("keydown", cancel_polygon_proxy)
641
document.addEventListener("keydown", backspace_polygon_proxy)
642
cancel_button.addEventListener("click", cancel_polygon_proxy)
643
cancel_button.style.display = "flex"
644
confirm_button.addEventListener("click", close_polygon_proxy)
645
confirm_button.style.display = "flex"
646
backspace_button.addEventListener("click", backspace_polygon_proxy)
647
backspace_button.style.display = "flex"
648
if shape_type == "shape-polygon":
649
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
650
polygon.classList.add("shape-polygon")
651
elif shape_type == "shape-polyline":
652
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polyline")
653
polygon.classList.add("shape-polyline")
654
polygon.setAttribute("fill", "none")
655
polygon.setAttribute("data-object-type", "")
656
polygon.classList.add("shape")
657
new_shape.appendChild(polygon)
658
zone.appendChild(new_shape)
659
zone.style.cursor = "crosshair"
660
elif shape_type == "shape-point":
661
point = document.createElementNS("http://www.w3.org/2000/svg", "circle")
662
zone_rect = zone.getBoundingClientRect()
663
point.setAttribute("cx", str((event.clientX - zone_rect.left) / zone_rect.width * image.naturalWidth))
664
point.setAttribute("cy", str((event.clientY - zone_rect.top) / zone_rect.height * image.naturalHeight))
665
point.setAttribute("r", "0")
666
point.classList.add("shape-point")
667
point.classList.add("shape")
668
point.setAttribute("data-object-type", "")
669
670
new_shape = make_shape_container()
671
zone_rect = zone.getBoundingClientRect()
672
673
new_shape.appendChild(point)
674
zone.appendChild(new_shape)
675
676
new_shape = None
677
678
update_transform()
679
680
681
682
for button in list(document.getElementById("shape-selector").children):
683
button.addEventListener("click", create_proxy(switch_shape))
684
print("Shape", button.id, "is available")
685
686
zone.addEventListener("mousemove", follow_cursor_proxy)
687
zone.addEventListener("click", create_proxy(open_shape))
688
689
# Zoom
690
zoom_container = document.getElementById("annotation-zoom-container")
691
scale = 1
692
translate_x = 0
693
translate_y = 0
694
695
def update_transform():
696
zone.style.transform = f"scale({scale}) translate({translate_x}px, {translate_y}px)"
697
object_list.style.transform = f"scale({1/scale})" # neutralise the zoom
698
vertical_ruler.style.transform = f"scale({1/scale})"
699
horizontal_ruler.style.transform = f"scale({1/scale})"
700
vertical_ruler_2.style.transform = f"scale({1/scale})"
701
horizontal_ruler_2.style.transform = f"scale({1/scale})"
702
703
for svg in zone.getElementsByClassName("shape-container"):
704
# Because vector-effect="non-scaling-stroke" doesn't work when an HTML ancestor is
705
# scaled, we need to scale the shapes down then scale them back up
706
svg.style.transform = f"scale({1/scale})"
707
svg.style.transformOrigin = "0 0"
708
for shape in svg.children:
709
shape.setAttribute("transform", f"scale({scale})")
710
shape.setAttribute("transform-origin", "0 0")
711
712
scale_exponent = 0
713
714
zoom_offset = (0, 0)
715
716
def on_wheel(event):
717
global scale, scale_exponent, translate_x, translate_y
718
if object_list.matches(":hover"):
719
return
720
721
event.preventDefault()
722
723
# Adjust scale exponent and compute new scale.
724
scale_exponent += (-1 if event.deltaY > 0 else 1) / 4
725
new_scale = 2 ** scale_exponent
726
scale = new_scale
727
728
update_transform()
729
730
731
is_dragging = False
732
start_x = 0
733
start_y = 0
734
735
def on_mouse_down(event):
736
global is_dragging, start_x, start_y
737
if object_list.matches(":hover"):
738
return
739
740
if event.button == 0:
741
is_dragging = True
742
start_x = event.clientX
743
start_y = event.clientY
744
745
def on_mouse_move(event):
746
global translate_x, translate_y
747
if object_list.matches(":hover"):
748
return
749
750
if is_dragging:
751
translate_x += event.movementX / scale
752
translate_y += event.movementY / scale
753
update_transform()
754
755
def on_mouse_up(event):
756
global is_dragging
757
if object_list.matches(":hover"):
758
return
759
is_dragging = False
760
761
def on_touch_start(event):
762
global last_touch_positions
763
764
if event.touches.length == 2:
765
event.preventDefault()
766
last_touch_positions = [
767
(event.touches[0].clientX, event.touches[0].clientY),
768
(event.touches[1].clientX, event.touches[1].clientY)
769
]
770
771
def on_touch_move(event):
772
global translate_x, translate_y, last_touch_positions
773
774
if event.touches.length == 2:
775
event.preventDefault()
776
777
current_positions = [
778
(event.touches[0].clientX, event.touches[0].clientY),
779
(event.touches[1].clientX, event.touches[1].clientY)
780
]
781
782
delta_x = sum(p[0] - lp[0] for p, lp in zip(current_positions, last_touch_positions)) / 2
783
delta_y = sum(p[1] - lp[1] for p, lp in zip(current_positions, last_touch_positions)) / 2
784
785
translate_x += delta_x
786
translate_y += delta_y
787
788
last_touch_positions = current_positions
789
790
update_transform()
791
792
def on_touch_end(event):
793
global last_touch_positions
794
if event.touches.length == 0:
795
last_touch_positions = None
796
797
zoom_container.addEventListener("wheel", on_wheel)
798
zoom_container.addEventListener("mousedown", on_mouse_down)
799
document.addEventListener("mousemove", on_mouse_move)
800
document.addEventListener("mouseup", on_mouse_up)
801
zone.addEventListener("touchstart", on_touch_start)
802
zone.addEventListener("touchmove", on_touch_move)
803
zone.addEventListener("touchend", on_touch_end)
804
805
# Load existing annotations, if any
806
put_shapes(await load_shapes())
807
print("Ready!")
808