Autonomous Line Following Robot
Camera-based line tracking on a Pico 2, from chassis CAD to closed-loop steering
- Context
- Northwestern coursework
- Role
- Individual project — mechanical design, electronics, and control
Goal
Design and fabricate a robot that autonomously follows a line using vision alone.
Photo — the finished robot in hand
Hardware
An OV7670 camera interfaced to a Raspberry Pi Pico 2, with two DAGU gearmotors driven by PWM.
Mechanical
Designed the chassis in SolidWorks with mounting for the motors, camera, battery pack, and breadboard. 3D printed the chassis and the spoked wheels, with O-rings seated on the rims as tires for traction.
Vision and Control
The controller thresholds a single camera row and steers on its center of mass. For that row it sums per-pixel brightness to get a row average, then re-scans the row and pushes every pixel either to black or white against that average — a self-adjusting threshold that tolerates changing lighting. It then computes the intensity-weighted center of mass in x.
Video — raw camera feed before thresholding
// threshold and then find the center of mass of a row
int findLine(int row){
int pos = 0;
int r = row*IMAGESIZEX; // find the index of the start of the row in the pixel array
int sumMass = 0;
int sumMassR = 0;
int i;
// find the row average brightness
int sumBright = 0;
for(i=0;i<IMAGESIZEX;i++){
sumBright = sumBright + picture.r[r+i] + picture.g[r+i] + picture.b[r+i];
}
int avgBright = sumBright / IMAGESIZEX;
// threshold the row
for(i=0;i<IMAGESIZEX;i++){
int mass = picture.r[r+i] + picture.g[r+i] + picture.b[r+i];
if (mass < avgBright){
// not bright enough, set pixel to black
picture.r[r+i] = 0;
picture.g[r+i] = 0;
picture.b[r+i] = 0;
}
else {
// set to white
picture.r[r+i] = 255;
picture.g[r+i] = 255;
picture.b[r+i] = 255;
}
}
// calculate the center of mass of the thresholded row
for(i=0;i<IMAGESIZEX;i++){
int mass = picture.r[r+i] + picture.g[r+i] + picture.b[r+i];
sumMass = sumMass + mass;
sumMassR = sumMassR + mass*i;
}
float centerOfMass = (float) sumMassR / sumMass;
return (int)centerOfMass;
}
The offset between that centroid and the frame center drives the motors: inside a small deadband both motors run at half duty and the robot goes straight; outside it, the inside motor’s duty is scaled down proportionally to the offset to turn, and past a hard limit that motor is cut to zero for a sharp turn.
int main()
{
while (true) {
// B is right
// A is left
// move straight
if(offset > -5 && offset < 5) {
pwm_set_gpio_level(PWMA, WRAP / 2);
pwm_set_gpio_level(PWMB, WRAP / 2);
}
else {
// turn left --> decrease speed of left
if(offset < 0) {
pwm_set_gpio_level(PWMB, WRAP / 4);
if(offset < -30) {
pwm_set_gpio_level(PWMA, 0);
}
else {
float duty = .25 - (1.0/4.0/25.0)*(-1*offset - 5);
pwm_set_gpio_level(PWMA, WRAP * duty);
}
}
//turn right --> decrease speed of right
else {
pwm_set_gpio_level(PWMA, WRAP / 4);
if(offset > 30) {
pwm_set_gpio_level(PWMB, 0);
}
else {
float duty = .25 - (1.0/4.0/25.0)*(offset - 5);
pwm_set_gpio_level(PWMB, WRAP * duty);
}
}
}
}
}
Result
Completed a curved track autonomously.
Video — the robot running the curved track