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 • 15.57 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
new_shape = None
254
zone.removeEventListener("click", make_bbox_proxy)
255
document.removeEventListener("keydown", cancel_bbox_proxy)
256
cancel_button.removeEventListener("click", cancel_bbox_proxy)
257
258
bbox_pos = None
259
vertical_ruler.style.display = "none"
260
horizontal_ruler.style.display = "none"
261
vertical_ruler_2.style.display = "none"
262
horizontal_ruler_2.style.display = "none"
263
zone.style.cursor = "auto"
264
cancel_button.style.display = "none"
265
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
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.appendChild(rectangle)
290
zone.appendChild(new_shape)
291
292
minx = min(x0, x1)
293
miny = min(y0, y1)
294
maxx = max(x0, x1)
295
maxy = max(y0, y1)
296
297
rectangle.setAttribute("x", str(minx * image.naturalWidth))
298
rectangle.setAttribute("y", str(miny * image.naturalHeight))
299
rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth))
300
rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight))
301
rectangle.setAttribute("fill", "none")
302
rectangle.setAttribute("data-object-type", "")
303
rectangle.classList.add("shape-bbox")
304
rectangle.classList.add("shape")
305
306
# Add event listeners to the new shape
307
rectangle.addEventListener("click", select_shape_proxy)
308
309
cancel_bbox(None)
310
311
312
polygon_points = []
313
314
315
def make_polygon(event):
316
global new_shape, polygon_points
317
318
print(new_shape.outerHTML)
319
polygon = new_shape.children[0]
320
321
zone_rect = zone.getBoundingClientRect()
322
323
polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width,
324
(event.clientY - zone_rect.top) / zone_rect.height))
325
326
# Update the polygon
327
polygon.setAttribute("points", " ".join(
328
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
329
polygon_points]))
330
331
332
def reset_polygon():
333
zone.removeEventListener("click", make_polygon_proxy)
334
document.removeEventListener("keydown", close_polygon_proxy)
335
document.removeEventListener("keydown", cancel_polygon_proxy)
336
document.removeEventListener("keydown", backspace_polygon_proxy)
337
confirm_button.style.display = "none"
338
cancel_button.style.display = "none"
339
backspace_button.style.display = "none"
340
confirm_button.removeEventListener("click", close_polygon_proxy)
341
cancel_button.removeEventListener("click", cancel_polygon_proxy)
342
backspace_button.removeEventListener("click", backspace_polygon_proxy)
343
polygon_points.clear()
344
345
zone.style.cursor = "auto"
346
347
348
def close_polygon(event):
349
if event is not None and hasattr(event, "key") and event.key != "Enter":
350
return
351
# Polygon is already there, but we need to remove the events
352
reset_polygon()
353
354
355
def cancel_polygon(event):
356
if event is not None and hasattr(event, "key") and event.key != "Escape":
357
return
358
# Delete the polygon
359
new_shape.remove()
360
reset_polygon()
361
362
363
def backspace_polygon(event):
364
if event is not None and hasattr(event, "key") and event.key != "Backspace":
365
return
366
if not polygon_points:
367
return
368
polygon_points.pop()
369
polygon = new_shape.children[0]
370
polygon.setAttribute("points", " ".join(
371
[f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in
372
polygon_points]))
373
374
375
close_polygon_proxy = create_proxy(close_polygon)
376
cancel_polygon_proxy = create_proxy(cancel_polygon)
377
backspace_polygon_proxy = create_proxy(backspace_polygon)
378
379
380
def open_shape(event):
381
global new_shape, bbox_pos
382
if bbox_pos or shape_type == "select":
383
return
384
print("Creating a new shape of type:", shape_type)
385
if not polygon_points:
386
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
387
new_shape.setAttribute("width", "100%")
388
new_shape.setAttribute("height", "100%")
389
zone_rect = zone.getBoundingClientRect()
390
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
391
new_shape.classList.add("shape-container")
392
393
if shape_type == "shape-bbox":
394
helper_message.innerText = ("Define the first point at the intersection of the lines "
395
"by clicking on the image, or click the cross to cancel")
396
397
cancel_button.addEventListener("click", cancel_bbox_proxy)
398
document.addEventListener("keydown", cancel_bbox_proxy)
399
cancel_button.style.display = "block"
400
bbox_pos = None
401
zone.addEventListener("click", make_bbox_proxy)
402
vertical_ruler.style.display = "block"
403
horizontal_ruler.style.display = "block"
404
zone.style.cursor = "crosshair"
405
elif shape_type == "shape-polygon":
406
helper_message.innerText = ("Click on the image to define the points of the polygon, "
407
"press escape to cancel, enter to close, or backspace to "
408
"remove the last point")
409
410
if not polygon_points:
411
zone.addEventListener("click", make_polygon_proxy)
412
document.addEventListener("keydown", close_polygon_proxy)
413
document.addEventListener("keydown", cancel_polygon_proxy)
414
document.addEventListener("keydown", backspace_polygon_proxy)
415
cancel_button.addEventListener("click", cancel_polygon_proxy)
416
cancel_button.style.display = "block"
417
confirm_button.addEventListener("click", close_polygon_proxy)
418
confirm_button.style.display = "block"
419
backspace_button.addEventListener("click", backspace_polygon_proxy)
420
backspace_button.style.display = "block"
421
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
422
polygon.setAttribute("fill", "none")
423
polygon.setAttribute("data-object-type", "")
424
polygon.classList.add("shape-polygon")
425
polygon.classList.add("shape")
426
new_shape.appendChild(polygon)
427
zone.appendChild(new_shape)
428
zone.style.cursor = "crosshair"
429
430
431
for button in list(document.getElementById("shape-selector").children):
432
button.addEventListener("click", create_proxy(switch_shape))
433
print("Shape", button.id, "is available")
434
435
zone.addEventListener("mousemove", follow_cursor_proxy)
436
zone.addEventListener("click", create_proxy(open_shape))
437