roundabout,
created on Thursday, 5 September 2024, 07:25:13 (1725521113),
received on Thursday, 5 September 2024, 07:25:39 (1725521139)
Author identity: vlad <vlad.muntoiu@gmail.com>
aa59915a80bdadddd7754ccc2cd167b334615d92
app.py
@@ -13,6 +13,8 @@ import sqlalchemy.dialects.postgresql
from os import path import mimetypes from PIL import Image import config import markdown
@@ -313,8 +315,11 @@ def picture(id):
if resource is None: return flask.abort(404) image = Image.open(path.join(config.DATA_PATH, "pictures", str(resource.id))) return flask.render_template("picture.html", resource=resource, file_extension=mimetypes.guess_extension(resource.file_format))file_extension=mimetypes.guess_extension(resource.file_format), size=image.size)
formats.md
@@ -3,8 +3,8 @@ Data formats
This document describes the various data formats that are used in the system. Raw annotation data (client → server)-------------------------------------Raw annotation data -------------------The client sends raw data for image annotations in a JSON format which is a list of shapes. Each shape is a dictionary with the following keys:
@@ -36,6 +36,9 @@ of shapes. Each shape is a dictionary with the following keys:
is a human-readable string that must be registered in the system before being used on shapes. The server sends the same data back to the client, to use to show the existing annotations for an image. ### Example ~~~json
static/style.css
@@ -6,6 +6,7 @@
--text-soft: #000000C0; --text-softer: #0000009A; --text-faint: #00000066; --color-shape-label: #0097A7; --color-shape-label-text: #ffffff;} p {
@@ -30,6 +31,7 @@ iconify-icon {
#annotation-image { user-drag: none; -webkit-user-drag: none; image-rendering: pixelated;} #annotation-zone > svg {
@@ -100,10 +102,62 @@ iconify-icon {
stroke-width: 6px; } .shape-container {.shape-container, .shape-container-viewonly {pointer-events: none; } #annotation-helper-message { min-height: 2lh; } .shape-container-viewonly > .shape { fill: var(--color-info); stroke: var(--color-info); fill-opacity: 0; stroke-opacity: 0; transition: fill-opacity 0.25s cubic-bezier(0.61, 1, 0.88, 1), stroke-opacity 0.25s cubic-bezier(0.61, 1, 0.88, 1); } .shape-container-viewonly > .shape-polyline { fill: none; } .shape-container-viewonly > .shape-point { stroke: var(--color-info); } .shape-label { font-size: 1.5rem; font-weight: 500; color: var(--color-shape-label-text); pointer-events: none; height: 32px; padding-left: 8px; padding-right: 8px; display: flex; align-items: center; justify-content: center; background: var(--color-shape-label); width: max-content; box-shadow: var(--shadow-button); /* top-left corner is square to point to the shape */ border-radius: 0 16px 16px 16px; opacity: 0; transition: opacity 0.25s cubic-bezier(0.61, 1, 0.88, 1), transform 0.375s cubic-bezier(0.16, 1, 0.3, 1); transform: scale(0); transform-origin: top left; z-index: 3; } /* Only show region parts when checkbox is checked */ #annotation-zone:has(+ x-buttonbox #show-objects:checked) > .shape-container-viewonly > foreignObject > .shape-label { opacity: 1; transform: scale(1); } #annotation-zone:has(+ x-buttonbox #show-shapes:checked) > .shape-container-viewonly > .shape { fill-opacity: 0.1; stroke-opacity: 1; }
templates/picture.html
@@ -5,7 +5,54 @@
<h1>{{ resource.title }}</h1> <p>{{ resource.description }}</p> <p>{{ resource.file_format }}</p> <img src="/raw/picture/{{ resource.id }}" alt="{{ resource.title }}"><div id="annotation-zone"> <img id="annotation-image" src="/raw/picture/{{ resource.id }}" alt="{{ resource.title }}"> {% for region in resource.regions %} {% if region.json.type == "bbox" %} <svg class="shape-container-viewonly" viewBox="0 0 {{ size[0] }} {{ size[1] }}"> <rect x="{{ region.json.shape.x * size[0] }}" y="{{ region.json.shape.y * size[1] }}" width="{{ region.json.shape.width * size[0] }}" height="{{ region.json.shape.height * size[1] }}" fill="none" class="shape-bbox shape" ></rect> </svg> {% elif region.json.type == "polygon" %} <svg class="shape-container-viewonly" viewBox="0 0 {{ size[0] }} {{ size[1] }}"> <polygon points="{% for point in region.json.shape %}{{ point.x * size[0] }},{{ point.y * size[1] }} {% endfor %}" fill="none" class="shape-polygon shape"></polygon> {% set top = region.json.shape | sort(attribute='y') | last %} {% set left = region.json.shape | sort(attribute='x') | first %} {% set bottom = region.json.shape | sort(attribute='y') | first %} {% set right = region.json.shape | sort(attribute='x') | last %} {% set centre_x = (left.x + right.x) / 2 %} {% set centre_y = (top.y + bottom.y) / 2 %} <foreignObject height="100%" width="100%" x="{{ centre_x * size[0] }}" y="{{ centre_y * size[1] }}"> <div class="shape-label"> {{ region.object_id }} </div> </foreignObject> </svg> {% elif region.json.type == "polyline" %} <svg class="shape-container-viewonly" viewBox="0 0 {{ size[0] }} {{ size[1] }}"> <polyline points="{% for point in region.json.shape %}{{ point.x * size[0] }},{{ point.y * size[1] }} {% endfor %}" fill="none" class="shape-polyline shape"></polyline> </svg> {% elif region.json.type == "point" %} <svg class="shape-container-viewonly" viewBox="0 0 {{ size[0] }} {{ size[1] }}"> <circle cx="{{ region.json.shape.x * size[0] }}" cy="{{ region.json.shape.y * size[1] }}" r="0" fill="none" class="shape-point shape"></circle> </svg> {% endif %} {% endfor %} </div> <x-buttonbox> <label> <input type="checkbox" id="show-shapes" checked> Show shapes </label> <label> <input type="checkbox" id="show-objects" checked> Show objects </label> </x-buttonbox><p> <a href="{{ resource.origin_url }}">Original source</a> | <a href="/raw/picture/{{ resource.id }}">View</a> |