# Stage 3 report: dominant RANSAC road points

Date: 14 July 2026
Project: `classical-lidar-lane-detection`
Architecture plan: <https://qld-popularity-smoke-likely.trycloudflare.com/lidar_lane_detection_architecture_plan.html>

## 1. Outcome

Stage 3 is complete. The package now fits one deterministic dominant road
plane to the Stage-2 ROI and retains complete PCD rows within a fixed
point-to-plane distance. It exposes masks aligned to both the ROI and the raw
point cloud, reports the fitted plane and support statistics, and produces a
human-readable PNG for every inspected frame.

This stage extracts road-surface points. It does not yet decide which road
points are lane markings.

### How to read the Stage 3 plot

Both panels show only points that already passed the Stage-2 ROI crop; PCD
points rejected by that crop are not drawn. In the **left top-down panel**,
green points are RANSAC road-plane inliers and gray points are ROI points that
the plane rejected. Its vertical direction is forward distance (`x`, near at
bottom), and its horizontal direction is lateral position (`y`, vehicle-left
at left).

The **right residual panel** redraws those same ROI points with forward
distance on the horizontal axis and signed perpendicular distance to the plane
on the vertical axis; lateral position is intentionally collapsed. The center
of the orange band is the plane (`0 m`). With the plane normal oriented upward,
positive residuals are above it and negative residuals are below it. Green
points may be on either side: they pass because they are within `+/-0.12 m`.
Gray points are outside that band. The orange lines are the RANSAC distance
threshold, not the ROI boundary; very large residuals are clipped at the plot
edge.

## 2. Fixed road-plane contract

The implementation follows the architecture plan's fixed Stage-3 choice:

| Setting | Value | Purpose |
|---|---:|---|
| RANSAC iterations | 300 | Small fixed CPU budget for a single dominant plane. |
| Random seed | 0 | Identical samples and output for identical input. |
| Inlier distance | `<= 0.12 m` | Retain one fixed band around the plane. |
| Maximum plane tilt | `20 degrees` | Reject wall-like planes before support comparison. |
| Final refinement | Orthogonal least-squares plane on winning support | Reduce dependence on the three sampled points. |

The model is stored as a unit-normal plane:

```text
a*x + b*y + c*z + d = 0
```

Candidate planes are oriented with positive `c`. Their tilt is measured from
the horizontal road plane. RANSAC chooses the valid candidate with the most
inliers; equal-support candidates are resolved by lower mean inlier distance.
After one orthogonal refinement, the final inlier mask is recomputed with the
same `0.12 m` threshold.

The 20-degree check is necessary road semantics, not a second detector. A
purely largest-plane implementation can select a wall when wall returns
outnumber road returns. The synthetic suite includes exactly that case.

No parameter was tuned against K-Lane labels. Stage 3 never reads label,
intensity, or reflectivity values to fit or classify the road surface.

## 3. Code and interfaces added

- `src/lidar_lane_detection/road.py` contains the immutable configuration,
  deterministic RANSAC fit, plane/result records, aligned masks, and report.
- `src/lidar_lane_detection/road_diagnostics.py` creates the two-view PNG.
- `src/lidar_lane_detection/road_cli.py` runs the real-data inspection and
  writes both machine-readable and human-readable artifacts.
- `tests/test_road.py` covers tilted-road recovery, complete-row preservation,
  determinism, wall rejection, invalid configuration, and insufficient input.
- `pyproject.toml` exposes the workflow as `klane-stage3`.
- `README.md` and `src/lidar_lane_detection/__init__.py` expose the Stage-3
  boundary and public interface.

`RoadPointResult` contains:

- every retained complete PCD row;
- a Boolean inlier mask aligned to the Stage-2 ROI rows;
- a Boolean inlier mask aligned to all raw point-cloud rows;
- the unit plane normal and offset;
- signed point-to-plane distances for every ROI row;
- fixed configuration, support counts, support fraction, and residual summary.

Inputs are not mutated, and retained rows preserve their original order.

## 4. Human-readable artifacts

The artifact landing page is
[`../artifacts/stage3/README.md`](../artifacts/stage3/README.md). It links one
PNG and one JSON report for each frame. The PNG is the primary inspection
artifact:

- **Top-down road support:** green road-plane inliers over gray rejected ROI
  points, in the accepted K-Lane orientation (forward at top, positive lateral
  `y` at left).
- **Signed plane residual:** the same points plotted by forward distance and
  signed distance to the fitted plane. Orange horizontal lines are the fixed
  `+/-0.12 m` acceptance band.
- **Header:** frame conditions, road/ROI counts, support fraction, threshold,
  tilt, and the actual plane equation.

One representative panel is
[`../artifacts/stage3/seq_01_001270427447090_road_points.png`](../artifacts/stage3/seq_01_001270427447090_road_points.png).
The JSON files remain useful for reproduction, but they are no longer the only
way to inspect the result.

