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