Micropython: Usando la placa TTGO T-Display de Lilygo. Parte 3

Algo que no puede faltar en un módulo o driver para controlar un display gráfico es la capacidad de cargar imágenes en distintos formatos. Esto nos permite usar imágenes como fondo en una pantalla sobre la que imprimiremos luego textos como valores numéricos o datos, incluir íconos que complementen la información textual o, porque no, para hacer un juego que aproveche al máximo las posibilidades de la placa. Después de ver distintos métodos para dibujar elementos gráficos y escribir textos con distintas fuentes sobre el display, en esta entrega nos concentraremos precisamente en analizar cómo podemos mostrar imágenes de distintos formatos.

Para mostrar imágenes en el display contamos con un método muy potente que nos permite mover datos a un bloque determinado de la pantalla:

blit_buffer (buffer, x, y, ancho, alto)

Este método copia la variable buffer al display, a partir de la coordenada x, y como un bloque de imagen de ancho y alto especificado.

Recordemos que cada punto en la pantalla requiere de 2 bytes para especificar su color.

Fig. 1. Codificación RGB565

Por lo tanto, dos bytes como 11111000, 00000000 en binario o 0xF8, 0x00 en hexadecimal representan un punto de color rojo. Si quisiéramos dibujar un cuadrado rojo de 3 x 3 pixeles, necesitaríamos 3 x 3 = 9 de estos bytes, haciendo un total de 18 bytes. Veamos como en el siguiente ejemplo como se puede usar este método para dibujar tres bloques de 3×3 pixeles, uno azul, otro verde y el último rojo en tres posiciones diferentes:

from machine import Pin, SPI
import st7789

spi = SPI(1, baudrate=30000000, polarity=1, phase=1, sck=Pin(18), mosi=Pin(19))
display = st7789.ST7789(
    spi, 
    135,
    240,
    reset=Pin(23, Pin.OUT),
    cs=Pin(5, Pin.OUT),
    dc=Pin(16, Pin.OUT),
    backlight=Pin(4, Pin.OUT),
    rotation=0)

display.init ()

#Bloque azul
buf = bytearray (b'\x00\x0F\x00\x0F\x00\x0F\x00\x0F\x00\x0F\x00\x0F\x00\x0F\x00\x0F\x00\x0F')
display.blit_buffer(buf, 20, 20, 3, 3)

#Bloque verde
buf = bytearray (b'\x07\x0E\x07\x0E\x07\x0E\x07\x0E\x07\x0E\x07\x0E\x07\x0E\x07\x0E\x07\x0E')
display.blit_buffer(buf, 20, 40, 3, 3)

#Bloque rojo
buf = bytearray (b'\xF8\x00\xF8\x00\xF8\x00\xF8\x00\xF8\x00\xF8\x00\xF8\x00\xF8\x00\xF8\x00')
display.blit_buffer(buf, 20, 60, 3, 3)

Cada bloque está definido por un bytearray que contiene los 18 bytes con la información de color.

El mismo método se puede emplear con una imagen de cualquier tamaño y complejidad utilizando un programa o aplicación on-line que convierta la imagen a uno o mas bytearray como los anteriores. Hay muchas de estas aplicaciones que generan estructuras de datos con la sintaxis del lenguaje C, pero lamentablemente en general no se adaptan con facilidad al código de Python sin un gran esfuerzo de edición manual. Mas adelante usaremos un programa que si genera el formato adecuado pero que está limitado a imágenes de hasta 256 colores.

Sin embargo esto no es un inconveniente porque tenemos otras maneras de cargar imágenes y mostrarlas, de formas muy sencillas y eficientes.

Una opción es almacenar la imagen en la memoria de la placa y aprovechando lo que aprendimos en artículos anteriores sobre el sistema de archivos, abrir la misma como un archivo binario y extraer la información de cada punto, para luego volcarla empleando el método blit_buffer.

Veamos un ejemplo con la imagen bender48.png, que tiene un tamaño de 48 x 48 pixeles

Fig. 2

Lo primero que debemos hacer es convertir esta imagen al formato RAW con la codificación de colores RGB565. Para realizar esta conversión podemos emplear la aplicación que se ofrece en la página Rinky-Dink Electronics, inicialmente diseñada para usar junto a una popular librería para Arduino.

Fig. 3. Conversión a formato RAW/RGB565

Una vez realizada la conversión se descargará un archivo “.raw” (en el caso del ejemplo bender48.raw) que debemos guardar en nuestra placa TTGO.

Para hacerlo se puede usar el mismo Thonny. Hay que tener activada la visualización de archivos (Visualización – Archivos) y en el panel de archivos locales (Este computador) buscar el archivo de la imagen, hacerle click con el botón derecho del mouse y seleccionar subir a /

Fig. 4. Subiendo la imagen al sistema de archivos

Con la imagen en formato raw ya copiada al sistema de archivos la podemos leer como un archivo binario para luego volcarla al display como se puede ver en el siguiente ejemplo:

from machine import Pin, SPI
import st7789

imagenAncho = 48
imagenAlto = 48

