kopia lustrzana https://github.com/peterhinch/micropython-nano-gui
Image display: support Python image files.
rodzic
baa0f0cc5f
commit
29ef2002b4
|
@ -127,13 +127,16 @@ necessarily a greyscale image). When asked for Data Formatting, select RAW.
|
||||||
## 2.2 Using img_cvt.py
|
## 2.2 Using img_cvt.py
|
||||||
|
|
||||||
This takes a PPM or PGM file and outputs a binary file in the correct format for
|
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
|
```bash
|
||||||
$ ./img_cvt.py test.ppm test.bin
|
$ ./img_cvt.py test.ppm test.bin
|
||||||
```
|
```
|
||||||
Mandatory positional args:
|
Mandatory positional args:
|
||||||
1. `infile` Input file path.
|
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:
|
Optional args:
|
||||||
1. `-r` or `--rows` Expected image dimensions. If passed these are checked
|
1. `-r` or `--rows` Expected image dimensions. If passed these are checked
|
||||||
against the actual image and a warning printed on a mismatch.
|
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. 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 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
|
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:
|
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]
|
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
|
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
|
the bytearray containing the frame buffer. The three GUIs make the display
|
||||||
|
|
157
img_cvt.py
157
img_cvt.py
|
@ -14,6 +14,14 @@
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
import os
|
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
|
# Dithering data. Divisor followed by 3-tuples comprising
|
||||||
# row-offset, col-offset, multiplier
|
# 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
|
so.write(int.to_bytes(op, 2, "big")) # Red first
|
||||||
|
|
||||||
|
|
||||||
def conv(arr, fni, fno, height, width, color_mode):
|
# Convert an input stream, putting result on an output stream.
|
||||||
with open(fno, "wb") as fo:
|
def conv(arr, si, so, height, width, mode):
|
||||||
with open(fni, "rb") as fi:
|
fmt = si.readline() # Get file format
|
||||||
fmt = fi.readline() # Get file format
|
txt = si.readline()
|
||||||
txt = fi.readline()
|
while txt.startswith(b"#"): # Ignore comments
|
||||||
while txt.startswith(b"#"): # Ignore comments
|
txt = si.readline()
|
||||||
txt = fi.readline()
|
cols, rows = txt.split(b" ")
|
||||||
cols, rows = txt.split(b" ")
|
cols = int(cols)
|
||||||
cols = int(cols)
|
rows = int(rows)
|
||||||
rows = int(rows)
|
cdepth = int(si.readline())
|
||||||
cdepth = int(fi.readline())
|
if fmt[:2] != fmtstr[mode]:
|
||||||
fail = (fmt[:2] != b"P6") if color_mode else (fmt[:2] != b"P5")
|
quit("Source file contents do not match file extension.")
|
||||||
if fail:
|
so.write(b"".join((rows.to_bytes(2, "big"), cols.to_bytes(2, "big"))))
|
||||||
quit("Source file contents do not match file extension.")
|
if height is not None and width is not None:
|
||||||
fo.write(b"".join((rows.to_bytes(2, "big"), cols.to_bytes(2, "big"))))
|
if not (cols == width and rows == height):
|
||||||
if height is not None and width is not None:
|
print(f"Warning: Specified dimensions {width}x{height}")
|
||||||
if not (cols == width and rows == height):
|
print(f"do not match those in source file {cols}x{rows}")
|
||||||
print(
|
print(f"Writing file, dimensions rows = {rows}, cols = {cols}")
|
||||||
f"Warning: Specified dimensions {width}x{height} do not match those in source file {cols}x{rows}"
|
if mode == GS4_HMSB: # 4-bit greyscale
|
||||||
)
|
convgs(arr, rows, cols, si, so)
|
||||||
print(f"Writing file, dimensions rows = {rows}, cols = {cols}")
|
elif mode == RGB565: # 16-bit color
|
||||||
if not color_mode:
|
convrgb(arr, rows, cols, si, so, 16)
|
||||||
convgs(arr, rows, cols, fi, fo)
|
elif mode == GS8: # 8-bit color
|
||||||
mode = "4-bit greyscale"
|
convrgb(arr, rows, cols, si, so, 8)
|
||||||
elif color_mode == 1:
|
return rows, cols # Actual values from file
|
||||||
convrgb(arr, rows, cols, fi, fo, 16)
|
|
||||||
mode = "16-bit color RGB565"
|
|
||||||
elif color_mode == 2:
|
# **** Python code generation
|
||||||
convrgb(arr, rows, cols, fi, fo, 8)
|
class ByteWriter:
|
||||||
mode = "8-bit color RRRGGGBB"
|
bytes_per_line = 16
|
||||||
print(f"File {fno} written in {mode}.")
|
|
||||||
|
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 ****
|
# **** 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.
|
A greyscale pgm file is output in 4-bit greyscale.
|
||||||
By default the Atkinson dithering algorithm is used. Other options are FS
|
By default the Atkinson dithering algorithm is used. Other options are FS
|
||||||
(Floyd–Steinberg), Burke, Sierra and None.
|
(Floyd–Steinberg), Burke, Sierra and None.
|
||||||
|
|
||||||
|
If the output filename extension is ".py" a Python sourcefile will be output.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -195,19 +259,38 @@ if __name__ == "__main__":
|
||||||
)
|
)
|
||||||
parser.add_argument("--rgb565", action="store_true", help="Create 16-bit RGB565 file.")
|
parser.add_argument("--rgb565", action="store_true", help="Create 16-bit RGB565 file.")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
# print(args.dither)
|
|
||||||
# quit("Done")
|
|
||||||
if not os.path.isfile(args.infile):
|
if not os.path.isfile(args.infile):
|
||||||
quit("Source image filename does not exist")
|
quit("Source image filename does not exist")
|
||||||
|
|
||||||
extension = os.path.splitext(args.infile)[1].upper()
|
extension = os.path.splitext(args.infile)[1].upper()
|
||||||
|
|
||||||
if extension == ".PPM":
|
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":
|
elif extension == ".PGM":
|
||||||
if args.rgb565:
|
if args.rgb565:
|
||||||
quit("--rgb565 arg can only be used with color images.")
|
quit("--rgb565 arg can only be used with color images.")
|
||||||
cmode = 0 # Greyscale image
|
mode = GS4_HMSB # Greyscale image
|
||||||
else:
|
else:
|
||||||
quit("Source image file should be a ppm or pgm file.")
|
quit("Source image file should be a ppm or pgm file.")
|
||||||
arr = dither_options[args.dither]
|
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()
|
||||||
|
|
Ładowanie…
Reference in New Issue