Performance improvements, version selection

This commit is contained in:
Casey 2024-01-18 18:26:08 +03:00
parent 49fa1d7547
commit e61774d4dd
Signed by: hkc
GPG Key ID: F0F6CFE11CDB0960
1 changed files with 44 additions and 16 deletions

View File

@ -4,6 +4,7 @@ from typing import BinaryIO, TextIO
from PIL import Image from PIL import Image
from argparse import ArgumentParser, RawTextHelpFormatter from argparse import ArgumentParser, RawTextHelpFormatter
from textwrap import dedent from textwrap import dedent
from functools import lru_cache
class Converter: class Converter:
@ -28,31 +29,34 @@ class Converter:
PIX_BITS = [[1, 2], [4, 8], [16, 0]] PIX_BITS = [[1, 2], [4, 8], [16, 0]]
MAX_DIFF = (3**0.5) * 255 MAX_DIFF = 3 * 255
def __init__(self, image: Image.Image): def __init__(self, image: Image.Image):
self._img = image.convert("P", palette=Image.ADAPTIVE, colors=16) self._img = image.convert("P", palette=Image.ADAPTIVE, colors=16)
self._imgdata = self._img.load()
self._palette: list[int] = self._img.getpalette() or [] self._palette: list[int] = self._img.getpalette() or []
if len(self._palette) < 16 * 3: if len(self._palette) < 16 * 3:
self._palette += [0] * ((16 * 3) - len(self._palette)) self._palette += [0] * ((16 * 3) - len(self._palette))
@lru_cache
def _brightness(self, i: int) -> float: def _brightness(self, i: int) -> float:
r, g, b = self._palette[i * 3 : (i + 1) * 3] r, g, b = self._palette[i * 3 : (i + 1) * 3]
return (r + g + b) / 768 return (r + g + b) / 768
@lru_cache
def _distance(self, a: int, b: int) -> float: def _distance(self, a: int, b: int) -> float:
r1, g1, b1 = self._palette[a * 3 : (a + 1) * 3] r1, g1, b1 = self._palette[a * 3 : (a + 1) * 3]
r2, g2, b2 = self._palette[b * 3 : (b + 1) * 3] r2, g2, b2 = self._palette[b * 3 : (b + 1) * 3]
return ( rd, gd, bd = r1 - r2, g1 - g2, b1 - b2
(r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2 return (rd * rd + gd * gd + bd * bd) / self.MAX_DIFF
) ** 0.5 / self.MAX_DIFF
@lru_cache
def _get_colors(self, x: int, y: int) -> tuple[int, int]: def _get_colors(self, x: int, y: int) -> tuple[int, int]:
brightest_i, brightest_l = 0, 0 brightest_i, brightest_l = 0, 0
darkest_i, darkest_l = 0, 768 darkest_i, darkest_l = 0, 768
for oy, line in enumerate(self.PIX_BITS): for oy, line in enumerate(self.PIX_BITS):
for ox, bit in enumerate(line): for ox, bit in enumerate(line):
pix = self._img.getpixel((x + ox, y + oy)) pix = self._imgdata[x + ox, y + oy]
brightness = self._brightness(pix) brightness = self._brightness(pix)
if brightness > brightest_l: if brightness > brightest_l:
brightest_l, brightest_i = brightness, pix brightest_l, brightest_i = brightness, pix
@ -69,11 +73,11 @@ class Converter:
for oy, line in enumerate(self.PIX_BITS): for oy, line in enumerate(self.PIX_BITS):
for ox, bit in enumerate(line): for ox, bit in enumerate(line):
if not self._is_darker( if not self._is_darker(
dark_i, bri_i, self._img.getpixel((x + ox, y + oy)) dark_i, bri_i, self._imgdata[x + ox, y + oy]
): ):
out |= bit out |= bit
# bottom right pixel fix? # bottom right pixel fix?
if not self._is_darker(dark_i, bri_i, self._img.getpixel((x + 1, y + 2))): if not self._is_darker(dark_i, bri_i, self._imgdata[x + 1, y + 2]):
out ^= 31 out ^= 31
dark_i, bri_i = bri_i, dark_i dark_i, bri_i = bri_i, dark_i
return out, dark_i, bri_i return out, dark_i, bri_i
@ -89,24 +93,31 @@ class Converter:
fp.write(bytes([(value & 0x7F) | 0x80])) fp.write(bytes([(value & 0x7F) | 0x80]))
value >>= 7 value >>= 7
def export_binary(self, io: BinaryIO): def export_binary(self, io: BinaryIO, version: int = -1):
if self._img.width <= 510 and self._img.height <= 765: if version == -1:
if self._img.width <= 255 * 2 and self._img.height < 255 * 3:
version = 0
else:
version = 1
if version == 0:
io.write(b"CCPI") # old format io.write(b"CCPI") # old format
io.write(bytes([self._img.width // 2, self._img.height // 3, 0])) io.write(bytes([self._img.width // 2, self._img.height // 3, 0]))
io.write(bytes(self._palette[: 16 * 3])) io.write(bytes(self._palette[: 16 * 3]))
else: elif version == 1:
io.write(b"CPI\x01") # CPIv1 io.write(b"CPI\x01") # CPIv1
self._write_varint(io, self._img.width // 2) self._write_varint(io, self._img.width // 2)
self._write_varint(io, self._img.height // 3) self._write_varint(io, self._img.height // 3)
io.write(bytes(self._palette[: 16 * 3])) io.write(bytes(self._palette[: 16 * 3]))
written = 0 else:
raise ValueError(f"invalid version {version}")
for y in range(0, self._img.height - 2, 3): for y in range(0, self._img.height - 2, 3):
line: bytearray = bytearray() line: bytearray = bytearray()
for x in range(0, self._img.width - 1, 2): for x in range(0, self._img.width - 1, 2):
ch, bg, fg = self._get_block(x, y) ch, bg, fg = self._get_block(x, y)
line.extend([(ch + 0x80) & 0xFF, fg << 4 | bg]) line.extend([(ch + 0x80) & 0xFF, fg << 4 | bg])
written += io.write(line) io.write(line)
assert written == (self._img.width // 2) * (self._img.height // 3) * 2
def export(self, io: TextIO): def export(self, io: TextIO):
io.write("local m = peripheral.find('monitor')\n") io.write("local m = peripheral.find('monitor')\n")
@ -163,6 +174,23 @@ def main():
type=int, type=int,
help="Height in characters", help="Height in characters",
) )
parser.add_argument(
"-V",
dest="cpi_version",
type=int,
default=-1,
choices=(-1, 0, 1),
help=dedent(
"""\
Force specific CPI version to be used.
Only applies to binary format.
Valid versions:
-V -1 Choose any fitting one
For images smaller than 255x255, uses CPIv0
-V 0 OG CPI, 255x255 maximum, uncompressed
-V 1 CPIv1, huge images, uncompressed"""
),
)
parser.add_argument( parser.add_argument(
"-p", "-p",
dest="placement", dest="placement",
@ -260,7 +288,7 @@ def main():
converter.export(fp) converter.export(fp)
else: else:
with open(args.output_path, "wb") as fp: with open(args.output_path, "wb") as fp:
converter.export_binary(fp) converter.export_binary(fp, args.cpi_version)
if __name__ == "__main__": if __name__ == "__main__":