Rendering images

I’ve noticed several threads asking about uploading images – for poi, POV, and general display – each of which dried up soon after someone said “I’ll write something to do images and post it here”.

I got tired of waiting so I wrote a little python program to read an image and output a pattern to the console:

import argparse, os
from PIL import Image

# Parse command line.
parser = argparse.ArgumentParser()
parser.add_argument("pictureFile", help="The picture to be converted")
args = parser.parse_args()

#################
# TBD: PARSE COMMANDLINE ARGUMENTS TO SUPPORT MULTIPLE IMAGE FILES:
numImages = 1
#################

# Generate pattern code
print("//  Image helpers")
print("function numImages() { return _images.length; }")
print("function numFrames(image) { return _images[image].length; }")
print("function numRows(image, frame) { return _images[image][frame].length; }")
print("function numColumns(image, frame) { return _images[image][frame][0].length; }")

print("//  Pixel helpers")
print("function putPixel(image, frame, row, column, value) { _images[image][frame][row][column] = value; }")
print("function getPixel(image, frame, row, column) { return _images[image][frame][row][column]; }")
print("function packRGBA(r, g, b, a) { return (r << 8) + g + (b >> 8) + (a >> 16); }")
print("function _r(pixval) { return ((pixval >> 8) & 0xff) >> 8; }")
print("function _g(pixval) { return ((pixval) & 0xff) >> 8; }")
print("function _b(pixval) { return ((pixval << 8) & 0xff) >> 8; }")
print("function _a(pixval) { return ((pixval << 16) & 0xff) >> 8; }")
print("// Rendering functions")
print("export function render2D(index, x, y) {")
print("  dimension = sqrt(pixelCount); row = trunc(x * dimension); col = trunc(y * dimension);")
print("  pixval = getPixel(0, 0, row, col); rgb(_r(pixval), _g(pixval), _b(pixval));")
print("}")

# Allocate arrays for image data
print("//  Image storage")
print("var _images = array(%u); //  allocate image storage" % numImages)
for iterImage in range(numImages):

    # read image and add its bitmaps to the pattern
    filename, extension = os.path.splitext(args.pictureFile)
    with Image.open(args.pictureFile, mode='r').convert('RGB') as im:
        # load the image so we can access it....is this necessary or will the other calls trigger a lazy-load?
        pixels = im.load()
        print("// Image #%u (%ux%u) generated from '%s'" % \ 
           (iterImage, im.width, im.height, args.pictureFile))
        # Process frames
        imageFrames = 1
        if getattr(im, "is_animated", False):
            imageFrames = im.n_frames
        print("  //getImage(%u) = array(%u); // allocate frame storage" % (iterImage, imageFrames))
        print("  _images[%u] = array(%u); // allocate frame storage" % (iterImage, imageFrames))

        # Dump image frame(s)
        for iterFrame in range(imageFrames):
            print("    // Frame #%u" % iterFrame)
            print("    _images[%u][%u] = array(%u); // allocate row storage" % (iterImage, iterFrame, im.height))

            # Dump image pixels
            for iterRow in range(im.height):
                print("      // Row #%u: " % iterRow)
                print("      _images[%u][%u][%u] = array(%u); // allocate column storage" % (iterImage, iterFrame, iterRow, im.width))
                for iterCol in range(im.width):
                    _r, _g, _b = im.getpixel((iterCol, iterRow))
                    _a = 0 // TBD: figure out where transparency is stored
                    numPixelsPerTextRow = 3
                    if iterCol % numPixelsPerTextRow == 0:
                        print('      ', end='')
                    print("putPixel(%u,%u,%u,%u,packRGBA(0x%02x,0x%02x,0x%02x,0x%02x)); " % (iterImage, iterFrame, iterRow, iterCol, _r, _g, _b, _a), end='')
                    if iterCol % numPixelsPerTextRow == (numPixelsPerTextRow - 1):
                        print('')

                # Finish this row.
                print("      // end Row #%u" % iterRow)

            # Finish this frame.
            print("    // end Frame #%u" % iterFrame)

        # Finish this image.
        print("  // end Image #%u" % iterImage)

    im.close()

You can pipe the result to a text file, add your own touches to the render functions, and then paste it into the Pixelblaze editor.

I’ve been playing around with displaying small images like icons on a 16x16 panel of WS2812s (for an example, see the pattern called “Rainbow Smiley” in the pattern library), but I’ve been very underwhelmed with most of the results because the color gamut of the LEDs seems very small – anything more complicated than a 16-color bitmap ends up a muddy mess.

I know that LEDs are not LCDs, and additive color is not subtractive color, and color rendition depends on the global brightness level…but the existence of things like DiamondVision and deadmau5’ cube makes me think that it ought to be possible to render a few more shades of color than I’m getting.

Any ideas how I can get more shades – gamma correction, a different LED type, …?

3 Likes

Looking over the code, it looks like you just forgot to divide the rgb values by 255 when you unpack them, so they’ll be in the 0…1 range that Pixelblaze likes. Also, a little gamma correction helps.

Try changing your render2D function to do this, and see if it helps:
(A few minutes later: I just tried this with a fragment of a photograph – it actually looks fine, given the 16x16 resolution.)

dimension = sqrt(pixelCount);

export function render2D(index, x, y) {
 row = trunc(x * dimension); col = trunc(y * dimension);
 
 pixval = getPixel(0, 0, row, col); 
 r = _r(pixval)/255; 
 g = _g(pixval)/255;
 b = _b(pixval)/255;
 
 rgb(r*r,g*g,b*b);
}
3 Likes

Pair programming to the rescue — that looks much better.

Thanks @zranger1!

1 Like

Thanks for doing this! It’s a super useful tool to have, and I’m glad I could help a bit.

1 Like

any advice on how to adapt the generated pattern for a 1D POV like a poi/staff? The generated code only has render 2d. Thanks in advance!

I would start with incrementing one dimension every frame. You could also do this based on time or accelerometer readings.

This first step would be to have a global variable (persisting between frames) col = 0;
then instead of col = trunc(y * dimension); in render() do col = (col+1) % dimension; in beforeRender().

