# 09 — Stage 6 BEV cleanup

Date: 16 July 2026

Project: `classical-lidar-lane-detection`

Plan: [`07-Plan-From-Here-On.md`](07-Plan-From-Here-On.md)

## Executive summary

Here is the short version of what I built and what the result actually means.

- **The setup:** Stage 6 now takes the canonical path we agreed on: current
  Stage 3, raw Stage-4 reflectivity, and raw Stage-5 global Otsu. It does not
  consume the Stage-5.1 top-decile/row-budget proposal. I evaluated it on the
  same 40-frame development slice used in the earlier audits: ten evenly
  spaced labeled frames from each of four sequences. Thirty-nine were readable;
  the same known malformed sequence-4 PCD was recorded and skipped.

- **The problem:** Raw Stage 5 contains useful lane returns mixed with broad
  scan structures and scattered bright road clutter. The first, geometry-only
  cleanup was much too aggressive because genuine lane paint often appears in
  one LiDAR frame as short transverse scan fragments—not as neat little
  forward-facing lines. Rejecting every locally transverse component destroyed
  recall. I discarded that configuration instead of polishing a bad result.

- **What I implemented:** Candidates are rasterized at `288 x 288`, which is
  `0.16 m` forward by `0.08 m` lateral per cell. Stage 6 closes only short gaps
  in the forward direction, extracts connected components, records their
  physical span, orientation, elongation, support density, reflectivity, and
  point support, then conservatively removes weak long/wide structures. A
  separate chain pass can restore a weak compact fragment only when multiple
  narrow fragments line up ahead/behind it. The final mask is also reduced to
  K-Lane's `144 x 144` grid, while the `288 x 288` mask remains available for
  Stage 7.

- **The outcome:** On this development slice, K-Lane-tolerant precision rises
  from **22.59% to 25.75%**, recall changes from **13.84% to 13.63%**, and F1
  rises from **17.16% to 17.83%**. Put another way, cleanup removes 1,777
  tolerant false-positive cells while losing 46 true-positive label hits. It
  keeps 98.48% of the baseline true positives. Twenty-nine frames improve,
  five are unchanged, and five get worse.

- **The honest interpretation:** Stage 6 is doing the narrow job it can do:
  reducing false positives without a large recall collapse. It is not a cure
  for the low raw recall. Large magenta gaps remain in the plots because Stage
  5 never selected evidence there, and cleanup cannot create it. Stage 7 can
  connect supported observations with fitted geometry, but it should not be
  expected to recover lanes when the upstream anchors are absent.

- **One decision worth looking at:** The direct component filter produces
  **17.831%** tolerant F1. The dash-chain restoration adds six components but
  no additional tolerant true-positive hit on this slice, leaving final F1 at
  **17.825%**. The chain behavior is implemented and tested, but the real-data
  ablation does not currently justify claiming it helps. Before checkpoint 10,
  I recommend deciding whether to validate the complete configured output or
  disable chain restoration and validate the slightly better direct-filter
  output.

- **What I would check first:** Open the artifact index, then compare the best
  cleanup example with the worst one. In the plots, purple is what Stage 6
  removed; the final error map tells you whether those removals were useful.
  The exact links and legend are in the next section.

This checkpoint stops here. Checkpoint 10—the held-out validation and freeze
decision—has not started.

## What you can inspect

Start with the human-readable artifact index:

- [full visual and numeric index](../artifacts/09_stage6_bev_cleanup/README.md);
- [best observed F1 improvement](../artifacts/09_stage6_bev_cleanup/images/seq_04_001583943622370_stage6.png),
  from 23.06% to 25.13%;
- [largest observed regression](../artifacts/09_stage6_bev_cleanup/images/seq_01_001487706805290_stage6.png),
  from 24.63% to 23.71%;
- [complete per-frame table](../artifacts/09_stage6_bev_cleanup/benchmark.csv);
- [machine-readable aggregate](../artifacts/09_stage6_bev_cleanup/benchmark.json); and
- [per-frame component reports](../artifacts/09_stage6_bev_cleanup/reports/).

The code entry points are
[`bev_cleanup.py`](../src/lidar_lane_detection/bev_cleanup.py),
[`bev_cleanup_diagnostics.py`](../src/lidar_lane_detection/bev_cleanup_diagnostics.py),
and
[`bev_cleanup_benchmark_cli.py`](../src/lidar_lane_detection/bev_cleanup_benchmark_cli.py).
The synthetic behavior checks are in
[`test_bev_cleanup.py`](../tests/test_bev_cleanup.py).

## How to read each six-panel plot

Read panels 1 through 4 from left to right as an ablation:

1. **Raw Stage 5:** every green cell contains at least one raw global-Otsu
   candidate point.