spi = SPI(1, baudrate=30000000, polarity=1, phase=1, sck=Pin(18), mosi=Pin(19))
display = st7789.ST7789(
    spi, 
    135,
    240,
    reset=Pin(23, Pin.OUT),
    cs=Pin(5, Pin.OUT),
    dc=Pin(16, Pin.OUT),
    backlight=Pin(4, Pin.OUT),
    rotation=0)

display.init ()

file = open ("bender48.raw", "rb")
buf = file.read ()
file.close ()

#Calcula la posicion para que quede al centro
posx = int ((display.width()/2)  - (imagenAncho/2))
posy = int ((display.height()/2) - (imagenAlto/2))

display.blit_buffer(buf, posx, posy, imagenAncho, imagenAlto)

Aprovecho este ejemplo para introducir otros dos métodos que pueden ser de utilidad:

width ()

Devuelve el ancho lógico del display (135 si no está rotado, 240 si lo está)

height ()

Devuelve la altura lógica del display (240 si no está rotado, 135 si lo está)


El procedimiento del ejemplo anterior funciona correctamente para imágenes pequeñas. Si intentan utilizarlo con una imagen a tamaño completo (135 x 240) obtendrán un mensaje de error o simplemente verán que no funciona porque el espacio de memoria necesario es demasiado grande (135 x 240 x 2 = 648000 bytes). Una alternativa es hacerlo por partes, para emplear un buffer de memoria menor. Por ejemplo de a una línea, como en este ejemplo (primero se debe convertir la imagen bigbuckbunny.jpg de la carpeta examples/T-DISPLAY/jpg/ a formato raw y copiarla a la memoria de la placa como ya vimos)

from machine import Pin, SPI
import st7789

spi = SPI(1, baudrate=30000000, polarity=1, phase=1, sck=Pin(18), mosi=Pin(19))
display = st7789.ST7789(
    spi, 
    135,
    240,
    reset=Pin(23, Pin.OUT),
    cs=Pin(5, Pin.OUT),
    dc=Pin(16, Pin.OUT),
    backlight=Pin(4, Pin.OUT),
    rotation=1)

display.init ()

file = open ("bigbuckbunny.raw", "rb")

for i in range (0, 136):

    #Lee una linea (240 x 2 bytes)
    buf = file.read (480)
    #Muestra una linea
    display.blit_buffer(buf, 0, i, 240, 1)

file.close ()

En el ejemplo anterior se hace un bucle por cada línea (135) y se van leyendo los 480 bytes correspondientes para ir volcándolos al display.


Otro método más sencillo para mostrar una imagen es jpg que de una manera muy simple toma una imagen en formato jpg desde el sistema de archivos y la vuelca a la pantalla. El formato es el siguiente:

jpg (archivo_jpg, x, y [, metodo])

Importa archivo_jpg desde el sistema de archivos y lo muestra en pantalla a partir de la posición x,y. Estas coordenadas corresponden al ángulo superior izquierdo de la imagen. Si se especifica el método FAST, se carga primero toda la imagen a la memoria RAM, lo que puede ser un problema para imágenes grandes como ya vimos. Si eso ocurre puede especificarse el método SLOW, con lo que se va copiando la imagen de a unidades de 8 bytes, ocupando poca RAM pero consumiendo mas tiempo.

En el siguiente ejemplo podemos ver como funciona este método (primero debemos copiar el archivo bigbuckbunny.jpg a la placa, sin convertir)

from machine import Pin, SPI
import st7789

import gc
gc.enable()
gc.collect()

spi = SPI(1, baudrate=30000000, polarity=1, phase=1, sck=Pin(18), mosi=Pin(19))
display = st7789.ST7789(
    spi, 
    135,
    240,
    reset=Pin(23, Pin.OUT),
    cs=Pin(5, Pin.OUT),
    dc=Pin(16, Pin.OUT),
    backlight=Pin(4, Pin.OUT),
    rotation=0)

display.init ()
display.rotation (1)

display.jpg("bigbuckbunny.jpg", 0, 0, st7789.SLOW)


Otro método similar para volcar un archivo de imagen al display es bitmap, cuyo formato es el siguiente:

bitmap (bitmap, x , y , indice)

Muestra una imagen bitmap en el display en las coordenadas x, y que se corresponden con la esquina superior izquierda de la misma. El parámetro indice permite seleccionar un bitmap de varios almacenados en el mismo módulo. (En la documentación del driver se especifica que indice es opcional, pero se produce un error si no se lo utiliza)

Para utilizar este método debemos convertir la imagen a un módulo de Python (“.py”). Para ello se puede utilizar el programa imgtobitmap.py que encontrarán en la carpeta utils/ del repositorio.

Para poder utilizar este programa necesitan tener instalado Python en su computadora. Aquí pueden ver como hacerlo si aún no lo tienen. También necesitarán una librería para tratamiento de imágenes llamada Pillow, que se instala de la siguiente forma (en el símbolo del sistema o ventana de comandos de Windows)

python -m pip install --upgrade pip
python -m pip install --upgrade Pillow

