Introduction: Creating a 3D Photo Image in a Hand-drawn Arbitrary Polygon, With Openscad

About: Retired but working.

(the .scad files were corrupted, and I just replaced them with the proper versions - Feb 2, 2021)

As part of a larger project to create 3D printable building models from photos, this is an interesting special case: hand drawing an arbitrary polygon and pasting a photo inside the polygon with Openscad!

Now there are other ways to achieve this goal, with Photoshop, Openscad surface(), and difference(), however that would not be any fun! Besides for other reasons I want the trace outline around a shape, and to be able to control image pasting onto non-flat surfaces. So this is an interesting special case!

This project also involved Openscad() implementation to determine if a point is inside or outside of a polygon.


Download openscad from version 2019.05 or later.
Download python from

Step 1:

Step 2: Drawing a Polygon on an Interesting Photo Image

We start with a photo .jpg or any format and typically use Photoshop or equivalent to drawn one or more polygons in a separate layer.

First, reduce the pixel dimensions of this image to at most 300 pixels wide. More pixels are just wasted processing. Note: don't overwrite your original image! We will need it in the next step.

Required: select a solid color for the polygon trace, as in orange-red around the whole tiger, and yellow around the head.

In the bottom left corner, make a small rectangle of the colors, needing to be only 6-10 pixels high and 10 pixels wide, and terminated with a white box. This key is used to find the traces!

Then in a layer, draw each of the outlining polygons, with a pen of width 4 pixels. The line cannot be smaller or much larger, use 4 pixels! Generally the best 3D model is created when this line stays outside, and not quite touching, the desired image. Notice my red line doesn't quite touch the tiger.
The line *must* smoothly close on itself, without bumps over the 4 pixel width.
The line should be smooth and not make abrupt corners. Round the corners, with no sharp turns more than 90 degrees. If you see areas where the line looks thinner than 4 pixels, touch it up to be 4-5 pixels.
In our first stage of processing, we will see if the line finder can successfully locate and follow these traces!

Note that trace lines can cross! But keep the crossings near perpendicular to each other. The line finder will need to look ahead across a blocking color and see the line continuation.

Now save the entire image, *without changing the pixel dimensions*, as a .png file, e.g. tiger-trace1.png Changing pixel dimensions would result in the lines changing from 4 pixels wide. It is fine to keep the original image in this file!

Finally process this .png file containing the color traces with python script
Your python download must include modules: sys, png, numpy, math
Obtain these modules with: pip3 install pypng
pip3 install numpy

Process the trace image file with:
python tiger-trace1.png > tiger-trace1.txt

For my tiger example, do:
python demo_image_in_poly-tiger-trace-outer.png > demo_image_tiger_trace-outer.txt

