picture-annotation.py
    
    Python script, ASCII text executable
        
            1
            from pyscript import document, fetch as pyfetch 
        
            2
            from pyscript.ffi import create_proxy 
        
            3
            import asyncio 
        
            4
            import json 
        
            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
            delete_button = document.getElementById("annotation-delete") 
        
            14
            previous_button = document.getElementById("annotation-previous") 
        
            15
            next_button = document.getElementById("annotation-next") 
        
            16
            save_button = document.getElementById("annotation-save") 
        
            17
             
        
            18
            object_list = document.getElementById("object-types") 
        
            19
             
        
            20
            confirm_button.style.display = "none" 
        
            21
            cancel_button.style.display = "none" 
        
            22
            backspace_button.style.display = "none" 
        
            23
            delete_button.style.display = "none" 
        
            24
            previous_button.style.display = "none" 
        
            25
            next_button.style.display = "none" 
        
            26
            shape_type = "" 
        
            27
            bbox_pos = None 
        
            28
            new_shape = None 
        
            29
            selected_shape = None 
        
            30
             
        
            31
             
        
            32
            def make_shape_container(): 
        
            33
                shape = document.createElementNS("http://www.w3.org/2000/svg", "svg") 
        
            34
                shape.setAttribute("width", "100%") 
        
            35
                shape.setAttribute("height", "100%") 
        
            36
                shape.setAttribute("viewBox", f"0 0 {image.naturalWidth} {image.naturalHeight}") 
        
            37
                shape.classList.add("shape-container") 
        
            38
             
        
            39
                return shape 
        
            40
             
        
            41
             
        
            42
            async def get_all_objects(): 
        
            43
                response = await pyfetch("/api/object-types") 
        
            44
                if response.ok: 
        
            45
                    return await response.json() 
        
            46
             
        
            47
             
        
            48
            def follow_cursor(event): 
        
            49
                rect = zone.getBoundingClientRect() 
        
            50
                x = event.clientX - rect.left 
        
            51
                y = event.clientY - rect.top 
        
            52
                vertical_ruler.style.left = str(x) + "px" 
        
            53
                horizontal_ruler.style.top = str(y) + "px" 
        
            54
             
        
            55
             
        
            56
            def change_object_type(event): 
        
            57
                global selected_shape 
        
            58
                if selected_shape is None: 
        
            59
                    return 
        
            60
                selected_shape.setAttribute("data-object-type", event.currentTarget.value) 
        
            61
             
        
            62
             
        
            63
            change_object_type_proxy = create_proxy(change_object_type) 
        
            64
             
        
            65
             
        
            66
            def list_shapes(): 
        
            67
                shapes = list(zone.getElementsByClassName("shape")) 
        
            68
                json_shapes = [] 
        
            69
                for shape in shapes: 
        
            70
                    shape_dict = {} 
        
            71
                    if shape.tagName == "rect": 
        
            72
                        shape_dict["type"] = "bbox" 
        
            73
                        shape_dict["shape"] = { 
        
            74
                            "x": float(shape.getAttribute("x")) / image.naturalWidth, 
        
            75
                            "y": float(shape.getAttribute("y")) / image.naturalHeight, 
        
            76
                            "w": float(shape.getAttribute("width")) / image.naturalWidth, 
        
            77
                            "h": float(shape.getAttribute("height")) / image.naturalHeight 
        
            78
                        } 
        
            79
                    elif shape.tagName == "polygon" or shape.tagName == "polyline": 
        
            80
                        if shape.tagName == "polygon": 
        
            81
                            shape_dict["type"] = "polygon" 
        
            82
                        elif shape.tagName == "polyline": 
        
            83
                            shape_dict["type"] = "polyline" 
        
            84
             
        
            85
                        points = shape.getAttribute("points").split(" ") 
        
            86
                        json_points = [] 
        
            87
                        for point in points: 
        
            88
                            x, y = point.split(",") 
        
            89
                            x, y = float(x), float(y) 
        
            90
                            json_points.append({ 
        
            91
                                "x": x / image.naturalWidth, 
        
            92
                                "y": y / image.naturalHeight 
        
            93
                            }) 
        
            94
             
        
            95
                        shape_dict["shape"] = json_points 
        
            96
                    elif shape.tagName == "circle" and shape.classList.contains("shape-point"): 
        
            97
                        shape_dict["type"] = "point" 
        
            98
                        shape_dict["shape"] = { 
        
            99
                            "x": float(shape.getAttribute("cx")) / image.naturalWidth, 
        
            100
                            "y": float(shape.getAttribute("cy")) / image.naturalHeight 
        
            101
                        } 
        
            102
                    else: 
        
            103
                        continue 
        
            104
             
        
            105
                    shape_dict["object"] = shape.getAttribute("data-object-type") 
        
            106
                    json_shapes.append(shape_dict) 
        
            107
             
        
            108
                return json_shapes 
        
            109
             
        
            110
             
        
            111
            def put_shapes(json_shapes): 
        
            112
                for shape in json_shapes: 
        
            113
                    new_shape = make_shape_container() 
        
            114
                    zone_rect = zone.getBoundingClientRect() 
        
            115
             
        
            116
                    if shape["type"] == "bbox": 
        
            117
                        rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect") 
        
            118
                        rectangle.setAttribute("x", str(shape["shape"]["x"] * image.naturalWidth)) 
        
            119
                        rectangle.setAttribute("y", str(shape["shape"]["y"] * image.naturalHeight)) 
        
            120
                        rectangle.setAttribute("width", str(shape["shape"]["w"] * image.naturalWidth)) 
        
            121
                        rectangle.setAttribute("height", str(shape["shape"]["h"] * image.naturalHeight)) 
        
            122
                        rectangle.setAttribute("fill", "none") 
        
            123
                        rectangle.setAttribute("data-object-type", shape["object"] or "") 
        
            124
                        rectangle.classList.add("shape-bbox") 
        
            125
                        rectangle.classList.add("shape") 
        
            126
                        new_shape.appendChild(rectangle) 
        
            127
                    elif shape["type"] == "polygon" or shape["type"] == "polyline": 
        
            128
                        polygon = document.createElementNS("http://www.w3.org/2000/svg", shape["type"]) 
        
            129
                        points = " ".join( 
        
            130
                            [f"{point['x'] * image.naturalWidth},{point['y'] * image.naturalHeight}" for point in shape["shape"]]) 
        
            131
                        polygon.setAttribute("points", points) 
        
            132
                        polygon.setAttribute("fill", "none") 
        
            133
                        polygon.setAttribute("data-object-type", shape["object"] or "") 
        
            134
                        polygon.classList.add(f"shape-{shape['type']}") 
        
            135
                        polygon.classList.add("shape") 
        
            136
                        new_shape.appendChild(polygon) 
        
            137
                    elif shape["type"] == "point": 
        
            138
                        point = document.createElementNS("http://www.w3.org/2000/svg", "circle") 
        
            139
                        point.setAttribute("cx", str(shape["shape"]["x"] * image.naturalWidth)) 
        
            140
                        point.setAttribute("cy", str(shape["shape"]["y"] * image.naturalHeight)) 
        
            141
                        point.setAttribute("r", "0") 
        
            142
                        point.classList.add("shape-point") 
        
            143
                        point.classList.add("shape") 
        
            144
                        point.setAttribute("data-object-type", shape["object"] or "") 
        
            145
                        new_shape.appendChild(point) 
        
            146
             
        
            147
                    zone.appendChild(new_shape) 
        
            148
             
        
            149
             
        
            150
            async def load_shapes(): 
        
            151
                resource_id = document.getElementById("resource-id").value 
        
            152
                response = await pyfetch(f"/picture/{resource_id}/get-annotations") 
        
            153
                if response.ok: 
        
            154
                    shapes = await response.json() 
        
            155
                    return shapes 
        
            156
             
        
            157
             
        
            158
            async def save_shapes(event): 
        
            159
                shapes = list_shapes() 
        
            160
                resource_id = document.getElementById("resource-id").value 
        
            161
                print("Saving shapes:", shapes) 
        
            162
                response = await pyfetch(f"/picture/{resource_id}/save-annotations", 
        
            163
                    method="POST", 
        
            164
                    headers={ 
        
            165
                        "Content-Type": "application/json" 
        
            166
                    }, 
        
            167
                    body=json.dumps(shapes) 
        
            168
                ) 
        
            169
                if response.ok: 
        
            170
                    return await response 
        
            171
             
        
            172
             
        
            173
            save_shapes_proxy = create_proxy(save_shapes) 
        
            174
            save_button.addEventListener("click", save_shapes_proxy) 
        
            175
            delete_shape_key_proxy = create_proxy(lambda event: event.key == "Delete" and delete_shape_proxy(None)) 
        
            176
            next_shape_key_proxy = create_proxy(lambda event: event.key == "ArrowRight" and next_shape_proxy(None)) 
        
            177
            previous_shape_key_proxy = create_proxy(lambda event: event.key == "ArrowLeft" and previous_shape_proxy(None)) 
        
            178
             
        
            179
             
        
            180
            def get_centre(shape): 
        
            181
                if shape.tagName == "rect": 
        
            182
                    x = float(shape.getAttribute("x")) + float(shape.getAttribute("width")) / 2 
        
            183
                    y = float(shape.getAttribute("y")) + float(shape.getAttribute("height")) / 2 
        
            184
                elif shape.tagName == "polygon": 
        
            185
                    points = shape.getAttribute("points").split(" ") 
        
            186
                    # Average of the extreme points 
        
            187
                    sorted_x = sorted([float(point.split(",")[0]) for point in points]) 
        
            188
                    sorted_y = sorted([float(point.split(",")[1]) for point in points]) 
        
            189
                    top = sorted_y[0] 
        
            190
                    bottom = sorted_y[-1] 
        
            191
                    left = sorted_x[0] 
        
            192
                    right = sorted_x[-1] 
        
            193
             
        
            194
                    x = (left + right) / 2 
        
            195
                    y = (top + bottom) / 2 
        
            196
                elif shape.tagName == "polyline": 
        
            197
                    points = shape.getAttribute("points").split(" ") 
        
            198
                    # Median point 
        
            199
                    x = float(points[len(points) // 2].split(",")[0]) 
        
            200
                    y = float(points[len(points) // 2].split(",")[1]) 
        
            201
                elif shape.tagName == "circle" and shape.classList.contains("shape-point"): 
        
            202
                    x = float(shape.getAttribute("cx")) 
        
            203
                    y = float(shape.getAttribute("cy")) 
        
            204
                else: 
        
            205
                    return None 
        
            206
             
        
            207
                return x, y 
        
            208
             
        
            209
             
        
            210
            async def focus_shape(shape): 
        
            211
                global selected_shape 
        
            212
             
        
            213
                if shape_type != "select": 
        
            214
                    return 
        
            215
                if selected_shape is not None: 
        
            216
                    selected_shape.classList.remove("selected") 
        
            217
             
        
            218
                selected_shape = shape 
        
            219
             
        
            220
                selected_shape.classList.add("selected") 
        
            221
             
        
            222
                objects = await get_all_objects() 
        
            223
             
        
            224
                delete_button.style.display = "block" 
        
            225
                next_button.style.display = "block" 
        
            226
                previous_button.style.display = "block" 
        
            227
                document.addEventListener("keydown", delete_shape_key_proxy) 
        
            228
                document.addEventListener("keydown", next_shape_key_proxy) 
        
            229
                document.addEventListener("keydown", previous_shape_key_proxy) 
        
            230
             
        
            231
                object_list.style.display = "flex" 
        
            232
                object_list.style.left = str(get_centre(shape)[0] / image.naturalWidth * 100) + "%" 
        
            233
                object_list.style.top = str(get_centre(shape)[1] / image.naturalHeight * 100) + "%" 
        
            234
             
        
            235
                object_list.innerHTML = "" 
        
            236
             
        
            237
                new_radio = document.createElement("input") 
        
            238
                new_radio.setAttribute("type", "radio") 
        
            239
                new_radio.setAttribute("name", "object-type") 
        
            240
                new_radio.setAttribute("value", "") 
        
            241
                new_label = document.createElement("label") 
        
            242
                new_label.appendChild(new_radio) 
        
            243
                new_label.append("Undefined") 
        
            244
                object_list.appendChild(new_label) 
        
            245
                new_radio.addEventListener("change", change_object_type_proxy) 
        
            246
             
        
            247
                selected_object = selected_shape.getAttribute("data-object-type") 
        
            248
                if not selected_object: 
        
            249
                    new_radio.setAttribute("checked", "") 
        
            250
             
        
            251
                for object, description in objects.items(): 
        
            252
                    new_radio = document.createElement("input") 
        
            253
                    new_radio.setAttribute("type", "radio") 
        
            254
                    new_radio.setAttribute("name", "object-type") 
        
            255
                    new_radio.setAttribute("value", object) 
        
            256
                    if selected_object == object: 
        
            257
                        new_radio.setAttribute("checked", "") 
        
            258
                    new_label = document.createElement("label") 
        
            259
                    new_label.appendChild(new_radio) 
        
            260
                    new_label.append(object) 
        
            261
                    object_list.appendChild(new_label) 
        
            262
                    new_radio.addEventListener("change", change_object_type_proxy) 
        
            263
             
        
            264
             
        
            265
            async def select_shape(event): 
        
            266
                await focus_shape(event.target) 
        
            267
             
        
            268
             
        
            269
            async def next_shape(event): 
        
            270
                global selected_shape 
        
            271
                if selected_shape is None: 
        
            272
                    return 
        
            273
             
        
            274
                selected_svg = selected_shape.parentNode 
        
            275
             
        
            276
                while selected_svg is not None: 
        
            277
                    next_sibling = selected_svg.nextElementSibling 
        
            278
                    if next_sibling and next_sibling.classList.contains("shape-container"): 
        
            279
                        selected_svg = next_sibling 
        
            280
                        break 
        
            281
                    elif next_sibling is None: 
        
            282
                        # If no more siblings, loop back to the first child 
        
            283
                        selected_svg = selected_svg.parentNode.firstElementChild 
        
            284
                        while selected_svg is not None and not selected_svg.classList.contains( 
        
            285
                                "shape-container"): 
        
            286
                            selected_svg = selected_svg.nextElementSibling 
        
            287
                        break 
        
            288
                    else: 
        
            289
                        selected_svg = next_sibling 
        
            290
             
        
            291
                if selected_svg: 
        
            292
                    shape = selected_svg.firstElementChild 
        
            293
                    await focus_shape(shape) 
        
            294
             
        
            295
             
        
            296
            async def previous_shape(event): 
        
            297
                global selected_shape 
        
            298
                if selected_shape is None: 
        
            299
                    return 
        
            300
             
        
            301
                selected_svg = selected_shape.parentNode 
        
            302
             
        
            303
                while selected_svg is not None: 
        
            304
                    next_sibling = selected_svg.previousElementSibling 
        
            305
                    if next_sibling and next_sibling.classList.contains("shape-container"): 
        
            306
                        selected_svg = next_sibling 
        
            307
                        break 
        
            308
                    elif next_sibling is None: 
        
            309
                        # If no more siblings, loop back to the last child 
        
            310
                        selected_svg = selected_svg.parentNode.lastElementChild 
        
            311
                        while selected_svg is not None and not selected_svg.classList.contains( 
        
            312
                                "shape-container"): 
        
            313
                            selected_svg = selected_svg.previousElementSibling 
        
            314
                        break 
        
            315
                    else: 
        
            316
                        selected_svg = next_sibling 
        
            317
             
        
            318
                if selected_svg: 
        
            319
                    shape = selected_svg.firstElementChild 
        
            320
                    await focus_shape(shape) 
        
            321
             
        
            322
             
        
            323
            def unselect_shape(event): 
        
            324
                global selected_shape 
        
            325
             
        
            326
                if selected_shape is not None: 
        
            327
                    selected_shape.classList.remove("selected") 
        
            328
                    selected_shape = None 
        
            329
             
        
            330
                object_list.innerHTML = "" 
        
            331
                object_list.style.display = "none" 
        
            332
                delete_button.style.display = "none" 
        
            333
                next_button.style.display = "none" 
        
            334
                previous_button.style.display = "none" 
        
            335
                document.removeEventListener("keydown", delete_shape_key_proxy) 
        
            336
                document.removeEventListener("keydown", next_shape_key_proxy) 
        
            337
                document.removeEventListener("keydown", previous_shape_key_proxy) 
        
            338
             
        
            339
             
        
            340
            def delete_shape(event): 
        
            341
                global selected_shape 
        
            342
                if selected_shape is None: 
        
            343
                    return 
        
            344
                # Shape is SVG shape inside SVG so we need to remove the parent SVG 
        
            345
                selected_shape.parentNode.remove() 
        
            346
                selected_shape = None 
        
            347
                object_list.innerHTML = "" 
        
            348
                object_list.style.display = "none" 
        
            349
                delete_button.style.display = "none" 
        
            350
                next_button.style.display = "none" 
        
            351
                previous_button.style.display = "none" 
        
            352
                document.removeEventListener("keydown", delete_shape_key_proxy) 
        
            353
                document.removeEventListener("keydown", next_shape_key_proxy) 
        
            354
                document.removeEventListener("keydown", previous_shape_key_proxy) 
        
            355
             
        
            356
             
        
            357
            select_shape_proxy = create_proxy(select_shape) 
        
            358
            unselect_shape_proxy = create_proxy(unselect_shape) 
        
            359
            delete_shape_proxy = create_proxy(delete_shape) 
        
            360
            next_shape_proxy = create_proxy(next_shape) 
        
            361
            previous_shape_proxy = create_proxy(previous_shape) 
        
            362
             
        
            363
            delete_button.addEventListener("click", delete_shape_proxy) 
        
            364
            next_button.addEventListener("click", next_shape_proxy) 
        
            365
            previous_button.addEventListener("click", previous_shape_proxy) 
        
            366
             
        
            367
            # These are functions usable in JS 
        
            368
            cancel_bbox_proxy = create_proxy(lambda event: cancel_bbox(event)) 
        
            369
            make_bbox_proxy = create_proxy(lambda event: make_bbox(event)) 
        
            370
            make_polygon_proxy = create_proxy(lambda event: make_polygon(event)) 
        
            371
            follow_cursor_proxy = create_proxy(follow_cursor) 
        
            372
             
        
            373
             
        
            374
            def switch_shape(event): 
        
            375
                global shape_type 
        
            376
                object_list.innerHTML = "" 
        
            377
                unselect_shape(None) 
        
            378
                shape = event.currentTarget.id 
        
            379
                shape_type = shape 
        
            380
                if shape_type == "select": 
        
            381
                    # Add event listeners to existing shapes 
        
            382
                    print(len(list(document.getElementsByClassName("shape"))), "shapes found") 
        
            383
                    for shape in document.getElementsByClassName("shape"): 
        
            384
                        print("Adding event listener to shape:", shape) 
        
            385
                        shape.addEventListener("click", select_shape_proxy) 
        
            386
                    image.addEventListener("click", unselect_shape_proxy) 
        
            387
                    helper_message.innerText = "Click on a shape to select" 
        
            388
                    # Cancel the current shape creation 
        
            389
                    if shape_type == "shape-bbox": 
        
            390
                        cancel_bbox(None) 
        
            391
                    elif shape_type == "shape-polygon": 
        
            392
                        cancel_polygon(None) 
        
            393
                    elif shape_type == "shape-polyline": 
        
            394
                        cancel_polygon(None) 
        
            395
                else: 
        
            396
                    # Remove event listeners for selection 
        
            397
                    for shape in document.getElementsByClassName("shape"): 
        
            398
                        print("Removing event listener from shape:", shape) 
        
            399
                        shape.removeEventListener("click", select_shape_proxy) 
        
            400
                    image.removeEventListener("click", unselect_shape_proxy) 
        
            401
                    helper_message.innerText = "Select a shape type then click on the image to begin defining it" 
        
            402
                print("Shape is now of type:", shape) 
        
            403
             
        
            404
             
        
            405
            def select_mode(): 
        
            406
                global shape_type 
        
            407
                shape_type = "select" 
        
            408
                document.getElementById("select").click() 
        
            409
             
        
            410
             
        
            411
            vertical_ruler = document.getElementById("annotation-ruler-vertical") 
        
            412
            horizontal_ruler = document.getElementById("annotation-ruler-horizontal") 
        
            413
            vertical_ruler_2 = document.getElementById("annotation-ruler-vertical-secondary") 
        
            414
            horizontal_ruler_2 = document.getElementById("annotation-ruler-horizontal-secondary") 
        
            415
            helper_message = document.getElementById("annotation-helper-message") 
        
            416
             
        
            417
            helper_message.innerText = "Select a shape type then click on the image to begin defining it" 
        
            418
             
        
            419
             
        
            420
            def cancel_bbox(event): 
        
            421
                global bbox_pos, new_shape 
        
            422
             
        
            423
                # Key must be ESCAPE 
        
            424
                if event is not None and hasattr(event, "key") and event.key != "Escape": 
        
            425
                    return 
        
            426
             
        
            427
                if new_shape is not None and event is not None: 
        
            428
                    # Require event so the shape is kept when it ends normally 
        
            429
                    new_shape.remove() 
        
            430
                zone.removeEventListener("click", make_bbox_proxy) 
        
            431
                document.removeEventListener("keydown", cancel_bbox_proxy) 
        
            432
                cancel_button.removeEventListener("click", cancel_bbox_proxy) 
        
            433
             
        
            434
                bbox_pos = None 
        
            435
                vertical_ruler.style.display = "none" 
        
            436
                horizontal_ruler.style.display = "none" 
        
            437
                vertical_ruler_2.style.display = "none" 
        
            438
                horizontal_ruler_2.style.display = "none" 
        
            439
                zone.style.cursor = "auto" 
        
            440
                cancel_button.style.display = "none" 
        
            441
                helper_message.innerText = "Select a shape type then click on the image to begin defining it" 
        
            442
                new_shape = None 
        
            443
             
        
            444
             
        
            445
            def make_bbox(event): 
        
            446
                global new_shape, bbox_pos 
        
            447
                zone_rect = zone.getBoundingClientRect() 
        
            448
             
        
            449
                if bbox_pos is None: 
        
            450
                    helper_message.innerText = "Now define the second point" 
        
            451
             
        
            452
                    bbox_pos = [(event.clientX - zone_rect.left) / zone_rect.width, 
        
            453
                                (event.clientY - zone_rect.top) / zone_rect.height] 
        
            454
                    vertical_ruler_2.style.left = str(bbox_pos[0] * 100) + "%" 
        
            455
                    horizontal_ruler_2.style.top = str(bbox_pos[1] * 100) + "%" 
        
            456
                    vertical_ruler_2.style.display = "block" 
        
            457
                    horizontal_ruler_2.style.display = "block" 
        
            458
             
        
            459
                else: 
        
            460
                    x0, y0 = bbox_pos.copy() 
        
            461
                    x1 = (event.clientX - zone_rect.left) / zone_rect.width 
        
            462
                    y1 = (event.clientY - zone_rect.top) / zone_rect.height 
        
            463
             
        
            464
                    rectangle = document.createElementNS("http://www.w3.org/2000/svg", "rect") 
        
            465
             
        
            466
                    new_shape = make_shape_container() 
        
            467
                    zone_rect = zone.getBoundingClientRect() 
        
            468
             
        
            469
                    new_shape.appendChild(rectangle) 
        
            470
                    zone.appendChild(new_shape) 
        
            471
             
        
            472
                    minx = min(x0, x1) 
        
            473
                    miny = min(y0, y1) 
        
            474
                    maxx = max(x0, x1) 
        
            475
                    maxy = max(y0, y1) 
        
            476
             
        
            477
                    rectangle.setAttribute("x", str(minx * image.naturalWidth)) 
        
            478
                    rectangle.setAttribute("y", str(miny * image.naturalHeight)) 
        
            479
                    rectangle.setAttribute("width", str((maxx - minx) * image.naturalWidth)) 
        
            480
                    rectangle.setAttribute("height", str((maxy - miny) * image.naturalHeight)) 
        
            481
                    rectangle.setAttribute("fill", "none") 
        
            482
                    rectangle.setAttribute("data-object-type", "") 
        
            483
                    rectangle.classList.add("shape-bbox") 
        
            484
                    rectangle.classList.add("shape") 
        
            485
             
        
            486
                    # Add event listeners to the new shape 
        
            487
                    rectangle.addEventListener("click", select_shape_proxy) 
        
            488
             
        
            489
                    cancel_bbox(None) 
        
            490
             
        
            491
             
        
            492
            polygon_points = [] 
        
            493
             
        
            494
             
        
            495
            def make_polygon(event): 
        
            496
                global new_shape, polygon_points 
        
            497
             
        
            498
                polygon = new_shape.children[0] 
        
            499
             
        
            500
                zone_rect = zone.getBoundingClientRect() 
        
            501
             
        
            502
                polygon_points.append(((event.clientX - zone_rect.left) / zone_rect.width, 
        
            503
                                       (event.clientY - zone_rect.top) / zone_rect.height)) 
        
            504
             
        
            505
                # Update the polygon 
        
            506
                polygon.setAttribute("points", " ".join( 
        
            507
                        [f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in 
        
            508
                         polygon_points])) 
        
            509
             
        
            510
             
        
            511
            def reset_polygon(): 
        
            512
                global new_shape, polygon_points 
        
            513
             
        
            514
                zone.removeEventListener("click", make_polygon_proxy) 
        
            515
                document.removeEventListener("keydown", close_polygon_proxy) 
        
            516
                document.removeEventListener("keydown", cancel_polygon_proxy) 
        
            517
                document.removeEventListener("keydown", backspace_polygon_proxy) 
        
            518
                confirm_button.style.display = "none" 
        
            519
                cancel_button.style.display = "none" 
        
            520
                backspace_button.style.display = "none" 
        
            521
                confirm_button.removeEventListener("click", close_polygon_proxy) 
        
            522
                cancel_button.removeEventListener("click", cancel_polygon_proxy) 
        
            523
                backspace_button.removeEventListener("click", backspace_polygon_proxy) 
        
            524
                polygon_points.clear() 
        
            525
             
        
            526
                zone.style.cursor = "auto" 
        
            527
                new_shape = None 
        
            528
             
        
            529
             
        
            530
            def close_polygon(event): 
        
            531
                if event is not None and hasattr(event, "key") and event.key != "Enter": 
        
            532
                    return 
        
            533
                # Polygon is already there, but we need to remove the events 
        
            534
                reset_polygon() 
        
            535
             
        
            536
             
        
            537
            def cancel_polygon(event): 
        
            538
                if event is not None and hasattr(event, "key") and event.key != "Escape": 
        
            539
                    return 
        
            540
                # Delete the polygon 
        
            541
                new_shape.remove() 
        
            542
                reset_polygon() 
        
            543
             
        
            544
             
        
            545
            def backspace_polygon(event): 
        
            546
                if event is not None and hasattr(event, "key") and event.key != "Backspace": 
        
            547
                    return 
        
            548
                if not polygon_points: 
        
            549
                    return 
        
            550
                polygon_points.pop() 
        
            551
                polygon = new_shape.children[0] 
        
            552
                polygon.setAttribute("points", " ".join( 
        
            553
                        [f"{point[0] * image.naturalWidth},{point[1] * image.naturalHeight}" for point in 
        
            554
                         polygon_points])) 
        
            555
             
        
            556
             
        
            557
            close_polygon_proxy = create_proxy(close_polygon) 
        
            558
            cancel_polygon_proxy = create_proxy(cancel_polygon) 
        
            559
            backspace_polygon_proxy = create_proxy(backspace_polygon) 
        
            560
             
        
            561
             
        
            562
            def open_shape(event): 
        
            563
                global new_shape, bbox_pos 
        
            564
                if bbox_pos or shape_type == "select": 
        
            565
                    return 
        
            566
                print("Creating a new shape of type:", shape_type) 
        
            567
             
        
            568
                if shape_type == "shape-bbox": 
        
            569
                    helper_message.innerText = ("Define the first point at the intersection of the lines " 
        
            570
                                                "by clicking on the image, or click the cross to cancel") 
        
            571
             
        
            572
                    cancel_button.addEventListener("click", cancel_bbox_proxy) 
        
            573
                    document.addEventListener("keydown", cancel_bbox_proxy) 
        
            574
                    cancel_button.style.display = "block" 
        
            575
                    bbox_pos = None 
        
            576
                    zone.addEventListener("click", make_bbox_proxy) 
        
            577
                    vertical_ruler.style.display = "block" 
        
            578
                    horizontal_ruler.style.display = "block" 
        
            579
                    zone.style.cursor = "crosshair" 
        
            580
                elif shape_type == "shape-polygon" or shape_type == "shape-polyline": 
        
            581
                    if shape_type == "shape-polygon": 
        
            582
                        helper_message.innerText = ("Click on the image to define the points of the polygon, " 
        
            583
                                                    "press escape to cancel, enter to close, or backspace to " 
        
            584
                                                    "remove the last point") 
        
            585
                    elif shape_type == "shape-polyline": 
        
            586
                        helper_message.innerText = ("Click on the image to define the points of the polyline, " 
        
            587
                                                    "press escape to cancel, enter to finish, or backspace to " 
        
            588
                                                    "remove the last point") 
        
            589
             
        
            590
                    if not polygon_points and not new_shape: 
        
            591
                        new_shape = make_shape_container() 
        
            592
                        zone_rect = zone.getBoundingClientRect() 
        
            593
             
        
            594
                    if not polygon_points and int(new_shape.children.length) == 0: 
        
            595
                        zone.addEventListener("click", make_polygon_proxy) 
        
            596
                        document.addEventListener("keydown", close_polygon_proxy) 
        
            597
                        document.addEventListener("keydown", cancel_polygon_proxy) 
        
            598
                        document.addEventListener("keydown", backspace_polygon_proxy) 
        
            599
                        cancel_button.addEventListener("click", cancel_polygon_proxy) 
        
            600
                        cancel_button.style.display = "block" 
        
            601
                        confirm_button.addEventListener("click", close_polygon_proxy) 
        
            602
                        confirm_button.style.display = "block" 
        
            603
                        backspace_button.addEventListener("click", backspace_polygon_proxy) 
        
            604
                        backspace_button.style.display = "block" 
        
            605
                        if shape_type == "shape-polygon": 
        
            606
                            polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon") 
        
            607
                            polygon.classList.add("shape-polygon") 
        
            608
                        elif shape_type == "shape-polyline": 
        
            609
                            polygon = document.createElementNS("http://www.w3.org/2000/svg", "polyline") 
        
            610
                            polygon.classList.add("shape-polyline") 
        
            611
                        polygon.setAttribute("fill", "none") 
        
            612
                        polygon.setAttribute("data-object-type", "") 
        
            613
                        polygon.classList.add("shape") 
        
            614
                        new_shape.appendChild(polygon) 
        
            615
                        zone.appendChild(new_shape) 
        
            616
                        zone.style.cursor = "crosshair" 
        
            617
                elif shape_type == "shape-point": 
        
            618
                    point = document.createElementNS("http://www.w3.org/2000/svg", "circle") 
        
            619
                    zone_rect = zone.getBoundingClientRect() 
        
            620
                    point.setAttribute("cx", str((event.clientX - zone_rect.left) / zone_rect.width * image.naturalWidth)) 
        
            621
                    point.setAttribute("cy", str((event.clientY - zone_rect.top) / zone_rect.height * image.naturalHeight)) 
        
            622
                    point.setAttribute("r", "0") 
        
            623
                    point.classList.add("shape-point") 
        
            624
                    point.classList.add("shape") 
        
            625
                    point.setAttribute("data-object-type", "") 
        
            626
             
        
            627
                    new_shape = make_shape_container() 
        
            628
                    zone_rect = zone.getBoundingClientRect() 
        
            629
             
        
            630
                    new_shape.appendChild(point) 
        
            631
                    zone.appendChild(new_shape) 
        
            632
             
        
            633
                    new_shape = None 
        
            634
             
        
            635
             
        
            636
             
        
            637
            for button in list(document.getElementById("shape-selector").children): 
        
            638
                button.addEventListener("click", create_proxy(switch_shape)) 
        
            639
                print("Shape", button.id, "is available") 
        
            640
             
        
            641
            zone.addEventListener("mousemove", follow_cursor_proxy) 
        
            642
            zone.addEventListener("click", create_proxy(open_shape)) 
        
            643
             
        
            644
            # Load existing annotations, if any 
        
            645
            put_shapes(await load_shapes()) 
        
            646
            print("Ready!") 
        
            647