Skip to content

Validate zip members before extracting KITTI - #1

Merged
MKamel7 merged 6 commits into
mainfrom
checkkk-fixes
Sep 1, 2026
Merged

Validate zip members before extracting KITTI#1
MKamel7 merged 6 commits into
mainfrom
checkkk-fixes

Conversation

@MKamel7

@MKamel7 MKamel7 commented Aug 30, 2026

Copy link
Copy Markdown
Owner

fetch_kitti called zf.extractall() with no member validation while its sibling
fetch_vkitti already used tarfile's filter="data". ZipFile has no equivalent,
so the check is written out: an entry resolving outside the target directory
now refuses to unpack. The archives come from a fixed official URL, so this
guards against that URL or host changing rather than against KITTI.

fetch_kitti called zf.extractall() with no member validation while its sibling
fetch_vkitti already used tarfile's filter="data". ZipFile has no equivalent,
so the check is written out: an entry resolving outside the target directory
now refuses to unpack. The archives come from a fixed official URL, so this
guards against that URL or host changing rather than against KITTI.
… slice

Three roadmap items, and the first two turn out to be the same refactor.

ONE MATCHER. scripts/render_demo.py carried its own copy of the greedy
descending-score assignment loop. Close to the real one and not identical: it
had no notion of neutral ground truth, so a box on a Person_sitting counted as
claiming nothing in the picture while the metric declined to score it at all.
The demo and the reported number could disagree about the same frame and
nothing in the repository could notice. ape.match.judge_frame now does the
matching and keeps the per-object detail; assign_frame is a view of it that
throws the detail away, and the demo asks the same function the metric does.
tests/test_outcomes.py asserts the two cannot diverge across seven frame
shapes, including the ones where matching order decides the answer.

The pycocotools comparison still agrees, which is the real evidence the
refactor changed nothing: 238 tests pass, up from 218.

A TAXONOMY FOR THE FALSE POSITIVES. ape.localisation already splits the MISSES
into "never seen" and "seen and boxed badly" and explains why that matters.
The false positives had no such split, so a detector that fires twice on one
pedestrian and a detector that invents pedestrians in empty road produced the
same number. ape.outcomes sorts every wrong box into duplicate, misclassified,
mislocalised or hallucinated, reading each verdict off the match the metric
already performed rather than matching a second time.

Measured on KITTI with yolov8s:

  Car         33752 FP   1% duplicate   0% misclassified  16% mislocalised  83% hallucinated
  Pedestrian  10808 FP   0% duplicate   6% misclassified   8% mislocalised  86% hallucinated

Four fifths of the wrong boxes are on nothing at all, which is the category a
safety argument cares about most: the only one that makes a vehicle brake for
empty road, and the only one with no cause visible in the ground truth.
Duplicates are almost absent, so NMS is not the problem.

MORE THAN AP PER SLICE. false_negative_rate and recall_at_precision, both read
off the precision envelope for the same reason AP is. Reported by the CLI and
carried in results.json.

  Car         FNR 18.2%   recall at 90% precision 64.7%   at 50% 80.9%
  Pedestrian  FNR 31.3%   recall at 90% precision  0.2%   at 50% 60.7%

The pedestrian row is the finding, and it is the argument for the whole item.
An AP of 0.506 reads as a mediocre but usable detector. It is not usable at
high precision at all: demand 90% precision and it returns two pedestrians in a
thousand. No threshold buys both, and the aggregate hides that completely.
Three items and one decline. Metamorphic robustness first because it gives
most of the domain-shift story from data already fetched, and because it
proves the harness can express a degradation result before any licence-gated
download is worth doing.
A second dataset changes the scene, the camera, the labelling policy and the
class balance at once, so a drop in AP has four candidate causes and the result
is a number rather than a finding. A perturbation changes exactly one thing by
a stated amount and leaves the ground truth identical, which is what makes
these metamorphic relations rather than augmentations.

Five perturbations, each one a camera actually suffers: brightness, contrast,
blur, JPEG round-trip and a fog veil. `Detector.detect_image` is extracted so a
perturbed frame is scored through the same pipeline as the baseline; a sweep
that ran a different pipeline would be measuring the pipeline.

