# Stargate Eggbeater, Floydfest Edition

Here’s the latest and greatest on the Stargate Eggbeater project. We have some new Python code to share, and some new artwork and notes on preparing images for our artistic friends (this means you, if you’re good at drawing flying saucers…)

We’ve made a ton of progress on this project since the breakthrough tip Grant offered us. I split the “lightpaint.py” code up into two components, one that scans a batch of images and saves them (using Python’s “pickle” module) and a second one that loads the .pickle files and cycles through images. The process of importing PNG files takes several minutes when we are working with a few dozen images, and this isn’t desirable especially for testing and re-testing code in Dan’s garage. The “pickle” system helps that a little bit.

FWIW, I have read that there are faster ways of reading a PNG image. Using Numpy’s array object may be faster than the method in Phillip’s script. But, the “pickle” solution works well enough for our purposes right now.

Out new python scripts are included here for anyone who’s interested. In summary:

• scan_images.py runs through a fileglob list and creates a “.pickle” file corresponding to every PNG image in the list. Edit the “fileGlob” variable to choose images (e.g. fileGlob = “*.png”)
• lightpaint-FF.py is our latest variation on the script by Phillip Burgess. It needs to open .pickle files created by the scan_images script, so our fileGlob variable here is “/images/*.pickle”
• a couple of utility scripts are helpful in preprocessing images. They rely on ImageMagick:
• padWithBlack.py adds black rectangles to the top and bottom of an image. Sometimes this is desirable, since a rectangular image will be highly distorted near the top & bottom of the hoop. The width of the black padding is specified in the script as a percentage, e.g. borderWidthPercent = 0.1 means that a 100 pixel high image will be padded with 10 pixels each at the top and bottom.
• MirrorAndResize.py takes an image (possibly padded with black margins already), mirrors it about its top edge, and saves the result as a 142-pixel high PNG, resizing if necessary.

Thanks to the python imaging tools and image processing scripts, now we can rapidly test new images and see what works and what doesn’t. We’ve got a nice collection of artwork to show off at FloydFest this weekend (pending approval from the festival organizers), so keep an eye out if you’re at FloydFest. We also hereby request submissions of more trippy images from our artistic friends! Read the info on the new artwork gallery page if interested.

Next goals for the project:

• We’ve still got a hall effect sensor, and with some effort should be able to tell Python how to see when a rotation has completed. That will hopefully allow for stationary images and/or videos.
• We also are interested in an interactive mode—whether this entails an NES controller that allows users to cycle through preset patterns, or an iphone app that sends simple touchscreen-drawn bitmaps to the raspberryPi, or any of a million variations.
• Wiring up a cheap microphone would allow sound-reactive patterns. Maybe using a package like this?  Haven’t investigated in detail yet.

OK, here’s some Python code.

`lightpaint-ff.py`

```#!/usr/bin/python

# Light painting / POV demo for Raspberry Pi using
# Adafruit Digital Addressable RGB LED flex strip.

# Kevin Foster modified this code to separate the task of opening PNG
# files from the task of displaying them. I used python's "pickle"
# module to do this. The script scan_images.py should be run once when
# new PNG files are to be added. This script generates a ".pickle" file
# for each PNG file it encounters. This new version of lightpaint.py
# loads the .pickle files ONLY, NOT the .PNG files. Hopefully this will
# result in quicker preprocessing times when starting the lightpaint.py
# script on a large set of images.

import RPi.GPIO as GPIO, Image, time
import fcntl
import array
import glob
import random
import pickle

# Configurable values
fileGlob  = "images/*.pickle"
dev       = "/dev/spidev0.0"
playMode = "random-weighted"

defaultTime = 240 # seconds to display image, if not specified in filename

# Print the time (for the logfile)
print "lightpaint-FF.py started at {}".format(time.asctime(time.localtime()))

# Open SPI device
spidev    = file(dev, "wb")

filesList = sorted(glob.glob(fileGlob))
numImages = len(filesList)

print "Found {} images saved as .pickle files. Loading...".format(numImages)
time.sleep(2)

# using the hack suggested by Grant:
fcntl.ioctl(spidev, 0x40046b04, array.array('L', [5000000]))
# (note: this line isn't needed here since I use ioctl in the loop below
# for changing refresh rate on-the-fly. But, it seems to cause weird behavior,
# so I might be inclined to remove that code later and just set the speed
# here.)

# Rather than simply storing the contents of a single image in a bytearray
# for feeding to the SPI device, we want to store the contents of MANY
# images.
column = []
width = []
for thisImage in range(numImages):

filename = filesList[thisImage]

print "Loading image #{a} of {b} ({c})...".format(a=thisImage+1,b=numImages,c=filename)

# Use "pickle" package to save image data to a file, and reload it later.
# (Now that we can display dozens of images in a loop, the amount of
# processing time needed to load these things is substantial.)
#
# If PNG files are edited/updated, the corresponding .png.pickle files
# need to be deleted so the program can re-process them and generate
# new pickle files.
#
# Pickle might be a sloppy solution. Other image
# libraries besides PIL (Python Imaging Library, aka the "Image" module)
# have been developed, some of them for use with numpy. Numpy is part
# of SciPy and is an array manipulation package. By vectorizing the code
# in the loop below and using a numpy-based graphics library, we could
# speed this up without using pickle and redundant datafiles. Something
# to do after FloydFest.

pickleFile = open(filename, 'rb')
column.append(pickleData)
print "{w}x{h} pixels".format(w=len(column[thisImage]),h=((len(column[thisImage][0]) - 1) / 3))
pickleFile.close()

width.append(len(column[thisImage]))

# Then it's a trivial matter of writing each column to the SPI port.
print "Displaying?"

if playMode == "normal":
thisImage = 0

while True:

# The following loop runs based on the value of "playMode" (set above).
#
# "normal" : loop through the glob list in filename-sorted order.
# "random":  chooses files completely randomly
# "random-weighted": Assign arbitrary weights to subsets of images
# more modes to be added..

if playMode == "random":
thisImage = random.randrange(numImages)
thisFileName = filesList[thisImage]
elif playMode == "random-weighted":
randomNumber = random.random()
if randomNumber > 0.5: # Play Dan's images 50 percent of the time.
subSet = [k for k in filesList if 'dan' in k]
else:
# otherwise, pick a non-Dan file
subSet = [k for k in filesList if 'dan' not in k]
thisImage_subset = random.randrange(len(subSet))
thisFileName = subSet[thisImage_subset]
thisImage = filesList.index(thisFileName)
elif playMode == "normal":
thisFileName = filesList[thisImage]

# Decide how long to show the image, based on filename.
#
timeToDisplay = defaultTime
if (thisFileName[0:2]=="L01"):          # L01 means short
timeToDisplay = 30 % seconds
elif (thisFileName[0:2]=="L02"):	# L02 means medium
timeToDisplay = 60 % seconds
elif (thisFileName[0:2]=="L03"):	# L03 means long
timeToDisplay = 120 % seconds

imageStartTime = time.time() # start the clock

print "Current file: {}".format(thisFileName)
print "Display time: {} seconds".format(timeToDisplay)
print ""

# Now loop through the image
i = 0
while time.time() - imageStartTime < timeToDisplay:
spidev.write(column[thisImage][i])
spidev.flush()
i = i+1
if i >= width[thisImage]:
i = 0

if playMode == "normal":
thisImage = thisImage + 1
print "new thisImage {}".format(thisImage)
if thisImage >= numImages:
thisImage = 0
print "new thisImage {}".format(thisImage)```

`scan_images.py`

```#!/usr/bin/python

# this script contains the first part of the old lightpaint.py script.
# it scans through a working directory (using a fileglob if desired) and
# checks to see which PNG files have a corresponding .pickle file.
# Where one does not exist, the PNG file is opened, scanned, and saved
# using python's pickle module. Later, when lightpaint is run, the
# .pickle files are loaded and used to generate images.

import Image, time
import array
import glob
import random
import pickle
import os

# Configurable values
fileGlob  = "*.png"

# load images in RGB format and get dimensions:
filesList = glob.glob(fileGlob)
numImages = len(filesList)

# Calculate gamma correction table.  This includes
# LPD8806-specific conversion (7-bit color w/high bit set).
gamma = bytearray(256)
for i in range(256):
gamma[i] = 0x80 | int(pow(float(i) / 255.0, 2.5) * 127.0 + 0.5)

# Rather than simply storing the contents of a single image in a bytearray
# for feeding to the SPI device, we want to store the contents of MANY
# images.
for thisImage in range(numImages):

filename = filesList[thisImage]

print "Looking for %s.pickle..."  % filename

# Use "pickle" package to save image data to a file, and reload it later.
# (Now that we can display dozens of images in a loop, the amount of
# processing time needed to load these things is substantial.)
#
# If PNG files are edited/updated, the corresponding .png.pickle files
# need to be deleted so the program can re-process them and generate
# new pickle files.
#
# Pickle might be a sloppy solution. Other image
# libraries besides PIL (Python Imaging Library, aka the "Image" module)
# have been developed, some of them for use with numpy. Numpy is part
# of SciPy and is an array manipulation package. By vectorizing the code
# in the loop below and using a numpy-based graphics library, we could
# speed this up without using pickle and redundant datafiles. Something
# to do after FloydFest.

pickleFileName = filename+".pickle"
if os.path.exists(pickleFileName):
print "Pickle file already exists!"
else:
print "No pickle file exists. Opening image..."
img = (Image.open(filename).convert("RGB"))
width = (img.size[0])
height = (img.size[1])
print "%dx%d pixels" % img.size

# Create list of bytearrays, one for each column of image.
# R, G, B byte per pixel, plus extra '0' byte at end for latch.
print "Allocating..."
column = ([0 for x in range(width)])
for x in range(width):
column[x] = bytearray(height * 3 + 1)

# Convert 8-bit RGB image into column-wise GRB bytearray list.
print "Converting..."
for x in range(width):
for y in range(height):
value = pixels[x, y]
y3 = y * 3
column[x][y3]     = gamma[value[1]]
column[x][y3 + 1] = gamma[value[0]]
column[x][y3 + 2] = gamma[value[2]]

outFile = filename + ".pickle"
fid = open(outFile, 'wb')
dd = pickle.dump(column,fid)
fid.close()
print "New datafile {} created!".format(outFile)

print "OK, done scanning all files. The .pickle files will be used by lightpaint."```

`MirrorAndResize.py`

```import glob
import os

# quick image resizer/flipper for Stargate Eggbeater
# by Kevin Foster

# This script quickly mirrors a set of images about the horizontal axis
# and resizes to a height of 142. It requires ImageMagick to work.
# (Utility "convert" is part of the ImageMagick program).

# the variable globStr can be used to quickly select a list of images
# to batch process. Note that ImageMagick can take many image formats
# including JPG as input. The output will be a PNG file.
globStr = "*.png"
# examples:
# to resize all the PNG images in the current directory, use:
#           globStr = "*.png"
# to resize all the JPG images in the current directory, use:
#           globStr = "*.jpg"
# Watch out for case-sensitivity!
# to resize only dan's images (if they are prefixed with "dan_":
#           globStr = "dan_*.png"
# etc.

imageList = glob.glob(globStr)

for thisImg in range(len(imageList)):

fileName = imageList[thisImg]

print "Opening {}...".format(fileName)

fileNameBase = os.path.basename(fileName)
flippedFN = fileNameBase + "flipped"
print "Creating temporary image, {}".format(flippedFN)
os.system("convert {fn} -flip  {nfn}".format(fn=fileName, nfn=flippedFN))

OutputFileName = fileNameBase + "_mirror.png"
print "Combining to images and generating 142-px high output file: {}".format(OutputFileName)
os.system("convert -reverse {fn} {nfn} -append {ofn}".format(fn=fileName, nfn=flippedFN, ofn=OutputFileName))

os.system("mogrify  -resize x142  {ofn}".format(ofn=OutputFileName))

print "Removing temporary image {}".format(flippedFN)
os.system("rm {}".format(flippedFN))```

`padWithBlack.py`

```import Image
import glob
import os

# pad an image with black for Stargate Eggbeater
# by Kevin Foster

# This script adds a black border to top and bottom of images. The width
# of the border is specified in percent. Its purpose is to provide some
# "margin" space for rectangular images when the top & bottom are likely
# to be obscured on the hoop.

# 0.2 = 20 percent, etc.
borderWidthPercent = .1

# the variable globStr can be used to quickly select a list of images
# to batch process. Note that ImageMagick can take many imageformats
# including JPG as input. The output will be a PNG file.
globStr = "*.png"

# examples:
# to pad all the PNG images in the current directory, use:
#           globStr = "*.png"
# to pad all the JPG images in the current directory, use:
#           globStr = "*.jpg"
# etc.

imageList = glob.glob(globStr)

for thisImg in range(len(imageList)):

fileName = imageList[thisImg]

print "Opening {}...".format(fileName)

fid = open(fileName)

File = Image.open(fid)

Width = File.size[0]
Height = File.size[1]

fid.close()

borderWidth = (borderWidthPercent*Height)

fileNameBase = os.path.basename(fileName)
outFN = fileNameBase + "_padded.png"
print "Creating output image, {}".format(outFN)

# This adds a black border all around the image.
os.system("convert   {fn} -bordercolor black -border {bw} {nfn}".format(
fn=fileName, bw=borderWidth, nfn=outFN))

# This removes the black border from the sides (top stays).

os.system("convert   {nfn} -crop {w}x{h}+{bw}+0 {nfn}".format(
nfn=outFN, w=Width, h = (Height + (2*borderWidth)),
bw = borderWidth))```

## 3 thoughts on “Stargate Eggbeater, Floydfest Edition”

1. Eric

Have you noticed any problems with jitter due to the Pi being a multitasking OS? Or is it a non-issue? I’m looking into doing a smaller PoV display with a high speed stepper spinning it. My plan was to have a Teensy 3.0 slaved to a Pi to handle motor step pulse generation and also to buffer data out to the LPD8806 strips, but if I can avoid that and use just a Pi it would save some parts.

1. Kevin

Eric,

Nope, no problems with jitter. With a smaller hoop you’ll probably be spinning at higher RPM and thus probably use a faster refresh rate than us(?) If I recall correctly, the setting we used (5000000 in the fcntl.ioctl() call above) wasn’t the fastest it could go. It would be easy enough to verify speed without setting up the Teensy, I suppose. As for jitter at the higher speed, based on my limited observations it wasn’t a problem either.

Cheers,
Kevin

1. Eric

Thanks for the input, I do plan on going a bit faster, aiming for 900 RPM. If I estimate 5uS of jitter at that speed, it should only be a visible change of ~0.002 degrees (not noticeable). But I’m pretty sure the Pi can’t keep up with stepper pulse generation, especially if I use micro-stepping. Mechanically, I am looking at doing something similar to this, but with a slip ring for power and data transfer. I’ll report back once my hardware arrives and I get some tests done.