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