Twitch LED Matrix - Part 4: Static Images/Logo's

Heads Up: As I share my IRL adventures and tech builds here on the CabinetTerror blog, you might spot some affiliate links. If you click on those links to buy streaming gear or grab parts for your own LED screens, I may earn a small commission at absolutely no extra cost to you. It helps fund future projects, and I only recommend stuff I actually use and love. Thanks for the support!

Welcome to Part 4 of the Twitch LED Matrix masterclass! So far, we have built a live text ticker (Part 1), added animated GIFs (Part 2), and coded full-room police flashers (Part 3).

Today, we are covering the final visual piece: displaying crisp, static images. This is perfect for flashing your channel's logo, an emote, or a sponsor graphic on the screen whenever a viewer triggers a command. Let's get to it!

1. Preparing and Transferring the Image

For the absolute best results on an LED matrix, you want to use a PNG file with a transparent background. That way, only the logo lights up, leaving the rest of the board completely black.

  1. Find or create your logo/emote. Rename the file to something simple, like logo.png.
  2. Open your SFTP client (like WinSCP or Cyberduck) and connect to your Pi (Host: twitchmatrix.local, User: twitchmatrix, plus your password).
  3. Drag and drop your logo.png file directly into the /home/twitchmatrix/ folder right next to your Python script.

2. Updating the Python Code

Adding a static image uses almost the exact same logic as our animated GIFs, but without the complicated "frame-by-frame" playback loop. It's much simpler: we just tell it to paint the image on the screen and hold it there for 5 seconds.

  1. SSH into your Pi via terminal/PowerShell: ssh twitchmatrix@twitchmatrix.local
  2. Open the script: nano twitch_scroller.py
  3. Update your code to include the new STATIC IMAGE sections. (This master script includes all of our previous work, plus the new logo code!):
#!/usr/bin/env python3
import time
import math
from PIL import Image, ImageSequence
from rgbmatrix import RGBMatrix, RGBMatrixOptions, graphics
import paho.mqtt.client as mqtt

# --- CONFIGURATION ---
MQTT_BROKER = "localhost"  
MQTT_TOPIC = "twitch/scroll"
LED_ROWS = 32      
LED_COLS = 64      
LED_CHAIN = 2  
GPIO_MAPPING = 'adafruit-hat'  

DEFAULT_MSG = "Waiting for Chat..." 
TEXT_HEIGHT = 22 
GREEN = graphics.Color(0, 255, 0)

current_message = DEFAULT_MSG
new_message_arrived = False
current_color = GREEN

def on_connect(client, userdata, flags, rc):
    client.subscribe(MQTT_TOPIC)

def on_message(client, userdata, msg):
    global current_message, new_message_arrived
    try:
        current_message = msg.payload.decode("utf-8").strip()
        new_message_arrived = True 
    except:
        pass

