hero-image
HOME
hero-image

3D LiDAR Scanner For Medical Imaging

hero-image
Ayaan Desai

Project Timeline

Jan 2025 - Apr-2025

OVERVIEW

- Designed and built a 3D LiDAR scanner from scratch using Fusion360 and 3D printing for structural components - Systematically created schematics in Ki-CAD to integrate a Time-Of-Flight sensor, 2 servo motors, and an ESP 8266. - Integrated LiDAR sensor technology with C++ programming to capture and process 3D spatial data - Developed software for real-time mapping and visualization, improving scanning accuracy and efficiency - Demonstrated the scanner’s ability to generate high-resolution 3D models for medical imaging applications

HighlightS


SKILLS

Electrical SystemsEmbedded SystemsC++RoboticsMedical ImagingComputer Aided DesignMicrocontroller programming

SUPPORTING MATERIALS

Additional Details

Arduino IDE code:

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <FS.h>

#include "Adafruit_VL53L0X.h"
Adafruit_VL53L0X tof = Adafruit_VL53L0X();


const char *renderer = 
#include "Renderer.h"

#include <Servo.h>

ESP8266WebServer server(80);

//servo instances
Servo yawServo;
Servo pitchServo;

/************** configuration *************/
const int pinTrigger = 12; //D6
const int pinEcho = 13; //D7
const int pinServoYaw = 16; //D0
const int pinServoPitch = 14; //D5

const bool useTimeOfFlight = true;  //false: ultrasonic, true: tof

//angle step per measurement. 1 best resolution (slowest), >1 lower res
const int stepYaw = 1;
const int stepPitch = 1;
//readings to avarage per position (more readings give softer results but take longer)
const int avgCount = 1;

// limit scan range
const int yawRange[] = {0, 80};
const int pitchRange[] = {0, 90};

// limit scan distance
const float validRange[] = {0.05, 0.30};
const float offset = 0.012;

// add offset to position calculation
const float yawOffset = -45;
const float pitchOffset = -45;

// delays for scans
static const int fixedDelay = 20;
static const int delayPerStep = 10;


//read ultrasonic distance
float readDistanceUltrasound()
{
  digitalWrite(pinTrigger, LOW);
  delayMicroseconds(2);
  digitalWrite(pinTrigger, HIGH);
  delayMicroseconds(10);
  digitalWrite(pinTrigger, LOW);
  return pulseIn(pinEcho, HIGH, 26000) * 0.000345f * 0.5f; 
}

//read fime of flight distance
float readDistanceTimeOfFlight()
{
  VL53L0X_RangingMeasurementData_t measure; 
  tof.rangingTest(&measure, false);

  if (measure.RangeStatus != 4)
    return measure.RangeMilliMeter * 0.001f;
  else
    return 0;  
}

