This commit is contained in:
T 2024-11-10 02:27:36 +01:00
parent 313d3aa471
commit acafc0fc87
3 changed files with 347 additions and 293 deletions

85
filters.py Normal file
View File

@ -0,0 +1,85 @@
########################################################################
# Filter Api #
########################################################################
import string
import time
import numpy as np
import scipy
import config
class MakeBrightnessFilter:
def __init__(self, intensity):
self.intensity = intensity
def __call__(self, img):
return (img * self.intensity).astype(np.uint8)
def FlipXFilter(intensity):
return intensity[:, ::-1, :]
def FlipYFilter(intensity):
return intensity[::-1, :, :]
class MakeBrightnessImageFilter:
def __init__(self, name):
self.filter_img = scipy.misc.imread("filter/" + name + ".png", flatten=True) / 255
# img = np.transpose(img)
def __call__(self, img):
img = img.astype(float)
for i in range(img.shape[2]):
img[:, :, i] *= self.filter_img
return img.astype(np.uint8)
def strings(s):
allowed_chars = string.ascii_letters + string.digits + "+-*/()."
i = 0
outlist = []
while i < len(s):
if s[i] not in allowed_chars:
raise Exception("Unexpected char " + s[i])
if s[i] not in string.ascii_letters:
i += 1
continue
out = ""
while i < len(s) and s[i] in string.ascii_letters + string.digits:
out += s[i]
i += 1
outlist.append(out)
return outlist
def eval_safer(expr, x, y, t):
symbols = {"x": x, "y": y, "t": t,
"sin": np.sin, "cos": np.cos, "exp": np.exp, "tan": np.tan}
strs = strings(expr)
for s in strs:
if s not in symbols.keys():
raise Exception(f"Unexpected symbol: {s}")
return eval(expr, {}, symbols)
class MakeBrightnessExprFilter:
def __init__(self, expr: str):
self.expr = expr.replace(" ", "")
self.t0 = time.time()
self.x, self.y = np.meshgrid(np.arange(config.ScreenX), np.arange(config.ScreenY))
eval_safer(self.expr, 0, 0, 0) # check expression
def __call__(self, img):
t = time.time() - self.t0
img = img.astype(float)
filter_ = 0 * self.x + eval_safer(self.expr, self.x, self.y, t)
filter_ = np.clip(np.nan_to_num(filter_), 0, 1)
for i in range(img.shape[2]):
img[:, :, i] *= filter_
return img.astype(np.uint8)

View File

