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