//perform a scan and write points to vertices.js as an array that can be used by js and webgl
void scan()
{  
  yawServo.attach(pinServoYaw);
  pitchServo.attach(pinServoPitch);
  //this centers the servos according the range
  yawServo.write((yawRange[0] + yawRange[1]) / 2);
  pitchServo.write((pitchRange[0] + pitchRange[1]) / 2);
  delay(3000);

  //opening a file for write.. overwriting if exists
  File f = SPIFFS.open("/distance.js", "w");
  if (!f) 
  {
    Serial.println("file open failed");
    return;
  }
  //writing a JS array directly into the file, this get's included in the HTML
  f.print("var vertices = new Float32Array([");
  //scanning the range
  for(int yaw = yawRange[0]; yaw < yawRange[1]; yaw += stepYaw)
  {
    //returning to the starting position of the pitch column
    yawServo.write(yaw);
    pitchServo.write(pitchRange[0]);
    delay(1000);

    //proceeding the pitch
    for(int pitch = pitchRange[0]; pitch < pitchRange[1]; pitch += stepPitch)
    {
      pitchServo.write(pitch);
      float d;

      //this part could be simplified.. ended up being similar for both sensor types
      if(useTimeOfFlight)
      {
        //a delay to have time to reach the position
        delay(fixedDelay + delayPerStep  * stepPitch);
        //averaging several readings 
        float avg = 0;
        int svgc = 0;
        for(int i = 0; i < avgCount; i++)
        {
          delay(40);
          float d = readDistanceTimeOfFlight();
          //only consider redings within the range
          if(d >= validRange[0] && d <= validRange[1])
          {
            avg +=d;
            svgc++;
          }
        }
        //calculate the averege
        d = avg / max(1, svgc);        
      }
      else
      {
        //similar as above
        delay(fixedDelay + delayPerStep * stepPitch);
        float avg = 0;
        int svgc = 0;
        for(int i = 0; i < avgCount; i++)
        {
          delay(10);
          float d = readDistanceUltrasound();
          if(d >= validRange[0] && d <= validRange[1])
          {
            avg +=d;
            svgc++;
          }
        }
        d = avg / max(1, svgc);
      }
      //if reading was not out of range, calculate the position and write to the JS array in the file... (and on the serial output)
      if(d > 0)
      {
        float yawf = (yaw + yawOffset) * M_PI / 180;
        float pitchf = (pitch + pitchOffset) * M_PI / 180;
        float od = offset + d;
        float x = -sin(yawf) * od * cos(pitchf);
        float y = cos(yawf) * od * cos(pitchf);
        float z = od * sin(pitchf);
        String vertex = String(x, 3) + ", " + String(y, 3) + ", " + String(z, 3);
        Serial.println(vertex);
        f.print(vertex + ", ");
      }
      else
        Serial.println("far");
    }
  }
  //closing arrays and files
  f.print("]);");
  f.close();
  yawServo.detach();
  pitchServo.detach();
}

//serve main page
void returnRenderer()
{
  server.send (200, "text/html", renderer);
}

//serve stored vertices
void returnVertices()
{
  File file = SPIFFS.open("/distance.js", "r");
  server.streamFile(file, "script/js");
  file.close();
}

//setup. executed first
void setup() 
{
   SPIFFS.begin();

  // Initialize Serial
  Serial.begin(115200);
  delay(3000);

  // Initialize Sensors
  if(useTimeOfFlight)
  {
    tof.begin();
  }
  else
  {
    pinMode(pinTrigger, OUTPUT);
  }

  // Turn off WiFi initially
  WiFi.mode(WIFI_OFF);   
  delay(1000);  // Give some time for the WiFi module to reset

  // Perform a scan
  scan();

  // Start WiFi Access Point
  WiFi.mode(WIFI_AP);  // Ensure it's set to Access Point mode
  delay(500);  // Wait for WiFi to initialize
  if (WiFi.softAP("Scanner")) {
    Serial.println("WiFi Access Point Ready");
  } else {
    Serial.println("Failed to start AP");
  }
  
  Serial.print("Soft-AP IP address = ");
  Serial.println(WiFi.softAPIP());

  // Start Web Server
  server.on("/", returnRenderer);
  server.on("/vertices.js", returnVertices);
  server.begin();  
}

void loop() 
{
  //handle the server
  server.handleClient();
}

The project showcased was first intended to use ultrasound technology rather than LiDAR, but due to technical complications, LiDAR was chosen. The code above allows the use of either technology. This project utilized a VL530X ToF sensor for distance readings, paired with two servos (yaw and pitch), and an ESP8266 microcontroller. The microcontroller was programmed to take readings every 1 degree, controlled by the yaw servo. After the yaw servo completed 180 degrees, the pitch would move 1 degree down. The depth and location of all these points were saved to a file, which was accessed by an open-source renderer using WebGL. This allowed me to create a point cloud of a 3D object in digital space, which was then using the ESP8266's wifi capabilities communicated to a device.

A few scans I was able to achieve:

Screenshot 2025-11-18 235448.png

Screenshot 2025-11-18 235436.png

CAD Files:


Screenshot 2025-11-18 235504.png

Screenshot 2025-11-18 235516.png

Screenshot 2025-11-18 235528.png


lowinertia
Portfolio Builder for Engineers
Created by Aram Lee
© 2025 Low Inertia. All rights reserved.