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.
# ----> http://adafruit.com/products/306

# 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") 

# load images  

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:
# http://stargateeggbeater.com/raspberry-pi-spi-speed-and-lpd8806-rgb-strips-using-python/#comments
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')
	pickleData = pickle.load(pickleFile)
	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"))
		pixels = (img.load())
		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))

Raspberry Pi SPI Speed and LPD8806 RGB Strips using Python

So we recently decided to move this project to the Raspberry Pi board away from Arduino. The capabilities and processing power of the Pi are much greater than Arduino. The Raspberry Pi community is growing extremely fast and amazing projects are happening everywhere.

We have successfully loaded images into the SGEB, but the refresh rate is too slow, much slower then the Arduino was. We need some help figuring out how to turn up the SPI speed with our current software arrangement.

I used  Brian Hensley’s method for loading the SPI drivers on to Wheezy http://www.brianhensley.net/2012/07/getting-spi-working-on-raspberry-pi.html

Then used Phillip Burgess’ at Adafruit ‘Light Painting’ Python script to process images and output to the LPD8806 http://learn.adafruit.com/light-painting-with-raspberry-pi/software

The image processing script is amazing and solves tons of problems we had with the Arduino.

But I can’t figure out how to set the SPI speed through the Python scripts. 

I had attempted to use the Occidentalis OS as Adafruit suggested but the SPI Max Speed is set to 500 Khz in arch/arm/machbc2708/bcm2708.c   Compiling a Kernel proved to difficult for me to manage.

Am I mistaken that the SPI drivers are different between the 2 methods?