Measured over 500 KITTI frames with yolov8s, worst drop against the
unperturbed baseline:

    blur 4 px          Car -16.3%   Pedestrian -17.4%
    contrast 0.8       Car -13.0%   Pedestrian -17.6%
    jpeg quality 10    Car  -8.4%   Pedestrian -11.7%
    fog 0.6            Car  -8.0%   Pedestrian  -7.2%
    brightness +/-0.6  Car  -0.2%   Pedestrian  -2.2%

Three findings. Exposure is free and defocus is not, so a pipeline worrying
about tunnel mouths is worrying about the wrong thing. Pedestrians degrade
faster than cars under every perturbation except fog, and the gap widens with
strength: at blur radius 2 it is 3.3% against 8.5%. And nothing falls off a
cliff, which is a result a single number at one operating point could not have
shown either way.

Crop is deliberately excluded. It moves the boxes, so the ground truth would
have to be transformed with it, and a bug in that transform would be
indistinguishable from a real drop.

The fog is a uniform veil rather than depth-aware, so it understates the
distance dependence that matters most for ADAS. Said in the module, in the
report and in the README rather than left for a reader to assume otherwise.

32 tests on the perturbations themselves: strength zero is exactly the
identity, size and mode are preserved, each one changes the image, each is
deterministic, and each does the specific thing it claims rather than merely
something.
The overall figures and every slice cell already carried a 95% interval; the
Easy, Moderate and Hard rows did not, and they sat in the same table as the
overall row that did. Reporting them as bare numbers beside a row showing a
range invites exactly the comparison the intervals exist to prevent: Easy
against Hard looks like a finding until you can see how much of the gap the
sample explains.

Same bootstrap, resampling frames rather than objects, and the tiers are now
assembled per frame so the interval and the point estimate come from the same
assignment rather than from two passes.
Two questions nothing here could answer. mAP asks how often the detector is
right; calibration asks whether it KNOWS how often it is right. And every
measurement here asks how well it did on some data; the OOD score asks whether
that data is the data we validated against.

src/ape/calibration.py bins detections by confidence and reports what each band
delivered: a reliability diagram as data, ECE, MCE, and overconfidence error.

THE SIGN IS THE POINT. ECE is symmetric, so a detector claiming 0.4 while right
0.9 of the time scores exactly as badly as one claiming 0.9 while right 0.4 of
the time. Those are not equally dangerous: the first is timid and wastes
performance, the second is confidently wrong, which is the failure ISO 21448
exists for. A test constructs that pair and asserts ECE cannot separate them
while overconfidence error can.

Slices are cut on the DETECTION rather than the ground truth. Every other slice
here cuts on ground-truth attributes, which exist only for objects that are
really there, so cutting calibration that way would silently drop every false
positive. False positives are exactly where overconfidence does its damage. Box
height stands in for range, which is weaker than KITTI's labelled distance and
is the only proxy a box corresponding to nothing can have.

src/ape/ood.py fits an operating envelope over six image statistics and scores
frames by Mahalanobis distance. Mahalanobis and not a per-feature z-score
because the features covary: a foggy frame is brighter AND lower contrast AND
has fewer edges together, and scoring each independently treats one joint
excursion as three unremarkable ones. A test puts two probes equally far out on
every individual feature, one along the correlation and one across it, and
asserts the second scores an order of magnitude higher.

Stated as a limit rather than left to be discovered: this is a feature-space
novelty detector, nothing is trained, and it cannot see a semantically novel
object rendered at ordinary brightness and contrast.

agreement() exists because an OOD score nobody has validated is a number rather
than evidence. It reports an AUC that sits at 0.5 for a score carrying no
information, so a score that cannot rank the degraded frames first can be seen
not to have earned the right to gate anything. The output is named
triggering_candidates, not triggering conditions: a triggering condition is a
scenario a person reasons about, and promoting a statistic straight into a
safety artefact is the shortcut that name refuses to take.

Verified: 315 passed, both new modules at 100% statement and branch coverage,
ruff clean, mypy strict clean. Break-it pass: 8 of 8 seeded defects killed,
including overconfidence collapsing back into ECE, ignored detections counted
as failures, and Mahalanobis degrading to a per-feature distance.
@MKamel7
MKamel7 merged commit 206cc99 into main Sep 1, 2026
4 checks passed
@MKamel7
MKamel7 deleted the checkkk-fixes branch September 2, 2026 21:32
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