MAE 106 Mechanical Systems Laboratory · UC Irvine · seven-person team · Spring 2025

Compressed-Air Line-Following Car

A compressed-air piston engine turns the wheels, and a camera and a grayscale sensor have to find the lane on their own. I owned both ends of that — the pneumatic powertrain, the grayscale sensor, and an eight-neighbourhood border tracer run from both ends of the frame, so that a lane broken by a crossing or by worn paint still leaves a continuous centerline for the steering angle to be measured off.

The car on the test track: a low carbon-fiber chassis with a single tall support column, a scooter tire slid over the top of that column serving as the air store, air lines running down it, and a black painted lane curving across the white floor.
Distance on one fill
62.3 mmeasured · 86% of the 72.67 m the air should give
Lane reconstruction
94%100 real-world frames · within 5 px
Vision throughput
45 fps188 × 120, CPU only, no GPU

Part 1

An engine that runs on nothing but stored air

bench test — cylinder, crank, flywheel
The engine on the bench, off the car. Air in on the left, then piston, connecting rod, crank, flywheel on the right — the whole conversion from stored pressure to rotation.
The same mechanism as a CAD motion study — this is how the geometry was checked before anything was cut.

The air path, end to end

A 5 L tyre charged to 2 bar is the whole tank. Air passes a 2-position 5-port hand-lever valve mounted directly above the cylinder — supply in, two ports out to the cylinder's opposite chambers, two exhausts. Pulling or pushing its rod swaps which chamber is pressurized and which is venting.

That rod is not left to a hand. A bracket ties it to the piston rod, so the piston's own travel drags the valve across at the end of each stroke and reverses the air path on itself. The cylinder reciprocates unattended, and the eccentric turns.

The cylinder is 20 mm bore × 45 mm stroke. Its piston drives a connecting rod and crank, and the crank drives the wheel through a 2GT belt on two 60-tooth pulleys — 1:1, chosen for the tooth profile because backlash here shows up as lost stroke. One full out-and-back cycle turns the wheel exactly once.

What it actually returned

Expanding 5 L from 2 bar down to 0.3 bar through that cylinder, against a 10 cm wheel, works out to 72.67 m. The car ran 62.3 m.

The rest of the chassis

  • A single column carrying the tyre, not four — the load goes down one centered path, which cut mass and left the deck clear for the air lines
  • Parallelogram steering — a servo at the center of the chassis drives both front wheels through a parallelogram linkage
  • Carbon-fiber deck, cut with large openings — the car has to move on a fixed and very small energy budget, so every gram is a meter

Part 2

Rebuilding the lane where it disappears

bidirectional 8-neighborhood tracing · Kalman-smoothed reconstruction
The RealSense is clamped to the chassis at wheel height in a ring bracket, angled down at the floor ahead.

The problem, and the result

The centerline is one pixel per row, and the steering angle is the mean of those over the bottom 15 rows, at about 0.4° per pixel — fifteen pixels are the only thing the car steers on. And a lane is not continuous: crossings break it by design, worn paint by accident.

A conventional tracer, walking upward from the bottom, stops at the first break. Worse, when the break sits low the pass never gets going and the centerline collapses to a vertical line down the middle — which the angle calculation reads as a perfectly straight road, at the moment the car is on a broken one.

On the right is the whole point of the method, before any of the mechanism: frames that a single-direction tracer gives up on, and the centerline carried straight across the crossing.

Getting a clean binary first

Grayscale frame, 5 × 5 Gaussian blur, histogram equalization, then Otsu's method — plus a hand-tunable bias on top of Otsu's answer. Raising it closes gaps, lowering it sharpens the lane but lets noise in. That one knob does most of the work of adapting to a new floor.

Four rows, two columns. Left: original grayscale frames — the top one a clean uninterrupted lane as the control case, the three below it cut through by a crossing. Right: the same frames binarized, with a thin centerline running continuously across each break.
Left, what the camera sees. Right, what comes out. Top row is the control case, the three below it are crossings — the case the method was written for.
Left: the original frame in which the lane has worn away into two separate patches with a wide gap between them. Right: the algorithm's own binarized view of the same frame, with the reconstructed centerline drawn in pink running down the upper patch, across the gap, and on down the lower patch.
A harder case: the lane is worn through, leaving two patches and nothing between. Left the frame, right the binarization, reconstructed centerline in pink — traced down the upper patch, carried across, picked up again below.

Walking the border, twice

The tracer itself is not mine. Eight-neighborhood border following is a standard technique, and so is the Otsu binarization in front of it. What follows is what I built on top of them.

From the center of the bottom row, walk outward until the color flips — one seed on the lane's left edge, one on its right. From each, the tracker scans the eight neighbors in a fixed rotation, and every time the scan crosses into white the last black pixel becomes the next seed. Run once, it stops dead at the first break.

So the same routine runs again from the top down, rotations mirrored. Each pass survives until it hits the defect from its own side: the bottom-up trace dies at y₁, the top-down at y₂, and the band between them is what has to be rebuilt. A break is called on a row-to-row jump of more than 5 px going up, 3 px coming down.

Two things the seed search has to survive. A start row with no colour flip makes it step toward the middle, up to 30 rows, rather than give up — give up and y₂ stays −1 and reconstruction never fires. And when the lane sits entirely to one side, scanning outward finds only the near border; the far one is recovered by walking that white run to its end.

Filling the gap