## 5. Automated behavior verified

The new tests establish that:

1. A noisy tilted road plane is recovered from mixed road and obstacle points.
2. Complete intensity/reflectivity rows stay aligned and in source order.
3. ROI and raw-point masks have the correct shapes and counts.
4. Repeated runs with the fixed seed produce identical models and masks.
5. A vertical wall with more points than the road is rejected by the tilt
   policy while the smaller horizontal road is recovered.
6. Fewer than three ROI points fail with a clear domain error.
7. Invalid distance, iteration, and tilt settings fail immediately.

The complete project suite result is:

```text
Ran 12 tests in 0.254s
OK
```

This includes the five Stage-1 tests, three Stage-2 tests, and four Stage-3
test methods. The complete source and test trees also pass Python bytecode
compilation.

## 6. Five-frame real-data run

The `klane-stage3` command reused the five evenly spaced sequence-1 frames from
the Stage-2 smoke run:

### How to read road share

Road share is the internal support ratio
`RANSAC road-plane inlier points / Stage-2 ROI points`. For example, the first
row is `13,714 / 19,272 = 71.16%`. It is not ground-truth road recall or an
accuracy score. K-Lane supplies lane-marking labels in a BEV grid, not a
ground/not-ground label for every raw PCD return, and Stage 3 intentionally
does not use labels. The ratio is therefore a diagnostic for how well one plane
explains the cropped scene, not a measure of how many true road points were
captured.

| Timestamp | ROI points | Road points | Road share | Plane tilt | Fit time |
|---|---:|---:|---:|---:|---:|
| `001270427447090` | 19,272 | 13,714 | 71.16% | 0.83 deg | 112.97 ms |
| `001334420901190` | 23,409 | 14,739 | 62.96% | 1.37 deg | 96.49 ms |
| `001412513600980` | 19,347 | 13,263 | 68.55% | 1.10 deg | 90.41 ms |
| `001455209231720` | 16,070 | 13,324 | 82.91% | 1.84 deg | 92.78 ms |
| `001501304913440` | 21,592 | 8,442 | 39.10% | 1.49 deg | 82.40 ms |

All five plane tilts are physically plausible for this sensor-frame smoke set,
and the panels show broad road-aligned scan support rather than a vertical
structure. The support fraction varies substantially. In particular, the last
frame's 39.10% support is visible in its panel and should be carried forward as
a limitation of one global plane, not hidden by averaging.

The fit-time median for this run was 92.78 ms. Times cover RANSAC and refinement
only; they exclude PCD parsing, ROI cropping, reporting, and PNG rendering. This
is a smoke measurement, not a formal benchmark.

The machine-readable run manifest is
[`../artifacts/stage3/seq_01_road_validation.json`](../artifacts/stage3/seq_01_road_validation.json).

## 7. What deserves your attention

- Green scan lines are **road-plane points**, not detected lane markings. Lane
  paint becomes distinguishable only when Stage 4 uses reflectivity.
- The single-plane model visibly loses support where the surface is not well
  represented by one plane. That is the intended V1 limitation.
- The fixed `0.12 m` band includes returns close to the road plane, which can
  include low non-road objects. Later reflectivity and geometry stages—not a
  larger Stage-3 ruleset—must reject those for lane detection.
- Sequence 1 is `urban, night, uncrowded`. This smoke run verifies operation and
  inspectability on real K-Lane data; it is not a cross-condition evaluation.
- Fit timing varies by machine and load. The seed fixes the sampled models, not
  wall-clock duration.

## 8. Explicitly not done

To preserve the YAGNI boundary, this stage did not add:

- multiple planes, progressive ground segmentation, a height grid, or curb
  logic;
- adaptive thresholds, configuration search, or label-driven tuning;
- reflectivity/Otsu thresholding;
- BEV morphology, connected components, or lane-line fitting;
- metrics, temporal accumulation, ML, GPU, ROS, or company-data paths;
- interactive plotting or a new visualization dependency.

The PNG writer reuses the existing Pillow dependency.

## 9. Reproduction

Run from the project root:

```bash
env UV_CACHE_DIR=/tmp/uv-cache uv sync

env UV_CACHE_DIR=/tmp/uv-cache uv run klane-stage3 \
  /mnt/arnavs_hdd/data/02-lane-detection/K-Lane \
  --sequence 1 \
  --sample-count 5 \
  --output-dir artifacts/stage3

env UV_CACHE_DIR=/tmp/uv-cache uv run python -m unittest discover -s tests -v
env UV_CACHE_DIR=/tmp/uv-cache uv run python -m compileall -q src tests
```

## 10. Stage status and next boundary

Stage 3 is implemented, unit-tested, exercised on five real K-Lane frames,
visually inspectable, and documented. No Stage-3 issue is open.

The next architecture stage is to use K-Lane reflectivity from these retained
road points. Reflectivity thresholding has not been started here.