2. **Longitudinal gap closing:** green survives unchanged; orange is a newly
   filled short gap in the same lateral column.
3. **Direct component filter:** green is retained; purple is rejected by the
   component shape, support, and strength policy.
4. **Final plus dash chains:** green passed directly; orange was restored only
   because compatible compact components formed an aligned forward chain.
5. **Ground truth:** magenta is a labeled K-Lane lane cell. The label is loaded
   only after prediction and never changes Stage 6.
6. **Strict error map:** white is an exact same-cell true positive, green is a
   false positive, and magenta is a false negative.

For every BEV panel, the vehicle is below the image, forward is toward the top,
and positive lateral `y` is to the left. The panel header reports tolerant F1
after every operation so you can tell which operation helped or hurt that
specific frame.

## How to read the result tables

- **Precision (P):** of what Stage 6 retained, how much is compatible with a
  labeled lane cell?
- **Recall (R):** of the labeled lane cells, how many have nearby retained
  evidence?
- **F1:** the balance between precision and recall.
- **Strict:** prediction and label must occupy exactly the same cell.
- **K-Lane tolerant:** a one-cell offset is accepted, matching K-Lane's
  confidence behavior; the outer border is not scored.
- **TP / FP / FN:** true-positive, false-positive, and false-negative cell
  counts micro-summed across all successful frames.

These are candidate-mask metrics, not final fitted-lane metrics. A candidate
mask is sparse by design; Stage 7 is responsible for turning good anchors into
continuous geometry.

## Exact operation-by-operation results

### K-Lane one-cell-tolerant metric

| Output after operation | TP | FP | FN | Precision | Recall | F1 |
|---|---:|---:|---:|---:|---:|---:|
| Raw Stage 5 | 3,023 | 10,360 | 18,819 | 22.59% | 13.84% | 17.16% |
| Short forward gap close | 3,023 | 10,442 | 18,819 | 22.45% | 13.84% | 17.12% |
| Direct component filter | 2,977 | 8,572 | 18,865 | 25.78% | 13.63% | 17.83% |
| Final plus dash chains | 2,977 | 8,583 | 18,865 | 25.75% | 13.63% | 17.83% |

The gap close alone is deliberately small and slightly negative: it adds local
continuity, not semantic evidence. Its value is that connected components can
then measure a short broken structure as one object. The component filter is
where the false-positive reduction occurs. The chain pass is neutral to
slightly negative on this development sample.

### Strict same-cell metric

| Output after operation | TP | FP | FN | Precision | Recall | F1 |
|---|---:|---:|---:|---:|---:|---:|
| Raw Stage 5 | 796 | 11,724 | 21,305 | 6.36% | 3.60% | 4.60% |
| Short forward gap close | 797 | 11,807 | 21,304 | 6.32% | 3.61% | 4.59% |
| Direct component filter | 779 | 9,872 | 21,322 | 7.31% | 3.52% | 4.76% |
| Final plus dash chains | 779 | 9,883 | 21,322 | 7.31% | 3.52% | 4.76% |

The large strict-versus-tolerant gap is expected for sparse LiDAR candidate
cells and a 144-grid label. Both are reported so a tolerance improvement
cannot hide exact-cell behavior.

## Per-sequence result

| Sequence | Frames | Raw tolerant F1 | Final P | Final R | Final F1 |
|---|---:|---:|---:|---:|---:|
| `seq_01` | 10 | 24.71% | 34.33% | 19.81% | 25.12% |
| `seq_02` | 10 | 15.42% | 39.74% | 9.65% | 15.53% |
| `seq_03` | 10 | 18.42% | 28.35% | 14.50% | 19.19% |
| `seq_04` | 9 | 8.10% | 8.02% | 10.12% | 8.95% |

Stage 6 improves aggregate F1 in every sequence, but sequence 4 remains much
weaker than the others. That agrees with checkpoint 08: sequence 4 often has
poor lane-corridor support before cleanup begins.

## The implemented cleanup policy

### 1. High-resolution rasterization

Raw candidate points are projected into `288 x 288` metric BEV cells. Each
cell retains the maximum raw reflectivity and the number of candidate points.
The mean frame contains 432.03 occupied high-resolution cells, which become
321.03 occupied cells on the `144 x 144` label grid.

### 2. Directed gap close

Only gaps of at most two empty high-resolution cells are closed, and only when
candidate evidence bounds the gap in the same lateral column. This bridges at
most `0.32 m` in the forward direction. There is no lateral dilation and no
six-cell-per-row budget.

### 3. Component measurements

Eight-connected components are measured in metric coordinates. Every report
contains:

- supported cells and morphology-filled cells;
- support density and candidate-point count;
- forward span and lateral span;
- PCA orientation and elongation;
- centroid;
- maximum reflectivity and its percentile among that frame's raw Stage-5
  candidate points; and