Una vez completada esta instalación, debemos seleccionar una imagen (en formato JPG, PNG o BMP) y con 1 a 8 bits de color por pixel (es decir, un máximo de 256 colores) y convertirla a módulo Python. Por ejemplo la imagen t1.png de muestra incluida en la carpeta utils/ del repositorio que tiene un tamaño de 64 x 64 pixeles y 16 colores (4 bits por pixel).

Fig. 5

Para convertirla debemos usar imgtobitmap de la siguiente forma:

imgtobitmap.py t1.png 4 >tostadora.py

Luego debemos cargar el módulo generado (tostadora.py en este caso) a la memoria de la placa e importarlo en nuestro código, como en el siguiente ejemplo:

from machine import Pin, SPI
import st7789
import tostadora


spi = SPI(1, baudrate=30000000, polarity=1, phase=1, sck=Pin(18), mosi=Pin(19))
display = st7789.ST7789(
    spi, 
    135,
    240,
    reset=Pin(23, Pin.OUT),
    cs=Pin(5, Pin.OUT),
    dc=Pin(16, Pin.OUT),
    backlight=Pin(4, Pin.OUT),
    rotation=0,
    buffer_size=64*64*2)

display.init ()

display.bitmap(tostadora, 30, 20, 0)
    

En la carpeta examples/T-DISPLAY/toasters/ pueden encontrar cuatro módulos ya convertidos de cuatro imágenes similares (t1.png a t4.png) que podemos emplear para realizar una animación sencilla si las mostramos en pantalla sucesivamente (debemos cargar primero los cuatro módulos t1.py a t4.py en la placa).

from time import sleep
from machine import Pin, SPI
import st7789
import t1, t2, t3, t4

#Crea una lista con las 4 imagenes
animacion = [t1, t2, t3, t4]

spi = SPI(1, baudrate=30000000, polarity=1, phase=1, sck=Pin(18), mosi=Pin(19))
display = st7789.ST7789(
    spi, 
    135,
    240,
    reset=Pin(23, Pin.OUT),
    cs=Pin(5, Pin.OUT),
    dc=Pin(16, Pin.OUT),
    backlight=Pin(4, Pin.OUT),
    rotation=0,
    buffer_size=64*64*2)

display.init ()

while (True):
    for imagen in animacion:
        display.bitmap(imagen, 30, 20, 0)
        sleep (0.1)

Si queremos mostrar íconos o animaciones simples con formato monocromo (imágenes de un sólo color), podemos emplear el método map_bitarray_to_rgb565 que nos permite mostrarlas en pantalla con cualquier color:

map_bitarray_to_rgb565 (bitarray, buffer, ancho, color_frente, color_fondo)

Pasa bitarray a buffer, con el ancho especificado. Los bits en 1 de bitarray se muestran de color color_frente y los bits a cero en color color_fondo. Para mostrar buffer en la pantalla se debe emplear el método blit_buffer

from machine import Pin, SPI
import st7789

anchoImagen = 16
altoImagen = 16

imagen = bytearray([0b00000111, 0b11100000,
                    0b00011111, 0b11111000,
                    0b00111111, 0b11111100,
                    0b01111100, 0b11110110,
                    0b01111100, 0b11110010,
                    0b11111111, 0b11110001,
                    0b11111111, 0b11100001,
                    0b11111111, 0b11000001,
                    0b11111111, 0b10000001,
                    0b11111000, 0b00000001,
                    0b11110000, 0b00000001,
                    0b01110000, 0b11000010,
                    0b01110000, 0b11000110,
                    0b00111000, 0b00001100,
                    0b00001110, 0b00011000,
                    0b00000111, 0b11100000])

spi = SPI(1, baudrate=30000000, polarity=1, phase=1, sck=Pin(18), mosi=Pin(19))
display = st7789.ST7789(
    spi, 
    135,
    240,
    reset=Pin(23, Pin.OUT),
    cs=Pin(5, Pin.OUT),
    dc=Pin(16, Pin.OUT),
    backlight=Pin(4, Pin.OUT),
    rotation=0)

display.init ()

#Buffer de 16 x 16 x 2 pixeles
buf = bytearray(512)

display.map_bitarray_to_rgb565 (imagen, buf, anchoImagen, st7789.RED, st7789.BLACK)
display.blit_buffer(buf, 10, 10, anchoImagen, altoImagen)
            

Finalmente, hay dos métodos que nos permiten encender y apagar el backlight:

on ()

Enciende el backlight

off ()

Apaga el backlight

Conclusión

En este artículo vimos distintas formas de cargar imágenes en el display, usando distintos métodos que se adaptan a distintas situaciones y necesidades. El tema no está agotado ni mucho menos, hay mucho para probar y seguir investigando y las posibilidades son casi infinitas. Espero haberles dado un vistazo general de las capacidades de esta placa para que les sirva como punto de partida para sus propios proyectos. Como siempre, cualquier duda o sugerencia la pueden dejar mas abajo en la sección de comentarios.

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

A %d blogueros les gusta esto: