Raspberry Pi Pico 2/MicroPython read bmp images and draw on ST7789 display pixel-by-pixel.

Follow the former exercises of Raspberry Pi Pico 2/MicroPython display on 1.54" 240x240 ST7789 SPI IPS and Python code to read .bmp info from header (work on Desktop Python and MicroPython), this exercise read bmp images, and draw on ST7789 display pixel-by-pixel.



Exercise code:

mpy_pico2_bmp.py
"""
Raspberry Pi Pico/MicroPython exercise
to display on 1.54" IPS 240x240 with SPI ST7789 driver
Pixel-by-Pixel

Read, decode bmp in /images/ directory, and disply on 240x240 SPI ST7789 display.
Target:
- 240x240 RGB565 .bmp
- 240x240 RGB888 .bmp

ref:

Using library: russhughes/st7789py_mpy
https://github.com/russhughes/st7789py_mpy

to install st7789py_mpy on Raspberry Pi Pico 2/MicroPython,
https://coxxect.blogspot.com/2024/10/raspberry-pi-pico-2micropython-display.html

to know more about bmp structure:
https://coxxect.blogspot.com/2024/10/python-code-to-read-bmp-info-from.html

to convert jpg to RGB565/RGB888 bmp using GIMP, read the video in:
https://coxxect.blogspot.com/2024/10/python-code-to-read-bmp-info-from.html

Python/cv2 code to batch convert jpg to 240x240 RGB888 bmp,
check the Python code in this post,
https://coxxect.blogspot.com/2024/11/raspberry-pi-pico-2micropython-read-bmp.html.

Connection:
-----------
GND   GND
VCC   3V3
SCL   GP18
SDA   GP19
RES   GP20
DC    GP21
CS    GP17
BLK   GP22
      GP16 (dummy, not used)

"""

import os, sys
from machine import Pin, SPI
import time
import st7789py as st7789

disp_sck  = 18 # default SCK of SPI(0)
disp_mosi = 19 # default MOSI of SPI(0)
disp_miso = 16  # not use
disp_res = 20
disp_dc  = 21
disp_cs  = 17
disp_blk = 22

print("====================================")
print(sys.implementation[0], os.uname()[3],
      "\nrun on", os.uname()[4])
print("====================================")

DISP_WIDTH = 240
DISP_HEIGHT = 240

# it's found that:
# - cannot set baudrate
# - even I set miso=None, miso will be assigned deault GP16 or previous assigned miso
disp_spi = SPI(0, baudrate=60000000, sck=Pin(disp_sck), mosi=Pin(disp_mosi), miso=None)
print(disp_spi)

display = st7789.ST7789(disp_spi,
                        DISP_WIDTH, DISP_HEIGHT,
                        reset=Pin(disp_res, Pin.OUT),
                        cs=Pin(disp_cs, Pin.OUT),
                        dc=Pin(disp_dc, Pin.OUT),
                        backlight=Pin(disp_blk, Pin.OUT))
print(st7789.__name__, display.width, "x", display.height)
display.fill(st7789.WHITE)
time.sleep(0.5)
display.fill(st7789.BLACK)

# Read bmp

img_path = '/images/'

bmp_file = img_path + 'img_01_240_RGB565.bmp'

def read_bmp(filename):
    print("Read:", filename)
    with open(filename, 'rb') as f:
        bmp_data = f.read()
    return bmp_data

def get_compression_method(num):
    switcher = {
        0: "BI_RGB",
        1: "BI_RLE8",
        2: "BI_RLE4",
        3: "BI_BITFIELDS",
        4: "BI_JPEG",
        5: "BI_PNG",
        6: "BI_ALPHABITFIELDS",
        11: "BI_CMYK",
        12: "BI_CMYKRLE8",
        13: "BI_CMYKRLE4",
        }
    
    return switcher.get(num, "unknown")

bmp_data = read_bmp(bmp_file)
#buffer = bytearray(bmp_data)
print("len(bmp_data)", type(bmp_data), len(bmp_data))
#print(bmp_data)
print("Signature:",chr(bmp_data[0]), chr(bmp_data[1]))

