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 • 18.06 kiB
Python script, ASCII text executable
        
            
1
from pyscript import document
2
from pyodide.ffi import create_proxy
3
from pyodide.http import pyfetch
4
import asyncio
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
17
object_list = document.getElementById("object-types")
18
19
confirm_button.style.display = "none"
20
cancel_button.style.display = "none"
21
backspace_button.style.display = "none"
22
delete_button.style.display = "none"
23
previous_button.style.display = "none"
24
next_button.style.display = "none"
25
shape_type = ""
26
bbox_pos = None
27
new_shape = None
28
selected_shape = None
29
30
31
async def get_all_objects():
32
response = await pyfetch("/api/object-types")
33
if response.ok:
34
return await response.json()
35
36
37
def follow_cursor(event):
38
rect = zone.getBoundingClientRect()
39
x = event.clientX - rect.left
40
y = event.clientY - rect.top
41
vertical_ruler.style.left = str(x) + "px"
42
horizontal_ruler.style.top = str(y) + "px"
43
44
45
def change_object_type(event):
46
global selected_shape
47
if selected_shape is None:
48
return
49
selected_shape.setAttribute("data-object-type", event.currentTarget.value)
50
51
52
change_object_type_proxy = create_proxy(change_object_type)
53
54
55
async def focus_shape(shape):
56
global selected_shape
57
58
if shape_type != "select":
59
return
60
if selected_shape is not None:
61
selected_shape.classList.remove("selected")
62
63
selected_shape = shape
64
65
selected_shape.classList.add("selected")
66
67
objects = await get_all_objects()
68
69
delete_button.style.display = "block"
70
next_button.style.display = "block"
71
previous_button.style.display = "block"
72
73
object_list.innerHTML = ""
74
75
new_radio = document.createElement("input")
76
new_radio.setAttribute("type", "radio")
77
new_radio.setAttribute("name", "object-type")
78
new_radio.setAttribute("value", "")
79
new_label = document.createElement("label")
80
new_label.appendChild(new_radio)
81
new_label.append("Undefined")
82
object_list.appendChild(new_label)
83
new_radio.addEventListener("change", change_object_type_proxy)
84
85
selected_object = selected_shape.getAttribute("data-object-type")
86
if not selected_object:
87
new_radio.setAttribute("checked", "")
88
89
for object, description in objects.items():
90
new_radio = document.createElement("input")
91
new_radio.setAttribute("type", "radio")
92
new_radio.setAttribute("name", "object-type")
93
new_radio.setAttribute("value", object)
94
if selected_object == object:
95
new_radio.setAttribute("checked", "")
96
new_label = document.createElement("label")
97
new_label.appendChild(new_radio)
98
new_label.append(object)
99
object_list.appendChild(new_label)
100
new_radio.addEventListener("change", change_object_type_proxy)
101
102
103
async def select_shape(event):
104
await focus_shape(event.target)
105
106
107
async def next_shape(event):
108
global selected_shape
109
if selected_shape is None:
110
return
111
112
selected_svg = selected_shape.parentNode
113
114
while selected_svg is not None:
115
next_sibling = selected_svg.nextElementSibling
116
if next_sibling and next_sibling.classList.contains("shape-container"):
117
selected_svg = next_sibling
118
break
119
elif next_sibling is None:
120
# If no more siblings, loop back to the first child
121
selected_svg = selected_svg.parentNode.firstElementChild
122
while selected_svg is not None and not selected_svg.classList.contains(
123
"shape-container"):
124
selected_svg = selected_svg.nextElementSibling
125
break
126
else:
127
selected_svg = next_sibling
128
129
if selected_svg:
130
shape = selected_svg.firstElementChild
131
await focus_shape(shape)
132
133
134
async def previous_shape(event):
135
global selected_shape
136
if selected_shape is None:
137
return
138
139
selected_svg = selected_shape.parentNode
140
141
while selected_svg is not None:
142
next_sibling = selected_svg.previousElementSibling
143
if next_sibling and next_sibling.classList.contains("shape-container"):
144
selected_svg = next_sibling
145
break
146
elif next_sibling is None:
147
# If no more siblings, loop back to the last child
148
selected_svg = selected_svg.parentNode.lastElementChild
149
while selected_svg is not None and not selected_svg.classList.contains(
150
"shape-container"):
151
selected_svg = selected_svg.previousElementSibling
152
break
153
else:
154
selected_svg = next_sibling
155
156
if selected_svg:
157
shape = selected_svg.firstElementChild
158
await focus_shape(shape)
159
160
161
def unselect_shape(event):
162
global selected_shape
163
164
if selected_shape is not None:
165
selected_shape.classList.remove("selected")
166
selected_shape = None
167
168
object_list.innerHTML = ""
169
delete_button.style.display = "none"
170
next_button.style.display = "none"
171
previous_button.style.display = "none"
172
173
174
def delete_shape(event):
175
global selected_shape
176
if selected_shape is None:
177
return
178
# Shape is SVG shape inside SVG so we need to remove the parent SVG
179
selected_shape.parentNode.remove()
180
selected_shape = None
181
object_list.innerHTML = ""
182
delete_button.style.display = "none"
183
next_button.style.display = "none"
184
previous_button.style.display = "none"
185
186
187
select_shape_proxy = create_proxy(select_shape)
188
unselect_shape_proxy = create_proxy(unselect_shape)
189
delete_shape_proxy = create_proxy(delete_shape)
190
next_shape_proxy = create_proxy(next_shape)
191
previous_shape_proxy = create_proxy(previous_shape)
192
193
delete_button.addEventListener("click", delete_shape_proxy)
194
next_button.addEventListener("click", next_shape_proxy)
195
previous_button.addEventListener("click", previous_shape_proxy)
196
197
# These are functions usable in JS
198
cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event))
199
make_bbox_proxy = create_proxy(lambda event: make_bbox(event))
200
make_polygon_proxy = create_proxy(lambda event: make_polygon(event))
201
follow_cursor_proxy = create_proxy(follow_cursor)
202
203
204
def switch_shape(event):
205
global shape_type
206
object_list.innerHTML = ""
207
unselect_shape(None)
208
shape = event.currentTarget.id
209
shape_type = shape
210
if shape_type == "select":
211
# Add event listeners to existing shapes
212
print(len(list(document.getElementsByClassName("shape"))), "shapes found")
213
for shape in document.getElementsByClassName("shape"):
214
print("Adding event listener to shape:", shape)
215
shape.addEventListener("click", select_shape_proxy)
216
image.addEventListener("click", unselect_shape_proxy)
217
helper_message.innerText = "Click on a shape to select"
218
# Cancel the current shape creation
219
match shape_type:
220
case "shape-bbox":
221
cancel_bbox(None)
222
case "shape-polygon":
223
cancel_polygon(None)
224
case "shape-polyline":
225
cancel_polygon(None)
226
else:
227
# Remove event listeners for selection
228
for shape in document.getElementsByClassName("shape"):
229
print("Removing event listener from shape:", shape)
230
shape.removeEventListener("click", select_shape_proxy)
231
image.removeEventListener("click", unselect_shape_proxy)
232
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
233
print("Shape is now of type:", shape)
234
235
236
vertical_ruler = document.getElementById("annotation-ruler-vertical")
237
horizontal_ruler = document.getElementById("annotation-ruler-horizontal")
238
vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary")
239
horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary")
240
helper_message = document.getElementById("annotation-helper-message")
241
242
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
243
244
245
def cancel_bbox(event):
246
global bbox_pos, new_shape
247
248
# Key must be ESCAPE
249
if event is not None and hasattr(event, "key") and event.key != "Escape":
250
return
251
252
if new_shape is not None and event is not None:
253
# Require event so the shape is kept when it ends normally
254
new_shape.remove()
255
zone.removeEventListener("click", make_bbox_proxy)
256
document.removeEventListener("keydown", cancel_bbox_proxy)
257
cancel_button.removeEventListener("click", cancel_bbox_proxy)
258
259
bbox_pos = None
260
vertical_ruler.style.display = "none"
261
horizontal_ruler.style.display = "none"
262
vertical_ruler_2.style.display = "none"
263
horizontal_ruler_2.style.display = "none"
264
zone.style.cursor = "auto"
265
cancel_button.style.display = "none"
266
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
267
new_shape = None
268
269
270
def make_bbox(event):
271
global new_shape, bbox_pos
272
zone_rect = zone.getBoundingClientRect()
273
274
if bbox_pos is None:
275
helper_message.innerText = "Now define the second point"
276
277
bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width,
278
(event.clientY - zone_rect.top) / zone_rect.height]
279
vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%"
280
horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%"
281
vertical_ruler_2.style.display = "block"
282
horizontal_ruler_2.style.display = "block"
283
284
else:
285
x0, y0 = bbox_pos.copy()
286
x1 = (event.clientX - zone_rect.left) / zone_rect.width
287
y1 = (event.clientY - zone_rect.top) / zone_rect.height
288
289
rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect")
290
291
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
292
new_shape.setAttribute("width", "100%")
293
new_shape.setAttribute("height", "100%")
294
zone_rect = zone.getBoundingClientRect()
295
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
296
new_shape.classList.add("shape-container")
297
298
new_shape.appendChild(rectangle)
299
zone.appendChild(new_shape)
300
301
minx = min(x0, x1)
302
miny = min(y0, y1)
303
maxx = max(x0, x1)
304
maxy = max(y0, y1)
305
306
rectangle.setAttribute("x", str(minx * image.naturalWidth))
307
rectangle.setAttribute("y", str(miny * image.naturalHeight))
308
rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth))
309
rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight))
310
rectangle.setAttribute("fill", "none")
311
rectangle.setAttribute("data-object-type", "")
312
rectangle.classList.add("shape-bbox")
313
rectangle.classList.add("shape")
314
315
# Add event listeners to the new shape
316
rectangle.addEventListener("click", select_shape_proxy)
317
318
cancel_bbox(None)
319
320
321
polygon_points = []
322
323
324
def make_polygon(event):
325
global new_shape, polygon_points
326
327
polygon = new_shape.children[0]
328
329
zone_rect = zone.getBoundingClientRect()
330
331
polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width,
332
(event.clientY - zone_rect.top) / zone_rect.height))
333
334
# Update the polygon
335
polygon.setAttribute("points", " ".join(
336
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
337
polygon_points]))
338
339
340
def reset_polygon():
341
global new_shape, polygon_points
342
343
zone.removeEventListener("click", make_polygon_proxy)
344
document.removeEventListener("keydown", close_polygon_proxy)
345
document.removeEventListener("keydown", cancel_polygon_proxy)
346
document.removeEventListener("keydown", backspace_polygon_proxy)
347
confirm_button.style.display = "none"
348
cancel_button.style.display = "none"
349
backspace_button.style.display = "none"
350
confirm_button.removeEventListener("click", close_polygon_proxy)
351
cancel_button.removeEventListener("click", cancel_polygon_proxy)
352
backspace_button.removeEventListener("click", backspace_polygon_proxy)
353
polygon_points.clear()
354
355
zone.style.cursor = "auto"
356
new_shape = None
357
358
359
def close_polygon(event):
360
if event is not None and hasattr(event, "key") and event.key != "Enter":
361
return
362
# Polygon is already there, but we need to remove the events
363
reset_polygon()
364
365
366
def cancel_polygon(event):
367
if event is not None and hasattr(event, "key") and event.key != "Escape":
368
return
369
# Delete the polygon
370
new_shape.remove()
371
reset_polygon()
372
373
374
def backspace_polygon(event):
375
if event is not None and hasattr(event, "key") and event.key != "Backspace":
376
return
377
if not polygon_points:
378
return
379
polygon_points.pop()
380
polygon = new_shape.children[0]
381
polygon.setAttribute("points", " ".join(
382
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
383
polygon_points]))
384
385
386
close_polygon_proxy = create_proxy(close_polygon)
387
cancel_polygon_proxy = create_proxy(cancel_polygon)
388
backspace_polygon_proxy = create_proxy(backspace_polygon)
389
390
391
def open_shape(event):
392
global new_shape, bbox_pos
393
if bbox_pos or shape_type == "select":
394
return
395
print("Creating a new shape of type:", shape_type)
396
397
match shape_type:
398
case "shape-bbox":
399
helper_message.innerText = ("Define the first point at the intersection of the lines "
400
"by clicking on the image, or click the cross to cancel")
401
402
cancel_button.addEventListener("click", cancel_bbox_proxy)
403
document.addEventListener("keydown", cancel_bbox_proxy)
404
cancel_button.style.display = "block"
405
bbox_pos = None
406
zone.addEventListener("click", make_bbox_proxy)
407
vertical_ruler.style.display = "block"
408
horizontal_ruler.style.display = "block"
409
zone.style.cursor = "crosshair"
410
case "shape-polygon" | "shape-polyline":
411
match shape_type:
412
case "shape-polygon":
413
helper_message.innerText = ("Click on the image to define the points of the polygon, "
414
"press escape to cancel, enter to close, or backspace to "
415
"remove the last point")
416
case "shape-polyline":
417
helper_message.innerText = ("Click on the image to define the points of the polyline, "
418
"press escape to cancel, enter to finish, or backspace to "
419
"remove the last point")
420
421
if not polygon_points and not new_shape:
422
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
423
new_shape.setAttribute("width", "100%")
424
new_shape.setAttribute("height", "100%")
425
zone_rect = zone.getBoundingClientRect()
426
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
427
new_shape.classList.add("shape-container")
428
429
if not polygon_points and int(len(new_shape.children)) == 0:
430
zone.addEventListener("click", make_polygon_proxy)
431
document.addEventListener("keydown", close_polygon_proxy)
432
document.addEventListener("keydown", cancel_polygon_proxy)
433
document.addEventListener("keydown", backspace_polygon_proxy)
434
cancel_button.addEventListener("click", cancel_polygon_proxy)
435
cancel_button.style.display = "block"
436
confirm_button.addEventListener("click", close_polygon_proxy)
437
confirm_button.style.display = "block"
438
backspace_button.addEventListener("click", backspace_polygon_proxy)
439
backspace_button.style.display = "block"
440
match shape_type:
441
case "shape-polygon":
442
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
443
polygon.classList.add("shape-polygon")
444
case "shape-polyline":
445
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polyline")
446
polygon.classList.add("shape-polyline")
447
polygon.setAttribute("fill", "none")
448
polygon.setAttribute("data-object-type", "")
449
polygon.classList.add("shape")
450
new_shape.appendChild(polygon)
451
zone.appendChild(new_shape)
452
zone.style.cursor = "crosshair"
453
case "shape-point":
454
point = document.createElementNS("http://www.w3.org/2000/svg", "circle")
455
zone_rect = zone.getBoundingClientRect()
456
point.setAttribute("cx", str((event.clientX - zone_rect.left) / zone_rect.width * image.naturalWidth))
457
point.setAttribute("cy", str((event.clientY - zone_rect.top) / zone_rect.height * image.naturalHeight))
458
point.setAttribute("r", "0")
459
point.classList.add("shape-point")
460
point.classList.add("shape")
461
point.setAttribute("data-object-type", "")
462
463
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
464
new_shape.setAttribute("width", "100%")
465
new_shape.setAttribute("height", "100%")
466
zone_rect = zone.getBoundingClientRect()
467
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
468
new_shape.classList.add("shape-container")
469
470
new_shape.appendChild(point)
471
zone.appendChild(new_shape)
472
473
new_shape = None
474
475
476
477
for button in list(document.getElementById("shape-selector").children):
478
button.addEventListener("click", create_proxy(switch_shape))
479
print("Shape", button.id, "is available")
480
481
zone.addEventListener("mousemove", follow_cursor_proxy)
482
zone.addEventListener("click", create_proxy(open_shape))
483