Introduction: DO YOUR OWN SCANNER 3D
The purpose of this project was to make a scanner:
1. That can scan pieces with maximun dimensions of 25cm in height and 10cm in width.
2. Cost-friendly
3. With the detail list of all materials used
The end result was very successfull and hopefully this instructable will help more people try it and have fun while learning.
YOUTUBE link of a scan:
https://youtu.be/oZjiIRMD-Ho
Step 1: The Hardware
The hardware of the scanner was made from scratch by machining each of the aluminum pieces used.
Three pieces were also designed specially to be printed in 3D, taking into account the advantages that this process entails in the moment of rapid prototypes.
List of materials in hardware for the Scanner structure:
-Worm gear, Acme 8mm (30cm)
-Bearings 608zz
-4 Bearings Sc8uu
-2 Linear rods (30cm)
-Acrilic Base
-30 Screws M4 15mm
-Metal Screeds
-Rivets and rivet gun
Step 2: 3D Printing Pieces
As mentioned before some of the pieces were made by using a 3D printer to facilitate the process of machining.
We used the software of CAD NX 8.
-The first piece (Top Left piece) was the base for the bearing 608zz. This piece is screwed to the top of the 3D scanner. (base_balero.stp)
-The second piece (Top Right piece) was designed to hold the sensor that will read the object. This piece is screwed to the screw spindle and 4 bolts sc8uu from the rear while the front is screwed the sensor. Being the piece that more geometric constraints presented turned out to be the most complicated to design. Due to variations in the dimensions of the characteristics generated by the machining done to the base of the scanner, within the design of this piece had to counteract these inaccuracies, adding complexity to it. The resulting piece was able to meet all required geometric constraints as well as to place the sensor at a acceptable distance for its work. (base_sensor.stp)
-The third piece (Buttom Left piece) is responsible for holding the rotating disk on which the desired scanned object is placed. This piece also fulfills the function of connecting the step motor with the disc, it was designed with nut holes that allowed it to better fit the piece to the engine shaft. (base_disco.stp)
-The fourth piece (Buttom Right piece) was made in acrilic by a company called Finito Lab. This base is responsible for holding the piece to be scanned. It is 12cm in diameter and has the four holes designed for the step motor to be fitted. (disco.stp)
Step 3: Assembly
For the assembly of the structure we need to join all of our hardware together. First we take our previously machined aluminum slabs. The two largest one will be top and bottom of a rectangular box that will be created once the rivet joins the other medium length slabs mechanically. Once we have the rectangular box and additional large slab should be joined standing just at the back. Now its time to set the 3D printed pieces in our assembly.
First we take the worm and settle it through the spindle. Then the 3D printed piece which is the base for the sensor should be also included in the spindle and joined mechanically with bearing. Bearing will aswell be supporting the two lineal rods. At the top of the spindle another bearing should be set with the second 3D printed piece which is attatched to the aluminum slab.
Once we have done the steps described above its turn to add the stepper motors with screws from the botom just leaving the shaft of the motor upside from the rectangular aluminum box. Then two couplers should be added to each of the stepper motors. One is fixed to the spindle and the other to the round rotating base. Its important to mention that this second couple was also a 3D printed part in order to get it fixed correctly to the rotating base.
In this way there is no more to do other than settling the sensor through the 3D printed sensor base and making the corresponding electronic connections.
Step 4: Electronics
Once the 3D scanner is assembled you are ready to add the electronic components.
The ones used for this scanner are the following:
- Arduino Uno
- CNC Shield Arduino
- 2 Stepper Motor NEMA 17
- Sharp sensor GP2Y0A41SK0F
- 2 Pololu drivers A4988
- Limit switch
- Charger 12V (Power source)
In the picture you can see how the electrical connections where made.
Be careful while placing the drivers in the CNC Shield, make sure the pines are correctly place and located referring to the shield.
Also, remember that is important to connect the motor terminals correctly because this is basic for the direction to where your motors supposed to move.
Step 5: Scanning the Piece
Now you are ready to scan your first piece.
Through the Arduino platform the connection with the infrarred sensor and the stepper motors was made, data was printed in the Serial Monitor and saved in an text file.
//Declare variables
int scannerValues;
int csPin=10;
int sensePin=A5;
int tStep=2;
int tDir=5;
int tEnable=8;
int push=13;
int zStep=4;
int zDir=7;
int value;
void setup()
{
//Define stepper pins as digital output pins
pinMode(tStep,OUTPUT);
pinMode(tDir,OUTPUT);
pinMode(tEnable,OUTPUT);
pinMode(zStep,OUTPUT);
pinMode(zDir,OUTPUT);
pinMode(push,INPUT);
//Set rotation direction of motors
digitalWrite(tDir,HIGH);
digitalWrite(zDir,HIGH);
//delay(100);
//Enable motor controllers
digitalWrite(tEnable,LOW);
// Open serial communications
Serial.begin(9600);
Serial.println(value);
}
void loop()
{
int vertDistance=10; //Total desired z-axis travel
int noZSteps=150; //No of z-steps per rotation. Distance = noSteps*0.05mm/step
int zCounts=50; //Total number of zCounts until z-axis returns home 15
//int thetaCounts=400;
int thetaCounts=200;
// Scan object
digitalWrite(zDir,HIGH);
delay(5000);
for (int j=0; j
{
for (int i=0; i
{
rotateMotor(tStep, 1); //Rotate theta motor one step
delay(200);
//double senseDistance=0; //Reset senseDistanceVariable;
double senseDistance=readAnalogSensor(); //Read Sharp sensor, calculate distance
}
Serial.print('\n');
rotateMotor(zStep, noZSteps); //Move z carriage up one step
delay(200);
}
// Scan complete. Rotate z-axis back to home and pause.
digitalWrite(zDir,LOW);
delay(10);
for (int j=0; j
{
rotateMotor(zStep, noZSteps);
delay(10);
}
for (int k=0; k<3600; k++) //Pause for one hour (3600 seconds), i.e. freeze until power off because scan is complete.
{
delay(1000);
}
}
//Rotation of platform
void rotateMotor(int pinNo, int steps)
{
for (int i=0; i
{
digitalWrite(pinNo, LOW); //LOW to HIGH changes creates the
delay(1);
digitalWrite(pinNo, HIGH); //"Rising Edge" so that the EasyDriver knows when to step.
delay(1);
//delayMicroseconds(500); // Delay so that motor has time to move
//delay(100); // Delay so that motor has time to move
}
}
//Read sensor data, mean and display
double readAnalogSensor()
{
//int noSamples=10;
int noSamples=100;
int sumOfSamples=0;
float senseValue=0;
float senseDistance=0;
for (int i=0; i
{
senseValue=analogRead(sensePin); //Perform analogRead
delay(2); //Delay to let analogPin settle -- not sure if necessary
sumOfSamples=sumOfSamples+senseValue; //Running sum of sensed distances
}
senseValue=sumOfSamples/noSamples; //Calculate mean
senseDistance=senseValue; //Convert to double
senseDistance=mapDouble(senseDistance,0.0,1023.0,0.0,5.0); //Convert analog pin reading to voltage
//Serial.print("Voltage: "); //Debug
//Serial.println(senseDistance); //Debug
//Serial.print(" | "); //Debug
//Sharp sensor GP2Y0A41SK0F calibration formula
senseDistance= -176.88*pow(senseDistance,6)+1154.3*pow(senseDistance,5)-3059.7*pow(senseDistance,4)+4197.3*pow(senseDistance,3)-3113.1*pow(senseDistance,2)+1149.8*senseDistance-139.8; //Convert voltage to distance in cm via cubic fit of Sharp sensor datasheet calibration
//Print to Serial - Debug
//Serial.print("Distance: "); //Debug
//Serial.println(senseDistance); //Debug
//Serial.print(senseValue);
//Serial.print(" | ");
Serial.print(senseDistance);
Serial.print("\t");
return senseDistance;
}
double mapDouble(double x, double in_min, double in_max, double out_min, double out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
Step 6: 3D Plotting
The data printed in the Serial Monitor has to be copy and paste in a text file that is going to be used in a Matlab program that take cares of plotting the 3D graph corresponding to the scanned piece.
Remember to change the name of the .txt in the Matlab program in order for you to 3D plot the correct data.
Here is the Matlab program:
//Declare variables
int scannerValues;
int csPin=10;
int sensePin=A5;
int tStep=2;
int tDir=5;
int tEnable=8;
int push=13;
int zStep=4;
int zDir=7;
int value;
void setup()
{
//Define stepper pins as digital output pins
pinMode(tStep,OUTPUT);
pinMode(tDir,OUTPUT);
pinMode(tEnable,OUTPUT);
pinMode(zStep,OUTPUT);
pinMode(zDir,OUTPUT);
pinMode(push,INPUT);
//Set rotation direction of motors
digitalWrite(tDir,HIGH);
digitalWrite(zDir,HIGH);
//delay(100);
//Enable motor controllers
digitalWrite(tEnable,LOW);
// Open serial communications
Serial.begin(9600);
Serial.println(value);
}
void loop()
{
int vertDistance=10; //Total desired z-axis travel
int noZSteps=150; //No of z-steps per rotation. Distance = noSteps*0.05mm/step
int zCounts=50; //Total number of zCounts until z-axis returns home 15
//int thetaCounts=400;
int thetaCounts=200;
// Scan object
digitalWrite(zDir,HIGH);
delay(5000);
for (int j=0; j
{
for (int i=0; i
{
rotateMotor(tStep, 1); //Rotate theta motor one step
delay(200);
//double senseDistance=0; //Reset senseDistanceVariable;
double senseDistance=readAnalogSensor(); //Read Sharp sensor, calculate distance
}
Serial.print('\n');
rotateMotor(zStep, noZSteps); //Move z carriage up one step
delay(200);
}
// Scan complete. Rotate z-axis back to home and pause.
digitalWrite(zDir,LOW);
delay(10);
for (int j=0; j
{
rotateMotor(zStep, noZSteps);
delay(10);
}
for (int k=0; k<3600; k++) //Pause for one hour (3600 seconds), i.e. freeze until power off because scan is complete.
{
delay(1000);
}
}
//Rotation of platform
void rotateMotor(int pinNo, int steps)
{
for (int i=0; i
{
digitalWrite(pinNo, LOW); //LOW to HIGH changes creates the
delay(1);
digitalWrite(pinNo, HIGH); //"Rising Edge" so that the EasyDriver knows when to step.
delay(1);
//delayMicroseconds(500); // Delay so that motor has time to move
//delay(100); // Delay so that motor has time to move
}
}
//Read sensor data, mean and display
double readAnalogSensor()
{
//int noSamples=10;
int noSamples=100;
int sumOfSamples=0;
float senseValue=0;
float senseDistance=0;
for (int i=0; i
{
senseValue=analogRead(sensePin); //Perform analogRead
delay(2); //Delay to let analogPin settle -- not sure if necessary
sumOfSamples=sumOfSamples+senseValue; //Running sum of sensed distances
}
senseValue=sumOfSamples/noSamples; //Calculate mean
senseDistance=senseValue; //Convert to double
senseDistance=mapDouble(senseDistance,0.0,1023.0,0.0,5.0); //Convert analog pin reading to voltage
//Serial.print("Voltage: "); //Debug
//Serial.println(senseDistance); //Debug
//Serial.print(" | "); //Debug
//Sharp sensor GP2Y0A41SK0F calibration formula
senseDistance= -176.88*pow(senseDistance,6)+1154.3*pow(senseDistance,5)-3059.7*pow(senseDistance,4)+4197.3*pow(senseDistance,3)-3113.1*pow(senseDistance,2)+1149.8*senseDistance-139.8; //Convert voltage to distance in cm via cubic fit of Sharp sensor datasheet calibration
//Print to Serial - Debug
//Serial.print("Distance: "); //Debug
//Serial.println(senseDistance); //Debug
//Serial.print(senseValue);
//Serial.print(" | ");
Serial.print(senseDistance);
Serial.print("\t");
return senseDistance;
}
double mapDouble(double x, double in_min, double in_max, double out_min, double out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
El programa de Arduino es cargado, este se tarda aproximadamente 40 minutos para que termine de escanear una pieza para continuar a copiar los datos obtenidos del escaneo del monitor serial y guardarlos en un archivo de texto “positions#.txt” para su próximo despliegue en gráfica con el programa de Matlab.
Programación Matlab
clear all;
clc;
%% Processing variables
maxDistance=18.5; %Upper limit -- raw scan value only scanning "air"
minDistance=3.5; %Lower limit -- raw scan value error: reporting negative reading
midThreshUpper=0.001; %Offset radius threshold around 0
midThreshLower=-midThreshUpper; %Offset radius threshold around 0
windowSize=3; %Window size for average filter to clean up mesh
interpRes=1; %Interpolation resolution, i.e. keep every interRes-th row
centerDistance=11.5;%9.2; %[cm] - Distance from scanner to center of turntable
zDelta=0.25;
rawData=importdata('positions4.txt');
num=numel(rawData);
disp(num);
rawData(rawData<0)=0; %Remove erroneous scans from raw data
%rawData(rawData=='X')=[];
%disp(rawData);
indeces=numel(rawData);
disp(indeces);
%indeces1=200*40*20; %Find indeces of '9999' delimiter in text file, indicating end of z-height scan
%disp(indeces1);
% Arrange into matrix, where each row corresponds to one z-height.
column=50;
%cont=indeces/column;
rawData(1,:)=[];
r=reshape(rawData,[200,column]);
r=r';
disp(r);
r(r>maxDistance)=NaN; %Remove scan values greater than maxDistance;
r(r
r=centerDistance-r; %Offset scan so that distance is with respect to turntable center of rotation
r=abs(r);
%disp('r');
%disp(r);
%Remove scan values around 0
midThreshUpperIdx=r>midThreshLower;
midThreshLowerIdx=r
midThreshIdx=midThreshUpperIdx.*midThreshLowerIdx;
r(midThreshIdx==1)=NaN;
% Create theta matrix with the same size as r -- each column in r corresponds to specific orientation
%theta=0:360/size(r,2):360;
theta=360:-360/size(r,2):0;
theta(end)=[];
theta=repmat(theta,[size(r,1) 1]);
%disp('theta');
%disp(theta);
theta=theta*pi/180; %Convert to radians
% Create z-height array where each row corresponds to one z-height
z=0:zDelta:size(r,1)*zDelta;
z(end)=[];
z=z';
z=repmat(z,[1,size(r,2)]);
%disp('r');
%disp(r);
[x,y,z]=pol2cart(theta,r,z); %Convert to cartesian coordinates
%disp('x');
%disp(x);
%disp('y');
%disp(y);
%disp('z');
%disp(z);
%Replace NaN values in x, y with nearest neighbor at the same height
for i=1:1:size(x,1)
if sum(isnan(x(i,:)))==size(x,2)
x(i:end,:)=[];
y(i:end,:)=[];
z(i:end,:)=[];
break;
end
end
for i=1:1:size(x,1)
latestValueIdx=find(~isnan(x(i,:)),1,'first');
latestX=x(i,latestValueIdx);
latestY=y(i,latestValueIdx);
for j=1:1:size(x,2)
if isnan(x(i,j))==0
latestX=x(i,j);
latestY=y(i,j);
else
x(i,j)=latestX;
y(i,j)=latestY;
end
end
end
%Resample array based on desired mesh resolution
interpIdx=1:interpRes:size(x,1);
xInterp=x(interpIdx,:);
yInterp=y(interpIdx,:);
zInterp=z(interpIdx,:);
%Smoothe data to eliminate more noise
h=fspecial('average',windowSize); %Define average filter
%h=fspecial('gaussian',10,1.25); %Define gaussian filter
xInterp=padarray(xInterp,[0, windowSize],'symmetric'); %Add symmetric duplicate padding along rows to correctly filter array edges
yInterp=padarray(yInterp,[0, windowSize],'symmetric'); %Add symmetric duplicate padding along rows to correctly filter array edges
% xInterp=filter2(h,xInterp); %Filter x
% yInterp=filter2(h,yInterp); %Filter y
xInterp=xInterp(:,windowSize:end-windowSize-1); %Remove padding
yInterp=yInterp(:,windowSize:end-windowSize-1); %Remove padding
%Force scan to wrap by duplicating first column values at end of arrays
xInterp(:,end)=xInterp(:,1);
yInterp(:,end)=yInterp(:,1);
zInterp(:,end)=zInterp(:,1);
%Add top to close shape
xTop=mean(xInterp(end,:));
yTop=mean(yInterp(end,:));
xInterp(end+1,:)=xTop;
yInterp(end+1,:)=yTop;
zInterp(end+1,:)=zInterp(end,1)-zInterp(end-1,1)+zInterp(end,1);
%surf(xInterp,yInterp,zInterp); %Plot point cloud as a mesh to verify that processing is correct
plot3(xInterp,yInterp,zInterp,'.b'); %Plot point cloud as a mesh to verify that processing is correct