Least-squares fit each side to x = ky + b — x as a function of row, so a near-vertical lane stays well-conditioned — on the 10 rows above y₂ and the 10 below y₁. Extrapolate both across the gap; the centerline is the row-by-row midpoint.

Two anchors rather than one, because a single-ended extrapolation lands wherever its slope points — across a wide gap, nowhere near the lane it is meant to rejoin. The two are crossfaded by row index on a t ²(3 − 2t) weight, so the handover happens in the middle. If neither end has two usable rows, the patch is skipped.

A 2-state Kalman filter — position and slope, row index playing the part of time — then runs down the rebuilt segment, predicting each row forward along the current slope before applying its measurement.

Ablated, this stage earns almost nothing. Across six synthetic gap cases, switching the filter off moves mean error from 2.09 px to 2.05 px — marginally better without it, because the span it smooths is already an analytic blend of two straight lines, and the measurement noise it exists to reject was removed one stage earlier.

The second anchor is what actually earns it. Scored row by row against synthetic ground truth across six gap cases — 130 patched rows — extrapolating from the upper fit alone gives 3.61 px mean and 13 px worst; using both ends gives 2.09 and 5 px. From one end the error accumulates monotonically: the patched line runs 4–5 px to the left of the lane, then snaps from column 103 straight back onto the true lane at 108 at the bottom of the band.

A binarized X-shaped junction with the extracted centerline drawn in three colours: blue below the break, green carried across the reconstructed span, red above it.
The three bands, color-coded. Blue is the bottom-up trace at the bottom, red the top-down trace at the top, and green is the reconstructed span — the piece carried straight across the crossing, where the lane's own borders are gone.
The track, in Webots A figure eight.
What the perception loop reports The grey line down the middle of the lane is the computed centerline; the log underneath is the per-frame angle and the steering command.
Same 100 real-world frames · 188 × 120
Metricsingle-directionthis method
Breakpoint recovery48%94%
Mean positional error16.3 px2.1 px
Centerline smoothness (σ)6.71.6
Throughput, CPU only45 fps

Every case

The algorithm running, case by case

synthetic camera frames driven through the real C implementation — left is the input, right is what it decides
Straight The control case: nothing to repair, so nothing should be invented.
Curve A single bend, still unbroken — the reconstruction path should stay out of the way.
Crossing The lane merges into the crossing bar, so the border trace has no edge left to follow.
Short gap The two passes die from opposite sides; the span between them is what gets rebuilt.
Long gap Twice the gap. Still bridged, but this is where the straight-line bridge starts to show — error roughly doubles.
S-bend Two reversing curves, no break — the same check as Curve, harder.

Part 3

Two sensors that disagree, and which one to believe

The grayscale sensor board photographed at an angle over a black-and-white boundary: one long PCB with eight downward-facing probe housings in a row along its underside, and an I2C header at one end.
The bar itself — eight probes on one board, read over I²C. No lens, no exposure, no frame to process: it reports eight numbers and it reports them immediately.
Plan view of the car on a curving lane. Eight probes are drawn in a row across the nose at −36.5, −27.5, −16.5, −5.5, 5.5, 16.5, 27.5 and 36.5 mm; the four on the right are filled black because the lane passes under them, and the midpoint of those is marked. A vertical line of length y = 100 mm runs back from the bar to a point between the two wheels, and the angle theta between that line and the line to the midpoint is marked in red, with the relation tan theta = x over y.
The whole method in one drawing. x is the midpoint of the probes that see black, y is the fixed 100 mm back to the point between the wheels, and the steer is the angle between them.

The camera is not the only thing watching the lane. An 8-channel grayscale bar reads over I²C at the car's nose — eight probes at fixed, known positions across the front, spanning ±36.5 mm. Any probe reading darker than the threshold is over the line.

The angle falls out of one piece of geometry: average the positions of the probes seeing black to get where the lane crosses the bar, join that to the turning centre a fixed 100 mm behind, and the angle of that line is the steer being asked for.

offset = mean { pi : gi < 170 }
θgray = arctan ( offset / 100 mm ) p = ±5.5, ±16.5, ±27.5, ±36.5 mm — probe positions across the nose; gi is that probe's reading. An arctangent, not a scale factor, because what is measured is the angle of a real line between two points on the car.

So there are two independent estimates of the same quantity, several times a second, and they do not always agree.

Why not just average them

The obvious answer is a fixed weighting, and that is what I wrote first — 0.7 × camera + 0.3 × grayscale, still in the source, commented out. It fails in the case that matters: when one sensor is wrong it drags the other with it, in proportion to a weight chosen before anything went wrong.

What shipped keys off disagreement instead:

  • Within 10° of each other — both are probably fine, take the mean
  • Further apart than that — one of them has been fooled, so take the one whose reading changed least since the last frame

The assumption: a sensor that has just jumped is the one that lost the line — to a crossing, glare, or worn paint. Which sensor that is changes moment to moment.


What I actually did

X-GB was a seven-person team for MAE 106; chassis fabrication and the circuit build were shared. Mine were the two halves this page is about: the pneumatic powertrain and the perception work on top of it. The eight-neighborhood tracer and Otsu binarization are existing techniques; mine is the reconstruction — both-ended tracing, break detection, the fit and extrapolation across the gap, the merge, and the fusion rule.

The comparison figures above are against a single-direction baseline I implemented for the purpose; they are not a benchmark against published systems.