For poi you want it to run as fast as possible, so (if the above works) precalculate everything (unpacking pixels, dividing by 255, and squaring) into a multi-dimensional array shaped like pixels[columns][rows][r,g,b] and then in beforeRender() look up the column subarray and set row = 0; then render() is only pixel = column[row]; row += 1; rgb(pixel[0],pixel[1],pixel[2]);

having a ton of issues, I’ll try to post as much detail as possible.

py -3 .\0-original.py .\logo.png

File "C:\Users\anthony\source\github\PixelBlaze\PixelBlaze\Patterns\POV Patterns\renderingImages\py\0-original.py", line 45
	print("// Image #%u (%ux%u) generated from '%s'" % \															^
SyntaxError: unexpected character after line continuation character
	
	resolution: remove the backslash, and put these two lines all on one line of code

File "C:\Users\anthony\source\github\PixelBlaze\PixelBlaze\Patterns\POV Patterns\renderingImages\py\0-original.py", line 64
_a = 0 // TBD: figure out where transparency is stored
            ^
SyntaxError: invalid syntax

	resolution: remove the //TBD etc.

 File "C:\Users\anthony\source\github\PixelBlaze\PixelBlaze\Patterns\POV Patterns\renderingImages\py\0-original.py", line 2, in <module>
from PIL import Image
ModuleNotFoundError: No module named 'PIL'

PIL is for python 2, I'm using python 3 (which should use PILLOW). Switch to python 2

py -2 .\0-original.py .\logo.png

File ".\0-original.py", line 67
print('      ', end='')
                   ^
SyntaxError: invalid syntax


https://stackoverflow.com/questions/2456148/getting-syntaxerror-for-print-with-keyword-argument-end
The correct idiom in Python 2.x for end=" " is:
print "foo" % bar,
(note the final comma, this makes it end the line with a space rather than a linebreak)

resolution: 
	change	
		print('      ', end='')
	to 
		print '      ',


File ".\1-edited.py", line 68
print("putPixel(%u,%u,%u,%u,packRGBA(0x%02x,0x%02x,0x%02x,0x%02x)); " % (iterImage, iterFrame, iterRow, iterCol, _r, _g, _b, _a), end='')
                                                                                                                                     ^
SyntaxError: invalid syntax	

	resolution: repeat above

py -2 .\1-edited.py .\logo.png
now outputs code to screen

py -2 .\1-edited.py .\logo.png > code.js

Paste code.js into Edit tab of PixelBlaze
no valid render function found
makes sense, this pattern only has render2D, my poi is 1d

replace rendering functions section with code below
// Rendering functions

var col = 0;
dimension = sqrt(pixelCount);
export function beforeRender(delta){
  col = (col+1) % dimension;
}


export function render(index) {
 row = trunc(index * dimension); 

 
 pixval = getPixel(0, 0, row, col); 
 r = _r(pixval)/255; 
 g = _g(pixval)/255;
 b = _b(pixval)/255;
 
 rgb(r*r,g*g,b*b);
}


export function render2D(index, x, y) {
 row = trunc(x * dimension); 
 col = trunc(y * dimension);
 
 pixval = getPixel(0, 0, row, col); 
 r = _r(pixval)/255; 
 g = _g(pixval)/255;
 b = _b(pixval)/255;
 
 rgb(r*r,g*g,b*b);
}

//  Image storage

no valid render function found

thinking at this point maybe I had too many pixels in my 50px50px bitmap logo for pixelblaze to parse, I reduced the image size to 10px10px

py -2 .\1-edited.py .\logo_10px.png > code_10px.js
paste code_10px.js into pixelblaze edit tab

error: array out of bounds
at line: function getPixel(image, frame, row, column) { return _images[image][frame][row][column]; }

Here’s my entire code file:

//  Image helpers
function numImages() { return _images.length; }
function numFrames(image) { return _images[image].length; }
function numRows(image, frame) { return _images[image][frame].length; }
function numColumns(image, frame) { return _images[image][frame][0].length; }
//  Pixel helpers
function putPixel(image, frame, row, column, value) { _images[image][frame][row][column] = value; }
function getPixel(image, frame, row, column) { return _images[image][frame][row][column]; }
function packRGBA(r, g, b, a) { return (r << 8) + g + (b >> 8) + (a >> 16); }
function _r(pixval) { return ((pixval >> 8) & 0xff) >> 8; }
function _g(pixval) { return ((pixval) & 0xff) >> 8; }
function _b(pixval) { return ((pixval << 8) & 0xff) >> 8; }
function _a(pixval) { return ((pixval << 16) & 0xff) >> 8; }
// Rendering functions

var col = 0;
dimension = sqrt(pixelCount);
export function beforeRender(delta){
  col = (col+1) % dimension;
}


export function render(index) {
 row = trunc(index * dimension); 

 
 pixval = getPixel(0, 0, row, col); 
 r = _r(pixval)/255; 
 g = _g(pixval)/255;
 b = _b(pixval)/255;
 
 rgb(r*r,g*g,b*b);
}


export function render2D(index, x, y) {
 row = trunc(x * dimension); 
 col = trunc(y * dimension);
 
 pixval = getPixel(0, 0, row, col); 
 r = _r(pixval)/255; 
 g = _g(pixval)/255;
 b = _b(pixval)/255;
 
 rgb(r*r,g*g,b*b);
}
//  Image storage
var _images = array(1); //  allocate image storage
// Image #0 (10x10) generated from '.\logo_small.png'
  //getImage(0) = array(1); // allocate frame storage
  _images[0] = array(1); // allocate frame storage
    // Frame #0
    _images[0][0] = array(10); // allocate row storage
      // Row #0: 
      _images[0][0][0] = array(10); // allocate column storage
       putPixel(0,0,0,0,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,0,1,packRGBA(0xc1,0x19,0x20,0x00));  putPixel(0,0,0,2,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,0,3,packRGBA(0x3d,0x09,0x0b,0x00));  putPixel(0,0,0,4,packRGBA(0xd3,0x1c,0x24,0x00));  putPixel(0,0,0,5,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,0,6,packRGBA(0x6d,0x0e,0x12,0x00));  putPixel(0,0,0,7,packRGBA(0x54,0x0b,0x0e,0x00));  putPixel(0,0,0,8,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,0,9,packRGBA(0x10,0x0a,0x00,0x00));        // end Row #0
      // Row #1: 
      _images[0][0][1] = array(10); // allocate column storage
       putPixel(0,0,1,0,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,1,1,packRGBA(0x1e,0x04,0x05,0x00));  putPixel(0,0,1,2,packRGBA(0xa6,0x16,0x1c,0x00));  
       putPixel(0,0,1,3,packRGBA(0xae,0x16,0x1c,0x00));  putPixel(0,0,1,4,packRGBA(0x87,0x12,0x17,0x00));  putPixel(0,0,1,5,packRGBA(0x69,0x0d,0x11,0x00));  
       putPixel(0,0,1,6,packRGBA(0xb8,0x18,0x1e,0x00));  putPixel(0,0,1,7,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,1,8,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,1,9,packRGBA(0x00,0x00,0x00,0x00));        // end Row #1
      // Row #2: 
      _images[0][0][2] = array(10); // allocate column storage
       putPixel(0,0,2,0,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,2,1,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,2,2,packRGBA(0x9a,0x13,0x19,0x00));  
       putPixel(0,0,2,3,packRGBA(0x77,0x10,0x14,0x00));  putPixel(0,0,2,4,packRGBA(0xe1,0x1c,0x24,0x00));  putPixel(0,0,2,5,packRGBA(0xae,0x17,0x1d,0x00));  
       putPixel(0,0,2,6,packRGBA(0x1b,0x03,0x04,0x00));  putPixel(0,0,2,7,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,2,8,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,2,9,packRGBA(0x00,0x00,0x00,0x00));        // end Row #2
      // Row #3: 
      _images[0][0][3] = array(10); // allocate column storage
       putPixel(0,0,3,0,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,3,1,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,3,2,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,3,3,packRGBA(0xbb,0x17,0x1f,0x00));  putPixel(0,0,3,4,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,3,5,packRGBA(0xb2,0x16,0x1d,0x00));  
       putPixel(0,0,3,6,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,3,7,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,3,8,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,3,9,packRGBA(0x00,0x00,0x00,0x00));        // end Row #3
      // Row #4: 
      _images[0][0][4] = array(10); // allocate column storage
       putPixel(0,0,4,0,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,4,1,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,4,2,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,4,3,packRGBA(0x8e,0x12,0x17,0x00));  putPixel(0,0,4,4,packRGBA(0x5a,0x0b,0x0f,0x00));  putPixel(0,0,4,5,packRGBA(0x36,0x07,0x09,0x00));  
       putPixel(0,0,4,6,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,4,7,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,4,8,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,4,9,packRGBA(0x00,0x00,0x00,0x00));        // end Row #4
      // Row #5: 
      _images[0][0][5] = array(10); // allocate column storage
       putPixel(0,0,5,0,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,5,1,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,5,2,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,5,3,packRGBA(0x89,0x11,0x16,0x00));  putPixel(0,0,5,4,packRGBA(0x61,0x0d,0x0f,0x00));  putPixel(0,0,5,5,packRGBA(0x32,0x06,0x08,0x00));  
       putPixel(0,0,5,6,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,5,7,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,5,8,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,5,9,packRGBA(0x00,0x00,0x00,0x00));        // end Row #5
      // Row #6: 
      _images[0][0][6] = array(10); // allocate column storage
       putPixel(0,0,6,0,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,6,1,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,6,2,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,6,3,packRGBA(0xb5,0x16,0x1d,0x00));  putPixel(0,0,6,4,packRGBA(0x17,0x03,0x04,0x00));  putPixel(0,0,6,5,packRGBA(0x7d,0x0f,0x13,0x00));  
       putPixel(0,0,6,6,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,6,7,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,6,8,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,6,9,packRGBA(0x00,0x00,0x00,0x00));        // end Row #6
      // Row #7: 
      _images[0][0][7] = array(10); // allocate column storage
       putPixel(0,0,7,0,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,7,1,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,7,2,packRGBA(0x10,0x02,0x02,0x00));  
       putPixel(0,0,7,3,packRGBA(0xad,0x15,0x1d,0x00));  putPixel(0,0,7,4,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,7,5,packRGBA(0xd0,0x1a,0x22,0x00));  
       putPixel(0,0,7,6,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,7,7,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,7,8,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,7,9,packRGBA(0x00,0x00,0x00,0x00));        // end Row #7
      // Row #8: 
      _images[0][0][8] = array(10); // allocate column storage
       putPixel(0,0,8,0,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,8,1,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,8,2,packRGBA(0x9c,0x13,0x19,0x00));  
       putPixel(0,0,8,3,packRGBA(0x39,0x08,0x0a,0x00));  putPixel(0,0,8,4,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,8,5,packRGBA(0xbc,0x18,0x20,0x00));  
       putPixel(0,0,8,6,packRGBA(0x1b,0x03,0x04,0x00));  putPixel(0,0,8,7,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,8,8,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,8,9,packRGBA(0x00,0x00,0x00,0x00));        // end Row #8
      // Row #9: 
      _images[0][0][9] = array(10); // allocate column storage
       putPixel(0,0,9,0,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,9,1,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,9,2,packRGBA(0xc1,0x19,0x21,0x00));  
       putPixel(0,0,9,3,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,9,4,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,9,5,packRGBA(0x30,0x06,0x08,0x00));  
       putPixel(0,0,9,6,packRGBA(0x7e,0x10,0x15,0x00));  putPixel(0,0,9,7,packRGBA(0x00,0x00,0x00,0x00));  putPixel(0,0,9,8,packRGBA(0x00,0x00,0x00,0x00));  
       putPixel(0,0,9,9,packRGBA(0x00,0x00,0x00,0x00));        // end Row #9
    // end Frame #0
  // end Image #0

This is a blast from the past! Here’s what got it going again for me:

  • Go back to Python 3. I just tried it with my normal setup, using Python 3.10.6 and Pillow 9.2.0, and the from PIL import Image statement works just fine. (I think they deliberately allow you to do that to maintain compatibility with the old version of PIL.)

  • In beforeRender(), I think col = (col+1) % dimension; will skip the first column unless you initialize col to -1 instead of 0.

  • in your render() function, if pixelCount is the number of pixels in a row, try
    row = trunc(index/pixelCount * (dimension - 1)) instead of (index * dimension).

Here's a version of the old pasted code with the syntax errors fixed:
import argparse, os
from PIL import Image

# Parse command line.
parser = argparse.ArgumentParser()
parser.add_argument("pictureFile", help="The picture to be converted")
args = parser.parse_args()

#################
# TBD: PARSE COMMANDLINE ARGUMENTS TO SUPPORT MULTIPLE IMAGE FILES:
numImages = 1
#################

# Generate pattern code
print("//  Image helpers")
print("function numImages() { return _images.length; }")
print("function numFrames(image) { return _images[image].length; }")
print("function numRows(image, frame) { return _images[image][frame].length; }")
print("function numColumns(image, frame) { return _images[image][frame][0].length; }")

print("//  Pixel helpers")
print("function putPixel(image, frame, row, column, value) { _images[image][frame][row][column] = value; }")
print("function getPixel(image, frame, row, column) { return _images[image][frame][row][column]; }")
print("function packRGBA(r, g, b, a) { return (r << 8) + g + (b >> 8) + (a >> 16); }")
print("function _r(pixval) { return ((pixval >> 8) & 0xff) >> 8; }")
print("function _g(pixval) { return ((pixval) & 0xff) >> 8; }")
print("function _b(pixval) { return ((pixval << 8) & 0xff) >> 8; }")
print("function _a(pixval) { return ((pixval << 16) & 0xff) >> 8; }")
print("// Rendering functions")
print("export function render2D(index, x, y) {")
print("  dimension = sqrt(pixelCount); row = trunc(x * dimension); col = trunc(y * dimension);")
print("  pixval = getPixel(0, 0, row, col); rgb(_r(pixval), _g(pixval), _b(pixval));")
print("}")

# Allocate arrays for image data
print("//  Image storage")
print("var _images = array(%u); //  allocate image storage" % numImages)
for iterImage in range(numImages):

    # read image and add its bitmaps to the pattern
    filename, extension = os.path.splitext(args.pictureFile)
    with Image.open(args.pictureFile, mode='r').convert('RGB') as im:
        # load the image so we can access it....is this necessary or will the other calls trigger a lazy-load?
        pixels = im.load()
        print("// Image #%u (%ux%u) generated from '%s'" % (iterImage, im.width, im.height, args.pictureFile))
        # Process frames
        imageFrames = 1
        if getattr(im, "is_animated", False):
            imageFrames = im.n_frames
        print("  //getImage(%u) = array(%u); // allocate frame storage" % (iterImage, imageFrames))
        print("  _images[%u] = array(%u); // allocate frame storage" % (iterImage, imageFrames))

        # Dump image frame(s)
        for iterFrame in range(imageFrames):
            print("    // Frame #%u" % iterFrame)
            print("    _images[%u][%u] = array(%u); // allocate row storage" % (iterImage, iterFrame, im.height))

            # Dump image pixels
            for iterRow in range(im.height):
                print("      // Row #%u: " % iterRow)
                print("      _images[%u][%u][%u] = array(%u); // allocate column storage" % (iterImage, iterFrame, iterRow, im.width))
                for iterCol in range(im.width):
                    _r, _g, _b = im.getpixel((iterCol, iterRow))
                    _a = 0 # TBD: figure out where transparency is stored
                    numPixelsPerTextRow = 3
                    if iterCol % numPixelsPerTextRow == 0:
                        print('      ', end='')
                    print("putPixel(%u,%u,%u,%u,packRGBA(0x%02x,0x%02x,0x%02x,0x%02x)); " % (iterImage, iterFrame, iterRow, iterCol, _r, _g, _b, _a), end='')
                    if iterCol % numPixelsPerTextRow == (numPixelsPerTextRow - 1):
                        print('')

                # Finish this row.
                print("      // end Row #%u" % iterRow)

            # Finish this frame.
            print("    // end Frame #%u" % iterFrame)

        # Finish this image.
        print("  // end Image #%u" % iterImage)

    im.close()
1 Like

Pattern is live, but no lights are blinking.
Just to clarify, this is one single strip of 50 pixels, so I would think column should auto-increment until _images[0][0].length and then loop back around. I don’t understand why dimension is sqrt(pixelCount)


//  Image helpers
function numImages() { return _images.length; }
function numFrames(image) { return _images[image].length; }
function numRows(image, frame) { return _images[image][frame].length; }
function numColumns(image, frame) { return _images[image][frame][0].length; }
//  Pixel helpers
function putPixel(image, frame, row, column, value) { _images[image][frame][row][column] = value; }
function getPixel(image, frame, row, column) { return _images[image][frame][row][column]; }
function packRGBA(r, g, b, a) { return (r << 8) + g + (b >> 8) + (a >> 16); }
function _r(pixval) { return ((pixval >> 8) & 0xff) >> 8; }
function _g(pixval) { return ((pixval) & 0xff) >> 8; }
function _b(pixval) { return ((pixval << 8) & 0xff) >> 8; }
function _a(pixval) { return ((pixval << 16) & 0xff) >> 8; }
// Rendering functions
export var col = -1;
export var row = 0;
dimension = sqrt(pixelCount);
export function beforeRender(delta){
  col = (col+1) % dimension;
}
export function render(index) {
  row = trunc(index/pixelCount * (dimension - 1));
  pixval = getPixel(0, 0, row, col);
  r = _r(pixval)/255;
  g = _g(pixval)/255;
  b = _b(pixval)/255;
  rgb(r*r,g*g,b*b);
}
export function render2D(index, x, y) {
  row = trunc(x * dimension);
  col = trunc(y * dimension);
  pixval = getPixel(0, 0, row, col);
  r = _r(pixval)/255;
  g = _g(pixval)/255;
  b = _b(pixval)/255;
  rgb(r*r,g*g,b*b);
}
//  Image storage
var _images = array(1); //  allocate image storage
// Image #0 (10x10) generated from '.\logo_small.png'
  //getImage(0) = array(1); // allocate frame storage
  _images[0] = array(1); // allocate frame storage
    // Frame #0
    _images[0][0] = array(10); // allocate row storage
      // Row #0: 
      _images[0][0][0] = array(10); // allocate column storage
      putPixel(0,0,0,0,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,0,1,packRGBA(0xc1,0x19,0x20,0x00)); putPixel(0,0,0,2,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,0,3,packRGBA(0x3d,0x09,0x0b,0x00)); putPixel(0,0,0,4,packRGBA(0xd3,0x1c,0x24,0x00)); putPixel(0,0,0,5,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,0,6,packRGBA(0x6d,0x0e,0x12,0x00)); putPixel(0,0,0,7,packRGBA(0x54,0x0b,0x0e,0x00)); putPixel(0,0,0,8,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,0,9,packRGBA(0x10,0x0a,0x00,0x00));       // end Row #0
      // Row #1: 
      _images[0][0][1] = array(10); // allocate column storage
      putPixel(0,0,1,0,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,1,1,packRGBA(0x1e,0x04,0x05,0x00)); putPixel(0,0,1,2,packRGBA(0xa6,0x16,0x1c,0x00)); 
      putPixel(0,0,1,3,packRGBA(0xae,0x16,0x1c,0x00)); putPixel(0,0,1,4,packRGBA(0x87,0x12,0x17,0x00)); putPixel(0,0,1,5,packRGBA(0x69,0x0d,0x11,0x00)); 
      putPixel(0,0,1,6,packRGBA(0xb8,0x18,0x1e,0x00)); putPixel(0,0,1,7,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,1,8,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,1,9,packRGBA(0x00,0x00,0x00,0x00));       // end Row #1
      // Row #2: 
      _images[0][0][2] = array(10); // allocate column storage
      putPixel(0,0,2,0,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,2,1,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,2,2,packRGBA(0x9a,0x13,0x19,0x00)); 
      putPixel(0,0,2,3,packRGBA(0x77,0x10,0x14,0x00)); putPixel(0,0,2,4,packRGBA(0xe1,0x1c,0x24,0x00)); putPixel(0,0,2,5,packRGBA(0xae,0x17,0x1d,0x00)); 
      putPixel(0,0,2,6,packRGBA(0x1b,0x03,0x04,0x00)); putPixel(0,0,2,7,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,2,8,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,2,9,packRGBA(0x00,0x00,0x00,0x00));       // end Row #2
      // Row #3: 
      _images[0][0][3] = array(10); // allocate column storage
      putPixel(0,0,3,0,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,3,1,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,3,2,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,3,3,packRGBA(0xbb,0x17,0x1f,0x00)); putPixel(0,0,3,4,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,3,5,packRGBA(0xb2,0x16,0x1d,0x00)); 
      putPixel(0,0,3,6,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,3,7,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,3,8,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,3,9,packRGBA(0x00,0x00,0x00,0x00));       // end Row #3
      // Row #4: 
      _images[0][0][4] = array(10); // allocate column storage
      putPixel(0,0,4,0,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,4,1,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,4,2,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,4,3,packRGBA(0x8e,0x12,0x17,0x00)); putPixel(0,0,4,4,packRGBA(0x5a,0x0b,0x0f,0x00)); putPixel(0,0,4,5,packRGBA(0x36,0x07,0x09,0x00)); 
      putPixel(0,0,4,6,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,4,7,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,4,8,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,4,9,packRGBA(0x00,0x00,0x00,0x00));       // end Row #4
      // Row #5: 
      _images[0][0][5] = array(10); // allocate column storage
      putPixel(0,0,5,0,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,5,1,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,5,2,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,5,3,packRGBA(0x89,0x11,0x16,0x00)); putPixel(0,0,5,4,packRGBA(0x61,0x0d,0x0f,0x00)); putPixel(0,0,5,5,packRGBA(0x32,0x06,0x08,0x00)); 
      putPixel(0,0,5,6,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,5,7,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,5,8,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,5,9,packRGBA(0x00,0x00,0x00,0x00));       // end Row #5
      // Row #6: 
      _images[0][0][6] = array(10); // allocate column storage
      putPixel(0,0,6,0,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,6,1,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,6,2,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,6,3,packRGBA(0xb5,0x16,0x1d,0x00)); putPixel(0,0,6,4,packRGBA(0x17,0x03,0x04,0x00)); putPixel(0,0,6,5,packRGBA(0x7d,0x0f,0x13,0x00)); 
      putPixel(0,0,6,6,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,6,7,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,6,8,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,6,9,packRGBA(0x00,0x00,0x00,0x00));       // end Row #6
      // Row #7: 
      _images[0][0][7] = array(10); // allocate column storage
      putPixel(0,0,7,0,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,7,1,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,7,2,packRGBA(0x10,0x02,0x02,0x00)); 
      putPixel(0,0,7,3,packRGBA(0xad,0x15,0x1d,0x00)); putPixel(0,0,7,4,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,7,5,packRGBA(0xd0,0x1a,0x22,0x00)); 
      putPixel(0,0,7,6,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,7,7,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,7,8,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,7,9,packRGBA(0x00,0x00,0x00,0x00));       // end Row #7
      // Row #8: 
      _images[0][0][8] = array(10); // allocate column storage
      putPixel(0,0,8,0,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,8,1,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,8,2,packRGBA(0x9c,0x13,0x19,0x00)); 
      putPixel(0,0,8,3,packRGBA(0x39,0x08,0x0a,0x00)); putPixel(0,0,8,4,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,8,5,packRGBA(0xbc,0x18,0x20,0x00)); 
      putPixel(0,0,8,6,packRGBA(0x1b,0x03,0x04,0x00)); putPixel(0,0,8,7,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,8,8,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,8,9,packRGBA(0x00,0x00,0x00,0x00));       // end Row #8
      // Row #9: 
      _images[0][0][9] = array(10); // allocate column storage
      putPixel(0,0,9,0,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,9,1,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,9,2,packRGBA(0xc1,0x19,0x21,0x00)); 
      putPixel(0,0,9,3,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,9,4,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,9,5,packRGBA(0x30,0x06,0x08,0x00)); 
      putPixel(0,0,9,6,packRGBA(0x7e,0x10,0x15,0x00)); putPixel(0,0,9,7,packRGBA(0x00,0x00,0x00,0x00)); putPixel(0,0,9,8,packRGBA(0x00,0x00,0x00,0x00)); 
      putPixel(0,0,9,9,packRGBA(0x00,0x00,0x00,0x00));       // end Row #9
    // end Frame #0
  // end Image #0


oh how are you posting code in that expandable/collapsible section?

@sunandmooncoture, I’ll take a little time tomorrow to give this code an update that’ll make it more reasonable to use.

dimension is set to sqrt(pixelCount) because the code was originally set up to run on square matrices. In this case, for now just change it to 10, or whatever the dimensions of your input bitmap happen to be.

And, I stared at it for a while before realizing… you’re not seeing any pixels because the original posted code has a duplicate fix for a bug that we discussed (And fixed. Apparently twice!) in the original thread.

When retrieving pixels, it needed to divide by 255 to convert the color values to the Pixelblaze’s (0,1) range. The original fix (simply dividing by 255) was put in render(), and somehow, later the posted version got another divide by 255 (by way of shifting 8 pixels to the right) put in getPixel().

For now, the immediate way to get it working is to take the /255s out of this section of render()/render2D. e.g this,

  r = _r(pixval)/255;
  g = _g(pixval)/255;
  b = _b(pixval)/255;

should be

  r = _r(pixval);
  g = _g(pixval);
  b = _b(pixval);

Once that’s fixed, with only 50 pixels you’re probably still going to want to be able to control the speed at which it changes columns. I’d suggest something like this for your beforeRender()

var colTimer = 0;
var col = 0;
var row;
export function beforeRender(delta){
  row = 0;
  colTimer += delta
  if (colTimer > 16.6667) {   // ms between col changes
    col = (col+1) % dimension;
    colTimer = 0;
  }
}

A delay of 16.6667 ms will change columns 60 times/second. You can tune this so it matches the desired speed of movement for a POV staff or wand.

To put code (or anything else) in a collapsible section:

  • click the gear icon in the message editor toolbar and select “Hide Details”
  • this will put a tagged section [details="Summary"],etc. in your message
  • paste your code/text into the “This text will be hidden” section
  • change “Summary” to the title you want for the collapsible section

There’s probably a shorter shortcut, but the somewhat clunky obvious way works very nicely.

Well, I got sidetracked playing with brightly lit Halloween costumes, so this took a little longer than I’d thought. But here’s a slightly modified version of @pixie’s image generation code, with a 1D renderer to make setup for POV wands and staves a little easier.

It takes the same parameters and supports the same image types as the original. The pattern behavior is slightly changed:

  • on a 2D display, it stretches (or squashes) the image to fit the LED display, ignoring aspect ratio.
  • on a 1D display, it displays each column of the image in succession. Speed is controlled by a UI slider, which controls how long each column will be displayed (milliseconds from 0 to 500.) Each image column is stretched (or squashed) to fill the available LEDs.

Give this a try when you get a chance, and let me know what other features might be helpful!

click here for Python 3 code!
import argparse, os
from PIL import Image

# Parse command line.
parser = argparse.ArgumentParser()
parser.add_argument("pictureFile", help="The picture to be converted")
args = parser.parse_args()

#################
# TBD: PARSE COMMANDLINE ARGUMENTS TO SUPPORT MULTIPLE IMAGE FILES:
numImages = 1
#################

# Generate pattern code
print("//  Image helpers")
print("function numImages() { return _images.length; }")
print("function numFrames(image) { return _images[image].length; }")
print("function numRows(image, frame) { return _images[image][frame].length; }")
print("function numColumns(image, frame) { return _images[image][frame][0].length; }")
print("")
print("//  Pixel helpers")
print("function putPixel(image, frame, row, column, value) { _images[image][frame][row][column] = value; }")
print("function getPixel(image, frame, row, column) { return _images[image][frame][row][column]; }")
print("function packRGBA(r, g, b, a) { return (r << 8) + g + (b >> 8) + (a >> 16); }")
print("function _r(pixval) { return ((pixval >> 8) & 0xff) >> 8; }")
print("function _g(pixval) { return ((pixval) & 0xff) >> 8; }")
print("function _b(pixval) { return ((pixval << 8) & 0xff) >> 8; }")
print("function _a(pixval) { return ((pixval << 16) & 0xff) >> 8; }")
print("")
print("// UI controls")
print("export function sliderSpeed(v) {")
print("  colDelay = v * v * 500;")
print("}")
print("")
print("// Rendering functions")
print("export var col = 0;")
print("var colTimer = 0;")
print("export var colDelay = 16.6667")
print("export function beforeRender(delta) {")
print("  colTimer += delta;")
print("  if (colTimer >= colDelay) {")
print("    col = (col + 1) % numColumns(0,0);")
print("    colTimer = 0;")
print("  }")
print("}")
print("")
print("export function render2D(index, x, y) {")
print("  row = x * numRows(0,0); col = y * numColumns(0,0);")
print("  pixval = getPixel(0, 0, row, col);")
print("  rgb(_r(pixval), _g(pixval), _b(pixval));")
print("}")
print("")
print("export function render(index,x) {")
print("  row = (index/pixelCount) * (numRows(0,0))")
print("  pixval = getPixel(0, 0, row, col);")
print("  rgb(_r(pixval), _g(pixval), _b(pixval));  ")
print("}")
print("")

# Allocate arrays for image data
print("//  Image storage")
print("var _images = array(%u); //  allocate image storage" % numImages)
for iterImage in range(numImages):

    # read image and add its bitmaps to the pattern
    filename, extension = os.path.splitext(args.pictureFile)
    with Image.open(args.pictureFile, mode='r').convert('RGB') as im:
        # load the image so we can access it....is this necessary or will the other calls trigger a lazy-load?
        pixels = im.load()
        print("// Image #%u (%ux%u) generated from '%s'" % (iterImage, im.width, im.height, args.pictureFile))
        # Process frames
        imageFrames = 1
        if getattr(im, "is_animated", False):
            imageFrames = im.n_frames
        print("  //getImage(%u) = array(%u); // allocate frame storage" % (iterImage, imageFrames))
        print("  _images[%u] = array(%u); // allocate frame storage" % (iterImage, imageFrames))

        # Dump image frame(s)
        for iterFrame in range(imageFrames):
            print("    // Frame #%u" % iterFrame)
            print("    _images[%u][%u] = array(%u); // allocate row storage" % (iterImage, iterFrame, im.height))

            # Dump image pixels
            for iterRow in range(im.height):
                print("      // Row #%u: " % iterRow)
                print("      _images[%u][%u][%u] = array(%u); // allocate column storage" % (iterImage, iterFrame, iterRow, im.width))
                for iterCol in range(im.width):
                    _r, _g, _b = im.getpixel((iterCol, iterRow))
                    _a = 0 # TBD: figure out where transparency is stored
                    numPixelsPerTextRow = 3
                    if iterCol % numPixelsPerTextRow == 0:
                        print('      ', end='')
                    print("putPixel(%u,%u,%u,%u,packRGBA(0x%02x,0x%02x,0x%02x,0x%02x)); " % (iterImage, iterFrame, iterRow, iterCol, _r, _g, _b, _a), end='')
                    if iterCol % numPixelsPerTextRow == (numPixelsPerTextRow - 1):
                        print('')

                # Finish this row.
                print("      // end Row #%u" % iterRow)

            # Finish this frame.
            print("    // end Frame #%u" % iterFrame)

        # Finish this image.
        print("  // end Image #%u" % iterImage)

    im.close()
1 Like

Success! The latest code is working, thanks so much for getting me this far!
Here’s the image I ran through the new python processor:

logo50px

Here’s the result on the poi:

You can see the image a little more clearly in the TV reflection. Capturing POV with cell phones is hard!

Couple things to note: the color looks a bit washed out. Like more pink than the original red image. I also started seeing a lot of this error when the UI was open:

“The controller has restarted from repeated errors and has loaded fail-safe settings. Proceed with caution - using this interface may persist fail-safe settings.”

Next, I tried the same thing with the full size rainbow image:

logo50px311px

And at first the PixelBlaze just froze. I figured the pattern file size was too large for pixel blaze. The js for 50*311px came out to about 829 Kb.
I had to reboot, back it up, delete all the other patterns, and add just this one back.

And now I’m getting the error “no valid render function” even though the render function was the same.

Have I reached the limit of storage capacity on PixelBlaze? It says 935K free before I paste in the 829k so it’s close but thought it would be okay. Maybe it’s a RAM issue not storage? Anyone know how much RAM the PixelBlaze has?

Would buying the XL fix this? Even if so, the standard PB doesn’t fit in a poi tube.

Will the Pico get an XL upgrade also? @wizard

For the record, I tried this using 36px on a TrinketM0 and got decent results using Adafruit’s similar approach of using python and PIL to generate Arduino code:
https://www.instagram.com/p/B1DSZbsg_YA/

I know people have said that this (storing large images/byte arrays) isn’t really what PixelBlaze was designed for (generative patterns) so maybe I shouldn’t be trying to use PixelBlaze for this.

I believe that a more efficient way to store and access pixel arrays is in the works. It seems to me that for very-speed-sensitive applications like poi, it might be a good idea for the pattern to just fill in a pixel buffer, and then the PixelBlaze streams it out at full data rate (like … from the other cpu core!).

In the meantime, you can mix “generative” with this specific pattern. Store just one Man in grayscale (or even black-and-white). The storage requirements and decode time per-pixel would be lower. Then at render time it’s just …

    Value = get_gray_pixel(index)
    if (Value > 0) {
        Hue = ... some algorithm ...
        Saturation = ... some algorithm ...
        hsv(Hue, Saturation, Value)
    }

Hmm, but then you are still using hsv() which is more expensive than rgb() … so maybe in beforeRender() you pick R,G,B and in render() you call hsv(ValueR, ValueG, Value*B).

Or maybe you want a gradient on the Man. Easy to do if you colour him in with code!

Runtime limits, yes, there’s no way the embedded microprocessor used can work on that much data at once. The whole thing has around 170k RAM usable for compiled pattern code, data, web requests, etc., and your arrays are limited to 10240 elements (40KB of data roughly) of that.

The flash storage is less of an issue, but still quite large. Source code is compressed, so I’m betting the saved file would be much smaller, not that it helps much!

If you really want a rainbow man, what I would do is get a black and white bitmap encoded as 1 bits per pixel, and use that to then paint a rainbow using hsv() instead of a full color bitmap. You can use the B&W bitmap to mask any generated pattern, be it rainbow or rendered fire via the new Perlin noise functions, or whatever.

A fairly easy way to do this is using the XBM image format, which many tools still support exporting to it. It’s an old format that was used for embedding black and white icons/bitmaps into C programs. Something like GIMP can do it, as well as command line tools like imagemagick. There are also online converters.

An XBM file format is actually a C source code and while it encodes only 8 bits per element (about 1/4th efficient for Pixelblaze elements) it would still be leaps and bounds more efficient for your man image.

An XBM looks like this when opened with a text editor:

#define test_width 16
#define test_height 7
static unsigned char test_bits[] = {
0x13, 0x00, 0x15, 0x00, 0x93, 0xcd, 0x55, 0xa5, 0x93, 0xc5, 0x00, 0x80,
0x00, 0x60 };

Screen Shot 2022-11-02 at 12.26.03 PM

And this can be converted to a PB compatible array with a few syntax changes:

var test_width = 16
var test_height = 7
var test_bits = [
0x13, 0x00, 0x15, 0x00, 0x93, 0xcd, 0x55, 0xa5, 0x93, 0xc5, 0x00, 0x80,
0x00, 0x60 ];

I ran your man through a converter with some code to show these kinds of bitmaps, for showing on a 2D matrix, but could be adapted for POV:

//draws the burning man bitmap converted to XBM and ported, in rainbow, with a scrolling animation

var man_width = 50
var man_height = 50
var man_bits = [
  0x3F, 0xFF, 0xCF, 0xFF, 0xE7, 0xFF, 0x01, 0x3F, 0xFC, 0x07, 0xFF, 0xE1, 
  0xFF, 0x03, 0x3F, 0xFC, 0x07, 0xFF, 0xC1, 0xFF, 0x01, 0x7F, 0xF8, 0x01, 
  0xFC, 0xF0, 0xFF, 0x01, 0x7F, 0xF8, 0x01, 0xF8, 0xF0, 0xFF, 0x03, 0xFF, 
  0x70, 0x20, 0x78, 0xF0, 0xFF, 0x03, 0xFF, 0x70, 0x70, 0x70, 0xF8, 0xFF, 
  0x03, 0xFF, 0xC1, 0x78, 0x18, 0xFE, 0xFF, 0x03, 0xFF, 0x83, 0x21, 0x0C, 
  0xFE, 0xFF, 0x03, 0xFF, 0xC7, 0x01, 0x0C, 0xFF, 0xFF, 0x03, 0xFF, 0x87, 
  0x01, 0x0E, 0xFF, 0xFF, 0x03, 0xFF, 0x0F, 0x03, 0x86, 0xFF, 0xFF, 0x03, 
  0xFF, 0x0F, 0x02, 0x82, 0xFF, 0xFF, 0x03, 0xFF, 0x1F, 0x06, 0x83, 0xFF, 
  0xFF, 0x03, 0xFF, 0x1F, 0x8C, 0xE1, 0xFF, 0xFF, 0x03, 0xFF, 0x3F, 0xFC, 
  0xE1, 0xFF, 0xFF, 0x03, 0xFF, 0x3F, 0xFC, 0xF1, 0xFF, 0xFF, 0x03, 0xFF, 
  0x7F, 0xF8, 0xF0, 0xFF, 0xFF, 0x03, 0xFF, 0x7F, 0xF8, 0xF0, 0xFF, 0xFF, 
  0x03, 0xFF, 0xFF, 0xF8, 0xF8, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xF8, 0xF8, 
  0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0x79, 0xFC, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 
  0xB1, 0xFC, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0x71, 0xFC, 0xFF, 0xFF, 0x03, 
  0xFF, 0xFF, 0x61, 0xFC, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xF9, 0xFC, 0xFF, 
  0xFF, 0x03, 0xFF, 0xFF, 0x61, 0xF8, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0x70, 
  0xFC, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0x71, 0xFC, 0xFF, 0xFF, 0x03, 0xFF, 
  0xFF, 0xF9, 0xFC, 0xFF, 0xFF, 0x03, 0xFF, 0xFF, 0xF9, 0xFC, 0xFF, 0xFF, 
  0x03, 0xFF, 0xFF, 0x70, 0xF8, 0xFF, 0xFF, 0x03, 0xFF, 0x7F, 0xF8, 0xF8, 
  0xFF, 0xFF, 0x03, 0xFF, 0x7F, 0xF8, 0xF0, 0xFF, 0xFF, 0x03, 0xFF, 0x7F, 
  0xF8, 0xF0, 0xFF, 0xFF, 0x03, 0xFF, 0x7F, 0xFC, 0xF1, 0xFF, 0xFF, 0x03, 
  0xFF, 0x7F, 0xFC, 0xE1, 0xFF, 0xFF, 0x03, 0xFF, 0x3F, 0xFC, 0xF1, 0xFF, 
  0xFF, 0x03, 0xFF, 0x3F, 0xFC, 0xE1, 0xFF, 0xFF, 0x03, 0xFF, 0x3F, 0xFE, 
  0xE1, 0xFF, 0xFF, 0x03, 0xFF, 0x3F, 0xFE, 0xC3, 0xFF, 0xFF, 0x03, 0xFF, 
  0x0F, 0xFE, 0xC3, 0xFF, 0xFF, 0x03, 0xFF, 0x0F, 0xFE, 0x83, 0xFF, 0xFF, 
  0x03, 0xFF, 0x0F, 0xFF, 0x87, 0xFF, 0xFF, 0x03, 0xFF, 0x87, 0xFF, 0x0F, 
  0xFF, 0xFF, 0x03, 0xFF, 0x07, 0xFF, 0x0F, 0xFF, 0xFF, 0x03, 0xFF, 0xC7, 
  0xFF, 0x0F, 0xFF, 0xFF, 0x03, 0xFF, 0xC3, 0xFF, 0x1F, 0xFE, 0xFF, 0x03, 
  0xFF, 0xC7, 0xFF, 0x1F, 0xFF, 0xFF, 0x03, 0xFF, 0xC7, 0xFF, 0x3F, 0xFF, 
  0xFF, 0x03, ];

var colorBands = 7

function bitmap(bits, width, height, x, y) {
  x = floor(x)
  y = floor(y)
  var index = floor(ceil(width/8) * y) + floor(x/8)
  if (index >= bits.length || index < 0)
    return 0
  var byte = bits[index];
  return (byte >> (x % 8)) & 0x1
}

export function beforeRender(delta) {
  t1 = time(.1)
  resetTransform();
  //x will increase for each colorBand
  translate(time(.15) * colorBands, 0)
  
}

export function render2D(index, x, y) {
  //for color, we'll quantize x into 8 bands of color
  h = floor(x)/colorBands
  s = 1
  //scale the x,y coordinates to the man bitmap size, wrapping x as it scrolls
  //note: using mod(x * man_width, man_width) to "wrap" so that the man repeats as x scrolls past the 0-1 range
  //note: using ! to invert the pixels since we want to draw white pixels, not black ones
  v = !bitmap(man_bits, man_width, man_height, mod(x * man_width, man_width), y * man_height)
  hsv(h, s, v)
}

That still leaves you with ~96% of your array element memory free.