Web platform for sharing free image data for ML and research

Homepage: https://datasets.roundabout-host.com

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