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
|
# 1. Displaying photo images
|
||||||
|
|
||||||
The display drivers in this repo were primarily designed for displaying geometric shapes
|
The display drivers in this repo were originally designed for displaying
|
||||||
and fonts. With a minor update they may also be used for image display. The method used is
|
geometric shapes and fonts. With a minor update they may also be used for image
|
||||||
ideal for full screen images however with suitable user code smaller images may be
|
display. The method used is ideal for full screen images however with suitable
|
||||||
rendered. It is also possible to overlay an image with GUI controls, although transparency
|
user code smaller images may be rendered. It is also possible to overlay an
|
||||||
is not supported.
|
image with GUI controls, although transparency is not supported.
|
||||||
|
|
||||||
The following notes apply
|
GUI references:
|
||||||
[nanogui](https://github.com/peterhinch/micropython-nano-gui)
|
[nanogui](https://github.com/peterhinch/micropython-nano-gui)
|
||||||
[micro-gui](https://github.com/peterhinch/micropython-micro-gui) and
|
[micro-gui](https://github.com/peterhinch/micropython-micro-gui)
|
||||||
[micropython-touch](https://github.com/peterhinch/micropython-touch).
|
[micropython-touch](https://github.com/peterhinch/micropython-touch)
|
||||||
|
|
||||||
Images for display should be converted to a [netpbm format](https://en.wikipedia.org/wiki/Netpbm),
|
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
|
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.
|
done using a CPython utility `img_cvt.py` documented below.
|
||||||
|
|
||||||
An updated driver has a `greyscale` method enabling the frame buffer contents to
|
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
|
## 1.2 Supported drivers
|
||||||
|
|
||||||
Currently only gc9a01 drivers are supported.
|
Currently gc9a01, ili948x, ili9341 and st7789 drivers are supported.
|
||||||
|
|
||||||
## 1.3 Monochrome images
|
## 1.3 Monochrome images
|
||||||
|
|
||||||
These may be displayed using 8-bit or 16-bit drivers by treating it as if it
|
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.
|
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;
|
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
|
`img_cvt.py` will convert it to 4-bit format. This utility uses error diffusion
|
||||||
results.
|
(dithering) to avoid banding and produced good results in testing.
|
||||||
|
|
||||||
## 1.4 Color images
|
## 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
|
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
|
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.
|
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
|
@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
|
# rgb565 - 16bit/pixel
|
||||||
n: int = 0
|
n: int = 0
|
||||||
x: int = 0
|
x: int = 0
|
||||||
while length:
|
while length:
|
||||||
c = source[x]
|
c = source[x]
|
||||||
dest[n] = lut[c >> 4] # current 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
|
n += 1
|
||||||
dest[n] = lut[c & 0x0F] # next pixel
|
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
|
n += 1
|
||||||
x += 1
|
x += 1
|
||||||
length -= 1
|
length -= 1
|
||||||
|
@ -88,11 +95,12 @@ class ST7789(framebuf.FrameBuffer):
|
||||||
orientation = display[2] # where x, y is the RAM offset
|
orientation = display[2] # where x, y is the RAM offset
|
||||||
self._spi_init = init_spi # Possible user callback
|
self._spi_init = init_spi # Possible user callback
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
|
self._gscale = False # Interpret buffer as index into color LUT
|
||||||
mode = framebuf.GS4_HMSB # Use 4bit greyscale.
|
mode = framebuf.GS4_HMSB # Use 4bit greyscale.
|
||||||
self.palette = BoolPalette(mode)
|
self.palette = BoolPalette(mode)
|
||||||
gc.collect()
|
gc.collect()
|
||||||
buf = bytearray(height * -(-width // 2)) # Ceiling division for odd widths
|
buf = bytearray(height * -(-width // 2)) # Ceiling division for odd widths
|
||||||
self._mvb = memoryview(buf)
|
self.mvb = memoryview(buf)
|
||||||
super().__init__(buf, width, height, mode)
|
super().__init__(buf, width, height, mode)
|
||||||
self._linebuf = bytearray(self.width * 2) # 16 bit color out
|
self._linebuf = bytearray(self.width * 2) # 16 bit color out
|
||||||
self._init(disp_mode, orientation)
|
self._init(disp_mode, orientation)
|
||||||
|
@ -206,6 +214,11 @@ class ST7789(framebuf.FrameBuffer):
|
||||||
# Row address set
|
# Row address set
|
||||||
self._wcd(b"\x2b", int.to_bytes((ys << 16) + ye, 4, "big"))
|
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.
|
# @micropython.native # Made virtually no difference to timing.
|
||||||
def show(self): # Blocks for 83ms @60MHz SPI
|
def show(self): # Blocks for 83ms @60MHz SPI
|
||||||
# Blocks for 60ms @30MHz SPI on TTGO in PORTRAIT mode
|
# 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
|
wd = -(-self.width // 2) # Ceiling division for odd number widths
|
||||||
end = self.height * wd
|
end = self.height * wd
|
||||||
lb = memoryview(self._linebuf)
|
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
|
if self._spi_init: # A callback was passed
|
||||||
self._spi_init(self._spi) # Bus may be shared
|
self._spi_init(self._spi) # Bus may be shared
|
||||||
self._dc(0)
|
self._dc(0)
|
||||||
|
@ -223,7 +237,7 @@ class ST7789(framebuf.FrameBuffer):
|
||||||
self._spi.write(b"\x2c") # RAMWR
|
self._spi.write(b"\x2c") # RAMWR
|
||||||
self._dc(1)
|
self._dc(1)
|
||||||
for start in range(0, end, wd):
|
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._spi.write(lb)
|
||||||
self._cs(1)
|
self._cs(1)
|
||||||
# print(ticks_diff(ticks_us(), ts))
|
# print(ticks_diff(ticks_us(), ts))
|
||||||
|
@ -237,7 +251,8 @@ class ST7789(framebuf.FrameBuffer):
|
||||||
clut = ST7789.lut
|
clut = ST7789.lut
|
||||||
wd = -(-self.width // 2)
|
wd = -(-self.width // 2)
|
||||||
lb = memoryview(self._linebuf)
|
lb = memoryview(self._linebuf)
|
||||||
buf = self._mvb
|
cm = self._gscale # color False, greyscale True
|
||||||
|
buf = self.mvb
|
||||||
line = 0
|
line = 0
|
||||||
for n in range(split):
|
for n in range(split):
|
||||||
if self._spi_init: # A callback was passed
|
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._spi.write(b"\x3c" if n else b"\x2c") # RAMWR/Write memory continue
|
||||||
self._dc(1)
|
self._dc(1)
|
||||||
for start in range(wd * line, wd * (line + lines), wd):
|
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)
|
self._spi.write(lb)
|
||||||
line += lines
|
line += lines
|
||||||
self._cs(1)
|
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