Skip to content

Commit 42cfce3

Browse files
committed
dither before rendering
1 parent 5a474b4 commit 42cfce3

File tree

4 files changed

+129
-67
lines changed

4 files changed

+129
-67
lines changed

backend/app/drivers/devices.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ def drivers_for_frame(frame: Frame) -> dict[str, Driver]:
1313
}
1414
if device == "pimoroni.inky_impression" or device == "pimoroni.inky_impression_7" or device == "pimoroni.inky_impression_13":
1515
device_drivers["gpioButton"] = DRIVERS["gpioButton"]
16+
if device == "pimoroni.inky_impression_7" or device == "pimoroni.inky_impression_13":
17+
device_drivers["inkyPython"].can_png = True
1618
elif device == "pimoroni.hyperpixel2r":
1719
device_drivers = {"inkyHyperPixel2r": DRIVERS["inkyHyperPixel2r"]}
1820
elif device == "framebuffer":

backend/app/drivers/drivers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class Driver:
1717
name="inkyPython",
1818
import_path="inkyPython/inkyPython",
1919
vendor_folder="inkyPython",
20+
can_png=False, # will be set to true for the frames that support this
2021
can_render=True,
2122
),
2223
"gpioButton": Driver(

frameos/src/drivers/inkyPython/inkyPython.nim

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import osproc, os, streams, pixie, json, options, strutils, strformat
1+
import osproc, os, streams, pixie, json, options, strutils, strformat, locks
22
import frameos/types
33
import frameos/utils/dither
4+
import frameos/utils/image
45

56
type ScreenInfo* = object
67
width*: int
@@ -16,6 +17,25 @@ type Driver* = ref object of FrameOSDriver
1617
lastImageData: seq[ColorRGBX]
1718
debug: bool
1819

20+
var
21+
lastPixelsLock: Lock
22+
lastPixels: seq[uint8] = @[]
23+
lastWidth: int
24+
lastHeight: int
25+
26+
proc setLastPixels*(image: seq[uint8], width: int, height: int) =
27+
withLock lastPixelsLock:
28+
lastPixels = image
29+
lastWidth = width
30+
lastHeight = height
31+
32+
proc getLastPixels*(): seq[uint8] =
33+
withLock lastPixelsLock:
34+
result = lastPixels
35+
36+
proc notifyImageAvailable*(self: Driver) =
37+
self.logger.log(%*{"event": "render:dither", "info": "Dithered image available"})
38+
1939
proc safeLog(logger: Logger, message: string): JsonNode =
2040
try:
2141
result = parseJson(message)
@@ -41,21 +61,6 @@ proc safeStartProcess*(cmd: string; args: seq[string] = @[];
4161
proc deviceArgs(dev: string): seq[string] =
4262
if dev.len > 0: @["--device", dev] else: @[]
4363

44-
proc paletteArgs(p: PaletteConfig, device: string): seq[string] =
45-
if p != nil and p.colors.len > 0:
46-
let arr = newJArray()
47-
for (r, g, b) in p.colors:
48-
arr.add( %* [r, g, b])
49-
@["--palette", $arr]
50-
elif device == "pimoroni.inky_impression_7" or device == "pimoroni.inky_impression_13":
51-
let arr = newJArray()
52-
for (r, g, b) in spectra6ColorPalette:
53-
if r < 256 and b < 256 and g < 256:
54-
arr.add( %* [r, g, b])
55-
@["--palette", $arr]
56-
else:
57-
@[]
58-
5964
proc init*(frameOS: FrameOS): Driver =
6065
discard frameOS.logger.safeLog("Initializing Inky driver")
6166

@@ -120,16 +125,39 @@ proc render*(self: Driver, image: Image) =
120125
discard self.logger.safeLog("Skipping render. Identical to last render.")
121126
return
122127
self.lastImageData = image.data
123-
let imageData = image.encodeImage(BmpFormat)
128+
129+
var imageData: seq[uint8]
130+
var extraArgs: seq[string] = @[]
131+
if self.device == "pimoroni.inky_impression_7" or self.device == "pimoroni.inky_impression_13":
132+
var palette: seq[(int, int, int)]
133+
if self.palette != nil and self.palette.colors.len > 0:
134+
let c = self.palette.colors
135+
palette = @[
136+
(c[0][0], c[0][1], c[0][2]),
137+
(c[1][0], c[1][1], c[1][2]),
138+
(c[2][0], c[2][1], c[2][2]),
139+
(c[3][0], c[3][1], c[3][2]),
140+
(999, 999, 999),
141+
(c[4][0], c[4][1], c[4][2]),
142+
(c[5][0], c[5][1], c[5][2]),
143+
]
144+
else:
145+
palette = spectra6ColorPalette
146+
imageData = ditherPaletteIndexed(image, palette)
147+
setLastPixels(imageData, image.width, image.height)
148+
self.notifyImageAvailable()
149+
extraArgs.add "--raw"
150+
else:
151+
imageData = cast[seq[uint8]](image.encodeImage(BmpFormat))
124152

125153
let pOpt =
126154
if self.mode == "nixos":
127155
safeStartProcess("/nix/var/nix/profiles/system/sw/bin/inkyPython-run",
128-
deviceArgs(self.device) & paletteArgs(self.palette, self.device),
156+
deviceArgs(self.device) & extraArgs,
129157
"/srv/frameos/vendor/inkyPython", self.logger)
130158
else:
131159
safeStartProcess("./env/bin/python3",
132-
@["run.py"] & deviceArgs(self.device) & paletteArgs(self.palette, self.device),
160+
@["run.py"] & deviceArgs(self.device) & extraArgs,
133161
"/srv/frameos/vendor/inkyPython", self.logger)
134162

135163
if pOpt.isNone:
@@ -186,3 +214,28 @@ proc render*(self: Driver, image: Image) =
186214
discard self.logger.safeLog(line)
187215

188216
process.close()
217+
218+
# Convert the rendered pixels to a PNG image. For accurate colors on the web.
219+
proc toPng*(rotate: int = 0): string =
220+
let width = lastWidth
221+
let height = lastHeight
222+
var outputImage = newImage(width, height)
223+
224+
let pixels = getLastPixels()
225+
if pixels.len == 0:
226+
raise newException(Exception, "No render yet")
227+
for y in 0 ..< height:
228+
for x in 0 ..< width:
229+
let index = y * width + x
230+
let pixelIndex = index div 2
231+
let pixelShift = (1 - (index mod 2)) * 4
232+
let pixel = (pixels[pixelIndex] shr pixelShift) and 0x07
233+
outputImage.data[index].r = spectra6ColorPalette[pixel][0].uint8
234+
outputImage.data[index].g = spectra6ColorPalette[pixel][1].uint8
235+
outputImage.data[index].b = spectra6ColorPalette[pixel][2].uint8
236+
outputImage.data[index].a = 255
237+
238+
if rotate != 0:
239+
return outputImage.rotateDegrees(rotate).encodeImage(PngFormat)
240+
241+
return outputImage.encodeImage(PngFormat)

frameos/vendor/inkyPython/run.py

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import inspect
55
import traceback
66
import json
7+
import numpy
78
from devices.util import log, init_inky, get_int_tuple
89

910
def read_binary_data():
@@ -36,73 +37,78 @@ def parse_palette(palette_str):
3637
if __name__ == "__main__":
3738
parser = argparse.ArgumentParser(add_help=False)
3839
parser.add_argument("--device", default="")
39-
parser.add_argument("--palette", default="")
40+
parser.add_argument("--raw", action="store_true")
4041
args, _ = parser.parse_known_args()
4142

4243
inky = init_inky(args.device)
4344
if not inky:
4445
sys.exit(1)
4546

46-
# If a palette is provided, override both palettes on the instance
47-
if args.palette:
48-
custom = parse_palette(args.palette)
49-
if custom:
50-
try:
51-
inky.DESATURATED_PALETTE = custom
52-
inky.SATURATED_PALETTE = custom
53-
log({"message": "applying custom palette", "palette": custom})
54-
except Exception as e:
55-
log({"warning": f"failed to apply custom palette: {e}"})
56-
else:
57-
log({"warning": "invalid --palette format; ignoring"})
58-
else:
59-
log({"message": "no custom palette provided; using inky default"})
60-
6147
resolution = getattr(inky, "resolution", (getattr(inky, "width", 0), getattr(inky, "height", 0)))
6248
width, height = get_int_tuple(resolution)
6349
colour = getattr(inky, "colour", getattr(inky, "color", None))
6450
log({ "inky": True, "width": width, "height": height, "color": colour })
6551

6652
data = read_binary_data()
67-
log({ "bytesReceived": len(data), "message": "rendering on eink display" })
53+
log({ "bytesReceived": len(data), "format": "dithered" if args.raw else "rgb", "message": "rendering on eink display" })
6854

69-
try:
70-
from PIL import Image
71-
except ImportError:
72-
log({ "error": "PIL python module not installed" })
73-
sys.exit(1)
74-
75-
try:
76-
image = Image.open(io.BytesIO(data))
77-
78-
set_image = getattr(inky, "set_image", None)
79-
if not callable(set_image):
80-
log({ "error": "inky.set_image() not available on this driver" })
55+
if args.raw:
56+
try:
57+
expected = (width * height + 1) // 2
58+
if len(data) != expected:
59+
log({ "error": f"expected {expected} bytes, got {len(data)}" })
60+
sys.exit(1)
61+
arr = numpy.frombuffer(data, dtype=numpy.uint8)
62+
buf = numpy.empty(width * height, dtype=numpy.uint8)
63+
buf[0::2] = arr >> 4
64+
buf[1::2] = arr & 0x0F
65+
inky.buf = buf.reshape((height, width))
66+
show = getattr(inky, "show", None)
67+
if not callable(show):
68+
log({ "error": "inky.show() not available on this driver" })
69+
sys.exit(1)
70+
show()
71+
except Exception as e:
72+
log({ "error": str(e), "stack": traceback.format_exc() })
73+
sys.exit(1)
74+
else:
75+
try:
76+
from PIL import Image
77+
except ImportError:
78+
log({ "error": "PIL python module not installed" })
8179
sys.exit(1)
8280

83-
# Try to match the signature len (1 or 2 params); fall back gracefully.
8481
try:
85-
signature = inspect.signature(set_image)
86-
if len(signature.parameters) == 2:
87-
set_image(image, saturation=1)
88-
elif len(signature.parameters) == 1:
89-
set_image(image)
90-
else:
91-
log({ "error": f"inky.set_image() expects {len(signature.parameters)} params; only 1 or 2 supported" })
82+
image = Image.open(io.BytesIO(data))
83+
84+
set_image = getattr(inky, "set_image", None)
85+
if not callable(set_image):
86+
log({ "error": "inky.set_image() not available on this driver" })
9287
sys.exit(1)
93-
except (ValueError, TypeError):
88+
89+
# Try to match the signature len (1 or 2 params); fall back gracefully.
9490
try:
95-
set_image(image, saturation=1)
96-
except TypeError:
97-
set_image(image)
91+
signature = inspect.signature(set_image)
92+
if len(signature.parameters) == 2:
93+
set_image(image, saturation=1)
94+
elif len(signature.parameters) == 1:
95+
set_image(image)
96+
else:
97+
log({ "error": f"inky.set_image() expects {len(signature.parameters)} params; only 1 or 2 supported" })
98+
sys.exit(1)
99+
except (ValueError, TypeError):
100+
try:
101+
set_image(image, saturation=1)
102+
except TypeError:
103+
set_image(image)
98104

99-
show = getattr(inky, "show", None)
100-
if not callable(show):
101-
log({ "error": "inky.show() not available on this driver" })
105+
show = getattr(inky, "show", None)
106+
if not callable(show):
107+
log({ "error": "inky.show() not available on this driver" })
108+
sys.exit(1)
109+
show()
110+
except Exception as e:
111+
log({ "error": str(e), "stack": traceback.format_exc() })
102112
sys.exit(1)
103-
show()
104-
except Exception as e:
105-
log({ "error": str(e), "stack": traceback.format_exc() })
106-
sys.exit(1)
107113

108114
sys.exit(0)

0 commit comments

Comments
 (0)