Skip to content

Lane proposals must be on the road, and the drivable surface becomes editable - #26

Merged
Sherin-SEF-AI merged 3 commits into
mainfrom
feat/lidar-3d-module
Jul 29, 2026
Merged

Lane proposals must be on the road, and the drivable surface becomes editable#26
Sherin-SEF-AI merged 3 commits into
mainfrom
feat/lidar-3d-module

Conversation

@Sherin-SEF-AI

Copy link
Copy Markdown
Owner

Two changes, both from a report that "the drivable area is not showing properly in the canvas".

It was showing correctly. Isolating the layers proved it: the polygon traces the road surface,
hugging the motorcycle wheels, wrapping the autorickshaw, following the kerb on one side and a
parked car on the other. Two other things were wrong, and this PR fixes both.

A proposed lane has to be on the road

The same frame carried six proposed lanes, every one an edge of a blue and white striped
hoarding, spanning up to 79% of the frame width and drawn as thick diagonals across the sky.
They buried the drivable overlay and made a correct segmentation look broken.

A lane detector finds bright linear structure, and an Indian dashcam frame is full of bright
linear structure that is not a lane: hoardings, flyover girders, kerbs, awnings. The
disqualifying fact was already computed, because the drivable mask says where the road is.

Three decisions carry it:

  • The margin. A lane boundary runs along the road's edge and its points legitimately fall
    outside it, so strict containment would delete exactly the road-edge lanes the ontology cares
    most about. Points may sit within 3% of frame width of the surface.
  • Missing evidence is not evidence of absence. A frame nobody has segmented keeps its lanes.
  • The threshold came from the corpus. Over 4,525 stored lanes, 97.8% sit on the road and
    2.2% do not: a filter cutting a tail rather than a rule purging a population.

A geometric test was tried first and thrown out. Rejecting a lane wider than it is tall reads as
obviously right, and on this corpus would have removed 73.6% of all lanes, because near the
horizon a lane really is nearly horizontal. Measuring it is the only reason it is not in here.

Wired into both creation paths, the interactive proposer and the pod ingest, and both report
what they rejected rather than quietly halving their own output. The stored corpus was swept the
same way: 101 implausible proposals removed, human-drawn lanes never touched.

The drivable surface becomes editable

DrivableMask has carried a human source since M2.2 and it could never be set. The refine
endpoint existed, was correct, and had no caller: nothing in the web client spoke to it, the
canvas drew the polygons with listening={false}, and the free-space tools were declared in the
editor registry and consumed by nothing. source read proposed on 2,478 of 2,479 masks.

Regions are now drawn, dragged and deleted on the same canvas that draws the lanes, in the three
classes the ternary mask carries. Fallback is offered alongside drivable rather than folded into
non-drivable, because the unpaved shoulder is what India drives on.

Verification

  • 9 new tests for the plausibility filter, and the ones that matter are the refusals to refuse:
    the road-edge lane just outside the polygon, the frame with no mask, the road split by a
    traffic island, the degenerate two-point polygon.
  • Full suite 1,304 passing, against the same 5 pre-existing failures tracked in
    tests/KNOWN_FAILURES.md (2 need a local Ollama, 3 are test-ordering pollution).
  • Drivable editing driven end to end in the browser: drew a fallback region, closed it, watched
    the PUT fire, saw three regions become four with the coverage readout following, confirmed
    source flipped to human in the database, then restored the frame to the model's output.

Reported as "the drivable area is not showing properly". It was showing correctly. Isolating the
layers proved it: the polygon traces the road surface, hugging the motorcycle wheels, wrapping
the autorickshaw, following the kerb on one side and a parked car on the other.

What was wrong sat on top of it. The same frame carried six proposed lanes, every one an edge of
a blue and white striped hoarding, spanning up to 79% of the frame width and drawn as thick
diagonals across the sky. They buried the drivable overlay and made a correct segmentation look
broken.

A lane detector finds bright linear structure, and an Indian dashcam frame is full of bright
linear structure that is not a lane: hoardings, flyover girders, kerbs, awnings. The
disqualifying fact was already computed, because the drivable mask says where the road is, and a
lane sits on the road while a line across the sky does not.

Three decisions carry it.

The margin, without which this would be worse than the problem. A lane boundary runs along the
edge of the drivable region and its points legitimately fall outside it, so strict containment
would delete exactly the road-edge lanes the ontology cares most about. Points may sit within 3%
of the frame width of the surface.

Missing evidence is not evidence of absence. A frame nobody has segmented keeps its lanes; a
proposer that produced nothing until somebody had run the segmenter first would be a worse
failure than this one.

And the threshold came from the corpus rather than from me. Scored over 4,525 stored lanes,
97.8% sit on the road and 2.2% do not, which is a filter cutting a tail rather than a rule
purging a population.