@ -1,222 +1,266 @@
<html> <!DOCTYPE html>
<head> <html lang="de">
<meta charset="utf-8"> <head>
<title>Pixelserver Interface</title> <meta charset="utf-8">
<title>Pixelserver Interface</title>
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png"> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=1, viewport-fit=auto"/>
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png"> <link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png">
<link rel="manifest" href="/site.webmanifest"> <link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png">
</head> <link rel="manifest" href="/site.webmanifest">
<body style="display: grid; grid-template-columns: auto auto auto;"> <style>
<h1 style="grid-column: 1 / 4; text-align: center;">Andreas <i>production-ready</i> Interface</h1> .main {
display: grid;
grid-gap: 1rem;
align-items: stretch;
}
<div> @media only screen {
.main {
grid-template-columns: repeat(1, 1fr);
}
}
@media only screen and (min-width: 800px) {
.main {
grid-template-columns: repeat(2, 1fr);
}
}
@media only screen and (min-width: 1111px) {
.main {
grid-template-columns: repeat(3, 1fr);
}
}
.col {
min-width: 300px;
/*max-width: 350px;*/
}
.just, .just-col {
display: flex;
}
.just > * {
flex: 1;
margin: 0 1rem;
}
.just-col {
flex-direction: column;
}
.just-col > * {
flex: 1;
margin: 2px 1rem;
}
#gammaform span {
display: inline-block;
width: 4rem;
}
.logging {
max-width: 500px;
}
.options {
max-width: 350px;
}
</style>
</head>
<body>
<h1 style="text-align: center;">Andreas <i>production-ready</i> Interface</h1>
<div class="main">
<div class="col">
<h2>Kommando:</h2> <h2>Kommando:</h2>
<form id='in' onSubmit="return request()"> <form id="in" class="just" onSubmit="return request()">
<select id="list"></select> <select id="list"></select>
<input id="args" /> <input id="args"/>
<button id="execute">Ausführen</button> <button id="execute">Ausführen</button>
</form> </form>
<h2>Intensität:</h2> <h2>Intensität:</h2>
<form id='brightnessform' onSubmit="return setbrightness()"> <form id="brightnessform" class="just" onSubmit="return setbrightness()">
<input id="brightness" value=1.0 /><br/> <input id="brightness" value="1.0"><br/>
<button id="sendbrightness">Setzen</button> <button id="sendbrightness">Setzen</button>
</form> </form>
</div> </div>
<div style="border-left: solid 1px; padding-left: 10px;"> <div class="options col">
<h2>Gamma:</h2> <h2>Gamma:</h2>
<form id='gammeform' onSubmit="return setgamma()"> <form id="gammaform" class="just-col" onSubmit="return setgamma()">
Rot: <input id="gammar" value=2.8 /><br/> <label><span>Rot: </span><input id="gammar" value="2.8"/></label><br/>
Grün: <input id="gammag" value=2.65 /><br/> <label><span>Grün: </span><input id="gammag" value="2.65"/></label><br/>
Blau: <input id="gammab" value=2.65 /><br/> <label><span>Blau: </span><input id="gammab" value="2.65"/></label><br/>
Weiß: <input id="gammaw" value=2.65 /><br/> <label><span>Weiß: </span><input id="gammaw" value="2.65"/></label><br/>
<button id="sendgamma">Setzen</button> <button id="sendgamma">Setzen</button>
</form> </form>
<h2>Flip:</h2> <h2>Flip:</h2>
<form id="flipform" onSubmit="return setFlip()"> <form id="flipform" class="just" onSubmit="return setFlip()">
<input id="flipx" type="checkbox" /> Flip X <br/> <label><input id="flipx" type="checkbox"/> Flip X</label>
<input id="flipy" type="checkbox" /> Flip Y <br/> <label><input id="flipy" type="checkbox"/> Flip Y</label>
<button id="sendflip">Setzen</button> <button id="sendflip">Setzen</button>
</form> </form>
<h2>Filterimage:</h2> <h2>Filterimage:</h2>
<form id="filteform" onSubmit="return setFilter()"> <form id="filterform" class="just" onSubmit="return setFilter()">
<input id="filtername" value="test"></intput><br/> <input id="filtername" value="test">
<button id="filterflip">Setzten</button> <button id="filterflip">Setzen</button>
</form> </form>
<h2>Filter expression:</h2> <h2>Filter expression:</h2>
<form id="filterexprform" onSubmit="return setFilterExpr()"> <form id="filterexprform" class="just" onSubmit="return setFilterExpr()">
<input id="filterexpr" value="0.5+0.25*sin(x/3+t)"></intput><br/> <input id="filterexpr" value="0.5+0.25*sin(x/3+t)">
<button>Setzten</button> <button>Setzen</button>
</form> </form>
</div> </div>
<div style="border-left: solid 1px; padding-left: 10px; width: 600px;"> <div class="logging col">
<h2>Crash Log:</h2> <h2>Crash Log:
<form id="crashlogform" onSubmit="return enableCrashLog()"> <button id="crashlogbtn" type="button" onclick="return enableCrashLog()">start</button>
<button>start</button> </h2>
</form> <textarea readonly id="crashlogs" style="width: 500px; height: 300px; display: none;"></textarea>
<textarea readonly id='crashlogs' style="width: 500px; height: 300px; display: none;"></textarea>
<h2>Log:</h2> <h2>Log:
<form id="logform" onSubmit="return enableLog()"> <button id="logbtn" type="button" onclick="return enableLog()">start</button>
<button>start</button> </h2>
</form> <textarea readonly id="logs" style="width: 500px; height: 300px; display: none;"></textarea>
<textarea readonly id='logs' style="width: 500px; height: 300px; display: none;"></textarea>
</div> </div>
<script> <script>
function getRaw(from, callback){ function getRaw(url, callback) {
var xhttp = new XMLHttpRequest(); const xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() { xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) { if (this.readyState === 4 && this.status === 200)
callback(xhttp.responseText); if (callback) callback(xhttp.responseText);
} };
}; xhttp.open("GET", url, true);
xhttp.open("GET", from, true); xhttp.send();
xhttp.send(); }
function getJSON(url, callback) {
getRaw(url, function (text) {
callback(JSON.parse(text));
});
}
function post(url, data) {
const formdata = new FormData();
for (const key in data)
formdata.append(key, data[key]);
const xhttp = new XMLHttpRequest();
// xhttp.onreadystatechange = function () {
// if (this.readyState === 4 && this.status === 200) {}
// };
xhttp.open("POST", url, true);
xhttp.send(formdata);
}
function populateForm(parameters) {
const list = document.getElementById("list");
for (const i in parameters) {
const app = parameters[i];
const name = app["name"];
const guiname = app["guiname"];
const persistent = app["persistent"];
const element = document.createElement("option");
element.value = name;
element.innerHTML = guiname;
element.dataset.persistent = persistent;
list.appendChild(element);
} }
function getJSON(from, callback){ list.onchange = function () {
getRaw(from, function(text){ const app = document.getElementById('list');
callback(JSON.parse(text)); const persistent = app.options[app.selectedIndex].dataset.persistent;
}); if (persistent == "true") {
} document.getElementById("args").style.display = "none";
} else {
function post(from, data){ document.getElementById("args").style.display = "inline";
var formdata = new FormData();
for (var key in data) {
formdata.append(key, data[key]);
} }
let xhttp = new XMLHttpRequest(); };
xhttp.onreadystatechange = function() { list.onchange();
if (this.readyState == 4 && this.status == 200) { document.getElementById("container").style.display = "block";
} }
};
xhttp.open("POST", from, true);
xhttp.send(formdata);
}
function populateForm(parameters){ function request() {
let list = document.getElementById("list"); let app = document.getElementById('list');
let val = app.options[app.selectedIndex].value;
let parameter = document.getElementById('args').value;
post("/apps/start/" + val, {"param": parameter});
return false;
}
parameters.map ( e => { function setgamma() {
}) const r = document.getElementById('gammar').value;
for (var i in parameters) { const g = document.getElementById('gammag').value;
let app = parameters[i]; const b = document.getElementById('gammab').value;
let name = app["name"]; const w = document.getElementById('gammaw').value;
let guiname = app["guiname"]; getRaw(`/setgamma/${r}/${g}/${b}/${w}`);
let persistent = app["persistent"]; return false;
let element = document.createElement("option"); }
element.value = name;
element.innerHTML = guiname;
element.dataset.persistent = persistent;
list.appendChild(element);
}
list.onchange = function(){ function setbrightness() {
let app = document.getElementById('list'); const i = document.getElementById('brightness').value;
let persistent = app.options[app.selectedIndex].dataset.persistent; getRaw("/setbrightness/" + i);
if (persistent == "true"){ return false;
document.getElementById("args").style.display = "none"; }
}else{
document.getElementById("args").style.display = "inline";
}
}
list.onchange();
document.getElementById("container").style.display = "block";
}
function request(){ function setFlip() {
let app = document.getElementById('list'); const x = document.getElementById('flipx').checked;
let val = app.options[app.selectedIndex].value; const y = document.getElementById('flipy').checked;
let parameter = document.getElementById('args').value; getRaw("/filter/flipx/" + x);
let url = "/apps/start/" + val; getRaw("/filter/flipy/" + y);
post(url, {"param": parameter}); return false;
return false; }
}
function setgamma(){ function setFilter() {
let r = document.getElementById('gammar').value; const i = document.getElementById('filtername').value;
let g = document.getElementById('gammag').value; getRaw("/filter/img/" + i);
let b = document.getElementById('gammab').value; return false;
let w = document.getElementById('gammaw').value; }
let url = "/setgamma/" + r+"/"+g+"/"+b+"/"+w;
getRaw(url, function test(){});
return false;
}
function setbrightness(){ function setFilterExpr() {
let i = document.getElementById('brightness').value; const expr = document.getElementById('filterexpr').value;
let url = "/setbrightness/" + i; post("/filter/expr/", {"expr": expr});
getRaw(url, function test(){}); return false;
return false; }
}
function setFlip(){ function updateLog() {
let x = document.getElementById('flipx').checked; getRaw("/apps/log", function (text) {
let y = document.getElementById('flipy').checked; document.getElementById('logs').value = text;
console.log(x); });
console.log(y); }
getRaw("/filter/flipx/" + x, function test(){});
getRaw("/filter/flipy/" + y, function test(){});
return false;
}
function setFilter(){ function updateCrashLog() {
let i = document.getElementById('filtername').value; getRaw("/apps/crashlog", function (text) {
let url = "/filter/img/" + i; document.getElementById('crashlogs').value = text;
getRaw(url, function test(){}); });
return false; }
}
function setFilterExpr(){
let expr = document.getElementById('filterexpr').value;
let url = "/filter/expr/";
post(url, {"expr": expr});
return false;
}
function updateLog(){
getRaw("/apps/log", function(text){
document.getElementById('logs').value = text;
});
}
function updateCrashLog(){
getRaw("/apps/crashlog", function(text){
document.getElementById('crashlogs').value = text;
});
}
function enableCrashLog(){
document.getElementById("crashlogform").style.display = "none";
document.getElementById("crashlogs").style.display = "block";
updateCrashLog();
setInterval(updateCrashLog, 1000);
return false;
}
function enableLog(){
document.getElementById("logform").style.display = "none";
document.getElementById("logs").style.display = "block";
updateLog();
setInterval(updateLog, 1000);
return false;
}
getJSON("/apps/list", populateForm);
</script>
</body>
function enableCrashLog() {
document.getElementById("crashlogbtn").remove();
document.getElementById("crashlogs").style.display = "block";
updateCrashLog();
setInterval(updateCrashLog, 1000);
return false;
}
function enableLog() {
document.getElementById("logbtn").remove();
document.getElementById("logs").style.display = "block";
updateLog();
setInterval(updateLog, 1000);
return false;
}
getJSON("/apps/list", populateForm);
</script>
</div>
</body>
</html> </html>

