March Madness: Spherify!
Uncategorized
Add comments
Mar 042010
I can’t just sit on the sidelines glowering at all the happy coders all month. Here’s my first code snippet for the month: spherify!
It’s a simple little python script that uses an image file as a heightmap, maps it on to a sphere, and spits out an STL file. It can be charitably described as “crude”. Usage:
Usage: spherify.py [options] source Options: -h, --help show this help message and exit -o OUTPATH, --output=OUTPATH output to given path -r RADIUS, --radius=RADIUS radius of lowest point in mm --height=HEIGHT additional radius of highest point in mm
You’ll need to have the python imaging library installed to use it. Source after the break.
#!/usr/bin/env python
# Spherify is a simple script that takes in an
# image and uses it as a heightmap for the surface
# of a sphere. The output is an STL file.
from PIL import Image
from optparse import OptionParser
import sys
from math import pi, sin, cos, sqrt
def f2s(f):
"Convert a float to a string with given precision."
# Really just to strip out sci. notation, which makes
# STL cry
return "%8.8f" % f
def normal(v1,v2,v3):
"Compute an approximate normal for a tri"
norm = [v1[i] + v2[i] + v3[i] for i in range(3)]
l = sqrt(sum(map(lambda x:x**2,norm)))
return tuple(map(lambda x:x/l,list(norm)))
def spherify(image,out,radius,height):
"Write out the given image as a sphere in STL format"
out.write("solid spherified\n") # start object
data = image.getdata()
(min,max) = image.getextrema()
(w,h) = image.size
y_rpp = pi/(h+2) # Y radians per pixel (adapted for poles)
x_rpp = 2*pi/w # X radians per pixel
def to_vertex(x,y,v=None):
"convert from (X,Y,value) to a 3-tuple coordinate"
if v == None:
v = data[(x%w) + (y-1)*w]
phi = y_rpp*y
theta = x_rpp*x
r = radius + (height*v)/max
c3 = r * cos(phi)
a = r * sin(phi)
c2 = a * -cos(theta)
c1 = a * sin(theta)
return (c1,c2,c3)
def print_facet(v1,v2,v3):
"print out a facet"
n = normal(v1,v2,v3)
out.write(" facet normal "+" ".join(map(f2s,n))+"\n")
out.write(" outer loop\n")
out.write(" vertex "+" ".join(map(f2s,v1))+"\n")
out.write(" vertex "+" ".join(map(f2s,v2))+"\n")
out.write(" vertex "+" ".join(map(f2s,v3))+"\n")
out.write(" endloop\n")
out.write(" endfacet\n")
# top triangles
top=to_vertex(0,0,data[0])
for x in range(w):
v1=top
v2=to_vertex(x,1)
v3=to_vertex(x+1,1)
print_facet(v1,v2,v3)
# middle triangles
for y in range(h-1):
for x in range(w):
v1=to_vertex(x,y+1)
v2=to_vertex(x+1,y+1)
v3=to_vertex(x,y+2)
v4=to_vertex(x+1,y+2)
print_facet(v1,v2,v3)
print_facet(v2,v3,v4)
# bottom triangles
bot=to_vertex(0,h+2,data[w*h-1])
for x in range(w):
v1=to_vertex(x,h+1,data[x+(h-1)*w])
v2=to_vertex(x+1,h+1,data[((x+1)%w)+(h-1)*w])
v3=bot
print_facet(v1,v2,v3)
out.write("endsolid\n") # end object
def main():
parser = OptionParser(usage="usage: %prog [options] source")
parser.add_option("-o","--output",dest="outPath",
help="output to given path")
parser.add_option("-r","--radius",dest="radius",
type="int", default="35",
help="radius of lowest point in mm")
parser.add_option("--height",dest="height",
type="int", default="10",
help="additional radius of highest point in mm")
(options,args) = parser.parse_args()
if len(args) != 1:
parser.error("Please provide a single input file.")
image = Image.open(args[0])
image = image.convert("L")
outFile = sys.stdout
if (options.outPath):
outFile = open(options.outPath)
spherify(image,outFile,options.radius,options.height)
if __name__ == "__main__":
main()