11 KiB
1. Displaying photo images
The display drivers in this repo were originally developed for displaying fonts and geometric shapes. 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.
GUI references:
nanogui
micro-gui
micropython-touch
Images for display should be converted to a netpbm format,
namely a .pgm
file for a monochrome image or .ppm
for color. This may be
done using a utility such as Gimp. Netpbm files use 8-bit values for greyscale
images. Color images are encoded as RGB888 (i.e. 8 bits for each primary color).
A CPython utility img_cvt.py
converts PPM files to RGB565 for 16-bit drivers,
RRRGGGBB for 8-bit color. PGM files are converted to 4-bit greyscale to enable a
monochrome image to display on a 4-bit driver. The img_cvt.py
utility is
provided in this repo and is documented below.
1.2 Supported drivers
Currently gc9a01, ili948x, ili9341 and st7789 drivers are supported.
An updated driver has a greyscale
method enabling the frame buffer contents to
be interpreted at show time as either color or greyscale. It also has a public
mvb
bound variable enabling the frame buffer to be loaded from an image.
1.3 Monochrome images
These may be displayed using 8-bit or 16-bit drivers by treating it as if it
were color. If the source image is color, a photo editor is used to convert it
to monochrome. It is then exported as a .ppm
color image and prepared for
display as if it were color (see section 2).
On 4-bit drivers the greyscale image should be exported as a .pgm
file;
img_cvt.py
will convert it to 4-bit format. The following is an example of a
monochrome image rendered on a 240x320 display with a 4-bit driver.
The image is overlaid by a touchable Button
widget.
1.4 Color images
These cannot be displayed on 4-bit drivers. On 8 or 16 bit drivers these should
be exported from the graphics program as a .ppm
color image. Then img_cvt.py
is used to convert the file to the correct color mode.
The following images were rendered on a 240x240 circular display using an 8-bit driver. Note the absence of banding, particularly on the cat which has a very gradual change of luminance (see dithering, below).
Images are overlaid by a touchable Button
widget.
1.5 Code samples
Files produced by img_cvt.py
are binary files. The first four bytes comprise
two 16-bit integers defining the numbers of rows and columns in the image. The
following is an example of a full-screen image display in microgui or
micropython-touch:
class MoonScreen(Screen):
def __init__(self):
super().__init__()
def after_open(self):
fn = "test.bin" # Image created by`img_cvt.py`
# The following line is required if a 4-bit driver is in use
# ssd.greyscale(True)
with open(fn, "rb") as f:
_ = f.read(4) # Read and discard rows and cols
f.readinto(ssd.mvb) # Read the image into the frame buffer
On nano-gui:
from color_setup import ssd # Create a display instance
from gui.core.nanogui import refresh
refresh(ssd) # Initialise display.
fn = "test.bin" # Image created by`img_cvt.py`
# The following line is required if a 4-bit driver is in use
# ssd.greyscale(True)
with open(fn, "rb") as f:
_ = f.read(4) # Read and discard rows and cols
f.readinto(ssd.mvb) # Read the image into the frame buffer
refresh(ssd)
These examples rely on the images being configured to precisely match the screen size. This is described in detail in section 2. In other cases the rows and cols values in the file must be used to populate a subset of the frame buffer pixels or to display a subset of the image pixels. Note that the built-in flash of some platforms can be slow. If there is a detectable pause in displaying the image this is likely to be the cause. Reading rows and columns:
with open("test.bin", "rb") as f:
rows = int.from_bytes(f.read(2), "big")
cols = int.from_bytes(f.read(2), "big")
2. Preparing an image for display
The following instructions assume that the aim is a full screen display using simple code as illustrated above. For partial display the initial setup is less critical.
2.1 Initial edit
These instructions are based on using Gimp, however other photo editors have similar functionality.
- Use the rectangle select tool to select the region for display. The aspect ratio should approximately match that of the screen. Dimensions are shown on the bottom status bar. The mode (portrait or landscape) must match the configuration of the screen.
- Select Image-Crop to selection.
- If converting a color image to monochrome, use Image-Mode-Greyscale.
- Select Image-Scale Image. Break the link between Width and Height and set the values to exactly match the screen dimensions.
- The image can be zoomed with + or +.
- Select Filters-Enhance-Sharpen (usharp mask) (optional).
- Export the image: File-Export As. Navigate to a directory. Set the extension to PPM to export an image for display on an 8- or 16-bit color driver. For an image for display on a 4-bit driver, set the extension to PGM (this is necessarily a greyscale image). When asked for Data Formatting, select RAW.
2.2 Using img_cvt.py
This takes a PPM or PGM file and outputs a binary file in the correct format for display; alternatively a Python source file may be output. The latter offers the option to freeze the file for fast display with minimal RAM use. Typical usage:
$ ./img_cvt.py test.ppm test.bin
Mandatory positional args:
infile
Input file path.outfile
Output file path. If the file extension is.py
a Python source file will be output. Optional args:-r
or--rows
Expected image dimensions. If passed these are checked against the actual image and a warning printed on a mismatch.-c
or--cols
.-d
or--dither
See below.--rgb565
For PPM files enables conversion to 16-bit RGB565 format.-h
or--help
Show help text.
2.3 Dithering
When reducing the number of bits in an image there are inevitable quantisation
errors. These can produce ugly banding on areas with gradually changing
luminosity. This can be largely eliminated using a process known as dithering
or error diffusion. The script supports the following values for -d
:
- Atkinson The default.
- Burke
- Sierra
- FS The Floyd-Steinberg algorithm.
- None
In testing the difference between these algorithms on most images was subtle;
except that selecting None
led to a substantial loss of quality.
3. Populating the Frame Buffer
A binary file may be used as in the examples in section 1.5: this is RAM efficient but file access from Flash tends to be slow.
Converting to Python source offers two routes for fast updates. A FrameBuffer
may be instantiated from the Python file and blitted to the device. Best suited
for small graphical elements such as sprites. Full screen images can be copied
to the device buffer - again yielding rapid updates. If the source file is
frozen this is RAM efficient and fast.
3.1 Binary output file format
The first four bytes comprise a count of rows, then cols, in big-endian format.
The following bytes are pixel data in a horizontally mapped format. Pixels
comprise 4, 8 or 16 bits depending on the selected format. Hence if pixels are
imagined as an array of size rows * cols
the sequence of pixels coming from the
input stream is:
p[0, 0],p[0, 1]...p[0, cols-1],p[1, 0],p[1,1]...p[1, cols-1]...p[rows-1, cols-1]
3.2 Python output file format
Bound variables:
source
The path of the source image file.rows
Image dimensions in pixels.cols
mode
Mode used to create a FrameBuffer: values correspond to FrameBuffer constants.data
Image data bytes, with layout as per binary file.
3.3 Using a Python image file
To display a full screen image it may be copied the device's underlying buffer:
import img # Python file containing the image
ssd.mvb[:] = img.data
An alternative approach is to create a second FrameBuffer
instance from the
image and blit it to the ssd
device (which is a FrameBuffer
subclass).
Unfortunately this issue
prevents creating a FrameBuffer
from a Flash-based bytes
object. However
blitting small RAM-based Python images would be useful for projects such as
games.
import img # Python file containing the image
ba = bytearray(img.data) # Put in RAM because of above issue
fb = framebuf.FrameBuffer(ba, img.cols, img.rows, img.mode)
ssd.blit(fb, col, row) # blit to a given location
Until this issue is resolved a frozen Python image may be blitted to all or part of the screen with this code:
from framebuf import RGB565, GS4_HMSB, GS8
size = {RGB565: 2, GS4_HMSB: 0, GS8: 1}
def blit(ssd, img, row=0, col=0):
def scale(x, sz):
return x * sz if sz else x // 2
mvb = ssd.mvb # Memoryview of display's bytearray.
irows = min(img.rows, ssd.height - row) # Clip rows
icols = min(img.cols, ssd.width - col) # Clip cols
if (mode := img.mode) != ssd.mode:
raise ValueError("Image and display have differing modes.")
sz = size[mode] # Allow for no. of bytes per pixel
ibytes = scale(img.cols, sz) # Bytes per row of unclipped image data
dbytes = scale(icols, sz) # Bytes per row to output to display
dwidth = scale(ssd.width, sz) # Display width in bytes
d = scale(row * ssd.width + col, sz) # Destination index
s = 0 # Source index
while irows:
mvb[d : d + dbytes] = img.data[s : s + dbytes]
s += ibytes
d += dwidth
irows -= 1
called as follows:
import my_picture as img # Python image file
# Instantiate ssd as per the GUI in use
blit(ssd, img, 0, 0) # Display at top left of screen
3.4 Frame Buffer Access
Updated display drivers have a mvb
bound variable: this is a memoryview
into
the bytearray containing the frame buffer. The three GUIs make the display
driver available as the ssd
object; since display drivers are subclassed from
FrameBuffer
the ssd.mvb
object may be used to populate all or part of the
frame buffer with an image.
Appendix 1 references
Netbpm PGM and PPM formats.
Dithering concepts.
Dithering algrithms.