- whether the component passed directly, by a chain, or not at all.

### 4. Conservative component rejection

The first geometry-only draft rejected locally transverse components. On the
39 readable development frames, it collapsed tolerant recall from 13.84% to
1.76% and F1 from 17.16% to 3.13%. Component analysis showed why: short real
lane fragments often follow the LiDAR scan direction locally. Orientation is
still measured and used to constrain chains, but it is no longer a blanket
rejection rule.

The selected development policy is:

- keep singleton components because blanket singleton removal cost too many
  valid sparse anchors;
- for components with two to four supported cells, reject only those whose
  maximum strength is in the bottom 10% of this frame's Stage-5 candidates;
- for components with at least five supported cells and either more than
  `0.48 m` forward span or more than `0.64 m` lateral span, require maximum
  strength in the top 10% of the frame's Stage-5 candidates; and
- preserve other compact components.

That final top-decile test is not Stage 5.1. It applies only to a connected
component already flagged as a long/wide clutter shape; it does not globally
replace Otsu and has no per-row cell cap.

### 5. Dash-chain preservation

A weak compact component may be restored only through a connected graph of at
least three compact components whose centers are at most `3.0 m` apart, at
most `0.24 m` laterally offset, and at most 15 degrees off the forward
direction. The group must span at least `1.0 m` longitudinally. Synthetic tests
cover aligned restoration, lateral misalignment, and direct-component
anchoring.

On this real development slice, only six of 6,766 total components are restored
exclusively by this rule. They add 11 tolerant false-positive cells and no
tolerant true-positive label hit. That is why the chain should receive special
attention at the checkpoint-10 freeze decision.

## Development tuning and why it is not validation

The configuration was selected after inspecting operation ablations on these
39 readable frames. The main candidates tested included strict geometry-only
filtering, progressively wider fragment chains, compact-component strength
gates, and long/wide-component strength gates. The strict geometry-only policy
was rejected immediately because of its recall collapse. The final policy was
chosen for conservative true-positive retention rather than maximum precision.

Because configuration selection and these reported measurements use the same
frames, the numbers are development evidence only. They are not an unbiased
estimate of generalization. Checkpoint 10 exists to freeze the chosen policy
first and then run a separate held-out sample.

## Runtime and output size

Across 39 successful frames, Stage-6 cleanup alone takes:

| Statistic | CPU time |
|---|---:|
| Median | 21.17 ms |
| Mean | 27.68 ms |
| 95th percentile | 59.81 ms |

The final mask averages 364.18 occupied `288 x 288` cells and 273.38 occupied
`144 x 144` cells per frame, versus 432.03 and 321.03 before cleanup.

## Failure handling

`seq_04/001680832844300` declares 786,432 PCD values but contains only 160,567.
The benchmark records this `KLaneFormatError`, continues with the remaining
frames, and includes the failure in both the JSON and artifact index. This is
the same malformed source frame seen in checkpoints 06 and 08.

## Verification

The implementation passes all **34** repository tests. Six Stage-6-specific
tests verify:

- forward-only short-gap closing;
- retention of a supported lane-like component;
- rejection of a weak transverse clutter structure;
- aligned compact-fragment chain restoration;
- refusal to chain laterally misaligned fragments;
- use of a direct component as a chain anchor; and
- rejection of misaligned Stage-4/Stage-5 inputs.

The source tree also passes Python bytecode compilation.

Reproduce the development benchmark with:

```bash
uv run klane-stage6 /mnt/arnavs_hdd/data/02-lane-detection/K-Lane \
  --sequence 1,2,3,4 \
  --sample-count 10 \
  --output-dir artifacts/09_stage6_bev_cleanup
```

If the environment cannot reach PyPI but the existing virtual environment is
already present, the equivalent offline command is:

```bash
PYTHONPATH=src .venv/bin/python \
  -m lidar_lane_detection.bev_cleanup_benchmark_cli \
  /mnt/arnavs_hdd/data/02-lane-detection/K-Lane \
  --sequence 1,2,3,4 \
  --sample-count 10 \
  --output-dir artifacts/09_stage6_bev_cleanup
```

## Recommendation and stop gate

Stage 6 is complete enough for review: code, tests, per-operation ablations,
full component reports, development metrics, plots, legends, and failure cases
are all present.

My recommendation is to keep the conservative component filter, because it
removes a meaningful number of false positives while retaining 98.48% of raw
Stage-5 tolerant true positives. I would not yet freeze the dash-chain pass:
its behavior is correct in controlled tests, but it provides no positive
evidence on this development slice. Choose direct-filter output or full
chain-restored output before checkpoint 10, freeze that choice, and then test
it without retuning on held-out frames.

Do not start Stage 7 until checkpoint 10 has made the Stage-6 freeze decision.
