This commit is contained in:
2022-05-27 11:21:49 +03:00
commit ca80875372
14 changed files with 438 additions and 0 deletions

7
code/Dockerfile Normal file
View File

@@ -0,0 +1,7 @@
FROM python:3
COPY requirements.txt /requirements.txt
RUN pip3 install --upgrade pip
RUN pip3 install -r /requirements.txt
COPY . /code/
WORKDIR /code
CMD ./start.me

162
code/labeler.py Normal file
View File

@@ -0,0 +1,162 @@
import time
import datetime
import os
import re
import json
import logging
from flask import (
Flask,
request,
session,
g,
redirect,
url_for,
abort,
render_template,
flash,
)
from revprox import ReverseProxied
# configuration
IMAGEDIR = "/data/images/"
LABELDIR = "/data/labels/"
CONFIG_FILE = "/data/config.json"
DEBUG = False
SECRET_KEY = os.environ.get("SECRETKEY", "development key")
for d in (IMAGEDIR, LABELDIR):
try:
os.makedirs(d)
except FileExistsError:
pass
app = Flask(__name__)
app.config.from_object(__name__)
app.wsgi_app = ReverseProxied(app.wsgi_app)
@app.before_request
def before_request_func():
try:
with open(app.config["CONFIG_FILE"], "rt") as fp:
g.config = json.load(fp)
g.labels = g.config["labels"]
except Exception as e:
logging.warning(e)
logging.warning("config.json could not be read. using defaults.")
g.labels = [
{"type": "checkbox", "name": "my_check", "default": "off"},
{"type": "text", "name": "my_text", "default": "1.0"},
{
"type": "range",
"name": "my_slider",
"default": "75",
"min": "0",
"max": "100",
},
]
g.config = {"title": "Labeler", "labels": g.labels}
for label in g.labels:
label['value'] = label['default']
def natural_key(string_):
"""See http://www.codinghorror.com/blog/archives/001018.html"""
return [int(s) if s.isdigit() else s for s in re.split(r"(\d+)", string_)]
def get_metadata_path(image_path):
base, ext = os.path.splitext(os.path.basename(image_path))
jsonpath = os.path.join(app.config["LABELDIR"], base + ".json")
return jsonpath
def get_current_image(images):
for i, f in enumerate(images):
jsonpath = get_metadata_path(f)
if not os.path.exists(jsonpath):
return i
return i
def get_image_list():
return sorted(
[
os.path.join(app.config["IMAGEDIR"], x)
for x in os.listdir(app.config["IMAGEDIR"])
if not x.endswith("json")
],
key=natural_key,
)
def get_metadata(imagepath):
try:
with open(get_metadata_path(imagepath), "rt") as fp:
values = json.load(fp)
metadata = [x.copy() for x in g.labels.copy()]
for label in metadata:
if label["name"] in values:
label["value"] = values[label["name"]]
else:
label["value"] = label["default"]
return metadata
except FileNotFoundError:
# Return defaults
return g.labels
except Exception as e:
logging.error(e)
# Return defaults
return g.labels
def set_metadata(values, imagepath):
metadata = [x.copy() for x in g.labels.copy()]
for label in metadata:
if not label["name"] in values:
values[label["name"]] = label["default"]
# logging.warning((path, values))
if not os.path.exists(
os.path.join(app.config["IMAGEDIR"], os.path.basename(imagepath))
):
return
with open(get_metadata_path(imagepath), "wt") as fp:
return json.dump(values, fp, indent=2, sort_keys=True)
@app.route("/", methods=["GET", "POST"])
@app.route("/image:<int:id>", methods=["GET", "POST"])
def show_image(id=None):
if request.method == "POST":
# parse form
image_name = request.form["image_name"]
metadata = {}
for key in request.form:
if key.startswith("label_"):
dictkey = key[6:]
metadata[dictkey] = str(request.form[key])
set_metadata(metadata, image_name)
images = get_image_list()
if id == None:
id = get_current_image(images)
else:
id = max(0, min(len(images) - 1, int(id)))
labels = get_metadata(images[id])
id_minus = max(0, min(len(images) - 1, int(id - 1)))
id_plus = max(0, min(len(images) - 1, int(id + 1)))
return render_template(
"show_image.html",
id=id,
count=len(images),
image=images[id],
image_name=os.path.basename(images[id]),
image_plus=images[id_plus],
labels=labels,
id_minus=id_minus,
id_plus=id_plus,
title=g.config.get("title", "Labeler"),
)

2
code/requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
flask
gunicorn

32
code/revprox.py Normal file
View File

@@ -0,0 +1,32 @@
class ReverseProxied(object):
'''Wrap the application in this middleware and configure the
front-end server to add these headers, to let you quietly bind
this to a URL other than / and to an HTTP scheme that is
different than what is used locally.
In nginx:
location /myprefix {
proxy_pass http://192.168.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Script-Name /myprefix;
}
:param app: the WSGI application
'''
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
if script_name:
environ['SCRIPT_NAME'] = script_name
path_info = environ['PATH_INFO']
if path_info.startswith(script_name):
environ['PATH_INFO'] = path_info[len(script_name):]
scheme = environ.get('HTTP_X_SCHEME', '')
if scheme:
environ['wsgi.url_scheme'] = scheme
return self.app(environ, start_response)

