picture-annotation.py
Python script, ASCII text executable
1from pyscript import document 2from pyodide.ffi import create_proxy 3from pyodide.http import pyfetch 4import asyncio 5 6document.getElementById("shape-options").style.display = "flex" 7 8image = document.getElementById("annotation-image") 9zone = document.getElementById("annotation-zone") 10confirm_button = document.getElementById("annotation-confirm") 11cancel_button = document.getElementById("annotation-cancel") 12object_list = document.getElementById("object-types") 13 14confirm_button.style.display = "none" 15cancel_button.style.display = "none" 16 17shape_type = "" 18bbox_pos = None 19new_shape = None 20selected_shape = None 21 22 23async def get_all_objects(): 24response = await pyfetch("/api/object-types") 25if response.ok: 26return await response.json() 27 28 29def follow_cursor(event): 30rect = zone.getBoundingClientRect() 31x = event.clientX - rect.left 32y = event.clientY - rect.top 33vertical_ruler.style.left = str(x) + "px" 34horizontal_ruler.style.top = str(y) + "px" 35 36 37def change_object_type(event): 38global selected_shape 39if selected_shape is None: 40return 41selected_shape.setAttribute("data-object-type", event.currentTarget.value) 42 43 44change_object_type_proxy = create_proxy(change_object_type) 45 46 47async def select_shape(event): 48global selected_shape 49if shape_type != "select": 50return 51if selected_shape is not None: 52selected_shape.classList.remove("selected") 53 54print("Selected shape:", event.target) 55 56selected_shape = event.target 57selected_shape.classList.add("selected") 58 59objects = await get_all_objects() 60 61object_list.innerHTML = "" 62 63new_radio = document.createElement("input") 64new_radio.setAttribute("type", "radio") 65new_radio.setAttribute("name", "object-type") 66new_radio.setAttribute("value", "") 67new_label = document.createElement("label") 68new_label.appendChild(new_radio) 69new_label.append("Undefined") 70object_list.appendChild(new_label) 71new_radio.addEventListener("change", change_object_type_proxy) 72 73for object, description in objects.items(): 74new_radio = document.createElement("input") 75new_radio.setAttribute("type", "radio") 76new_radio.setAttribute("name", "object-type") 77new_radio.setAttribute("value", object) 78new_label = document.createElement("label") 79new_label.appendChild(new_radio) 80new_label.append(object) 81object_list.appendChild(new_label) 82new_radio.addEventListener("change", change_object_type_proxy) 83 84print(objects) 85 86 87def unselect_shape(event): 88global selected_shape 89 90if selected_shape is not None: 91selected_shape.classList.remove("selected") 92selected_shape = None 93 94 95select_shape_proxy = create_proxy(select_shape) 96unselect_shape_proxy = create_proxy(unselect_shape) 97 98 99# These are functions usable in JS 100cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event)) 101make_bbox_proxy = create_proxy(lambda event: make_bbox(event)) 102follow_cursor_proxy = create_proxy(follow_cursor) 103 104 105def switch_shape(event): 106global shape_type 107object_list.innerHTML = "" 108unselect_shape(None) 109shape = event.currentTarget.id 110shape_type = shape 111if shape_type == "select": 112# Add event listeners to existing shapes 113print(len(list(document.getElementsByClassName("shape"))), "shapes found") 114for shape in document.getElementsByClassName("shape"): 115print("Adding event listener to shape:", shape) 116shape.addEventListener("click", select_shape_proxy) 117image.addEventListener("click", unselect_shape_proxy) 118helper_message.innerText = "Click on a shape to select" 119else: 120# Remove event listeners for selection 121for shape in document.getElementsByClassName("shape"): 122print("Removing event listener from shape:", shape) 123shape.removeEventListener("click", select_shape_proxy) 124image.removeEventListener("click", unselect_shape_proxy) 125helper_message.innerText = "Select a shape type then click on the image to begin defining it" 126print("Shape is now of type:", shape) 127 128vertical_ruler = document.getElementById("annotation-ruler-vertical") 129horizontal_ruler = document.getElementById("annotation-ruler-horizontal") 130vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary") 131horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary") 132helper_message = document.getElementById("annotation-helper-message") 133 134helper_message.innerText = "Select a shape type then click on the image to begin defining it" 135 136 137def cancel_bbox(event): 138global bbox_pos, new_shape 139 140if new_shape is not None and event is not None: 141# Require event so the shape is kept when it ends normally 142new_shape.remove() 143new_shape = None 144zone.removeEventListener("click", make_bbox_proxy) 145document.removeEventListener("keydown", cancel_bbox_proxy) 146cancel_button.removeEventListener("click", cancel_bbox_proxy) 147 148bbox_pos = None 149vertical_ruler.style.display = "none" 150horizontal_ruler.style.display = "none" 151vertical_ruler_2.style.display = "none" 152horizontal_ruler_2.style.display = "none" 153document.body.style.cursor = "auto" 154cancel_button.style.display = "none" 155helper_message.innerText = "Select a shape type then click on the image to begin defining it" 156 157def make_bbox(event): 158global new_shape, bbox_pos 159zone_rect = zone.getBoundingClientRect() 160 161if bbox_pos is None: 162helper_message.innerText = "Now define the second point" 163 164bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width, 165(event.clientY - zone_rect.top) / zone_rect.height] 166vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%" 167horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%" 168vertical_ruler_2.style.display = "block" 169horizontal_ruler_2.style.display = "block" 170 171else: 172x0, y0 = bbox_pos.copy() 173x1 = (event.clientX - zone_rect.left) / zone_rect.width 174y1 = (event.clientY - zone_rect.top) / zone_rect.height 175 176rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect") 177 178new_shape.appendChild(rectangle) 179zone.appendChild(new_shape) 180 181minx = min(x0, x1) 182miny = min(y0, y1) 183maxx = max(x0, x1) 184maxy = max(y0, y1) 185 186rectangle.setAttribute("x", str(minx * image.naturalWidth)) 187rectangle.setAttribute("y", str(miny * image.naturalHeight)) 188rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth)) 189rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight)) 190rectangle.setAttribute("fill", "none") 191rectangle.setAttribute("data-object-type", "") 192rectangle.classList.add("shape-bbox") 193rectangle.classList.add("shape") 194 195# Add event listeners to the new shape 196rectangle.addEventListener("click", select_shape_proxy) 197 198cancel_bbox(None) 199 200def open_shape(event): 201global new_shape, bbox_pos 202if bbox_pos or shape_type == "select": 203return 204print("Creating a new shape of type:", shape_type) 205new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg") 206new_shape.setAttribute("width", "100%") 207new_shape.setAttribute("height", "100%") 208zone_rect = zone.getBoundingClientRect() 209new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}") 210new_shape.classList.add("shape-container") 211 212if shape_type == "shape-bbox": 213helper_message.innerText = ("Define the first point at the intersection of the lines " 214"by clicking on the image, or click the cross to cancel") 215 216cancel_button.addEventListener("click", cancel_bbox_proxy) 217document.addEventListener("keydown", cancel_bbox_proxy) 218cancel_button.style.display = "block" 219bbox_pos = None 220zone.addEventListener("click", make_bbox_proxy) 221vertical_ruler.style.display = "block" 222horizontal_ruler.style.display = "block" 223document.body.style.cursor = "crosshair" 224 225 226for button in list(document.getElementById("shape-selector").children): 227button.addEventListener("click", create_proxy(switch_shape)) 228print("Shape", button.id, "is available") 229 230 231zone.addEventListener("mousemove", follow_cursor_proxy) 232zone.addEventListener("click", create_proxy(open_shape)) 233