size_of_bmp_file = int.from_bytes(bmp_data[2:6], 'little')
print("--- size_of_bmp_file = ", size_of_bmp_file)

print("Reserved:", bmp_data[6], bmp_data[7])
print("Reserved:", bmp_data[8], bmp_data[9])

offset = int.from_bytes(bmp_data[10:14], 'little')
print("--- offset = ", offset)

size_of_header = int.from_bytes(bmp_data[14:18], 'little')
print("--- size_of_header:", size_of_header)

image_width = int.from_bytes(bmp_data[18:22], 'little')
print("--- image_width:", image_width)
image_height = int.from_bytes(bmp_data[22:26], 'little')
print("--- image_height:", image_height)

number_of_color_planes = int.from_bytes(bmp_data[26:28], 'little')
print("--- number_of_color_planes (must be 1):", number_of_color_planes)

num_of_bits_per_pixel = int.from_bytes(bmp_data[28:30], 'little')
print("--- num_of_bits_per_pixel:", num_of_bits_per_pixel)

compression_method = int.from_bytes(bmp_data[30:34], 'little')
print("--- compression_method:", compression_method, get_compression_method(compression_method))

image_size = int.from_bytes(bmp_data[34:38], 'little')
print("--- image_size:", image_size)

horizontal_resolution = int.from_bytes(bmp_data[38:42], 'little')
print("--- horizontal_resolution:", horizontal_resolution)
vertical_resolution = int.from_bytes(bmp_data[42:46], 'little')
print("--- vertical_resolution:", vertical_resolution)

number_of_colors = int.from_bytes(bmp_data[46:50], 'little')
print("--- number_of_colors:", number_of_colors)
number_of_important_colors = int.from_bytes(bmp_data[50:54], 'little')
print("--- number_of_important_colors:", number_of_important_colors)

print("=============================================")
print("What concerned are:")
print("Signature:",chr(bmp_data[0]), chr(bmp_data[1]))
print("offset:", offset)
print("image_width:", image_width)
print("image_height:", image_height)
print("num_of_bits_per_pixel:", num_of_bits_per_pixel)
print("=============================================")

IMG_WIDTH = 240
IMG_HEIGHT = 240
# Verify the bmp image
if chr(bmp_data[0]) != 'B' or chr(bmp_data[1]) != 'M':
    print("Must be bmp")
elif image_width != IMG_WIDTH or image_height != IMG_HEIGHT:
    print("Must be 240 x 240")
else:
    
    if num_of_bits_per_pixel == 24:
        print("Assume " + bmp_file + " is 240x240 RGB888 bmp, continuous.")
        
        byte_per_pixel = 3
        for y in range(IMG_HEIGHT):
            y_offset = (y*IMG_WIDTH*byte_per_pixel) + offset
            for x in range(IMG_WIDTH):
                i = (x*byte_per_pixel) + y_offset
                # Because bmp start from bottom-left,
                # so we have to invert the y order.
                display.pixel(x, IMG_HEIGHT-y, st7789.color565(bmp_data[i+2],
                                                               bmp_data[i+1],
                                                               bmp_data[i]))

    elif num_of_bits_per_pixel == 16:
        print("Assume " + bmp_file + " is 240x240 RGB565 bmp, continuous.")
        
        byte_per_pixel = 2
        for y in range(IMG_HEIGHT):
            y_offset = (y*IMG_WIDTH*byte_per_pixel) + offset
            for x in range(IMG_WIDTH):
                i = (x*byte_per_pixel) + y_offset
                # Because bmp start from bottom-left,
                # so we have to invert the y order.
                display.pixel(x, IMG_HEIGHT-y, int.from_bytes(bmp_data[i:i+2], 'little'))
                
    else:
        print("Must be 16 or 24 bit per pixel")


