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.57 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
confirm_button.removeEventListener("click", close_polygon_proxy)
243
cancel_button.removeEventListener("click", cancel_polygon_proxy)
244
polygon_points.clear()
245
246
document.body.style.cursor = "auto"
247
248
249
def cancel_polygon(event):
250
if event is not None and hasattr(event, "key") and event.key != "Escape":
251
return
252
# Delete the polygon
253
new_shape.remove()
254
zone.removeEventListener("click", make_polygon_proxy)
255
document.removeEventListener("keydown", close_polygon_proxy)
256
document.removeEventListener("keydown", cancel_polygon_proxy)
257
confirm_button.style.display = "none"
258
cancel_button.style.display = "none"
259
confirm_button.removeEventListener("click", close_polygon_proxy)
260
cancel_button.removeEventListener("click", cancel_polygon_proxy)
261
262
polygon_points.clear()
263
264
document.body.style.cursor = "auto"
265
266
267
def backspace_polygon(event):
268
if event is not None and hasattr(event, "key") and event.key != "Backspace":
269
return
270
if not polygon_points:
271
return
272
polygon_points.pop()
273
polygon = new_shape.children[0]
274
polygon.setAttribute("points", " ".join([f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in polygon_points]))
275
276
277
close_polygon_proxy = create_proxy(close_polygon)
278
cancel_polygon_proxy = create_proxy(cancel_polygon)
279
backspace_polygon_proxy = create_proxy(backspace_polygon)
280
281
282
def open_shape(event):
283
global new_shape, bbox_pos
284
if bbox_pos or shape_type == "select":
285
return
286
print("Creating a new shape of type:", shape_type)
287
if not polygon_points:
288
new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg")
289
new_shape.setAttribute("width", "100%")
290
new_shape.setAttribute("height", "100%")
291
zone_rect = zone.getBoundingClientRect()
292
new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}")
293
new_shape.classList.add("shape-container")
294
295
if shape_type == "shape-bbox":
296
helper_message.innerText = ("Define the first point at the intersection of the lines "
297
"by clicking on the image, or click the cross to cancel")
298
299
cancel_button.addEventListener("click", cancel_bbox_proxy)
300
document.addEventListener("keydown", cancel_bbox_proxy)
301
cancel_button.style.display = "block"
302
bbox_pos = None
303
zone.addEventListener("click", make_bbox_proxy)
304
vertical_ruler.style.display = "block"
305
horizontal_ruler.style.display = "block"
306
document.body.style.cursor = "crosshair"
307
elif shape_type == "shape-polygon":
308
helper_message.innerText = ("Click on the image to define the points of the polygon, "
309
"press escape to cancel, enter to close, or backspace to "
310
"remove the last point")
311
312
if not polygon_points:
313
zone.addEventListener("click", make_polygon_proxy)
314
document.addEventListener("keydown", close_polygon_proxy)
315
document.addEventListener("keydown", cancel_polygon_proxy)
316
document.addEventListener("keydown", backspace_polygon_proxy)
317
cancel_button.addEventListener("click", cancel_polygon_proxy)
318
cancel_button.style.display = "block"
319
confirm_button.addEventListener("click", close_polygon_proxy)
320
confirm_button.style.display = "block"
321
backspace_button.addEventListener("click", backspace_polygon_proxy)
322
backspace_button.style.display = "block"
323
polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
324
polygon.setAttribute("fill", "none")
325
polygon.setAttribute("data-object-type", "")
326
polygon.classList.add("shape-polygon")
327
polygon.classList.add("shape")
328
new_shape.appendChild(polygon)
329
zone.appendChild(new_shape)
330
document.body.style.cursor = "crosshair"
331
332
333
for button in list(document.getElementById("shape-selector").children):
334
button.addEventListener("click", create_proxy(switch_shape))
335
print("Shape", button.id, "is available")
336
337
338
zone.addEventListener("mousemove", follow_cursor_proxy)
339
zone.addEventListener("click", create_proxy(open_shape))
340