Image display: support Python image files.

pull/70/head
Peter Hinch 2024-06-21 18:09:11 +01:00
rodzic baa0f0cc5f
commit 29ef2002b4
2 zmienionych plików z 165 dodań i 41 usunięć

Wyświetl plik

@ -127,13 +127,16 @@ 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. Typical usage:
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:
```bash
$ ./img_cvt.py test.ppm test.bin
```
Mandatory positional args:
1. `infile` Input file path.
2. `outfile` Output file path.
2. `outfile` Output file path. If the file extension is `.py` a Python source
file will be output.
Optional args:
1. `-r` or `--rows` Expected image dimensions. If passed these are checked
against the actual image and a warning printed on a mismatch.
@ -159,7 +162,16 @@ except that selecting `None` led to a substantial loss of quality.
# 3. Populating the Frame Buffer
## 3.1 Output file format
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
@ -168,7 +180,36 @@ imagined as an array of size `rows * cols` the sequence of pixels coming from th
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 Frame Buffer Access
## 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
* `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:
```py
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](https://github.com/micropython/micropython/pull/15285)
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.
```py
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
```
## 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

Wyświetl plik

@ -14,6 +14,14 @@
import argparse
import sys
import os
from io import BytesIO
# FrameBuffer constants with string mappings
RGB565 = 1
GS4_HMSB = 2
GS8 = 6
modestr = {RGB565: "16-bit color RGB565", GS8: "8-bit color RRRGGGBB", GS4_HMSB: "4-bit greyscale"}
fmtstr = {RGB565: b"P6", GS8: b"P6", GS4_HMSB: b"P5"} # Netbpm file ID strings
# Dithering data. Divisor followed by 3-tuples comprising
# row-offset, col-offset, multiplier
@ -124,37 +132,91 @@ def convrgb(arr, rows, cols, si, so, bits):
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}.")
# Convert an input stream, putting result on an output stream.
def conv(arr, si, so, height, width, mode):
fmt = si.readline() # Get file format
txt = si.readline()
while txt.startswith(b"#"): # Ignore comments
txt = si.readline()
cols, rows = txt.split(b" ")
cols = int(cols)
rows = int(rows)
cdepth = int(si.readline())
if fmt[:2] != fmtstr[mode]:
quit("Source file contents do not match file extension.")
so.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}")
print(f"do not match those in source file {cols}x{rows}")
print(f"Writing file, dimensions rows = {rows}, cols = {cols}")
if mode == GS4_HMSB: # 4-bit greyscale
convgs(arr, rows, cols, si, so)
elif mode == RGB565: # 16-bit color
convrgb(arr, rows, cols, si, so, 16)
elif mode == GS8: # 8-bit color
convrgb(arr, rows, cols, si, so, 8)
return rows, cols # Actual values from file
# **** Python code generation
class ByteWriter:
bytes_per_line = 16
def __init__(self, stream, varname):
self.stream = stream
self.stream.write(f"{varname} =\\\n")
self.bytecount = 0 # For line breaks
def _eol(self):
self.stream.write("'\\\n")
def _eot(self):
self.stream.write("'\n")
def _bol(self):
self.stream.write("b'")
# Output a single byte
def obyte(self, data):
if not self.bytecount:
self._bol()
self.stream.write(f"\\x{data:02x}")
self.bytecount += 1
self.bytecount %= self.bytes_per_line
if not self.bytecount:
self._eol()
# Output from a sequence
def odata(self, bytelist):
for byt in bytelist:
self.obyte(byt)
# ensure a correct final line
def eot(self): # User force EOL if one hasn't occurred
if self.bytecount:
self._eot()
self.stream.write("\n")
# Create a bound variable. Quote if it's a string.
def write_var(stream, name, arg):
s = f'{name} = "{arg}"\n' if isinstance(arg, str) else f"{name} = {arg}\n"
stream.write(s)
# Write Python source using data stream on sd
def writepy(ip_stream, op_stream, rows, cols, mode, fname):
op_stream.write("# Code generated by img_cvt.py.")
write_var(op_stream, "version", "0.1")
write_var(op_stream, "source", fname)
write_var(op_stream, "rows", rows)
write_var(op_stream, "cols", cols)
write_var(op_stream, "mode", mode)
bw_data = ByteWriter(op_stream, "data")
ip_stream.seek(4) # Skip 4 bytes of dimension data
bw_data.odata(ip_stream.read())
bw_data.eot()
# **** Parse command line arguments ****
@ -176,6 +238,8 @@ 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 the output filename extension is ".py" a Python sourcefile will be output.
"""
if __name__ == "__main__":
@ -195,19 +259,38 @@ if __name__ == "__main__":
)
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
mode = RGB565 if args.rgb565 else GS8 # Color image 16/8 bits
elif extension == ".PGM":
if args.rgb565:
quit("--rgb565 arg can only be used with color images.")
cmode = 0 # Greyscale image
mode = GS4_HMSB # 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)
ofextension = os.path.splitext(args.outfile)[1].upper()
try:
si = open(args.infile, "rb")
except OSError:
quit(f"Cannot open {args.infile} for reading.")
fmode = "w" if ofextension == ".PY" else "wb" # binary or text file
ftype = "Python" if ofextension == ".PY" else "Binary"
try:
sp = open(args.outfile, fmode) # Binary file
except OSError:
quit(f"Cannot open {args.outfile} for writing.")
try:
if ofextension == ".PY":
with BytesIO() as so: # Write to stream. Return dimensions from file
rows, cols = conv(arr, si, so, args.rows, args.cols, mode)
writepy(so, sp, rows, cols, mode, args.infile)
else:
rows, cols = conv(arr, si, sp, args.rows, args.cols, mode)
print(f"{ftype} file {args.outfile} written in {modestr[mode]}.")
finally:
si.close()
sp.close()