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.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