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 • 16.74 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
else:
225
# Remove event listeners for selection
226
for shape in document.getElementsByClassName("shape"):
227
print("Removing event listener from shape:", shape)
228
shape.removeEventListener("click", select_shape_proxy)
229
image.removeEventListener("click", unselect_shape_proxy)
230
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
231
print("Shape is now of type:", shape)
232
233
234
vertical_ruler = document.getElementById("annotation-ruler-vertical")
235
horizontal_ruler = document.getElementById("annotation-ruler-horizontal")
236
vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary")
237
horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary")
238
helper_message = document.getElementById("annotation-helper-message")
239
240
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
241
242
243
def cancel_bbox(event):
244
global bbox_pos, new_shape
245
246
# Key must be ESCAPE
247
if event is not None and hasattr(event, "key") and event.key != "Escape":
248
return
249
250
if new_shape is not None and event is not None:
251
# Require event so the shape is kept when it ends normally
252
new_shape.remove()
253
zone.removeEventListener("click", make_bbox_proxy)
254
document.removeEventListener("keydown", cancel_bbox_proxy)
255
cancel_button.removeEventListener("click", cancel_bbox_proxy)
256
257
bbox_pos = None
258
vertical_ruler.style.display = "none"
259
horizontal_ruler.style.display = "none"
260
vertical_ruler_2.style.display = "none"
261
horizontal_ruler_2.style.display = "none"
262
zone.style.cursor = "auto"
263
cancel_button.style.display = "none"
264
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
265
new_shape = None
266
267
268
def make_bbox(event):
269
global new_shape, bbox_pos
270
zone_rect = zone.getBoundingClientRect()
271
272
if bbox_pos is None:
273
helper_message.innerText = "Now define the second point"
274
275
bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width,
276
(event.clientY - zone_rect.top) / zone_rect.height]
277
vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%"
278
horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%"
279
vertical_ruler_2.style.display = "block"
280
horizontal_ruler_2.style.display = "block"
281
282
else:
283
x0, y0 = bbox_pos.copy()
284
x1 = (event.clientX - zone_rect.left) / zone_rect.width
285
y1 = (event.clientY - zone_rect.top) / zone_rect.height
286
287
rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect")
288
289
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
290
new_shape.setAttribute("width", "100%")
291
new_shape.setAttribute("height", "100%")
292
zone_rect = zone.getBoundingClientRect()
293
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
294
new_shape.classList.add("shape-container")
295
296
new_shape.appendChild(rectangle)
297
zone.appendChild(new_shape)
298
299
minx = min(x0, x1)
300
miny = min(y0, y1)
301
maxx = max(x0, x1)
302
maxy = max(y0, y1)
303
304
rectangle.setAttribute("x", str(minx * image.naturalWidth))
305
rectangle.setAttribute("y", str(miny * image.naturalHeight))
306
rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth))
307
rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight))
308
rectangle.setAttribute("fill", "none")
309
rectangle.setAttribute("data-object-type", "")
310
rectangle.classList.add("shape-bbox")
311
rectangle.classList.add("shape")
312
313
# Add event listeners to the new shape
314
rectangle.addEventListener("click", select_shape_proxy)
315
316
cancel_bbox(None)
317
318
319
polygon_points = []
320
321
322
def make_polygon(event):
323
global new_shape, polygon_points
324
325
polygon = new_shape.children[0]
326
327
zone_rect = zone.getBoundingClientRect()
328
329
polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width,
330
(event.clientY - zone_rect.top) / zone_rect.height))
331
332
# Update the polygon
333
polygon.setAttribute("points", " ".join(
334
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
335
polygon_points]))
336
337
338
def reset_polygon():
339
global new_shape, polygon_points
340
341
zone.removeEventListener("click", make_polygon_proxy)
342
document.removeEventListener("keydown", close_polygon_proxy)
343
document.removeEventListener("keydown", cancel_polygon_proxy)
344
document.removeEventListener("keydown", backspace_polygon_proxy)
345
confirm_button.style.display = "none"
346
cancel_button.style.display = "none"
347
backspace_button.style.display = "none"
348
confirm_button.removeEventListener("click", close_polygon_proxy)
349
cancel_button.removeEventListener("click", cancel_polygon_proxy)
350
backspace_button.removeEventListener("click", backspace_polygon_proxy)
351
polygon_points.clear()
352
353
zone.style.cursor = "auto"
354
new_shape = None
355
356
357
def close_polygon(event):
358
if event is not None and hasattr(event, "key") and event.key != "Enter":
359
return
360
# Polygon is already there, but we need to remove the events
361
reset_polygon()
362
363
364
def cancel_polygon(event):
365
if event is not None and hasattr(event, "key") and event.key != "Escape":
366
return
367
# Delete the polygon
368
new_shape.remove()
369
reset_polygon()
370
371
372
def backspace_polygon(event):
373
if event is not None and hasattr(event, "key") and event.key != "Backspace":
374
return
375
if not polygon_points:
376
return
377
polygon_points.pop()
378
polygon = new_shape.children[0]
379
polygon.setAttribute("points", " ".join(
380
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
381
polygon_points]))
382
383
384
close_polygon_proxy = create_proxy(close_polygon)
385
cancel_polygon_proxy = create_proxy(cancel_polygon)
386
backspace_polygon_proxy = create_proxy(backspace_polygon)
387
388
389
def open_shape(event):
390
global new_shape, bbox_pos
391
if bbox_pos or shape_type == "select":
392
return
393
print("Creating a new shape of type:", shape_type)
394
395
if shape_type == "shape-bbox":
396
helper_message.innerText = ("Define the first point at the intersection of the lines "
397
"by clicking on the image, or click the cross to cancel")
398
399
cancel_button.addEventListener("click", cancel_bbox_proxy)
400
document.addEventListener("keydown", cancel_bbox_proxy)
401
cancel_button.style.display = "block"
402
bbox_pos = None
403
zone.addEventListener("click", make_bbox_proxy)
404
vertical_ruler.style.display = "block"
405
horizontal_ruler.style.display = "block"
406
zone.style.cursor = "crosshair"
407
elif shape_type == "shape-polygon" or shape_type == "shape-polyline":
408
match shape_type:
409
case "shape-polygon":
410
helper_message.innerText = ("Click on the image to define the points of the polygon, "
411
"press escape to cancel, enter to close, or backspace to "
412
"remove the last point")
413
case "shape-polyline":
414
helper_message.innerText = ("Click on the image to define the points of the polyline, "
415
"press escape to cancel, enter to finish, or backspace to "
416
"remove the last point")
417
418
if not polygon_points and not new_shape:
419
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
420
new_shape.setAttribute("width", "100%")
421
new_shape.setAttribute("height", "100%")
422
zone_rect = zone.getBoundingClientRect()
423
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
424
new_shape.classList.add("shape-container")
425
426
if not polygon_points and int(len(new_shape.children)) == 0:
427
zone.addEventListener("click", make_polygon_proxy)
428
document.addEventListener("keydown", close_polygon_proxy)
429
document.addEventListener("keydown", cancel_polygon_proxy)
430
document.addEventListener("keydown", backspace_polygon_proxy)
431
cancel_button.addEventListener("click", cancel_polygon_proxy)
432
cancel_button.style.display = "block"
433
confirm_button.addEventListener("click", close_polygon_proxy)
434
confirm_button.style.display = "block"
435
backspace_button.addEventListener("click", backspace_polygon_proxy)
436
backspace_button.style.display = "block"
437
match shape_type:
438
case "shape-polygon":
439
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
440
polygon.classList.add("shape-polygon")
441
case "shape-polyline":
442
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polyline")
443
polygon.classList.add("shape-polyline")
444
polygon.setAttribute("fill", "none")
445
polygon.setAttribute("data-object-type", "")
446
polygon.classList.add("shape")
447
new_shape.appendChild(polygon)
448
zone.appendChild(new_shape)
449
zone.style.cursor = "crosshair"
450
451
452
for button in list(document.getElementById("shape-selector").children):
453
button.addEventListener("click", create_proxy(switch_shape))
454
print("Shape", button.id, "is available")
455
456
zone.addEventListener("mousemove", follow_cursor_proxy)
457
zone.addEventListener("click", create_proxy(open_shape))
458