- Spring 2025: a car, a lot, a roll of tape
UCSD's ECE/MAE 148 gives each team a 1/10-scale car, a ROS 2 software stack and a parking lot. Team 3's final project had the car follow a dashed line of yellow tape and, when its camera spotted garbage, drive to it, stop, and scoop it up with a servo arm. David wrote all of the software. The rest of the team built the car itself, including the 3D-printed parts.
- Seeing the tape
The lane node started from the course TA's example and was reworked for this car. It watches one band of each frame (rows 50 to 70 percent of the height, the middle 70 percent of the width), keeps pixels whose HSV color falls in the tuned yellow range (hue 18 to 50, saturation at least 75, value at least 145), cleans the mask with a blur, one erosion and four dilations, and keeps only blobs whose rotated bounding box is between 15 and 112 px wide. The steering error is where the blobs sit relative to a centre line placed at 55 percent of the width. With one dash in the band the error points at that dash; with several, the node averages them on a straight and picks the nearest line outside the threshold band on a curve. The team's calibration photo shows them tuning with a much taller band (62 percent of the height) than the final file keeps. Panel 1 runs that pipeline on every frame with each calibration value on a slider.
- One camera, two readers
Everything starts at the OAK-D Lite. The camera node runs the Roboflow garbage detector on the camera itself and publishes the frame, a detection flag, a steering error toward the nearest detection, and the widest box's width in pixels. The lane node reads the same frame and publishes its own steering error on /centroid. The guidance node listens for detections for three seconds at startup and then commits to one source for the whole run, so a run is either lane-following or garbage-seeking, while the width topic can stop the car in both.
- Closing the loop
Each steering error becomes a /cmd_vel message: steering from a PID controller (Kp 0.2, Ki 0, Kd 0.1) clamped to plus or minus 1, and throttle scheduled on the error, full 0.2 while the error stays inside the 0.15 band and easing linearly to 0.1 as the error grows, so the car slows into corners. The derivative divides by a fixed 1/20 s while frames arrive at 10 Hz, which doubles the effective Kd; panel 3 lets you compare the two.
- The garbage run
When the widest detection passes 200 px, the guidance node sends zero throttle and spawns the servo node, which drives a PCA9685 over I2C to swing the scoop arm from 0 to 100 degrees and collect the item. After five seconds the node runs a fixed maneuver of five timed steps (right 80 degrees, forward 0.4 m, back 0.4 m, left 80 degrees, forward 0.34 m), then waits 1.2 s and resumes.
- Rebuilt for this page (2026-09-23)
The TypeScript ports were written with AI coding tools and checked against David's original node files, which the build imports and runs under stand-in ROS modules. Across 12 rendered frames and 8 parameter sets, the lane port publishes the same /centroid values and produces every intermediate image bit for bit (OpenCV 4.11). The guidance port sends the same 448 /cmd_vel messages and log lines over a 45-second message stream, including two sweeps. What is simulated: the camera frames (a synthetic lot seen through a 640x480 camera, a width read from the team's own detector screenshot), the detections (garbage boxes projected into that camera, standing in for the Roboflow model), and the car's physics (a bicycle model with assumed wheelbase, steering lock and speed per throttle).