Update st7789 driver for greyscale. Add img_cvt.py.

pull/70/head
Peter Hinch 2024-06-07 10:52:35 +01:00
rodzic 1bd163d72d
commit 77f5b211e5
3 zmienionych plików z 254 dodań i 23 usunięć

Wyświetl plik

@ -1,15 +1,15 @@
# 1. Displaying photo images
The display drivers in this repo were primarily designed for displaying geometric shapes
and fonts. With a minor update they may also be used for image display. The method used is
ideal for full screen images however with suitable user code smaller images may be
rendered. It is also possible to overlay an image with GUI controls, although transparency
is not supported.
The display drivers in this repo were originally designed for displaying
geometric shapes and fonts. With a minor update they may also be used for image
display. The method used is ideal for full screen images however with suitable
user code smaller images may be rendered. It is also possible to overlay an
image with GUI controls, although transparency is not supported.
The following notes apply
[nanogui](https://github.com/peterhinch/micropython-nano-gui)
[micro-gui](https://github.com/peterhinch/micropython-micro-gui) and
[micropython-touch](https://github.com/peterhinch/micropython-touch).
GUI references:
[nanogui](https://github.com/peterhinch/micropython-nano-gui)
[micro-gui](https://github.com/peterhinch/micropython-micro-gui)
[micropython-touch](https://github.com/peterhinch/micropython-touch)
Images for display should be converted to a [netpbm format](https://en.wikipedia.org/wiki/Netpbm),
namely a `.pgm` file for a monochrome image or `.ppm` for color. This may be
@ -19,21 +19,21 @@ greyscale to enable a monochrome image to display on a 4-bit driver. This is
done using a CPython utility `img_cvt.py` documented below.
An updated driver has a `greyscale` method enabling the frame buffer contents to
be interpreted at show time as either color or greyscale. This
be interpreted at show time as either color or greyscale.
## 1.2 Supported drivers
Currently only gc9a01 drivers are supported.
Currently gc9a01, ili948x, ili9341 and st7789 drivers are supported.
## 1.3 Monochrome images
These may be displayed using 8-bit or 16-bit drivers by treating it as if it
were color: exporting the image from the graphics program as a `.ppm` color
were color: by exporting the image from the graphics program as a `.ppm` color
image and using `img_cvt.py` to convert it to the correct color mode.
On 4-bit drivers the image should be exported as a `.pgm` greyscale;
`img_cvt.py` will convert it to 4-bit format. In testing this produced good
results.
`img_cvt.py` will convert it to 4-bit format. This utility uses error diffusion
(dithering) to avoid banding and produced good results in testing.
## 1.4 Color images
@ -79,3 +79,6 @@ size. In other cases the rows and cols values must be used to populate a subset
of the frame buffer pixels or to display a subset of the image pixels. Secondly
the built-in flash of some platforms can be slow. If there is a visible pause in
displaying the image this is likely to be the cause.
## doc to be continued

Wyświetl plik

@ -37,15 +37,22 @@ WAVESHARE_13 = (0, 0, 16) # Waveshare 1.3" 240x240 LCD contributed by Aaron Mit
@micropython.viper
def _lcopy(dest: ptr16, source: ptr8, lut: ptr16, length: int):
def _lcopy(dest: ptr16, source: ptr8, lut: ptr16, length: int, gscale: bool):
# rgb565 - 16bit/pixel
n: int = 0
x: int = 0
while length:
c = source[x]
dest[n] = lut[c >> 4] # current pixel
n += 1
dest[n] = lut[c & 0x0F] # next pixel
p = c >> 4 # current pixel
q = c & 0x0F # next pixel
if gscale:
dest[n] = (p >> 1 | p << 4 | p << 9 | ((p & 0x01) << 15)) ^ 0xFFFF
n += 1
dest[n] = (q >> 1 | q << 4 | q << 9 | ((q & 0x01) << 15)) ^ 0xFFFF
else:
dest[n] = lut[p] # current pixel
n += 1
dest[n] = lut[q] # next pixel
n += 1
x += 1
length -= 1
@ -88,11 +95,12 @@ class ST7789(framebuf.FrameBuffer):
orientation = display[2] # where x, y is the RAM offset
self._spi_init = init_spi # Possible user callback
self._lock = asyncio.Lock()
self._gscale = False # Interpret buffer as index into color LUT
mode = framebuf.GS4_HMSB # Use 4bit greyscale.
self.palette = BoolPalette(mode)
gc.collect()
buf = bytearray(height * -(-width // 2)) # Ceiling division for odd widths
self._mvb = memoryview(buf)
self.mvb = memoryview(buf)
super().__init__(buf, width, height, mode)
self._linebuf = bytearray(self.width * 2) # 16 bit color out
self._init(disp_mode, orientation)
@ -206,6 +214,11 @@ class ST7789(framebuf.FrameBuffer):
# Row address set
self._wcd(b"\x2b", int.to_bytes((ys << 16) + ye, 4, "big"))
def greyscale(self, gs=None):
if gs is not None:
self._gscale = gs
return self._gscale
# @micropython.native # Made virtually no difference to timing.
def show(self): # Blocks for 83ms @60MHz SPI
# Blocks for 60ms @30MHz SPI on TTGO in PORTRAIT mode
@ -215,7 +228,8 @@ class ST7789(framebuf.FrameBuffer):
wd = -(-self.width // 2) # Ceiling division for odd number widths
end = self.height * wd
lb = memoryview(self._linebuf)
buf = self._mvb
cm = self._gscale # color False, greyscale True
buf = self.mvb
if self._spi_init: # A callback was passed
self._spi_init(self._spi) # Bus may be shared
self._dc(0)
@ -223,7 +237,7 @@ class ST7789(framebuf.FrameBuffer):
self._spi.write(b"\x2c") # RAMWR
self._dc(1)
for start in range(0, end, wd):
_lcopy(lb, buf[start:], clut, wd) # Copy and map colors
_lcopy(lb, buf[start:], clut, wd, cm) # Copy and map colors
self._spi.write(lb)
self._cs(1)
# print(ticks_diff(ticks_us(), ts))
@ -237,7 +251,8 @@ class ST7789(framebuf.FrameBuffer):
clut = ST7789.lut
wd = -(-self.width // 2)
lb = memoryview(self._linebuf)
buf = self._mvb
cm = self._gscale # color False, greyscale True
buf = self.mvb
line = 0
for n in range(split):
if self._spi_init: # A callback was passed
@ -247,7 +262,7 @@ class ST7789(framebuf.FrameBuffer):
self._spi.write(b"\x3c" if n else b"\x2c") # RAMWR/Write memory continue
self._dc(1)
for start in range(wd * line, wd * (line + lines), wd):
_lcopy(lb, buf[start:], clut, wd) # Copy and map colors
_lcopy(lb, buf[start:], clut, wd, cm) # Copy and map colors
self._spi.write(lb)
line += lines
self._cs(1)

213
img_cvt.py 100755
Wyświetl plik

@ -0,0 +1,213 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# File formats: https://en.wikipedia.org/wiki/Netpbm
# Dithering:
# https://en.wikipedia.org/wiki/Floyd%E2%80%93Steinberg_dithering
# https://tannerhelland.com/2012/12/28/dithering-eleven-algorithms-source-code.html
# The dithering implementation is designed for genarality rather than efficiency as
# this ia a PC utility. Major speed and RAM usage improvements are possible if
# dithering is restricted to one algorithm and is to run on a microcontroller. See
# above refernced docs.
import argparse
import sys
import os
# Dithering data. Divisor followed by 3-tuples comprising
# row-offset, col-offset, multiplier
FS = (16, (0, 1, 7), (1, -1, 3), (1, 0, 5), (1, 1, 1)) # Floyd-Steinberg
BURKE = (32, (0, 1, 8), (0, 2, 4), (1, -2, 2), (1, -1, 4), (1, 0, 8), (1, 1, 4), (1, 2, 2))
ATKINSON = (8, (0, 1, 1), (0, 2, 1), (1, -1, 1), (1, 0, 1), (1, 1, 1), (2, 0, 1))
SIERRA = (
32,
(0, 1, 5),
(0, 2, 3),
(1, -2, 2),
(1, -1, 4),
(1, 0, 5),
(1, 1, 4),
(1, 2, 3),
(2, -1, 2),
(2, 0, 3),
(2, 1, 2),
)
dither_options = {"FS": FS, "Burke": BURKE, "Atkinson": ATKINSON, "Sierra": SIERRA, "None": None}
# Empirical results creating RRRGGGBB images: Dithering substantially improves some images.
# Differences between the algorithms are subtle.
# Apply dithering to an integer pixel array. Initially this contains raw integer
# vaues of a color, assumed to be left shifted by at least 4 bits. On each call
# the error e is added to forward pixels scaled according to the dithering tuple.
def dither(arr, ra, row, col, rows, cols, e):
if arr is not None:
div = arr[0]
for dr, dc, mul in arr[1:]:
r = row + dr
c = col + dc
if r < rows and 0 <= c < cols:
factor = round(e * mul / div)
if ra[r][c] + factor < 0xFFFF: # Only apply if it won't cause
ra[r][c] += factor # overflow.
# Convert a stream of 8-bit greyscale values to 4-bit values with Burke dithering
def convgs(arr, rows, cols, si, so):
ra = [] # Greyscale values
nibbles = [0, 0]
for row in range(rows):
ra.append([0] * cols)
for row in range(rows):
for col in range(cols):
ra[row][col] = int.from_bytes(si.read(1), "big") << 8 # 16 bit greyscale
for row in range(rows):
col = 0
while col < cols:
for n in range(2):
c = ra[row][col]
nibbles[n] = c >> 12
e = c & 0x0FFF # error
dither(arr, ra, row, col, rows, cols, e) # Adjust forward pixels
col += 1
# print(row, col, nibbles)
so.write(int.to_bytes((nibbles[0] << 4) | nibbles[1], 1, "big"))
# Convert a stream of RGB888 data to rrrgggbb
def convrgb(arr, rows, cols, si, so, bits):
red = []
grn = []
blu = []
for row in range(rows):
red.append([0] * cols)
grn.append([0] * cols)
blu.append([0] * cols)
for row in range(rows):
for col in range(cols):
red[row][col] = int.from_bytes(si.read(1), "big") << 8 # 16 bit values
grn[row][col] = int.from_bytes(si.read(1), "big") << 8
blu[row][col] = int.from_bytes(si.read(1), "big") << 8
if bits == 8:
for row in range(rows):
for col in range(cols):
r = red[row][col] & 0xE000
err = red[row][col] - r
dither(arr, red, row, col, rows, cols, err)
g = grn[row][col] & 0xE000
err = grn[row][col] - g
dither(arr, grn, row, col, rows, cols, err)
b = blu[row][col] & 0xC000
err = blu[row][col] - b
dither(arr, blu, row, col, rows, cols, err)
op = (red[row][col] >> 8) & 0xE0
op |= (grn[row][col] >> 11) & 0x1C
op |= (blu[row][col] >> 14) & 0x03
so.write(int.to_bytes(op, 1, "big"))
else: # RGB565
for row in range(rows):
for col in range(cols):
r = red[row][col] & 0xF800
err = red[row][col] - r
dither(arr, red, row, col, rows, cols, err)
g = grn[row][col] & 0xFC00
err = grn[row][col] - g
dither(arr, grn, row, col, rows, cols, err)
b = blu[row][col] & 0xF800
err = blu[row][col] - b
dither(arr, blu, row, col, rows, cols, err)
op = (red[row][col]) & 0xF800
op |= (grn[row][col] >> 5) & 0x07E0
op |= (blu[row][col] >> 11) & 0x001F
# Color mappings checked on display
so.write(int.to_bytes(op, 2, "big")) # Red first
def conv(arr, fni, fno, height, width, color_mode):
with open(fno, "wb") as fo:
with open(fni, "rb") as fi:
fmt = fi.readline() # Get file format
txt = fi.readline()
while txt.startswith(b"#"): # Ignore comments
txt = fi.readline()
cols, rows = txt.split(b" ")
cols = int(cols)
rows = int(rows)
cdepth = int(fi.readline())
fail = (fmt[:2] != b"P6") if color_mode else (fmt[:2] != b"P5")
if fail:
quit("Source file contents do not match file extension.")
fo.write(b"".join((rows.to_bytes(2, "big"), cols.to_bytes(2, "big"))))
if height is not None and width is not None:
if not (cols == width and rows == height):
print(
f"Warning: Specified dimensions {width}x{height} do not match those in source file {cols}x{rows}"
)
print(f"Writing file, dimensions rows = {rows}, cols = {cols}")
if not color_mode:
convgs(arr, rows, cols, fi, fo)
mode = "4-bit greyscale"
elif color_mode == 1:
convrgb(arr, rows, cols, fi, fo, 16)
mode = "16-bit color RGB565"
elif color_mode == 2:
convrgb(arr, rows, cols, fi, fo, 8)
mode = "8-bit color RRRGGGBB"
print(f"File {fno} written in {mode}.")
# **** Parse command line arguments ****
def quit(msg):
print(msg)
sys.exit(1)
DESC = """Convert a graphics Netpbm graphics file into a binary form for use with
MicroPython FrameBuffer based display drivers. Source files should be in raw
binary format. If pixel dimensions are passed they will be compared with those
stored in the source file. A warning will be output if they do not match. Output
file dimensions will always be those of the input file.
A color ppm graphics fle is output as an RRRGGGBB map unless the --rgb565 arg is
passed, in which case it is RRRR RGGG GGGB BBBB.
A greyscale pgm file is output in 4-bit greyscale.
By default the Atkinson dithering algorithm is used. Other options are FS
(FloydSteinberg), Burke, Sierra and None.
"""
if __name__ == "__main__":
parser = argparse.ArgumentParser(
__file__, description=DESC, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument("infile", type=str, help="Input file path")
parser.add_argument("outfile", type=str, help="Path and name of output file")
parser.add_argument("-r", "--rows", type=int, help="Image height (rows) in pixels")
parser.add_argument("-c", "--cols", type=int, help="Image width (cols) in pixels")
parser.add_argument(
"-d",
"--dither",
action="store",
default="Atkinson",
choices=["Atkinson", "Burke", "Sierra", "FS", "None"],
)
parser.add_argument("--rgb565", action="store_true", help="Create 16-bit RGB565 file.")
args = parser.parse_args()
# print(args.dither)
# quit("Done")
if not os.path.isfile(args.infile):
quit("Source image filename does not exist")
extension = os.path.splitext(args.infile)[1].upper()
if extension == ".PPM":
cmode = 1 if args.rgb565 else 2 # Color image
elif extension == ".PGM":
if args.rgb565:
quit("--rgb565 arg can only be used with color images.")
cmode = 0 # Greyscale image
else:
quit("Source image file should be a ppm or pgm file.")
arr = dither_options[args.dither]
conv(arr, args.infile, args.outfile, args.rows, args.cols, cmode)