Projects

DIY 3D Scanner

An Arduino pan-and-tilt scanner with a Sharp infrared distance sensor that turns each sweep into a 3D point cloud.

Boston, MA Principles of Integrated Engineering, Mini Project 2
The completed 3D scanner: an Arduino Uno on a breadboard wired to a pan-and-tilt mount with an MG996R servo and a Sharp infrared distance sensor on a white 3D-printed base
The finished scanner, with the Arduino on a breadboard and the 3D-printed pan-and-tilt head carrying the Sharp IR sensor.

For PIE Mini Project 2 we built a 3D scanner from an Arduino, two servos and a Sharp GP2Y0A02YK0F infrared distance sensor. The Arduino streams the readings over serial and a Python script converts them to Cartesian points and plots the point cloud; a scan of a cardboard letter K came out at 547 points after filtering.

37
pan positions (0–180° in 5° steps)
19
tilt positions (0–90° in 5° steps)
703
points per scan (37 × 19)
~70 s
scan time (100 ms per position)

Overview

The servos sweep the sensor across the scene in pan and tilt, so each reading is a distance at a known pair of angles. Converting those spherical readings to Cartesian coordinates gives a point cloud of whatever is in front of the scanner. The two hard parts were the sensor, which is noisy and follows an inverse power law rather than a straight line, and the trade-off between angular resolution and scan time.

Design

The printed parts are a base block that holds the pan servo, a round platform on the pan servo that carries the tilt servo, and a U-shaped bracket on the tilt servo that holds the sensor. The bracket geometry was chosen so it clears the base through the full tilt range.

CAD render of the full scanner assembly: a gray base block, a servo on top spinning a round platform, and an orange bracket holding a second servo and the Sharp IR sensor
CAD model of the full assembly, with the base, pan servo, rotating platform and the tilt bracket holding the sensor.
CAD render of the gray base block with the pan servo and its cross horn mounted on top
The base block with the pan servo.
CAD render of the round spinning platform with the tilt servo mounted vertically in an orange bracket
The rotating platform with the tilt servo.
CAD render of the orange U-shaped sensor mount holding the Sharp IR distance sensor
The U-shaped sensor bracket.

Electronics and control

Wiring schematic: an Arduino Uno R3 powers the IR sensor from 5V with its data line on A1, and drives two servo motors from pins D6 and D7, each on +5V and ground
Wiring, with the IR sensor on A1, the servos on D6 and D7, and everything on the Arduino's 5 V rail.

Software

Arduino

The Arduino steps the pan servo from 0 to 180° and the tilt servo from 0 to 90° in 5° increments, waits 100 ms at each position, reads the sensor on an analog pin and prints pan, tilt and the raw ADC value over serial.

Arduino · scanning control
// Arduino scanning control
#include <Servo.h>

Servo panServo, tiltServo;

void loop() {
  for (int pan = 0; pan <= 180; pan += 5) {
    panServo.write(pan);
    for (int tilt = 0; tilt <= 90; tilt += 5) {
      tiltServo.write(tilt);
      delay(100);

      int distance = analogRead(A0);
      Serial.print(pan); Serial.print(",");
      Serial.print(tilt); Serial.print(",");
      Serial.println(distance);
    }
  }
}

Python

The Python script reads the serial data, converts each ADC value to a distance with the calibration curve below, converts spherical to Cartesian coordinates and plots the points with matplotlib.

Python · processing and visualisation
# Python data processing and visualization
import numpy as np
import matplotlib.pyplot as plt

def spherical_to_cartesian(r, theta, phi):
    x = r * np.sin(np.radians(phi)) * np.cos(np.radians(theta))
    y = r * np.sin(np.radians(phi)) * np.sin(np.radians(theta))
    z = r * np.cos(np.radians(phi))
    return x, y, z

# Process scan data
points = []
for pan, tilt, sensor in scan_data:
    distance = 29.988 * pow((sensor * 5.0/1023.0), -1.173)
    x, y, z = spherical_to_cartesian(distance, pan, tilt)
    points.append([x, y, z])

# 3D visualization
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
ax.scatter(points[:,0], points[:,1], points[:,2])
plt.show()

IR sensor calibration

The Sharp GP2Y0A02YK0F outputs an analog voltage that falls with distance, following an inverse power law rather than a straight line. I measured the voltage at known distances and fitted a power-law curve:

Calibration: distance (cm) = 29.988 × V-1.173

where V is the sensor voltage (0–5 V). The Arduino's 10-bit ADC gives a value from 0 to 1023, so V = (ADC_value × 5.0) / 1023. The sensor's usable range is 20–150 cm; it is most accurate between 30 and 100 cm.

Spherical to Cartesian

Each reading is a point in spherical coordinates (r, θ, φ); the plot needs Cartesian (x, y, z):

x = r × sin(φ) × cos(θ)
y = r × sin(φ) × sin(θ)
z = r × cos(φ)

Filtering

The IR readings are noisy: ambient light, surface reflectivity and electrical noise all show up in the data. Filters used:

Angular resolution

Scan resolution is set by the servo step size. I used 5° steps as a compromise between point density and scan time: 37 pan positions by 19 tilt positions is 703 points per scan, at 100 ms per position about 70 s. A finer step (2°, say) gives a denser cloud but the scan time grows in proportion. 5° is enough detail for objects 20–50 cm across.

Results

The scanner on a workshop table pointed at a large cardboard letter K standing upright, with the Arduino and breadboard beside it and a laptop in the background
The scanner sweeping across a cardboard letter K.
Matplotlib side-view plot titled Horizontal Scan showing a row of red points at a constant Z of about 11.5 cm spread across Y from −13 to −8.5 cm, with a gap where the letter has no material
One horizontal sweep seen from the side, a flat row of points with a gap where the letter has no material.
3D scatter plot titled 3D Scan Results (547 points) showing the shape of the letter K in X, Y and Z centimetres, with points coloured by distance from 24 to 31 cm
The full 3D point cloud of the letter K, 547 points after filtering, coloured by measured distance.

The scanner produced usable point clouds of several objects within the sensor's 20–150 cm range, with the readings streaming to the plot as the scan ran. The letter K above came out at 547 points after filtering.

What I learned

The sensor was the core of the project. Its output follows an inverse power law rather than a straight line, so the calibration curve fitted from measurements at known distances is what makes the distances real, and the filtering (moving average, range check, median of repeated samples and the surface normal check) handles the noise from ambient light and surface reflectivity. On the tooling side I got comfortable with Arduino programming and serial communication, Python and matplotlib, CAD for the printed parts, servo control and timing, and spherical-to-Cartesian coordinate transforms.