mpy_pico2_bmp_slideshow.py, in slideshow.
"""
Raspberry Pi Pico/MicroPython exercise
to display on 1.54" IPS 240x240 with SPI ST7789 driver
Pixel-by-Pixel

Read, decode bmp images in /images/ directory, and disply on 240x240 SPI ST7789 display in Slideshow.
Target:
- 240x240 RGB565 .bmp
- 240x240 RGB888 .bmp

ref:

Using library: russhughes/st7789py_mpy
https://github.com/russhughes/st7789py_mpy

to install st7789py_mpy on Raspberry Pi Pico 2/MicroPython,
https://coxxect.blogspot.com/2024/10/raspberry-pi-pico-2micropython-display.html

to know more about bmp structure:
https://coxxect.blogspot.com/2024/10/python-code-to-read-bmp-info-from.html

to convert jpg to RGB565/RGB888 bmp using GIMP, read the video in:
https://coxxect.blogspot.com/2024/10/python-code-to-read-bmp-info-from.html

Python/cv2 code to batch convert jpg to 240x240 RGB888 bmp,
check the Python code in this post,
https://coxxect.blogspot.com/2024/11/raspberry-pi-pico-2micropython-read-bmp.html.

Connection:
-----------
GND   GND
VCC   3V3
SCL   GP18
SDA   GP19
RES   GP20
DC    GP21
CS    GP17
BLK   GP22
      GP16 (dummy, not used)

"""

import os, sys
from machine import Pin, SPI
import time
import st7789py as st7789

disp_sck  = 18 # default SCK of SPI(0)
disp_mosi = 19 # default MOSI of SPI(0)
disp_miso = 16  # not use
disp_res = 20
disp_dc  = 21
disp_cs  = 17
disp_blk = 22

print("====================================")
print(sys.implementation[0], os.uname()[3],
      "\nrun on", os.uname()[4])
print("====================================")

DISP_WIDTH = 240
DISP_HEIGHT = 240

# it's found that:
# - cannot set baudrate
# - even I set miso=None, miso will be assigned deault GP16 or previous assigned miso
disp_spi = SPI(0, baudrate=60000000, sck=Pin(disp_sck), mosi=Pin(disp_mosi), miso=None)
print(disp_spi)

display = st7789.ST7789(disp_spi,
                        DISP_WIDTH, DISP_HEIGHT,
                        reset=Pin(disp_res, Pin.OUT),
                        cs=Pin(disp_cs, Pin.OUT),
                        dc=Pin(disp_dc, Pin.OUT),
                        backlight=Pin(disp_blk, Pin.OUT))
print(st7789.__name__, display.width, "x", display.height)
display.fill(st7789.WHITE)
time.sleep(0.5)
display.fill(st7789.BLACK)

# Read bmp

img_path = '/images/'

def read_bmp(filename):
    print("Read:", filename)
    with open(filename, 'rb') as f:
        bmp_data = f.read()
    return bmp_data

def get_compression_method(num):
    switcher = {
        0: "BI_RGB",
        1: "BI_RLE8",
        2: "BI_RLE4",
        3: "BI_BITFIELDS",
        4: "BI_JPEG",
        5: "BI_PNG",
        6: "BI_ALPHABITFIELDS",
        11: "BI_CMYK",
        12: "BI_CMYKRLE8",
        13: "BI_CMYKRLE4",
        }
    
    return switcher.get(num, "unknown")

