Saturday, October 18, 2014

Android/IOIO 3D Laser Scanner

Once you have a 3D printer, it's a logical jump to start thinking about what you can repair with it. I had read articles on simple DIY laser scanners using webcams and a line laser, and decided it would be a fun project to build one and see how well they work.

Rather than using a webcam, I opted to use an Android phone (Galaxy Nexus) that I had as a spare, and an IOIO board that I already owned. I reasoned that the phones had high quality cameras, and that it should allow for higher resolution scanning than a cheap camera. I wrote a simple program on the phone to take the photos and drive a stepper motor to turn a turntable.

I printed this simple printable turntable design and wired up a EasyDriver stepper driver to the recommended stepper motor, wired for .45 degree steps. I found that things tended to slip on the slick turntable surface, so I found a grippy rubbery material similar to shelf lining in the kitchen section.

I placed a line laser on a printed adjustable mount that I designed  and set it at 45 degrees to the camera angle. The line laser from Adafruit makes a nice line.  A phone mount modified from an existing GameClip for the Nexus served as an easy mount for the phone.

I wrote a simple Android app based on the HelloIOIO example application that takes a picture, advances the stepper motor, and takes another. The saved images are processed  by a Python script that tries to identify the center of the laser line, which is surprisingly wide. Treating the center of rotation (the motor shaft) as the origin, it used trigonometry to figure out where the beam as intersected the part and then rotates the resulting points into their position based on how much the turntable has been turned.

This guy has an outstanding explanation of how the math works. I've put the Python script at the end of the blog post in case you are developing something similar.

The resulting point cloud in x,y,z format is loaded into Meshlab. The point cloud is turned into a part with a surface using the method described here.

So. How well does it work? Can I scan parts and then easily run them off on my printer?

It depends entirely on the shape of the object. Here's some examples.

Where the laser line strikes the part, it makes a nice smooth mesh. However, for most parts that are interesting, one area of the part will often shadow another, and detail in that area will be lost. Take a look at the area under the chin of the knight for one example. You also get shadowed areas as the part rotates when one part of the target sticks out a bit. See how the side of the face on the right side of the image below does not show the beam? That's a big chunk of detail that is lost. The mesh gets ugly there, and it would require work in a program like Blender to fix.

I did experiment with two lasers, one of each side of the part, to try to reduce that. I think this is a good approach, but it requires a significantly more precise rig than I made. When I tried to align the resulting two point clouds, I found that the two meshes were slightly different sizes. It appears to require very precise alignment and a stiff setup to keep it all straight if you are doing that. I scrapped this approach - maybe for a later build, but it would be a total redesign.

I also found problems with scanning parts that are glossy or otherwise reflective. The laser beam hits and scatters, rather than making a nice tight beam. Detail in that area is lost, sometimes resulting in the loss of entire faces. I tried scanning a part from a photocopier, and much of it was lost to this.

Finally, parts that were more square than oblong tended not to be illuminated by the beam, and detail was lost. An extreme example is shown in this scan of a wood block, where the entire top was lost.

These issues can be somewhat improved by changing laser angle, laser height, camera distance, etc.  However, it usually involves a tradeoff - you can pick one area to be well illuminated, but at the expense of another. They do work, and they are a very interesting project to play with. If you want to scan in models to do digital sculpting in Blender, as a starting point, they would work well. If the goal is to scan parts for replicating with a minimum of manipulation of the resulting model, it appears to require a more sophisticated approach. Dual beams would help, but you would still have shadowing issues. I think that to really do it right would require scanning the laser up and down while also rotating the part, which significantly complicates the mechanism. 

I enjoyed building it, but the limitations inherent in a single fixed beam are such that most anything I would scan would require extensive work in Blender to to make usable. I may come back to laser scanning at some point, but for now I think I'll just work on improving my skills in CAD. 

If you are going to build a scanner like this, the following things help a lot:

- level the platform you are building on
- level the turntable to avoid "wobbles" in the reconstructed part. I did this by shimming the stepper motor in the turntable mount
- make sure the line laser is at 90 degrees to the turntable surface

A system like David LaserScanner that allows you to scan the beam over the whole surface probably helps a great deal. I may try that at some point, but at this point other projects are calling.

Code for the reconstruction script follows. There is a lot of room for improvement.


from Tkinter import *
from PIL import Image
import math

thresholdBrightness = 600;
centerLine = 1368;
leftBound = 362
rightBound = 2287
upperBound = 996
lowerBound = 1368

def findBrightestToRight(im, y):
 rowBrightestPosition = 0
 rowBrightestValue = 0
 global centerLine
 leftEdge = -1
 rightEdge = -1
 pix = im.load()

 if y > lowerBound:
  return -1

 if y < upperBound:
  return -1

 for x in range(centerLine,rightBound,2):
  pixel = pix[x,y] #get rgb value of current pixel
  pixelBrightness = pixel[0] + pixel[1] + pixel[2]
  if pixelBrightness > rowBrightestValue:
   rowBrightestValue = pixelBrightness
   rowBrightestPosition = x

 if rowBrightestValue > thresholdBrightness:
  return rowBrightestPosition
  return -1

#todo: handle return of -1 properly


maxSteps = 0;
yPixelsPerMM = 1.00
xPixelsPerMM = 1.00
thetaDegrees = 27.0 #angle formed by laser line to camera
thetaRadians = math.radians(thetaDegrees)
rotationAngleDegrees = 0;
rotationAngleRadians = 0;
degreesPerStep = .45
result = ""

numberOfFiles = 800;

thetaRadians = math.radians(thetaDegrees)

#make blank files
f = open('scannerOutput1.asc', 'w')

for currentFile in range(0,numberOfFiles):
 print "Processing file " + str(currentFile) 
 filename = str(currentFile) + ".jpg"
 im = 
 imageWidth = im.size[0]
 imageHeight = im.size[1]
 rotationAngleRadians = math.radians(rotationAngleDegrees)
 print rotationAngleDegrees
 result1 = ""
 for y in range(0,imageHeight):
  rowBrightestPosition = findBrightestToRight(im, y)
  #print y
  ##invert y
  yPosition = ((imageHeight * 1.00) - y) * yPixelsPerMM;   #this will need to be scaled
  xPosition = ((rowBrightestPosition - centerLine)/(math.sin(thetaRadians))) * xPixelsPerMM; 
  zPosition = 0.00; 
  ##we need to rotate the x,y,0 position to it's final position based on stepper motor angle
  ##rotate about y axis to proper position according to current part rotation angle
  rotationAngleRadians = math.radians(rotationAngleDegrees)
  zRotated = (zPosition * math.cos(rotationAngleRadians)) - (xPosition * math.sin(rotationAngleRadians));
  xRotated = (zPosition * math.sin(rotationAngleRadians)) + (xPosition * math.cos(rotationAngleRadians));
  yRotated = yPosition;
  if rowBrightestPosition != -1:
   result1 =result1 + str(xRotated) + "," + str(yRotated) + "," + str(zRotated) + "\r\n";

 rotationAngleDegrees = rotationAngleDegrees + degreesPerStep
 f = open('scannerOutput1.asc', 'a')

print "Done."


  1. hello,
    could you open source the android app ? thanks,

  2. Hi Jason! I became interested in your project! I am a researcher at a university in Brazil, and I wonder if you released the android app! thank you!

  3. Is it worked??
    I had problem with z calculation...
    Can anyone help?

  4. Very interesting blog. A lot of blogs I see these days don't really provide anything that I'm interested in, but I'm most definitely interested in this one. Just thought that I would post and let you know scanning