Introduction: 3D Printed Photograph
The 3d printer in our office (an Objet Connex500) prints with a rigid, semitransparent white material that can be used to create these unique black and white photographic prints. These prints may be indecipherable when viewed from the side, but when backlit with a diffuse light, they recreate images with surprisingly high precision and even add some subtle dimensionality and texture to the scene.
By varying the thickness of a region of this semitransparent print you can control the amount of light that is able to pass through, thereby controlling the brightness (thinner regions of material will appear brighter and thicker regions darker). In this project, I've converted each individual greyscale pixel of an image to thickness, allowing me to precisely reproduce any greyscale image. The photos I've printed include an adorable picture my mom took of our cat Teddy (fig 4), Saturn and its moon Titan taken by the Cassini space probe (fig 5 and 6), and a huge print (19x16") of Mt. Williamson by Ansel Adams (fig 1, 2, and 3).
Step 1: The Code
All of these 3D models were generated algorithmically from Processing using the ModelBuilder library by Marius Watz. This library allows you to save 3D geometries in the STL file format, STL files that form a watertight mesh can be printed by a 3D printer.
To get started using this code yourself, download the latest version of the ModelBuilder library, unzip the file, and copy the folder into Processing's "libraries" folder. If you have installed the predecessor to the ModelBuilder library (called the Unlekker library), you will need to delete it. Once this is done restart Processing.
//image to 3d printable heightmap/lithophane //by Amanda Ghassaei //May 2013 //https://www.instructables.com/id/3D-Printed-Photograph/ /* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. */ //libraries import processing.opengl.*; import unlekker.util.*; import unlekker.modelbuilder.*; import ec.util.*; String name = "your_file_name_here.jpg";//name of file (with extension - this also works with pngs) //storage for dimensions int widthRes; int heightRes; float widthDim = 5;//width dimension (in inches) float widthScaled; float heightScaled; float zDim = 0.1;//max vertical displacement (in inches) float thickness = 0.02;//base thickness (in inches) boolean invert = true;//if true, then white areas are lower than black, if not true white areas are taller PImage img;//storage for image float pixeldata[];//storage for pixel array UVertexList v1,v2,v3,v4;//storage for verticies UGeometry geo;//storage for stl geometry void setup(){ img = loadImage(name);//load image //get dimensions of image widthRes = img.width; heightRes =img.height; size(widthRes,heightRes,P3D);//set dimensions of output image(img, 0,0);//display image loadPixels();//poad pixels into array pixeldata = new float[widthRes*heightRes];//initialize storage for pixel data for(int index=0;index<widthRes*heightRes;index++){ int getPixelData = pixels[index];//get data from pixels[] array pixeldata[index] = getPixelData&255;//convert to greyscale byte (0-255) } //initialize storage for stl geo = new UGeometry(); v1 = new UVertexList(); v2 = new UVertexList(); v3 = new UVertexList(); v4 = new UVertexList(); //draw stl if(invert){ //draw top for(int i=0;i<(heightRes-1);i++){ v1.reset(); v2.reset(); for(int j=0;j<widthRes;j++){ widthScaled = j/float(widthRes)*widthDim; //top layer v1.add(widthScaled,i/float(widthRes)*widthDim,(255-pixeldata[widthRes*i+j])*zDim/255+thickness); v2.add(widthScaled,(i+1)/float(widthRes)*widthDim,(255-pixeldata[widthRes*(i+1)+j])*zDim/255+thickness); } geo.quadStrip(v1,v2); } //draw sides v1.reset(); v2.reset(); v3.reset(); v4.reset(); for(int j=0;j<widthRes;j++){ widthScaled = j/float(widthRes)*widthDim; v1.add(widthScaled,0,(255-pixeldata[j])*zDim/255+thickness); v2.add(widthScaled,0,0); v3.add(widthScaled,(heightRes-1)/float(widthRes)*widthDim,(255-pixeldata[widthRes*(heightRes-1)+j])*zDim/255+thickness); v4.add(widthScaled,(heightRes-1)/float(widthRes)*widthDim,0); } geo.quadStrip(v2,v1); geo.quadStrip(v3,v4); //draw sides v1.reset(); v2.reset(); v3.reset(); v4.reset(); for(int i=0;i<heightRes;i++){ heightScaled = i/float(widthRes)*widthDim; v1.add(0,heightScaled,(255-pixeldata[widthRes*i])*zDim/255+thickness); v2.add(0,heightScaled,0); v3.add((widthRes-1)/float(widthRes)*widthDim,heightScaled,(255-pixeldata[widthRes*(i+1)-1])*zDim/255+thickness); v4.add((widthRes-1)/float(widthRes)*widthDim,heightScaled,0); } geo.quadStrip(v1,v2); geo.quadStrip(v4,v3); } else{ //draw top for(int i=0;i<(heightRes-1);i++){ v1.reset(); v2.reset(); for(int j=0;j<widthRes;j++){ widthScaled = j/float(widthRes)*widthDim; //top layer v1.add(widthScaled,i/float(widthRes)*widthDim,(pixeldata[widthRes*i+j])*zDim/255+thickness); v2.add(widthScaled,(i+1)/float(widthRes)*widthDim,(pixeldata[widthRes*(i+1)+j])*zDim/255+thickness); } geo.quadStrip(v1,v2); } //draw sides v1.reset(); v2.reset(); v3.reset(); v4.reset(); for(int j=0;j<widthRes;j++){ widthScaled = j/float(widthRes)*widthDim; v1.add(widthScaled,0,(pixeldata[j])*zDim/255+thickness); v2.add(widthScaled,0,0); v3.add(widthScaled,(heightRes-1)/float(widthRes)*widthDim,(pixeldata[widthRes*(heightRes-1)+j])*zDim/255+thickness); v4.add(widthScaled,(heightRes-1)/float(widthRes)*widthDim,0); } geo.quadStrip(v2,v1); geo.quadStrip(v3,v4); //draw sides v1.reset(); v2.reset(); v3.reset(); v4.reset(); for(int i=0;i<heightRes;i++){ heightScaled = i/float(widthRes)*widthDim; v1.add(0,heightScaled,(pixeldata[widthRes*i])*zDim/255+thickness); v2.add(0,heightScaled,0); v3.add((widthRes-1)/float(widthRes)*widthDim,heightScaled,(pixeldata[widthRes*(i+1)-1])*zDim/255+thickness); v4.add((widthRes-1)/float(widthRes)*widthDim,heightScaled,0); } geo.quadStrip(v1,v2); geo.quadStrip(v4,v3); } //draw bottom v1.reset(); v2.reset(); //add bottom four corners v1.add(0,0,0); v1.add(0,(heightRes-1)/float(widthRes)*widthDim,0); v2.add((widthRes-1)/float(widthRes)*widthDim,0,0); v2.add((widthRes-1)/float(widthRes)*widthDim,(heightRes-1)/float(widthRes)*widthDim,0); geo.quadStrip(v1,v2); //change extension of file name int dotPos = name.lastIndexOf("."); if (dotPos > 0) name = name.substring(0, dotPos); geo.writeSTL(this,name+".stl"); exit(); println("Finished"); }
Download the latest version of the Processing sketch from GitHub (download as a zip by clicking on the cloud button). Open the folder called Lithograph3DPrint. Copy any greyscale images you want to convert into this folder.
To run the sketch, replace the part in quotes in following line:
String name = "your_file_name_here";
with the name of your greyscale image. I believe .gif, .jpg, .tga, and .png files will all work fine, but I have only tested .jpg so far. Run the sketch, after a minute or two Processing will tell you that it is writing an STL file and eventually it will tell you that it is finished. The resulting file will be located in the sketch's folder named "NAME_OF_ORIGINAL_FILE.stl" You can open the stl file with a variety of CAD software and stl viewers, I like MeshLab for simple viewing (it's free and open source).
By default my sketch will scale images to 8" wide, with a base thickness of 0.02" and feature thickness of up to 0.1", you can change these setting by adjusting the variable at the top of the sketch.

Participated in the
3D Printing Contest

Participated in the
Epilog Challenge V
4 People Made This Project!
- MattO24 made it!
- langbatkyho made it!
- power882 made it!
- My Thing in 3D made it!
167 Comments
3 years ago
Will this software (ModelBuilder library, )work currently? I will be using a Makerbot printer. Thank you for sharing!
Question 4 years ago
What type of filament are you printing? Also, are you post processing the prints? The close up of the planet and moon print looks like it has been sprayed with something like an acrylic.
Answer 4 years ago
They're not printing with filament at all. Do a search for Object Connex 500. This is a high resolution UV resin based printer. They work kind of like the inkjet printer you would find in many peoples homes.
Question 4 years ago on Step 1
Hello,
I try to run your script, but Processing gives an error: The package "unlekker" does not exist.
In your script you use "import unlekker.util.*" and "import unlekker.modelbuilder.*".
How can I resolve it?
Thanks.
5 years ago
Did you think perhaps of making a YouTube video so we can all see how to do the process? It would greatly eliminate confusion and allow us all to participate in your terrific creation!
6 years ago
just look this cool trick & forget hot bed
http://www.thingiverse.com/thing:1911919
Enjoy & happy printing...
6 years ago
Hi there!
I have a problem when running the sketch, it shows ArrayIndexOutOfBoundsException
and error line int getPixelData = pixels[index];//get data from pixels[] array
My photo is 1080x1920. Another one, with 415x415, work one time, but in some others tests, create a STL file that isn´t the picture.
Anyway, awesome work!
6 years ago
Need some help please.. Problem Size/set dimensions
6 years ago
Hello! A really fantastic concept and resource however I'm having some difficulty with the STL file itself as only a flat model is created. No error messages appear.
I've tried both Mac and Windows and tested numerous combinations including increasing RAM allocation. I'm currently using Processing 2.2, Modelbuilder v0007a03 and the lLithograph3DPrinter-Master with the example cat image used in previous tests.
Any help and advice would be gratefully received.
7 years ago on Introduction
Your code works fine, but with old version of ModelBuilder, with version Mk2 I have errors in this line:
UGeometry geo;//storage for stl geometry
Error is: Cannot find a class type named "UGeometry"
Could you tell me why?
Sorry about my bad English :-)
Reply 7 years ago on Introduction
yeah, Mk2 was a big revision, so some of the classes are different. If you're interested in using Mk2 and wanted to learn more about it, you could try to rewrite my code to be compatible - I don't think too much of it would need to change.
7 years ago on Introduction
This tutorial needs more instructions, I don't even know what kind of code is this, or how i make this work
7 years ago on Introduction
Very interesting but how are they able to construct a 3D model from a single 2D image?
8 years ago on Introduction
And in case a 3D tomography imaging how I can create a printed replica.
8 years ago on Introduction
For some reason the image seem to output in reverse. Also the model seem not to be watertight. Any suggestions?
8 years ago on Introduction
hi! im using a makerbot replicator 2, i was able to get the stl file, but when i try to print it on the makerbot the printing does not work. Can anyone help me with this problem .
thankyou
8 years ago on Introduction
Hi ! I am kind of stuck at the beginning when run the sketch.
It shows "No library found for unlekker.util...."
Please help!
Thank you!
Reply 8 years ago on Introduction
First, locate where your Processing sketchbook. This is the default
directory of all of your Processing sketches. On a mac, this will
usually be:
/Users/Username/Documents/Processing
on a PC, it’s probably:
c:/My Documents/Processing/
If it doesn’t already exist, create a folder called “libraries” here. This is where you’ll install the 3rd party libraries.
Copy the extracted files to the Processing libraries folder. Most
libraries you download will automatically unpack with the right
directory structure. The full directory structure should look like this:
/Processing/libraries/simpleML/library/simpleML.jar
More generically:
/Processing/libraries/libraryName/library/libraryName.jar
Some libraries may include additional files in the “library” folder, as
well as the source code (which is commonly stored one directory up, in
the “libraryName” folder). If the library does not automatically unpack
itself with the above directory structure, you can manually create
these folders (using the finder or explorer) and place the
libraryName.jar file in the appropriate location yourself.
Restart Processing. If Processing was running while you
performed above, you will need to quit Processing and restart it in
order for the library to be recognized. Once you have restarted, if
everything has gone according to plan, the library will appear under the
“Sketch → Import Library.”
What to do once you have installed the library really depends on which library you have installed.
8 years ago on Introduction
I seem to be having troubles as well. When I run 'Lithograph3DPrint' it just opens a java window and nothing else seems to happen. Any suggestions?
Reply 8 years ago on Introduction
And I seem to be getting an eventual "out of memory error".