6
code/start.me Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
useradd --no-create-home -u $UID user
exec runuser user -m -c 'gunicorn -b 0.0.0.0:8080 -w 4 labeler:app'

1
code/static/data Symbolic link
View File

@@ -0,0 +1 @@
/data

2
code/static/script.js Normal file
View File

@@ -0,0 +1,2 @@

98
code/static/style.css Normal file
View File

@@ -0,0 +1,98 @@
body { font-family: sans-serif; background: #888; margin: 0px;
min-height: 100vh; width: 100vw; overflow-x: hidden; }
a, h1, h2 { color: #377ba8; }
h1, h2 { font-family: 'Georgia', serif; margin: 0; }
h1 { border-bottom: 2px solid #eee; }
h2 { font-size: 1.2em; }
tr,td,tbody { margin: 0px; }
.page { margin: 0em;
padding: 0em; background: #888; min-height: 100vh; width: 100vw;}
.entries { list-style: none; margin: 0; padding: 0; width:100%; min-height: 95vh; }
.large { font-size: 3em; }
.right { text-align: right; }
.center { text-align: center; }
#image { position: absolute;
left:0px; top:0px;
width: calc(100vw - 220px); /*height:95vh;*/ }
#img { max-width: calc(100vw - 220px); max-height:95vh;
width: auto; height: auto;
display: block; margin-left: auto;
margin-right: auto; }
#img_title { overflow-x: hidden;
overflow-wrap: anywhere;
}
#topright { position: absolute;
right:0px; top:0px;
/*width: 20vw;*/
height:95vh;
font-size: large; }
.inputcontainer {
z-index:1;
background-color: #aaa;
padding: 5px;
margin-bottom: 3px;
border-radius: 5px;
width: 210px;
}
input, select {
width: 180px;
font-size: large;
}
input[type="text"] {
}
input[type="submit"] {
}
input[type="checkbox"] {
width: 3em;
height: 1.5em;
}
label { display: block; }
output { display: block; font-weight: bold; }
.button_next {
width: 2em;
height: 3em;
font-size: large;
margin-top: 1em;
}
.float_left {
float: left;
}
.float_right {
float:right;
}
.button_save {
height: 3em;
margin-top: 1em;
margin-left: 1em;
margin-right: 1em;
clear: both;
}
.button_continue {
height: 4em;
margin-top: 3em;
margin-left: auto;
margin-right: auto;
width: 6em;
}
.preload {
position: absolute;
left: 0px;
bottom: 0px;
width: 1px;
height: 1px;
overflow: hidden;
}

View File

@@ -0,0 +1,14 @@
<!doctype html>
<head>
<title>{{ title }}</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<meta name="viewport" content="width=440" />
<script language="javascript" src="{{ url_for('static', filename='script.js') }}" ></script>
</head>
<body>
<div class=page>
{% block body %}{% endblock %}
</div>
</body>

View File

@@ -0,0 +1,53 @@
{% extends "layout.html" %}
{% block body %}
<div class="entries">
<div id="image">
<img id="img" src="{{ url_for('static', filename=image) }}" >
<div class=center id="img_title" >{{ image_name }} ({{id+1}}/{{count}})</div>
</div>
<div id="topright">
<form action="{{ url_for('show_image', id = id_plus) }}" method=post class=add-entry>
<input type=hidden value="{{ image_name }}" name=image_name>
{% for label in labels %}
<div class=inputcontainer>
<label>{{ label.name }}:</label>
<div class=center>
{% if label.type == "checkbox" %}
<input class=center type="checkbox" name="label_{{ label.name }}" {% if label.value == "on" %}checked{% endif %}>
{% endif %}
{% if label.type == "text" %}
<input type="text" name="label_{{ label.name }}" value="{{ label.value }}">
{% endif %}
{% if label.type == "number" %}
<input type="number" step="any" name="label_{{ label.name }}" value="{{ label.value }}">
{% endif %}
{% if label.type == "range" %}
<input type="range" name="label_{{ label.name }}" value="{{ label.value }}" min="{{ label.min }}" max="{{ label.max }}" oninput="this.nextElementSibling.value = this.value">
<output>{{label.value}}</output>
{% endif %}
{% if label.type == "select" %}
<select name="label_{{ label.name }}">
{% for opt in label.options %}
<option value="{{opt}}" {% if opt == label.value %}SELECTED{% endif %}>{{opt}}</option>
{% endfor %}
</select>
{% endif %}
</div>
</div>
{% endfor %}
<div class=center>
<input type=submit value="save" name=save class="button_save">
</div>
</form>
<div class=center>
<button class="button_next float_left" onclick="location.href='{{ url_for('show_image', id = id_minus) }}';">&larr;</button>
<button class="button_next float_right" onclick="location.href='{{ url_for('show_image', id = id_plus) }}';">&rarr;</button>
<button class="button_continue" onclick="location.href='{{ url_for('show_image') }}';">continue</button>
</div>
</div>
</div>
<img class=preload src="{{ url_for('static', filename=image_plus) }}" >
{% endblock %}