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