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