If you examine this .txt file with Notepad or equivalent notice the openscad array name defined in about the 4th line: tiger_trace1 = [ ...

You will need this variable name in customizing the demo_image_in_poly-tiger.scad file

Step 3: Creating an Input File Containing the Desired Image

This image should start from the same image as used in creating the trace polygon in the prior step. But I always create at least 2 resolutions of this image because openscad can take a really long time to process a lot of pixels. At the same time, we want a lot of pixels to create a nice 3D print. But at most 1000 pixels wide, for image-large.png. And also create image-small.png at about 150 pixels. These can be color images.
Most critical: this image must be the same viewpoint as the trace polygon image.

Given image-large and image-small.png, process them into greyscale with

python image-large.png > image-large.txt

python image-small.png > image-small.txt

Make note of the openscad variable names in the header of each file.

For my tiger example do:
python demo_image_in_poly-tiger-640x360.png > demo_image_tiger.txt

python demo_image_in_poly-tiger-300x169.png > demo_image_tiger_sm.txt

Step 4: Openscad: Discovering Color Polygon Traces in an Image

Create a directory 3d-myimage and download the above 3 files.
Rename these three files to remove the .txt, and let them be .scad files, for Openscad.

In addition we need the excellent nSplines library from thingiverse,

Install nSplines in a peer directory, which I call nSplines-2019 in the above .scad files.
NOTE: one bug must be fixed in nSplines/Naca_sweep.scad, see my file: Bug-in-nSplines-to-fix.txt

Move line-tracing-v8-2.scad.txt to a peer directory, called line-tracing, renaming it to line-tracing-v8.scad.
Move show_image_polygon-v15-2.scad.txt to a peer directory, called image-in-polygon, renaming it to show_image_in_polygon-v15.scad.

Copy demo_image_in_poly-tiger-2.scad to: image_in_poly-yourimage.scad

Open image_in_poly-yourimage.scad in openscad.
Note: the 2019.05 version can take a minute or two before the GUI opens, as it reads the entire source files first. You will find it more intuitive to launch Openscad directly, then Open the .scad file.
If any of your directory names are different, you may get some errors. Fix the names at the head of image_in_poly-yourimage.scad as needed.

You must update several filenames in demo_image_in_poly, in the openscad editor.
include <./demo_image_tiger_trace-outer.txt>; trace0 = demo_image_in_poly_tiger_trace_outer;
To be your trace.txt filename, and to be your variable name in the header of that file.


RefImages = [ ..., "demo_image_in_poly-tiget-trace-outer.png" to be your .png file containing your color traces.

and finally update:

include <./demo_image_tiger.txt; ImageToCut = demo_image_in_poly_tiger_640x360png;
to be your image filename (the one created by and your variable name in the header of that file.

The first stage of processing for demo_image_in_poly* is to discover the colored trace lines in your tiger-trace1.txt. This processing is enclosed in:
if (1) { //confirm trace lines are correct by showing on top of their image

Now run openscad Desing-Preview option. It will take some minutes to process and find the color traces.
In the Console pane it must show:
ECHO: find trace start 0
ECHO: color box [ r g b ]
ECHO: trace for color [r g b] has length: NNN
ECHO: find trace start 1 (if there is a second trace)

Now in the openscad model window, at X-axis value of 2000, you should see an image such as the image above in this step. The trace lines drawn in space above your image file. Each trace line *must* close on itself (to within 10-20 pixels). A colored sphere is shown above the start point of each trace.
If your traces don't cross the midsection of the image file, then you will need to adjust the call to LT_buildTopLineList, and the 2nd parameter value of TTx/2, to be some fraction of the image width where your trace line is present. Try TTX * 0.25 if your trace is in the left of the image, or TTX * 0.75 if in the right side of the image.
If your trace line starts, but does not complete and close upon itself, then you need to go back to the Photoshop and slightly increase the line width in the area where the trace terminates, as it means the search algorithm lost the line tracking!

Once your trace or traces are appearing correct and closing on top of the image, then we go to the next step.

Step 5: Openscad: Mapping the Image Onto the Polygon

The final 30 lines of code or so in demo_image_in_poly-tiger are two example calls to LT_ImageInsidePoly.
Around line 103 is the include to pull in your image file (as created by No color traces in this image, just the image!).
It is definitely best to start with the image-small.txt file, for faster processing while you get things working!

Change the if (0) {
to if (1) { to enable this code section.

Now run openscad Design-Preview. It will again process the traces, and now do the LT_ImageInsidePoly call.

The Console window should show (using the tiger demo) as below.

And the tiger image inside the tiger polygon should appear at x=0. The second circle polygon is at x=1000.

Try improving your image quality in this 3D model by adjusting ImgPixHeight between 1.0 and 5.0, and re-running.
The backing thickness on the 3D model can be set with BackingThickness.

Congrats! Have fun!

--- Console window output -----

Parsing design (AST generation)...
Used file cache size: 5 files

Compiling design (CSG Tree generation)...

ECHO: "find trace start", 0, "XYstart", [320, 2], "ref color", [231, 5, 5], "SunitV", [0, 1]

ECHO: "color box", [231, 5, 5], "noneFound at XY?", [false, [320, 100]]

ECHO: "trace for color", [231, 5, 5], "has length", 804

ECHO: "find trace start", 1, "XYstart", [320, 2], "ref color", [244, 247, 10], "SunitV", [0, 1]

ECHO: "color box", [244, 247, 10], "noneFound at XY?", [false, [320, 54]]

ECHO: "trace for color", [244, 247, 10], "has length", 392

ECHO: "tiger trace image data has pixels X, Y", 640, 360

ECHO: "Ref Colors For Trace Search", [[231, 5, 5], [244, 247, 10]]

ECHO: "show trace", 0, "of length", 402

ECHO: "show trace", 1, "of length", 196

ECHO: "LT_ImageInsidePoly: pix x y", 640, 360, "X min, max osu, Xincr", 0, 639, 0.998437, "Y min, max osu, Yincr", 0, 359, 0.997222, "showInPlusYbool", false, "trace len", 402, "flip TB, LR", true, true

ECHO: "LT_ImageInsidePoly: pix x y", 640, 360, "X min, max osu, Xincr", 0, 639, 0.998437, "Y min, max osu, Yincr", 0, 359, 0.997222, "showInPlusYbool", false, "trace len", 196, "flip TB, LR", true, true

Compiling design (CSG Products generation)...

Geometries in cache: 9

Geometry cache size in bytes: 112537128

CGAL Polyhedrons in cache: 0

CGAL cache size in bytes: 0

Compiling design (CSG Products normalization)...

Normalized CSG tree has 9 elements

Compile and preview finished.

Total rendering time: 0 hours, 2 minutes, 7 seconds