1

I would like to display a NumPy array as an image on the screen from a Python script. I need to do this without starting X, so I can't use OpenCV, PIL, etc.

Is it possible to do this using fbi in a subprocess started in the Python script?

Below is what I have tried to do. I expected to see the images in the 'images' folder displayed with a 1-second delay between them, but instead I get the error BrokenPipeError: [Errno 32] Broken Pipe

import os
import time
import numpy as np
from PIL import Image
from subprocess import Popen, PIPE

# Define the folder containing the images
folder_path = 'images'

# Loop through each file in the folder
for filename in os.listdir(folder_path):
    # Check if the file is an image
    if filename.endswith('.jpg') or filename.endswith('.jpeg') or filename.endswith('.png'):
        # Load the image into a NumPy array using PIL
        image = np.array(Image.open(os.path.join(folder_path, filename)))
        
        # Edit the image as desired 
        edited_image = image
        
        # Build the fbi command to display the edited image in full-screen mode
        command = 'sudo fbi -T 1 -noverbose -a -t 1 -u -blend 500 - '
        
        # Create a new process for fbi, and pass the edited image to its standard input
        with Popen(command.split(), stdin=PIPE) as process:
            process.stdin.write(Image.fromarray(edited_image).tobytes())
            process.communicate()  # Wait for fbi to complete
        
        # Wait for 1 second before displaying the next image
        time.sleep(1)

Thanks in advance!

Matt
  • 35
  • 3
  • Surely `fbi` will expect a JPEG or PNG rather than a bunch of pixels without any metadata such as image height and width? Try a plain binary read with `pathlib.Path('image.jpg').read_bytes()` and send that to `fbi`. – Mark Setchell Apr 06 '23 at 10:05
  • The above would be tantamount to `cat image.jpg | sudo fbi ... -` – Mark Setchell Apr 06 '23 at 10:12
  • Your question is confusing... 1) Why do you say you want to display a Numpy array when you actually display JPEGs and PNGs? 2) Why do you say you cannot use PIL, then go right ahead and use `PIL.Image.open()` ? 3) Why do you think a lack of X11 server means you cannot use OpenCV or PIL? – Mark Setchell Apr 06 '23 at 11:09
  • Hi Mark, thank you for the comments. The issue I have is displaying the images without an X11 server. (1) I need to modify the images in the Python script, in the form of NumPy arrays, so that's my starting point (2&3) I meant I can't use OpenCV or PIL to display the image (as in cv2.imshow()). I realise I can use these libraries as usual for all other purposes, sorry for the confusion. – Matt Apr 06 '23 at 14:02
  • I could save the NumPy array to a JPEG or PNG file, and then send that to fbi, but I would like to avoid the overhead as I am trying to do inference at a high frame rate. – Matt Apr 06 '23 at 14:33
  • Maybe this approach writing Numpy arrays directly to the framebuffer will work for you https://stackoverflow.com/a/58816979/2836621 – Mark Setchell Apr 06 '23 at 17:48
  • Or you can create a JPEG from Numpy *"in memory"* and send that to `fbi` without going via disk - let me know if that is interesting to you. – Mark Setchell Apr 06 '23 at 17:51

1 Answers1

0

I am still confused by your question, but now I think you are generating Numpy arrays rather than images and want to send them to the frame buffer from Python - maybe on a Raspberry Pi?

Here is an example of that:

#!/usr/bin/env python3

import numpy as np

def RGB888toRGB565(rgb888):
    """Convert RGB888 input array to RGB565 output array"""
    # Create 16-bit output image, same height and width as input
    rgb565 = np.zeros(rgb888.shape[:2], np.uint16)
    rgb565[:]  = (rgb888[:,:,0] & 0x1f).astype(np.uint16) << 11 
    rgb565[:] |= (rgb888[:,:,1] & 0x3f).astype(np.uint16) << 5
    rgb565[:] |= (rgb888[:,:,2] & 0x1f).astype(np.uint16)
    return rgb565

# Get the framebuffer height, width and pixel format (e.g. RGB565) by running "fbset" command
# $ fbset
# mode "1024x600"
#    geometry 1024 600 1024 600 16
#    timings 0 0 0 0 0 0 0
#    accel true
#    rgba 5/11,6/5,5/0,0/0
# endmode
#
w, h = 1024, 600

# Create a pure black RGB888 image same size as framebuffer
# then a red, green and blue one
blk = RGB888toRGB565( np.zeros((h,w,3), np.uint8) )
red = RGB888toRGB565( np.full((h,w,3), [255,0,0], np.uint8) )
grn = RGB888toRGB565( np.full((h,w,3), [0,255,0], np.uint8) )
blu = RGB888toRGB565( np.full((h,w,3), [0,0,255], np.uint8) )

# Open the framebuffer
with open('/dev/fb0', 'wb') as fb:
    fb.seek(0)
    fb.write(blk.tobytes())  # send black frame
    time.sleep(0.5)
    fb.seek(0)
    fb.write(red.tobytes())  # send red frame
    time.sleep(0.5)
    fb.seek(0)
    fb.write(grn.tobytes())  # send green frame
    time.sleep(0.5)
    fb.seek(0)
    fb.write(blu.tobytes())  # send blue frame
    time.sleep(0.5)

Obviously the code can be refactored to be tidier, but I wanted to just demonstrate the techniques rather than confuse anyone with super-compact, efficient, non-repetitive tricky code.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432