def displat_bmp(bmp_file):
    bmp_data = read_bmp(bmp_file)
    offset = int.from_bytes(bmp_data[10:14], 'little')
    image_width = int.from_bytes(bmp_data[18:22], 'little')
    image_height = int.from_bytes(bmp_data[22:26], 'little')
    num_of_bits_per_pixel = int.from_bytes(bmp_data[28:30], 'little')

    print("=============================================")
    print("What concerned are:")
    print("Signature:",chr(bmp_data[0]), chr(bmp_data[1]))
    print("offset:", offset)
    print("image_width:", image_width)
    print("image_height:", image_height)
    print("num_of_bits_per_pixel:", num_of_bits_per_pixel)
    print("=============================================")

    IMG_WIDTH = 240
    IMG_HEIGHT = 240
    # Verify the bmp image
    if chr(bmp_data[0]) != 'B' or chr(bmp_data[1]) != 'M':
        print("Must be bmp")
    elif image_width != IMG_WIDTH or image_height != IMG_HEIGHT:
        print("Must be 240 x 240")
    else:
    
        if num_of_bits_per_pixel == 24:
            print("Assume " + bmp_file + " is 240x240 RGB888 bmp, continuous.")
        
            byte_per_pixel = 3
            for y in range(IMG_HEIGHT):
                y_offset = (y*IMG_WIDTH*byte_per_pixel) + offset
                for x in range(IMG_WIDTH):
                    i = (x*byte_per_pixel) + y_offset
                    # Because bmp start from bottom-left,
                    # so we have to invert the y order.
                    display.pixel(x, IMG_HEIGHT-y, st7789.color565(bmp_data[i+2],
                                                                   bmp_data[i+1],
                                                                   bmp_data[i]))

        elif num_of_bits_per_pixel == 16:
            print("Assume " + bmp_file + " is 240x240 RGB565 bmp, continuous.")
        
            byte_per_pixel = 2
            for y in range(IMG_HEIGHT):
                y_offset = (y*IMG_WIDTH*byte_per_pixel) + offset
                for x in range(IMG_WIDTH):
                    i = (x*byte_per_pixel) + y_offset
                    # Because bmp start from bottom-left,
                    # so we have to invert the y order.
                    display.pixel(x, IMG_HEIGHT-y, int.from_bytes(bmp_data[i:i+2], 'little'))
                
        else:
            print("Must be 16 or 24 bit per pixel")
            
while True:
    
    for b_file in os.listdir(img_path):
        if b_file.endswith('.bmp'):
            print()
            bmp_file_full_path = img_path + b_file
            start_time = time.ticks_ms()
            displat_bmp(bmp_file_full_path)
            end_time = time.ticks_ms()
            print("time to draw: ", time.ticks_diff(end_time, start_time), "ms")
            time.sleep(2)
            



py_cv_batch_resize_RGB888.py, run on desktop (Windows 11) to prepare images in 240x240 RGB888 bmp.
"""
Python/cv2 to convert all jpg in 'images' folder to 240*240 RGB888 bmp in 'resized_images' folder.

OpenCV is needed.
To install OpenCV in Python virtual environment, read:
ref: https://coxxect.blogspot.com/2024/10/create-python-virtual-environment-in.html
"""
import os
import cv2

def resize_and_save_as_bmp(input_folder, output_folder, size):
    if not os.path.exists(output_folder):
        os.makedirs(output_folder)
    
    for filename in os.listdir(input_folder):
        if filename.endswith('.jpg'):
            img_path = os.path.join(input_folder, filename)
            img = cv2.imread(img_path)
            resized_img = cv2.resize(img, size)
            
            # Save as RGB888 BMP
            bmp_filename = os.path.splitext(filename)[0] + '_240_RGB888.bmp'
            bmp_path = os.path.join(output_folder, bmp_filename)
            cv2.imwrite(bmp_path, resized_img)

input_folder = 'images'
output_folder = 'resized_images'
size = (240, 240)

resize_and_save_as_bmp(input_folder, output_folder, size)


Updated@2024-11-07: For easier reference in the future post to resize/convert images, it is published in separate post Resize jpg and convert to bmp in RGB888 and RGB565 mode, using Python/GIMP. updated with RGB565 mode added.

Added@2024-11-09:
It is a slow approach. Because it draw in pixel-by-pixel, each involve a write sequency. But it help me understand bmp much more.

Here is another approach, form a bytearray passing to st7789pt blit_buffer() method, such that the driver write the whole buffer in one sequency to improve the speed much more.

Read: Raspberry Pi Pico 2/MicroPython display bmp images on 240x240 ST7789 SPI Display



Comments

Popular posts from this blog

480x320 TFT/ILI9488 SPI wih EP32C3 (arduino-esp32) using Arduino_GFX Library

my dev.tools - FNIRSI 2C23T 3-in-1 Dual Channel Oscilloscope/Multimeter/Signal Generator