A geometric test was tried first and thrown out. Rejecting a lane wider than it is tall reads as
obviously right, since a lane runs toward the vanishing point, and on this corpus it would have
removed 73.6% of all lanes: near the horizon a lane really is nearly horizontal. Measuring it is
the only reason it is not in here, and the note stays in the module so nobody adds it back.

Wired into both paths that create lanes, the interactive proposer and the pod ingest, and both
report what they rejected rather than quietly halving their own output. The stored corpus was
swept the same way: 101 implausible proposals removed, human-drawn lanes never touched, and the
six on the frame that started this are gone.

Nine tests, and the ones that matter are the refusals to refuse: the road-edge lane just outside
the polygon, the frame with no mask at all, the road split by a traffic island, the degenerate
two-point polygon that is a line rather than a region.
DrivableMask has carried a `human` source since M2.2 and it could never be set. The refine
endpoint existed, was correct, and had no caller: no function in the web client spoke to it, the
canvas drew the polygons with `listening={false}`, and the free-space tools were declared in the
editor registry and consumed by nothing. `source` read `proposed` on 2,478 of 2,479 masks, and
the one exception was a curl I ran while auditing.

So the layer was machine output a person could look at and not fix, which is a visualisation
rather than a label.

Regions are now drawn on the same canvas that draws the lanes, in the three classes the ternary
mask actually carries. Fallback is offered alongside drivable rather than folded into
non-drivable, because the unpaved shoulder is what India drives on and a surface model that
cannot say so is describing a different country.

A region and a lane get separate in-progress buffers. A lane is an open polyline and a region is
a closed area, so sharing one would render whichever was not being drawn incorrectly. The
in-progress outline is tinted in its own class colour, so which surface the next click adds to is
visible rather than remembered. Three points is the minimum that encloses anything; fewer is a
stray click, and closing it would write a degenerate sliver into the coverage statistics.

Verified by driving it: drew a fallback region, closed it, watched the PUT fire, saw the count go
from three regions to four and the coverage readout follow, confirmed `source` had flipped to
`human` in the database, then put the frame back to the model's output byte for byte.
…reasoner thought

Three additions, each closing a gap where the data existed and nothing could reach it.

## The review queue can see the reasoner's conflict

The ranking used uncertainty, diversity, rarity and an error term currently firing on 40% of the
corpus at a near-constant score. Meanwhile the reasoning layer computes, on every object, a
measure of how much the evidence disagrees with itself, stores it in provenance, and the queue
had no idea it was there.

Conflict rather than score, deliberately. A low score means the evidence agrees the label is
wrong, and the gate already routes those. Conflict means the evidence cannot settle: the
detector is confident and physics says impossible, or every path proposed a different class.
Those are the objects where a human adds the most and where nothing else in this ranking can
look. An adjudicate verdict counts as conflict whatever the number says, because that is the
layer stating out loud that it could not decide.

On the live queue it surfaces objects scoring 0.57 conflict with an error term of zero, which
no other term could have found.

## Behaviour search across the corpus

Every event route shipped session-scoped. That is right for reviewing a drive and cannot answer
the question the layer exists for, because "every illegal lane change while a signal was red" is
a fact about the fleet.

The conjunction is the part that matters and the part that is not a filtered list: it is a
temporal join within each session, done in SQL so the limit still means something. `within_ms`
widens both sides so "while" can mean "around", since a crossing a second after the light
changed is still a crossing at that light. Severity is expanded to its kinds before the query
rather than filtered after the fetch, for the same reason as everywhere else in this codebase.

Results carry the city, the vehicle, the actor's class and the offset into the drive, so an
answer is a list of places to go rather than a count. The page states the question back in
words, because a row of dropdowns says what is set and a sentence says what was asked, and that
is what somebody reads before trusting a zero.

## Approach on red, named for what the evidence carries

The question worth answering is whether the ego entered on red. The honest answer is that this
corpus cannot support it: ego speed exists on 846 frames of 40,222 and on none of the 173
sessions holding signal phases. Built on `ego_speed` it would have returned nothing on every
session anybody opened.

What the corpus does have is 3,061 signal tracks, and a signal's apparent geometry is an
ego-motion instrument: approaching a fixed object makes it grow and drift down the frame. So the
claim is the weaker, supportable one, and the kind is `signal_approach_on_red` rather than
`ran_red_light` because a growing box says the gap is shrinking and does not say a stop line was
crossed. Severity is notable, not violation, for the same reason.

It fires on almost nothing here and the reason is upstream and already reported: 91% of signal
phases in this corpus are a single frame, and exactly one red or amber phase spans the four
frames a trend needs. That is the label noise the flicker detector flags, not a fault in the
rule, and every refusal says which threshold it failed and why.

Both measures take a median at each end rather than a mean. A detector that loses the signal for
one frame emits a box at the origin, and a mean over three samples was enough to invert the
direction of travel and turn a real approach into a refusal.

16 tests. Suite 1,320 passing against the same 5 pre-existing failures.
@Sherin-SEF-AI
Sherin-SEF-AI merged commit 0634f52 into main Jul 29, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant