Automated Cookie Cutters: Creating the Height Map Data File

That process produces a grayscale height map PNG image in the proper orientation:

Jellyfish - prepared image
Jellyfish – prepared image

OpenSCAD, however, requires a flat ASCII data file to build a 3D model, as described there.

It turns out that a PGM (“Portable Graymap”) file is almost exactly what we need: a fixed format header in ASCII text, followed by the pixel data in either ASCII or binary. To get an ASCII-formatted PGM file from that PNG image:

convert jellyfish_prep.png -compress None jellyfish.pgm

The top of the PGM file looks like this:

149 159
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 255 255 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 255 255 255 255 255 255 255 255 255
... more data ...

The data isn’t in the XY array layout that OpenSCAD expects; It Would Be Very Nice If OpenSCAD could read PGM files, including the header describing the array size, but it doesn’t. Fortunately, some Bash-fu can handle the reformatting.

First, store the number of pixels along the X axis in a Bash variable:

ImageX=`identify -format '%[fx:w]' ${imagename}_prep.png`

Note the backticks around the whole mess that tell Bash to execute what’s inside and return the value. The -format operation returns the width as an integer, which is what we need.

Now, returning our attention to the PGM file, convert multiple blanks and line ends to single line ends, thus putting one entry on each output line with no other whitespace:

cat filename.pgm | tr -s ' \012' '\012'

Nota bene: there’s a leading blank in the first character string and the escape sequences should read “reverse-slash zero one two” to denote the Unix-style line end character (ASCII 10 = newline). I think the meta-markup works around the usual WordPress formatting, but ya never know what can go wrong.

The first four lines then contain the magic number, X size, Y size, and the maximum data value, respectively:


Those values are all known, because:

  • The magic number is P2 for ASCII PGM files. It would be useful to verify this, but …
  • The XY values correspond to the image size
  • The maximum data value will be 255 because of  the auto-level operation applied to the image’s 8-bit grayscaleness

Strip off the first four lines and wrap the remaining data into an array corresponding to the image size:

tail -n +5 | column -x -c $((8*${ImageX})) > filename.dat

The $((8*${ImageX)) magic comes from the way the column command works: it’s right-aligning each data values in an 8 character column, so you specify the total width of the result in character columns. Think of the parameter as specifying the screen width and you’ll be on the right track.

That fits neatly into a single line of Bash gibberish:

cat filename.pgm | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*${ImageX})) > filename.dat

The first complete line of that file goes on basically forever, but it actually has the right stuff. You can examine it thusly:

head -1 filename.dat

The file should then Just Work when sucked into OpenSCAD with the surface() function:


And, indeed, it does:

Jellyfish - surface model
Jellyfish – surface model

Note that it’s oriented with the head in the +Y direction, the tentacles (or whatever) in the -Y direction, and the freckle over the eye on the proper side. Here’s the original PNG image of the cookie for reference:

Jellyfish - height map image
Jellyfish – height map image

Using center=true centers the object on the XY plate, but the base of the solid remains at Z=-1. That makes some sense, as the “solid” part of the model lies below the Z values set by the data: the model includes a one unit thick slab below Z=0 for all points.

The convexity=10 parameter helps OpenSCAD’s quick rendering code (invoked by hitting F5 in the GUI) determine when it can stop looking for intersections between the visible ray and the object. It doesn’t affect the F6 CGAL compilation & export to STL. Because this routine will eventually be used only from the command line, the value doesn’t matter.

That works and the height map looks OK, but the model is too large in all directions and the slab below Z=0 has got to go. But it’s looking good…