An engine that runs on nothing but stored air
bench test — cylinder, crank, flywheelThe 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
Rebuilding the lane where it disappears
bidirectional 8-neighborhood tracing · Kalman-smoothed reconstructionThe 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.
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.
| Metric | single-direction | this method |
|---|---|---|
| Breakpoint recovery | 48% | 94% |
| Mean positional error | 16.3 px | 2.1 px |
| Centerline smoothness (σ) | 6.7 | 1.6 |
| Throughput, CPU only | — | 45 fps |
The algorithm running, case by case
synthetic camera frames driven through the real C implementation — left is the input, right is what it decidesTwo sensors that disagree, and which one to believe
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.
- Published — I wrote the algorithm up as a sole-authored journal paper: A Bidirectional 8-Neighborhood Border Tracking and Kalman-Based Centerline Reconstruction Method for Line-Following Robots, Applied and Computational Engineering 167, 122–129 (2025)
- Implemented — C and C++, CPU only on an embedded Linux board, no GPU and no NPU; the grayscale bar is read over I²C, the camera through OpenCV (source)
- Simulated — the perception loop was also exercised in Webots against a rendered track