def run_matrix():
    global current_message, new_message_arrived, current_color
    options = RGBMatrixOptions()
    options.rows = LED_ROWS
    options.cols = LED_COLS
    options.chain_length = LED_CHAIN
    options.hardware_mapping = GPIO_MAPPING
    options.drop_privileges = False
    options.brightness = 40  
    
    matrix = RGBMatrix(options = options)
    offscreen_canvas = matrix.CreateFrameCanvas()

    # --- 1. LOAD STATIC IMAGE ---
    try:
        logo_img = Image.open("/home/twitchmatrix/logo.png").convert('RGB')
        # .thumbnail keeps the original shape. .resize would stretch it!
        logo_img.thumbnail((offscreen_canvas.width, 32)) 
    except Exception as e:
        print("Could not load logo image:", e)
        logo_img = None

    # --- 2. LOAD ANIMATED GIF (From Part 2) ---
    pacman_frames = []
    try:
        pacman_gif = Image.open("/home/twitchmatrix/pacman.gif")
        for frame in ImageSequence.Iterator(pacman_gif):
            frame = frame.convert('RGB')
            frame = frame.resize((offscreen_canvas.width, 32), Image.NEAREST)
            pacman_frames.append(frame)
    except Exception as e:
        print("Could not load pacman gif:", e)
    
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message
    
    try:
        client.connect(MQTT_BROKER, 1883, 60)
        client.loop_start() 
    except:
        print("MQTT Connection Failed")

    pos = offscreen_canvas.width
    is_scrolling_custom = False
    hue = 0 

    while True:
        if new_message_arrived:
            
            # --- TRIGGER STATIC LOGO ---
            if current_message == "!logo" and logo_img:
                offscreen_canvas.Clear()
                
                # Math to perfectly center the image on the boards
                x_offset = (offscreen_canvas.width - logo_img.width) // 2
                y_offset = (offscreen_canvas.height - logo_img.height) // 2
                
                offscreen_canvas.SetImage(logo_img, x_offset, y_offset)
                offscreen_canvas = matrix.SwapOnVSync(offscreen_canvas)
                
                time.sleep(5) # Hold the image on screen for 5 seconds
                
                current_message = DEFAULT_MSG
                new_message_arrived = False
                pos = offscreen_canvas.width
                is_scrolling_custom = False
                continue

            # --- TRIGGER PAC-MAN (From Part 2) ---
            elif current_message == "!pacman" and pacman_frames:
                end_time = time.time() + 5  
                while time.time() < end_time:
                    for frame in pacman_frames:
                        offscreen_canvas.Clear()
                        x_offset = (offscreen_canvas.width - frame.width) // 2
                        y_offset = (offscreen_canvas.height - frame.height) // 2
                        offscreen_canvas.SetImage(frame, x_offset, y_offset)
                        offscreen_canvas = matrix.SwapOnVSync(offscreen_canvas)
                        time.sleep(0.1) 
                
                current_message = DEFAULT_MSG
                new_message_arrived = False
                pos = offscreen_canvas.width
                is_scrolling_custom = False
                continue

            # --- TRIGGER POLICE FLASHER (From Part 3) ---
            elif current_message == "!police":
                end_time = time.time() + 5  
                while time.time() < end_time:
                    offscreen_canvas.Fill(255, 0, 0)
                    offscreen_canvas = matrix.SwapOnVSync(offscreen_canvas)
                    time.sleep(0.15) 
                    
                    offscreen_canvas.Fill(0, 0, 255)
                    offscreen_canvas = matrix.SwapOnVSync(offscreen_canvas)
                    time.sleep(0.15) 
                
                current_message = DEFAULT_MSG
                new_message_arrived = False
                pos = offscreen_canvas.width
                is_scrolling_custom = False
                continue

            # --- NORMAL TEXT SCROLLING ---
            elif current_message != DEFAULT_MSG:
                pos = offscreen_canvas.width
                is_scrolling_custom = True 
                new_message_arrived = False 

        offscreen_canvas.Clear()
        
        if is_scrolling_custom:
            r = int(127 * math.sin(hue) + 128)
            g = int(127 * math.sin(hue + 2) + 128)
            b = int(127 * math.sin(hue + 4) + 128)
            current_color = graphics.Color(r, g, b)
            hue += 0.1 
        else:
            current_color = GREEN

        length = graphics.DrawText(offscreen_canvas, font, pos, TEXT_HEIGHT, current_color, current_message)
        pos -= 1

        if (pos + length < 0):
            if is_scrolling_custom:
                current_message = DEFAULT_MSG
                is_scrolling_custom = False
            pos = offscreen_canvas.width

        time.sleep(0.03) 
        offscreen_canvas = matrix.SwapOnVSync(offscreen_canvas)

if __name__ == "__main__":
    font = graphics.Font()
    font.LoadFont("/home/twitchmatrix/rpi-rgb-led-matrix/fonts/7x13.bdf")
    run_matrix()

Don't forget to replace the part of the 2nd to last line of code with your own Username, if you didn't use twitchmatrix.

Save it by pressing CTRL+X, then Y, then ENTER.

Restart the matrix service so the Pi can find your new image file:
sudo systemctl restart twitch_led.service

A Quick Note on Sizing!

In the code above, we used the .thumbnail command to load the image. This tells the Pi to shrink your image until it fits the 32-pixel height limit, but keep its original shape. It will be perfectly centered on your screen.

If you want the image to forcefully stretch all the way across a double-panel setup (like we did with Pac-Man in Part 2), you can simply change the word thumbnail to resize!

3. Linking to Lumia Stream

Finally, we map the trigger in Lumia Stream to complete the circuit.

  1. Open Lumia Stream on your PC.
  2. Go to Commands > Chat Commands > Add Command.
  3. Name the command logo.
  4. Go to the Lumia Actions tab, click Add Action, and choose MQTT.
  5. Fill out the box exactly like this:
    • Topic: twitch/scroll
    • Value: !logo
  6. Click Save, then click the Refresh button on your Lumia Dashboard to lock it in.

Test it Out!

Go to your Twitch chat and type !logo. Your custom graphic should proudly display right in the center of your LED board!

Comments