picture-annotation.py
Python script, ASCII text executable
1from pyscript import document 2from pyodide.ffi import create_proxy 3 4document.getElementById("shape-options").style.display = "flex" 5 6image = document.getElementById("annotation-image") 7zone = document.getElementById("annotation-zone") 8confirm_button = document.getElementById("annotation-confirm") 9cancel_button = document.getElementById("annotation-cancel") 10 11confirm_button.style.display = "none" 12cancel_button.style.display = "none" 13 14shape_type = "" 15bbox_pos = None 16new_shape = None 17 18 19def follow_cursor(event): 20rect = zone.getBoundingClientRect() 21x = event.clientX - rect.left 22y = event.clientY - rect.top 23vertical_ruler.style.left = str(x) + "px" 24horizontal_ruler.style.top = str(y) + "px" 25 26 27# These are functions usable in JS 28cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event)) 29make_bbox_proxy = create_proxy(lambda event: make_bbox(event)) 30follow_cursor_proxy = create_proxy(follow_cursor) 31 32 33def switch_shape(event): 34global shape_type 35shape = event.currentTarget.id 36shape_type = shape 37print("Shape is now of type:", shape) 38 39vertical_ruler = document.getElementById("annotation-ruler-vertical") 40horizontal_ruler = document.getElementById("annotation-ruler-horizontal") 41vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary") 42horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary") 43helper_message = document.getElementById("annotation-helper-message") 44 45helper_message.innerText = "Select a shape type then click on the image to begin defining it" 46 47 48def cancel_bbox(event): 49global bbox_pos, new_shape 50 51if new_shape is not None and event is not None: 52# Require event so the shape is kept when it ends normally 53new_shape.remove() 54new_shape = None 55zone.removeEventListener("click", make_bbox_proxy) 56document.removeEventListener("keydown", cancel_bbox_proxy) 57cancel_button.removeEventListener("click", cancel_bbox_proxy) 58 59bbox_pos = None 60vertical_ruler.style.display = "none" 61horizontal_ruler.style.display = "none" 62vertical_ruler_2.style.display = "none" 63horizontal_ruler_2.style.display = "none" 64document.body.style.cursor = "auto" 65cancel_button.style.display = "none" 66helper_message.innerText = "Select a shape type then click on the image to begin defining it" 67 68def make_bbox(event): 69global new_shape, bbox_pos 70zone_rect = zone.getBoundingClientRect() 71 72if bbox_pos is None: 73helper_message.innerText = "Now define the second point" 74 75bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width, 76(event.clientY - zone_rect.top) / zone_rect.height] 77vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%" 78horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%" 79vertical_ruler_2.style.display = "block" 80horizontal_ruler_2.style.display = "block" 81 82else: 83x0, y0 = bbox_pos.copy() 84x1 = (event.clientX - zone_rect.left) / zone_rect.width 85y1 = (event.clientY - zone_rect.top) / zone_rect.height 86 87rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect") 88 89new_shape.appendChild(rectangle) 90zone.appendChild(new_shape) 91 92minx = min(x0, x1) 93miny = min(y0, y1) 94maxx = max(x0, x1) 95maxy = max(y0, y1) 96 97rectangle.setAttribute("x", str(minx * image.naturalWidth)) 98rectangle.setAttribute("y", str(miny * image.naturalHeight)) 99rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth)) 100rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight)) 101rectangle.setAttribute("fill", "none") 102rectangle.classList.add("shape-bbox") 103 104cancel_bbox(None) 105 106def open_shape(event): 107global new_shape, bbox_pos 108if bbox_pos: 109return 110print("Creating a new shape of type:", shape_type) 111new_shape = document.createElementNS("http://www.w3.org/2000/svg", "svg") 112new_shape.setAttribute("width", "100%") 113new_shape.setAttribute("height", "100%") 114zone_rect = zone.getBoundingClientRect() 115new_shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}") 116 117if shape_type == "shape-bbox": 118helper_message.innerText = ("Define the first point at the intersection of the lines " 119"by clicking on the image, or click the cross to cancel") 120 121cancel_button.addEventListener("click", cancel_bbox_proxy) 122document.addEventListener("keydown", cancel_bbox_proxy) 123cancel_button.style.display = "block" 124bbox_pos = None 125zone.addEventListener("click", make_bbox_proxy) 126vertical_ruler.style.display = "block" 127horizontal_ruler.style.display = "block" 128document.body.style.cursor = "crosshair" 129 130 131for button in list(document.getElementById("shape-selector").children): 132button.addEventListener("click", create_proxy(switch_shape)) 133print("Shape", button.id, "is available") 134 135 136zone.addEventListener("mousemove", follow_cursor_proxy) 137zone.addEventListener("click", create_proxy(open_shape)) 138