diff --git a/backend/app/drivers/waveshare.py b/backend/app/drivers/waveshare.py index 6c615c0a..b2a2cf57 100644 --- a/backend/app/drivers/waveshare.py +++ b/backend/app/drivers/waveshare.py @@ -19,7 +19,7 @@ class WaveshareVariant: display_function: Optional[str] = None display_arguments: Optional[List[str]] = None init_returns_zero: bool = False - color_option: Literal["Unknown", "Black", "BlackRed", "FourGray", "SevenColor", "BlackWhiteYellowRed"] = "Unknown" + color_option: Literal["Unknown", "Black", "BlackWhiteRed", "FourGray", "SevenColor", "BlackWhiteYellowRed"] = "Unknown" # Colors if we can't autodetect VARIANT_COLORS = { @@ -59,7 +59,7 @@ class WaveshareVariant: "EPD_5in65f": "SevenColor", } -# TODO: BlackRed support +# TODO: BlackWhiteRed support # TODO: BlackWhiteYellowRed support https://www.waveshare.com/wiki/4.37inch_e-Paper_Module_(G)_Manual#Working_With_Raspberry_Pi # TODO: SevenColor support @@ -157,7 +157,7 @@ def convert_waveshare_source(variant_key: str) -> WaveshareVariant: if variant.display_arguments == ["Black"]: variant.color_option = "Black" elif variant.display_arguments == ["Black", "Red"]: - variant.color_option = "BlackRed" + variant.color_option = "BlackWhiteRed" elif variant.display_arguments == ["BlackWhiteYellowRed"]: variant.color_option = "BlackWhiteYellowRed" elif variant.display_arguments == ["FourGray"]: @@ -205,9 +205,9 @@ def write_waveshare_driver_nim(drivers: Dict[str, Driver]) -> str: waveshareDisplay.{variant.sleep_function}() proc renderImage*(image: seq[uint8]) = - {f'waveshareDisplay.{variant.display_function}(addr image[0])' if variant.color_option != 'BlackRed' else 'discard'} + {f'waveshareDisplay.{variant.display_function}(addr image[0])' if variant.color_option != 'BlackWhiteRed' else 'discard'} -proc renderImageBlackRed*(image1: seq[uint8], image2: seq[uint8]) = - {f'waveshareDisplay.{variant.display_function}(addr image1[0], addr image2[0])' if variant.color_option == 'BlackRed' else 'discard'} +proc renderImageBlackWhiteRed*(image1: seq[uint8], image2: seq[uint8]) = + {f'waveshareDisplay.{variant.display_function}(addr image1[0], addr image2[0])' if variant.color_option == 'BlackWhiteRed' else 'discard'} """ diff --git a/backend/list_devices.py b/backend/list_devices.py index b57c0648..81d2036c 100644 --- a/backend/list_devices.py +++ b/backend/list_devices.py @@ -23,7 +23,7 @@ for v in variants: color = { "Black": "Black/White", - "BlackRed": "Black/White/Red", + "BlackWhiteRed": "Black/White/Red", "BlackWhiteYellowRed": "Black/White/Yellow/Red (not implemented)", "FourGray": "4 Grayscale (not implemented!)", "SevenColor": "7 Color (not implemented!)", diff --git a/frameos/src/drivers/waveshare/driver.nim b/frameos/src/drivers/waveshare/driver.nim index 7d3f116f..d44094e1 100644 --- a/frameos/src/drivers/waveshare/driver.nim +++ b/frameos/src/drivers/waveshare/driver.nim @@ -30,5 +30,5 @@ proc renderImage*(image: seq[uint8]) = # This file is autogenerated for your frame on deploy waveshareDisplay.EPD_2in13_V3_Display_Base(addr image[0]) -proc renderImageBlackRed*(image1: seq[uint8], image2: seq[uint8]) = +proc renderImageBlackWhiteRed*(image1: seq[uint8], image2: seq[uint8]) = discard diff --git a/frameos/src/drivers/waveshare/types.nim b/frameos/src/drivers/waveshare/types.nim index c9cc6363..331c78fe 100644 --- a/frameos/src/drivers/waveshare/types.nim +++ b/frameos/src/drivers/waveshare/types.nim @@ -1,6 +1,6 @@ type ColorOption* = enum Black = "Black" - BlackRed = "BlackRed" + BlackWhiteRed = "BlackWhiteRed" SevenColor = "SevenColor" FourGray = "FourGray" BlackWhiteYellowRed = "BlackWhiteYellowRed" diff --git a/frameos/src/drivers/waveshare/waveshare.nim b/frameos/src/drivers/waveshare/waveshare.nim index 3fdb48a8..3ed88392 100644 --- a/frameos/src/drivers/waveshare/waveshare.nim +++ b/frameos/src/drivers/waveshare/waveshare.nim @@ -16,6 +16,8 @@ type Driver* = ref object of FrameOSDriver var lastFloatImageLock: Lock lastFloatImage: seq[float] + lastPixelsLock: Lock + lastPixels: seq[uint8] proc setLastFloatImage*(image: seq[float]) = withLock lastFloatImageLock: @@ -25,6 +27,14 @@ proc getLastFloatImage*(): seq[float] = withLock lastFloatImageLock: result = lastFloatImage +proc setLastPixels*(image: seq[uint8]) = + withLock lastPixelsLock: + lastPixels = image + +proc getLastPixels*(): seq[uint8] = + withLock lastPixelsLock: + result = lastPixels + proc init*(frameOS: FrameOS): Driver = let logger = frameOS.logger let width = waveshareDriver.width @@ -89,8 +99,8 @@ proc renderFourGray*(self: Driver, image: Image) = blackImage[index div 4] = blackImage[index div 4] or ((bw and 0b11) shl (6 - (index mod 4) * 2)) waveshareDriver.renderImage(blackImage) - -proc renderBlackRed*(self: Driver, image: Image) = +proc renderBlackWhiteRed*(self: Driver, image: Image) = + let pixels = ditherPaletteIndexed(image, @[(0, 0, 0), (255, 0, 0), (255, 255, 255)]) let rowWidth = ceil(image.width.float / 8).int var blackImage = newSeq[uint8](rowWidth * image.height) var redImage = newSeq[uint8](rowWidth * image.height) @@ -98,21 +108,23 @@ proc renderBlackRed*(self: Driver, image: Image) = for y in 0.. 100 and pixel.g > 50 and pixel.b > 50: 1 else: 0 - blackImage[index div 8] = blackImage[index div 8] or (bw shl (7 - (index mod 8))) - redImage[index div 8] = redImage[index div 8] or (red shl (7 - (index mod 8))) - - waveshareDriver.renderImageBlackRed(blackImage, redImage) + let index = y * rowWidth + x div 8 + let oneByte = pixels[inputIndex div 2] + let pixel = if inputIndex mod 2 == 0: oneByte shr 4 else: oneByte and 0x0F + let bw: uint8 = if pixel == 0: 1 else: 0 + let red: uint8 = if pixel == 1: 1 else: 0 + blackImage[index] = blackImage[index] or (bw shl (7 - (x mod 8))) + redImage[index] = redImage[index] or (red shl (7 - (x mod 8))) -proc renderSevenColor*(self: Driver, image: Image) = - raise newException(Exception, "7 color mode not yet supported") + waveshareDriver.renderImageBlackWhiteRed(blackImage, redImage) proc renderBlackWhiteYellowRed*(self: Driver, image: Image) = - raise newException(Exception, "Black White Yellow Red mode not yet supported") + let pixels = ditherPaletteIndexed(image, saturated4ColorPalette) + waveshareDriver.renderImage(pixels) + +proc renderSevenColor*(self: Driver, image: Image) = + let pixels = ditherPaletteIndexed(image, saturated7ColorPalette) + waveshareDriver.renderImage(pixels) proc render*(self: Driver, image: Image) = # Refresh at least every 12h to preserve display @@ -129,8 +141,8 @@ proc render*(self: Driver, image: Image) = case waveshareDriver.colorOption: of ColorOption.Black: self.renderBlack(image) - of ColorOption.BlackRed: - self.renderBlackRed(image) + of ColorOption.BlackWhiteRed: + self.renderBlackWhiteRed(image) of ColorOption.SevenColor: self.renderSevenColor(image) of ColorOption.FourGray: @@ -142,31 +154,58 @@ proc render*(self: Driver, image: Image) = # Convert the rendered pixels to a PNG image. For accurate colors on the web. proc toPng*(rotate: int = 0): string = - let pixels = getLastFloatImage() var outputImage = newImage(width, height) case waveshareDriver.colorOption: of ColorOption.Black: + let pixels = getLastFloatImage() for y in 0 ..< height: for x in 0 ..< width: let index = y * width + x - outputImage.data[index].r = (pixels[index] * 255).uint8 - outputImage.data[index].g = (pixels[index] * 255).uint8 - outputImage.data[index].b = (pixels[index] * 255).uint8 + let pixel = (pixels[index] * 255).uint8 + outputImage.data[index].r = pixel + outputImage.data[index].g = pixel + outputImage.data[index].b = pixel outputImage.data[index].a = 255 of ColorOption.FourGray: + let pixels = getLastFloatImage() for y in 0 ..< height: for x in 0 ..< width: let index = y * width + x - outputImage.data[index].r = (pixels[index] * 85).uint8 - outputImage.data[index].g = (pixels[index] * 85).uint8 - outputImage.data[index].b = (pixels[index] * 85).uint8 + let pixel = (pixels[index] * 85).uint8 + outputImage.data[index].r = pixel + outputImage.data[index].g = pixel + outputImage.data[index].b = pixel + outputImage.data[index].a = 255 + of ColorOption.BlackWhiteRed: + let pixels = getLastPixels() + for y in 0 ..< height: + for x in 0 ..< width: + let index = y * width + x + let pixel = (pixels[index div 4] shr ((3 - (index mod 4)) * 2)) and 0x03 + outputImage.data[index].r = if pixel == 0: 0 else: 255 + outputImage.data[index].g = if pixel == 2: 255 else: 1 + outputImage.data[index].b = if pixel == 2: 255 else: 1 outputImage.data[index].a = 255 - of ColorOption.BlackRed: - discard - of ColorOption.SevenColor: - discard of ColorOption.BlackWhiteYellowRed: - discard + let pixels = getLastPixels() + for y in 0 ..< height: + for x in 0 ..< width: + let index = y * width + x + let pixel = (pixels[index div 4] shr ((3 - (index mod 4)) * 2)) and 0x03 + outputImage.data[index].r = saturated4ColorPalette[pixel][0].uint8 + outputImage.data[index].g = saturated4ColorPalette[pixel][1].uint8 + outputImage.data[index].b = saturated4ColorPalette[pixel][2].uint8 + outputImage.data[index].a = 255 + of ColorOption.SevenColor: + let pixels = getLastPixels() + for y in 0 ..< height: + for x in 0 ..< width: + let index = y * width + x + let pixel = pixels[index] + outputImage.data[index].r = saturated7ColorPalette[pixel][0].uint8 + outputImage.data[index].g = saturated7ColorPalette[pixel][1].uint8 + outputImage.data[index].b = saturated7ColorPalette[pixel][2].uint8 + outputImage.data[index].a = 255 if rotate != 0: return outputImage.rotateDegrees(rotate).encodeImage(PngFormat) diff --git a/frameos/src/frameos/runner.nim b/frameos/src/frameos/runner.nim index f436f936..b6e79a2b 100644 --- a/frameos/src/frameos/runner.nim +++ b/frameos/src/frameos/runner.nim @@ -5,7 +5,7 @@ import scenes/default as defaultScene import frameos/channels import frameos/types import frameos/config -from frameos/utils/image import rotateDegrees, renderError, scaleAndDrawImage +import frameos/utils/image import drivers/drivers as drivers diff --git a/frameos/src/frameos/server.nim b/frameos/src/frameos/server.nim index 84d8f030..1fea339f 100644 --- a/frameos/src/frameos/server.nim +++ b/frameos/src/frameos/server.nim @@ -14,7 +14,6 @@ import strutils import drivers/drivers as drivers import frameos/types import frameos/channels -import frameos/utils/image from frameos/runner import getLastPng, triggerRender var globalFrameConfig: FrameConfig diff --git a/frameos/src/frameos/utils/dither.nim b/frameos/src/frameos/utils/dither.nim index ae3cca4b..0f9c6597 100644 --- a/frameos/src/frameos/utils/dither.nim +++ b/frameos/src/frameos/utils/dither.nim @@ -1,5 +1,54 @@ import math, pixie +const desaturated4ColorPalette* = @[ + (0, 0, 0), + (255, 255, 255), + (255, 255, 0), + (255, 0, 0), +] + +const saturated4ColorPalette* = @[ + (57, 48, 57), + (255, 255, 255), + (208, 190, 71), + (156, 72, 75), +] + +const desaturated7ColorPalette* = @[ + (0, 0, 0), + (255, 255, 255), + (0, 255, 0), + (0, 0, 255), + (255, 0, 0), + (255, 255, 0), + (255, 140, 0) +] + +const saturated7ColorPalette* = @[ + (57, 48, 57), + (255, 255, 255), + (58, 91, 70), + (61, 59, 94), + (156, 72, 75), + (208, 190, 71), + (177, 106, 73), +] + +proc clip8(value: int): uint8 {.inline.} = + if value < 0: return 0 + elif value > 255: return 255 + else: return value.uint8 + +proc nextPowerOfTwo(bits: int): int = + var n = bits - 1 + n = n or n shr 1 + n = n or n shr 2 + n = n or n shr 4 + n = n or n shr 8 + n = n or n shr 16 + n += 1 + return n + proc toGrayscaleFloat*(image: Image, grayscale: var seq[float], multiple: float = 1.0) = let width = image.width @@ -13,8 +62,8 @@ proc toGrayscaleFloat*(image: Image, grayscale: var seq[float], multiple: float proc floydSteinberg*(pixels: var seq[float], width, height: int) = let distribution = [7.0 / 16.0, 3.0 / 16.0, 5.0 / 16.0, 1.0 / 16.0] - u = [0, 1, 1, 1] # + y - v = [1, -1, 0, 1] # + x + dy = [0, 1, 1, 1] + dx = [1, -1, 0, 1] for y in 0..= 0) and (x + v[i] < width) and (y + u[i] < height): - pixels[(y + u[i]) * width + (x + v[i])] += error * distribution[i] + if (x + dx[i] >= 0) and (x + dx[i] < width) and (y + dy[i] < height): + pixels[(y + dy[i]) * width + (x + dx[i])] += error * distribution[i] + +proc closestPalette*(palette: seq[(int, int, int)], r, g, b: int): (int, int, int, int) = + # TODO: optimize with a lookup table + var index: int = 0 + var min = 99999999999 + for i in 0..= 0) and (x + dx[i] < width) and (y + dy[i] < height): + let errorIndex = (y + dy[i]) * width + (x + dx[i]) + img.data[errorIndex].r = clip8(img.data[errorIndex].r.int + (errorR * distribution[i] div 16)) + img.data[errorIndex].g = clip8(img.data[errorIndex].g.int + (errorG * distribution[i] div 16)) + img.data[errorIndex].b = clip8(img.data[errorIndex].b.int + (errorB * distribution[i] div 16)) + + return output