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 • 12.95 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
15
object_list = document.getElementById("object-types")
16
17
confirm_button.style.display = "none"
18
cancel_button.style.display = "none"
19
backspace_button.style.display = "none"
20
delete_button.style.display = "none"
21
shape_type = ""
22
bbox_pos = None
23
new_shape = None
24
selected_shape = None
25
26
27
async def get_all_objects():
28
response = await pyfetch("/api/object-types")
29
if response.ok:
30
return await response.json()
31
32
33
def follow_cursor(event):
34
rect = zone.getBoundingClientRect()
35
x = event.clientX - rect.left
36
y = event.clientY - rect.top
37
vertical_ruler.style.left = str(x) + "px"
38
horizontal_ruler.style.top = str(y) + "px"
39
40
41
def change_object_type(event):
42
global selected_shape
43
if selected_shape is None:
44
return
45
selected_shape.setAttribute("data-object-type", event.currentTarget.value)
46
47
48
change_object_type_proxy = create_proxy(change_object_type)
49
50
51
async def focus_shape(selected_shape):
52
selected_shape.classList.add("selected")
53
54
objects = await get_all_objects()
55
56
delete_button.style.display = "block"
57
58
object_list.innerHTML = ""
59
60
new_radio = document.createElement("input")
61
new_radio.setAttribute("type", "radio")
62
new_radio.setAttribute("name", "object-type")
63
new_radio.setAttribute("value", "")
64
new_label = document.createElement("label")
65
new_label.appendChild(new_radio)
66
new_label.append("Undefined")
67
object_list.appendChild(new_label)
68
new_radio.addEventListener("change", change_object_type_proxy)
69
70
selected_object = selected_shape.getAttribute("data-object-type")
71
if not selected_object:
72
new_radio.setAttribute("checked", "")
73
74
for object, description in objects.items():
75
new_radio = document.createElement("input")
76
new_radio.setAttribute("type", "radio")
77
new_radio.setAttribute("name", "object-type")
78
new_radio.setAttribute("value", object)
79
if selected_object == object:
80
new_radio.setAttribute("checked", "")
81
new_label = document.createElement("label")
82
new_label.appendChild(new_radio)
83
new_label.append(object)
84
object_list.appendChild(new_label)
85
new_radio.addEventListener("change", change_object_type_proxy)
86
87
88
async def select_shape(event):
89
global selected_shape
90
if shape_type != "select":
91
return
92
if selected_shape is not None:
93
selected_shape.classList.remove("selected")
94
95
print("Selected shape:", event.target)
96
97
selected_shape = event.target
98
99
await focus_shape(selected_shape)
100
101
102
def unselect_shape(event):
103
global selected_shape
104
105
if selected_shape is not None:
106
selected_shape.classList.remove("selected")
107
selected_shape = None
108
109
object_list.innerHTML = ""
110
delete_button.style.display = "none"
111
112
113
def delete_shape(event):
114
global selected_shape
115
if selected_shape is None:
116
return
117
# Shape is SVG shape inside SVG so we need to remove the parent SVG
118
selected_shape.parentNode.remove()
119
selected_shape = None
120
object_list.innerHTML = ""
121
delete_button.style.display = "none"
122
123
124
select_shape_proxy = create_proxy(select_shape)
125
unselect_shape_proxy = create_proxy(unselect_shape)
126
delete_shape_proxy = create_proxy(delete_shape)
127
128
delete_button.addEventListener("click", delete_shape_proxy)
129
130
131
# These are functions usable in JS
132
cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event))
133
make_bbox_proxy = create_proxy(lambda event: make_bbox(event))
134
make_polygon_proxy = create_proxy(lambda event: make_polygon(event))
135
follow_cursor_proxy = create_proxy(follow_cursor)
136
137
138
def switch_shape(event):
139
global shape_type
140
object_list.innerHTML = ""
141
unselect_shape(None)
142
shape = event.currentTarget.id
143
shape_type = shape
144
if shape_type == "select":
145
# Add event listeners to existing shapes
146
print(len(list(document.getElementsByClassName("shape"))), "shapes found")
147
for shape in document.getElementsByClassName("shape"):
148
print("Adding event listener to shape:", shape)
149
shape.addEventListener("click", select_shape_proxy)
150
image.addEventListener("click", unselect_shape_proxy)
151
helper_message.innerText = "Click on a shape to select"
152
else:
153
# Remove event listeners for selection
154
for shape in document.getElementsByClassName("shape"):
155
print("Removing event listener from shape:", shape)
156
shape.removeEventListener("click", select_shape_proxy)
157
image.removeEventListener("click", unselect_shape_proxy)
158
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
159
print("Shape is now of type:", shape)
160
161
vertical_ruler = document.getElementById("annotation-ruler-vertical")
162
horizontal_ruler = document.getElementById("annotation-ruler-horizontal")
163
vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary")
164
horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary")
165
helper_message = document.getElementById("annotation-helper-message")
166
167
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
168
169
170
def cancel_bbox(event):
171
global bbox_pos, new_shape
172
173
# Key must be ESCAPE
174
if event is not None and hasattr(event, "key") and event.key != "Escape":
175
return
176
177
if new_shape is not None and event is not None:
178
# Require event so the shape is kept when it ends normally
179
new_shape.remove()
180
new_shape = None
181
zone.removeEventListener("click", make_bbox_proxy)
182
document.removeEventListener("keydown", cancel_bbox_proxy)
183
cancel_button.removeEventListener("click", cancel_bbox_proxy)
184
185
bbox_pos = None
186
vertical_ruler.style.display = "none"
187
horizontal_ruler.style.display = "none"
188
vertical_ruler_2.style.display = "none"
189
horizontal_ruler_2.style.display = "none"
190
zone.style.cursor = "auto"
191
cancel_button.style.display = "none"
192
helper_message.innerText = "Select a shape type then click on the image to begin defining it"
193
194
def make_bbox(event):
195
global new_shape, bbox_pos
196
zone_rect = zone.getBoundingClientRect()
197
198
if bbox_pos is None:
199
helper_message.innerText = "Now define the second point"
200
201
bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width,
202
(event.clientY - zone_rect.top) / zone_rect.height]
203
vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%"
204
horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%"
205
vertical_ruler_2.style.display = "block"
206
horizontal_ruler_2.style.display = "block"
207
208
else:
209
x0, y0 = bbox_pos.copy()
210
x1 = (event.clientX - zone_rect.left) / zone_rect.width
211
y1 = (event.clientY - zone_rect.top) / zone_rect.height
212
213
rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect")
214
215
new_shape.appendChild(rectangle)
216
zone.appendChild(new_shape)
217
218
minx = min(x0, x1)
219
miny = min(y0, y1)
220
maxx = max(x0, x1)
221
maxy = max(y0, y1)
222
223
rectangle.setAttribute("x", str(minx * image.naturalWidth))
224
rectangle.setAttribute("y", str(miny * image.naturalHeight))
225
rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth))
226
rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight))
227
rectangle.setAttribute("fill", "none")
228
rectangle.setAttribute("data-object-type", "")
229
rectangle.classList.add("shape-bbox")
230
rectangle.classList.add("shape")
231
232
# Add event listeners to the new shape
233
rectangle.addEventListener("click", select_shape_proxy)
234
235
cancel_bbox(None)
236
237
238
polygon_points = []
239
240
def make_polygon(event):
241
global new_shape, polygon_points
242
243
print(new_shape.outerHTML)
244
polygon = new_shape.children[0]
245
246
zone_rect = zone.getBoundingClientRect()
247
248
polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width,
249
(event.clientY - zone_rect.top) / zone_rect.height))
250
251
# Update the polygon
252
polygon.setAttribute("points", " ".join([f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in polygon_points]))
253
254
255
def reset_polygon():
256
zone.removeEventListener("click", make_polygon_proxy)
257
document.removeEventListener("keydown", close_polygon_proxy)
258
document.removeEventListener("keydown", cancel_polygon_proxy)
259
document.removeEventListener("keydown", backspace_polygon_proxy)
260
confirm_button.style.display = "none"
261
cancel_button.style.display = "none"
262
backspace_button.style.display = "none"
263
confirm_button.removeEventListener("click", close_polygon_proxy)
264
cancel_button.removeEventListener("click", cancel_polygon_proxy)
265
backspace_button.removeEventListener("click", backspace_polygon_proxy)
266
polygon_points.clear()
267
268
zone.style.cursor = "auto"
269
270
271
def close_polygon(event):
272
if event is not None and hasattr(event, "key") and event.key != "Enter":
273
return
274
# Polygon is already there, but we need to remove the events
275
reset_polygon()
276
277
278
def cancel_polygon(event):
279
if event is not None and hasattr(event, "key") and event.key != "Escape":
280
return
281
# Delete the polygon
282
new_shape.remove()
283
reset_polygon()
284
285
286
def backspace_polygon(event):
287
if event is not None and hasattr(event, "key") and event.key != "Backspace":
288
return
289
if not polygon_points:
290
return
291
polygon_points.pop()
292
polygon = new_shape.children[0]
293
polygon.setAttribute("points", " ".join([f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in polygon_points]))
294
295
296
close_polygon_proxy = create_proxy(close_polygon)
297
cancel_polygon_proxy = create_proxy(cancel_polygon)
298
backspace_polygon_proxy = create_proxy(backspace_polygon)
299
300
301
def open_shape(event):
302
global new_shape, bbox_pos
303
if bbox_pos or shape_type == "select":
304
return
305
print("Creating a new shape of type:", shape_type)
306
if not polygon_points:
307
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
308
new_shape.setAttribute("width", "100%")
309
new_shape.setAttribute("height", "100%")
310
zone_rect = zone.getBoundingClientRect()
311
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
312
new_shape.classList.add("shape-container")
313
314
if shape_type == "shape-bbox":
315
helper_message.innerText = ("Define the first point at the intersection of the lines "
316
"by clicking on the image, or click the cross to cancel")
317
318
cancel_button.addEventListener("click", cancel_bbox_proxy)
319
document.addEventListener("keydown", cancel_bbox_proxy)
320
cancel_button.style.display = "block"
321
bbox_pos = None
322
zone.addEventListener("click", make_bbox_proxy)
323
vertical_ruler.style.display = "block"
324
horizontal_ruler.style.display = "block"
325
zone.style.cursor = "crosshair"
326
elif shape_type == "shape-polygon":
327
helper_message.innerText = ("Click on the image to define the points of the polygon, "
328
"press escape to cancel, enter to close, or backspace to "
329
"remove the last point")
330
331
if not polygon_points:
332
zone.addEventListener("click", make_polygon_proxy)
333
document.addEventListener("keydown", close_polygon_proxy)
334
document.addEventListener("keydown", cancel_polygon_proxy)
335
document.addEventListener("keydown", backspace_polygon_proxy)
336
cancel_button.addEventListener("click", cancel_polygon_proxy)
337
cancel_button.style.display = "block"
338
confirm_button.addEventListener("click", close_polygon_proxy)
339
confirm_button.style.display = "block"
340
backspace_button.addEventListener("click", backspace_polygon_proxy)
341
backspace_button.style.display = "block"
342
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
343
polygon.setAttribute("fill", "none")
344
polygon.setAttribute("data-object-type", "")
345
polygon.classList.add("shape-polygon")
346
polygon.classList.add("shape")
347
new_shape.appendChild(polygon)
348
zone.appendChild(new_shape)
349
zone.style.cursor = "crosshair"
350
351
352
for button in list(document.getElementById("shape-selector").children):
353
button.addEventListener("click", create_proxy(switch_shape))
354
print("Shape", button.id, "is available")
355
356
357
zone.addEventListener("mousemove", follow_cursor_proxy)
358
zone.addEventListener("click", create_proxy(open_shape))
359