153
main.py
View File

@ -4,7 +4,6 @@ import json
import logging import logging
import math import math
import os import os
import string
import subprocess import subprocess
import threading import threading
import time import time
@ -12,13 +11,12 @@ from collections import OrderedDict
import bottle import bottle
import numpy as np import numpy as np
import scipy.misc
import serial import serial
import config import config
import filters
logging.basicConfig(filename='pixelserver.log', level=config.LogLevel) logging.basicConfig(filename='pixelserver.log', level=config.LogLevel)
running = True
######################################################################## ########################################################################
@ -151,7 +149,7 @@ if config.UseGui:
def join(self, **kwargs): def join(self, **kwargs):
with self.cv: with self.cv:
self.cv.notify_all() self.cv.notify_all()
super().join() super().join(**kwargs)
######################################################################## ########################################################################
@ -162,6 +160,7 @@ class SerialWriter(threading.Thread):
super().__init__(daemon=True) super().__init__(daemon=True)
self.cv = threading.Condition() self.cv = threading.Condition()
self.datasource = datasource.addListener(self.cv) self.datasource = datasource.addListener(self.cv)
self.gamma_rgbw = 0, 0, 0, 0
self.updateGamma = False self.updateGamma = False
def run(self): def run(self):
@ -179,13 +178,15 @@ class SerialWriter(threading.Thread):
frame = self.datasource.getData() frame = self.datasource.getData()
data = frame.buffer.reshape((config.ScreenX * config.ScreenY * frame.channels,)).astype(np.uint8).tobytes() data = frame.buffer.reshape((config.ScreenX * config.ScreenY * frame.channels,)).astype(np.uint8).tobytes()
if self.updateGamma: if self.updateGamma:
buf = bytearray(b"\x00") * 4 * 256 r, g, b, w = self.gamma_rgbw
apply = lambda x, g: max(0, min(255, int(math.pow(x / 255, g) * 255)))
buf = bytearray(4 * 256)
for i in range(256): for i in range(256):
apply = lambda x, g: max(0, min(255, int(math.pow(x / 255, g) * 255))) buf[i] = apply(i, r)
buf[i] = apply(i, self.r) buf[i + 256] = apply(i, g)
buf[i + 256] = apply(i, self.g) buf[i + 256 * 2] = apply(i, b)
buf[i + 512] = apply(i, self.b) buf[i + 256 * 3] = apply(i, w)
buf[i + 512 + 256] = apply(i, self.w)
ser.write(b"\x02") ser.write(b"\x02")
ser.write(buf) ser.write(buf)
self.updateGamma = False self.updateGamma = False
@ -195,7 +196,7 @@ class SerialWriter(threading.Thread):
elif frame.channels == 4: elif frame.channels == 4:
ser.write(b"\03") ser.write(b"\03")
ser.write(data) ser.write(data)
logging.debug("Time to gui: " + str(time.time() - frame.created)) logging.debug(f"Time to gui: {time.time() - frame.created}")
ser.flush() ser.flush()
except Exception as e: except Exception as e:
if ser is not None: if ser is not None:
@ -209,11 +210,11 @@ class SerialWriter(threading.Thread):
def join(self, **kwargs): def join(self, **kwargs):
with self.cv: with self.cv:
self.cv.notify_all() self.cv.notify_all()
super().join() super().join(**kwargs)
def setGamma(self, r, g, b, w): def setGamma(self, r, g, b, w):
with self.cv: with self.cv:
self.r, self.g, self.b, self.w = r, g, b, w self.gamma_rgbw = r, g, b, w
self.updateGamma = True self.updateGamma = True
self.cv.notify_all() self.cv.notify_all()
@ -317,7 +318,7 @@ class AppRunner(threading.Thread):
self.requestedApp = app self.requestedApp = app
self.param = param self.param = param
self.cv.notify_all() self.cv.notify_all()
logging.info("Requesting app: " + str(app)) logging.info(f"Requesting app: {app}")
def startApp(self, i, param=""): def startApp(self, i, param=""):
app = config.Apps[i] app = config.Apps[i]
@ -373,90 +374,14 @@ class AppRunner(threading.Thread):
def setGamma(self, r, g, b, w): def setGamma(self, r, g, b, w):
self.serial.setGamma(r, g, b, w) self.serial.setGamma(r, g, b, w)
def setFilter(self, name, filter): def setFilter(self, name, filter_):
self.filters[name] = filter self.filters[name] = filter_
def removeFilter(self, name): def removeFilter(self, name):
if name in self.filters.keys(): if name in self.filters.keys():
del self.filters[name] del self.filters[name]
########################################################################
# Filter Api #
########################################################################
def MakeBrightnessFilter(intensity):
def filter(img):
return (img * intensity).astype(np.uint8)
return filter
def FlipXFilter(intensity):
return intensity[:, ::-1, :]
def FlipYFilter(intensity):
return intensity[::-1, :, :]
def MakeBrightnessImageFilter(name):
img = scipy.misc.imread("filter/" + name + ".png", flatten=True) / 255
# img = np.transpose(img)
def filter(intensity):
intensity = intensity.astype(float)
for i in range(intensity.shape[2]):
intensity[:, :, i] *= img
return intensity.astype(np.uint8)
return filter
def strings(s):
allowed_chars = string.ascii_letters + string.digits + "+-*/()."
i = 0
outlist = []
while i != len(s):
if s[i] not in allowed_chars:
raise Exception("Unexpected char " + s[i])
if s[i] not in string.ascii_letters:
i += 1
continue
out = ""
while i != len(s) and s[i] in string.ascii_letters + string.digits:
out += s[i]
i += 1
outlist.append(out)
return outlist
def eval_safer(expr, x, y, t):
symbols = {"x": x, "y": y, "t": t,
"sin": np.sin, "cos": np.cos, "exp": np.exp, "tan": np.tan}
strs = strings(expr)
for s in strs:
if s not in symbols.keys():
raise Exception("unexpected symbol: " + s)
return eval(expr, {}, symbols)
def MakeBrightnessExprFilter(expr):
t0 = time.time()
x, y = np.meshgrid(np.arange(config.ScreenX), np.arange(config.ScreenY))
eval_safer(expr, 0, 0, 0)
def filter(intensity):
t = time.time() - t0
intensity = intensity.astype(float)
filter = 0 * x + eval_safer(expr, x, y, t)
filter = np.clip(np.nan_to_num(filter), 0, 1)
for i in range(intensity.shape[2]):
intensity[:, :, i] *= filter
return intensity.astype(np.uint8)
return filter
######################################################################## ########################################################################
# Web Api # # Web Api #
######################################################################## ########################################################################
@ -530,17 +455,15 @@ def apps_running():
def index(): def index():
return bottle.static_file("index.html", root='html') return bottle.static_file("index.html", root='html')
@bottle.route("/<filepath:path>") @bottle.route("/<filepath:path>")
def serve_static(filepath): def serve_static(filepath):
return bottle.static_file(filepath, root='html') return bottle.static_file(filepath, root='html')
@bottle.route("/setgamma/<r>/<g>/<b>/<w>") @bottle.route("/setgamma/<r>/<g>/<b>/<w>")
def setGamma(r, g, b, w): def setGamma(r, g, b, w):
r = float(r) runner.setGamma(float(r), float(g), float(b), float(w))
g = float(g)
b = float(b)
w = float(w)
runner.setGamma(r, g, b, w)
return "ok" return "ok"
@ -549,14 +472,14 @@ def setIntensity(i):
i = float(i) i = float(i)
if not 0 <= i <= 1: if not 0 <= i <= 1:
return "bad_value" return "bad_value"
runner.setFilter("0_intensity", MakeBrightnessFilter(i)) runner.setFilter("0_intensity", filters.MakeBrightnessFilter(i))
return "ok" return "ok"
@bottle.route("/filter/flipx/<do>") @bottle.route("/filter/flipx/<do>")
def flipx(do): def flipx(do):
if do == "true": if do == "true":
runner.setFilter("1_flipx", FlipXFilter) runner.setFilter("1_flipx", filters.FlipXFilter)
else: else:
runner.removeFilter("1_flipx") runner.removeFilter("1_flipx")
return "ok" return "ok"
@ -565,7 +488,7 @@ def flipx(do):
@bottle.route("/filter/flipy/<do>") @bottle.route("/filter/flipy/<do>")
def flipy(do): def flipy(do):
if do == "true": if do == "true":
runner.setFilter("1_flipy", FlipYFilter) runner.setFilter("1_flipy", filters.FlipYFilter)
else: else:
runner.removeFilter("1_flipy") runner.removeFilter("1_flipy")
return "ok" return "ok"
@ -576,7 +499,7 @@ def setfilter(name):
if name == "none": if name == "none":
runner.removeFilter("3_imgfilter") runner.removeFilter("3_imgfilter")
else: else:
runner.setFilter("3_imgfilter", MakeBrightnessImageFilter(name)) runner.setFilter("3_imgfilter", filters.MakeBrightnessImageFilter(name))
return "ok" return "ok"
@ -586,21 +509,23 @@ def filter_expr():
if expr == "" or expr == "none": if expr == "" or expr == "none":
runner.removeFilter("5_brightnessfunction") runner.removeFilter("5_brightnessfunction")
else: else:
runner.setFilter("5_brightnessfunction", MakeBrightnessExprFilter(expr)) runner.setFilter("5_brightnessfunction", filters.MakeBrightnessExprFilter(expr))
return "ok" return "ok"
######################################################################## if __name__ == '__main__':
# Startup # ########################################################################
######################################################################## # Startup #
runner = AppRunner() ########################################################################
runner.start() running = True
runner = AppRunner()
runner.start()
# runner.setFilter("5_crazy", MakeBrightnessExprFilter("0.5+0.25*sin(x/3)/x")) # runner.setFilter("5_crazy", MakeBrightnessExprFilter("0.5+0.25*sin(x/3)/x"))
bottle.run(host=config.WebHost, port=config.WebPort) bottle.run(host=config.WebHost, port=config.WebPort)
######################################################################## ########################################################################
# Shutdown # # Shutdown #
######################################################################## ########################################################################
running = False running = False
runner.join() runner.join()