Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Palette based dithering #47

Merged
merged 4 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions backend/app/drivers/waveshare.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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"]:
Expand Down Expand Up @@ -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'}

"""
2 changes: 1 addition & 1 deletion backend/list_devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!)",
Expand Down
2 changes: 1 addition & 1 deletion frameos/src/drivers/waveshare/driver.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion frameos/src/drivers/waveshare/types.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
type ColorOption* = enum
Black = "Black"
BlackRed = "BlackRed"
BlackWhiteRed = "BlackWhiteRed"
SevenColor = "SevenColor"
FourGray = "FourGray"
BlackWhiteYellowRed = "BlackWhiteYellowRed"
95 changes: 67 additions & 28 deletions frameos/src/drivers/waveshare/waveshare.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -89,30 +99,32 @@ 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)

for y in 0..<image.height:
for x in 0..<image.width:
let inputIndex = y * image.width + x
let index = y * rowWidth * 8 + x
let pixel = image.data[inputIndex]
let weightedSum = pixel.r * 299 + pixel.g * 587 + pixel.b * 114
let bw: uint8 = if weightedSum < 128 * 1000: 0 else: 1
let red: uint8 = if pixel.r > 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
Expand All @@ -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:
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion frameos/src/frameos/runner.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion frameos/src/frameos/server.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
115 changes: 111 additions & 4 deletions frameos/src/frameos/utils/dither.nim
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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..<height:
for x in 0..<width:
Expand All @@ -24,5 +73,63 @@ proc floydSteinberg*(pixels: var seq[float], width, height: int) =
pixels[index] = value

for i in 0..<4:
if (x + v[i] >= 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..<palette.len:
let distance = abs(r - palette[i][0]) + abs(g - palette[i][1]) + abs(b - palette[i][2])
if distance < min:
min = distance
index = i
return (index, palette[index][0], palette[index][1], palette[index][2])


proc ditherPaletteIndexed*(image: Image, palette: seq[(int, int, int)]): seq[uint8] =
let
img = image.copy
width = img.width
height = img.height
distribution = [7, 3, 5, 1]
dy = [0, 1, 1, 1]
dx = [1, -1, 0, 1]
bits = nextPowerOfTwo(palette.len)

let rowWidth = ceil(width.float / bits.float).int
var output = newSeq[uint8](height * rowWidth)

for y in 0..<height:
for x in 0..<width:
let dataIndex = y * width + x

let imageR = img.data[dataIndex].r.int
let imageG = img.data[dataIndex].g.int
let imageB = img.data[dataIndex].b.int

let (index, palR, palG, palB) = closestPalette(palette, imageR, imageG, imageB)

let errorR = imageR - palR
let errorG = imageG - palG
let errorB = imageB - palB

case bits:
of 8: output[dataIndex] = index.uint8
of 4:
output[dataIndex div 2] = output[dataIndex div 2] or (index shl (5 - (dataIndex mod 2) * 4)).uint8
of 2:
output[dataIndex div 4] = output[dataIndex div 4] or (index shl (6 - (dataIndex mod 4) * 2)).uint8
of 1:
output[dataIndex div 8] = output[dataIndex div 8] or (index shl (7 - (dataIndex mod 8))).uint8
else: discard

for i in 0..<4:
if (x + dx[i] >= 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
Loading