← All projects

Autonomous Driving & Sign Reading in ROS

ENPH 353 machine-learning competition, UBC. With Raymond Wang, I built a controller for a simulated robot that drives a four-zone Gazebo course, dodges moving obstacles, and reads eight clue boards along the way.

57 / 57
full points, all 8 signs read
~40 s
to complete the course
99.8%
CNN validation accuracy
120 FPS
driving camera loop
Top-down map of the Gazebo competition course: paved road, dirt road, off-road section and mountain tunnel
The course: paved road, dirt road, an off-road section with a wandering Baby Yoda, and a tunnel up a mountain.

Architecture

Three ROS nodes. RobotController subscribes to the camera topics and publishes velocity commands on /B1/cmd_vel; it runs a state machine that picks a driving strategy per zone and captures sign images as it passes them. SignReader turns those images into strings for /score_tracker. A PyQt GuiApp starts and stops the run, teleports between zones for testing, and shows live frames, masks and per-frame timing.

Software flowchart: GuiApp, RobotController and SignReader with their ROS topics
Nodes and topics
PyQt control GUI showing raw camera frame, driving mask and last captured sign
The control GUI: raw frame, driving mask, last captured sign, 1 ms per frame.

Two cameras, two jobs

We shrank the robot and stacked two cameras on it. A 400×300 camera at 120 FPS feeds driving; a 3000×1500 camera is only subscribed to for the instant a sign is in view. Staying subscribed to it all the time dropped the frame rate, and at the speeds we were tuned for, a slow loop means erratic driving.

Default robot next to the modified robot with a lower body and two stacked cameras
Default robot (left) and ours: lower centre of mass, two stacked cameras.

Driving

Driving is proportional control on a single image row: find the target loc, normalize its offset from centre to an error in [−1, 1], steer against it, and slow down linearly as the error grows. What changes per zone is how loc is found: the left road edge on pavement, the white line (after erosion, dilation and small-contour removal) on dirt, the centroid of a pink marker in the roadless section, and on the mountain a white-line threshold that scales with scene brightness because the road starts dark and gets brighter. At 120 Hz it finished the course about 95% of the time.

Mountain zone camera frame and its road mask
Zone 4: brightness-adaptive thresholds on the mountain road.

Reading the signs

The robot never stops for a sign. The high-resolution frame is masked by the sign's blue border, perspective-warped to a flat rectangle, then split into characters: grayscale, Gaussian blur, adaptive Gaussian thresholding, and contour detection in the two text regions. Each bounding box is resized to 43×56 and classified by a small CNN.

Raw high-resolution frame with a clue board at an angle
Raw frame at speed
Clue board after homography, flat and ready for character segmentation
After the perspective warp

The network is two conv/pool stages, dropout, and three dense layers into a 36-way softmax (A–Z, 0–9), small enough not to slow the run. It trains on synthetic data from a generator that renders characters and applies small rotations, affine warps, edge enhancement and blur.

R vs P, 5 vs S

The first model looked fine on paper and still confused visually similar pairs. The confusion matrix made the failure specific, so the fix could be too: generate extra samples targeted at R/P and 5/S, move data from validation into training, add dense capacity, and retrain. Final accuracy was 100% on training and 99.8% on validation, and every sign was read correctly in the competition.

Confusion matrix of the first CNN showing off-diagonal errors
Before: errors on similar characters
Confusion matrix after retraining, nearly diagonal
After targeted data and retraining

What I'd change

The robot lost 1–2 seconds re-aligning at the tunnel entrance, and pedestrian and truck avoidance relied on start timing rather than detection. With more time, a drone would have side-stepped collisions entirely.

Stack: ROS, Gazebo, Python, OpenCV, TensorFlow/Keras, PyQt.