- Home
- Learn Linux
- Learn Electronics
- Raspberry Pi
- Programming
- Projects
- LPI certification
- News & Reviews
This is a follow on from my guide on creating vector images on a Raspberry Pi Pico with display pack. This guide is how to show bitmap (raster) images on a Raspberry Pi Pico, including static images and animations. A future article will include information on how a similar technique can be used to create sprites for interactive games.
There are a number of challenges in reading, viewing and displaying images on the Pico. This includes performance (as the Pico is a microcontroller it has much less computing power than a multipurpose computer) and lack memory (with only 256kB which includes the MicroPython interpreter and any other code). This means it's not going to be possible to use the standard python PNG libraries. As a result I will be creating custom files outside of the Pico which can help reduce the load required on the Pico and improve performance.
We are going to start by looking at the way the display pack works and how the data is sent to the screen.
The libraries for the display pack require a bytearray for storing the image, which is then sent to the screen through the update method. This is shown in the following code snippet.
width = display.get_width()
height = display.get_height()
# 2-bytes per pixel (RGB565)
display_buffer = bytearray(width * height * 2)
display.init(display_buffer)
The bytearray has 2 bytes per pixel, but we normally deal with color using 3 bytes per pixel with a byte for each of the elements Red, Green and Blue (RGB). To fit this into 2 bytes the code drops the least significant bits for each color using 5 bits for red, 6 bits for green and 5 bits for blue.This is shown in the image below:
I've created the following code to convert from an RGB value to a RGB565 two byte list:
def color_to_bytes (color):
r, g, b = color
bytes = [0,0]
bytes[0] = r & 0xF8
bytes[0] += (g & 0xE0) >> 5
bytes[1] = (g & 0x1C) << 3
bytes[1] += (b & 0xF8) >> 3
return bytes
The bytes are stored in the byte array based on row and column positioning. These can be read from the display buffer byte array using:
def get_display_bytes (x, y):
buffer_pos = (x*2) + (y*width*2)
byte_list = [display_buffer[buffer_pos],
display_buffer[buffer_pos+1]]
return (byte_list)
The first practical example I will show how to display a full screen image on the display pack. This can be used to show a static image, a background image prior to drawing a sprite or by showing multiple files it can show a simple animation. As mentioned the Pico does not have either the processing power or the memory to be able to handle data conversion in realtime so the files will instead be processed into a raw format that the Pico can read. This is done on a PC prior to uploading the files to the Pico.
Prior to converting an image to a format for the Pico it needs to be at the correct resolution. This means it needs to be exactly 240 x 135 pixels in size. This is best achieved with a graphics program that can apply an appropriate scaling algorithm to get best results. I recommend using Gimp free / open source graphics editor, but you can use other applications or a command line program such as Image Magick
.The following code using the Python PNG module. It uses a reader instance to read the RGBA value of each of the pixels. This is Red Green Blue (as we covered previously), but also the Alpha value. The alpha value is ignored for this particular program. The data is then converted to the RGB565 format and then written to a new file with the raw extension. There are some print statements which I've commented out, but can help you to understand the data that is read from the png file.
import png
infile = "penguintutorlogo-pico.png"
outfile = "logo-image.raw"
def color_to_bytes (color):
r, g, b = color
arr = bytearray(2)
arr[0] = r & 0xF8
arr[0] += (g & 0xE0) >> 5
arr[1] = (g & 0x1C) << 3
arr[1] += (b & 0xF8) >> 3
return arr
png_reader=png.Reader(infile)
image_data = png_reader.asRGBA8()
with open(outfile, "wb") as file:
#print ("PNG file \nwidth {}\nheight {}\n".
# format(image_data[0], image_data[1]))
#count = 0
for row in image_data[2]:
for r, g, b, a in zip(row[::4],
row[1::4],
row[2::4],
row[3::4]):
#print ("This pixel {:02x}{:02x}{:02x} {:02x}".
#format(r, g, b, a))
# convert to (RGB565)
img_bytes = color_to_bytes ((r,g,b))
file.write(img_bytes)
file.close()
This has been used against this image file to create a raw binary file that can be used by the Pico.
The output file from the convert command is a binary .raw file. This file needs to be transferred to the Pico. The easiest way I found of doing this is using the rshell command. It is available as a python pip package which can be installed on Linux using:
sudo pip3 install rshell
You then run the command as:
rshell
If it fails to connect then you may need to close Thonny and restart the Pico by disconnecting and reconnecting the USB connection. If you still have problems, it may be due to user permissions, see the rshell documentation for more information.
When connected you can navigate around your local computer and access the Pico using the /pyboard directory.
cd pico-sprites
cp logo-image.bin /pyboard
The following code reads in the file and the writes it directly to the display buffer byte array. The update is then called to update the screen. There is little error checking in the program, but it does illustrate the basic steps in reading a file and sending it to the display.
import picodisplay as display
width = display.get_width()
height = display.get_height()
# 2-bytes per pixel (RGB565)
display_buffer = bytearray(width * height * 2)
display.init(display_buffer)
display.set_backlight(1.0)
def setup():
blit_image_file ("logo-image.raw")
display.update()
# This is based on a binary image file (RGB565)
# with the same dimensions as the screen
# updates the global display_buffer directly
def blit_image_file (filename):
global display_buffer
with open (filename, "rb") as file:
position = 0
while position < (width * height * 2):
current_byte = file.read(1)
# if eof
if len(current_byte) == 0:
break
# copy to buffer
display_buffer[position] = ord(current_byte)
position += 1
file.close()
setup()
# Do nothing - but continue to display the image
while True:
pass
To create a simple animation then you can display static images one after another. This is something I've already done with my RGB Matrix display powered by Raspberry Pi, but the increased resolution of the display and the performance on the Pico makes this a little more challenging. Using the full resolution and loading each image one at a time with no delay then it took 90 seconds to display a 17 frame animation (approx 5 secs for each image).
First start with a series of static images. Typically this needs to be around 20 images or less. These can be created manually using a drawing package, presentation software or photos. Alternatively they could created using an animation package.
I created my using Blender 2D animation mode (tutorial example). I had to change the render size down to 240 x 135 and I exported the video based on every 10th frame giving a total of 17 frames (instead of 170). This gave me 17 png images.
As with the static image previously the images were converted to raw files to be used with the Pico. I created an updated conversion program which converted all 17 files into individual raw files. These were then uploaded to an animation directory on the Pico and an updated python file created to show each of the images in turn.
The code required can be downloaded below:
Performance is quite poor for creating an image with this resolution in MicroPython. There are a few alternative things that could be considered regarding performance which have their pros and cons.
These suggestions are based on an assumption that the performance bottleneck is with reading files from the flash storage into memory. I think that's a reasonable assumption, but would need to be tested when updating the code.
One thing I have considered is to reduce the resolution of each image. As well as hopefully improving the performance this should increase the number of files that can be stored on the flash storage. The disadvantage is obviously that this will mean that the images are not so clear on the display.
If the resolution was reduced by half, then it would be fairly easy to repeat each pixel in the file to represent a 2 x 2 pixel. The amount of data being written to the screen would still be the same, but the data being read from the flash storage would be much less.
A technique commonly used for video files is rather than updating all the pixels is to only update the changes in the image. This could be processed on a computer prior to uploading to the Pico.
For the example video then this would likely reduce the files considerably because the background image is unchanged. For other animations where there is a sudden transition then that may not have such a large improvement.
This could potentially have a significant improvement on performance however it does have a downside in terms of creating non deterministic speed frames. This is because some frames may involve reading and updating a lot more information than others. This could result in the animation not being smooth and if there are a lot of complex transitions could even result in larger files and more processing time for certain images.
Whilst the Raspberry Pi Pico with display pack is able to display image files and simple animations the animations are slow. There is also a limited number of image files that can be included in the animations.
This is not neccessarily the intended use of a Pico or a display pack, but it gives an idea of what the Pico is and isn't capable of doing when it comes to images and animations.
The next page will explain how a similar technique can be used for creating sprites on the screen which are useful for creating games.
For information on how you can use this for creating games see Guide to Micropython bitmap game sprites on the Raspberry Pi Pico Display pack