kopia lustrzana https://github.com/peterhinch/micropython-nano-gui
Update st7789 driver for greyscale. Add img_cvt.py.
rodzic
1bd163d72d
commit
77f5b211e5
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
(Floyd–Steinberg), 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)
|
Ładowanie…
Reference in New Issue