Third party cookies may be stored when visiting this site. Please see the cookie information.

PenguinTutor YouTube Channel

Drawing images and animations on the Raspberry Pi Pico Display Pack

Raspberry Pi Pico display pack showing a static image

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.

Understanding how images are displayed on the Pico Display Pack

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:

Pico Display Pack pixel value in bytearray RGB565

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)

Converting a PNG image to display pack bytearray

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.

Preparing the image file

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

.

Using Python PNG to read PNG files

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.

small logo image for Raspberry PI Pico Display Pack

Transferring binary files to the Raspberry Pi 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

Reading the raw file on the Pico and displaying it on the display pack

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

Creating animations

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:

Possible Performance Improvements

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.

Reduce the Resolution

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.

Only update changes

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.

Summary

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.

Creating Game Sprites on the Raspberry Pi Pico

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

More information

Blog links and videos on programming

Previous Graphics on the Raspberry Pi Pico
Graphics on the Raspberry Pi Pico
Next Bitmap Sprites on Raspberry Pi Pico
Bitmap Sprites on Raspberry Pi Pico