ProjectSidewalk / ProjectSidewalk/RampNet

Training augmentation is a single horizontal flip: measure downscale / photometric jitter as a transfer lever (downstream of #84)

Open
#82 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
7
Forks
1
Avg merge
4d 11h
Merged PRs (30d)
7

Description

Revised 2026-07-29 — this issue is now downstream of #84. The original version treated augmentation as a standalone lever. It isn't, because Stage 2 trains for exactly 1 epoch: every pano is seen once, so augmentation's classic payoff (multiplying effective dataset size across repeated exposure) does not apply here at all. See "What changed and why" at the bottom for the full revision note.

What's there now

Stage 2 training applies exactly one augmentation: random horizontal flip at p = 0.5, with correct label handling (x → 1 − x) — stage_two/train.py:202-207. The input transform is Resize(2048×4096) → ToTensor → ImageNet Normalize (train.py:224-229): no photometric variation, no scale variation, no other geometric variation. Validation runs with flip off. Both crop models are the same (stage_one/crop_model/*/train.py:71).

So the training-time invariance budget is one bit: left–right mirroring.

Why it's worth an experiment

#78 measured what that one augmentation is worth at test time and found the model already strongly flip-invariant — flip-TTA buys ≤1.3 marginal recall points per US split at the #54 operating point. That lever is exhausted, which is a useful negative signal: training-time hflip did its job, and nothing else has been tried.

Meanwhile the benchmark's live failure mode is cross-city / cross-imagery transfer. GT-measured deployment recall at 0.55 single-pass (from #78): richmond 0.895, bend 0.831, annapolis 0.738, morgantown 0.730, clovis 0.650 — a 24-point spread at a fixed threshold, with budapest worse still.

This is not a training-set-size experiment. The Stage 1 dataset is already 214,376 panoramas / 849,895 labels; the binding constraints are label noise and domain diversity, not sample count. That also makes this the natural no-new-data control for #59.

⚠️ The mechanism is weaker than it looks at 1 epoch — read before running

With --epochs 1, each of the 150,063 training panos is seen exactly once, with a single random augmentation draw. So the mechanism here is not "more effective data" and not "less overfitting from repeated exposure" — neither applies without repetition. The only mechanism left is distribution broadening: the training distribution becomes slightly wider in colour/scale space, which can help transfer, but is the weaker of the two effects.

Two consequences:

  1. Run #84 first. If the epoch curve shows the recipe should be longer, augmentation becomes a materially stronger intervention and should be tested at the chosen epoch count. Testing it at 1 epoch would under-read it and risks a false negative that kills a good idea.
  2. The training set already contains real photometric diversity — many US cities, capture dates, sun angles, camera generations. Synthetic jitter stacks on top of genuine variance, so expect diminishing returns relative to a single-city dataset where this trick shines.

What is actually legal on an equirectangular panorama

The input is a 2048×4096 equirectangular 360° projection, not a photo. That rules some standard augmentations in and others out — worth stating explicitly, because the intuitive list ("rotate it, warp the perspective") is mostly the illegal half:

transform legal? why
horizontal flip in use mirrors the world consistently; labels x → 1 − x
random downscale → upscale untried — now the top candidate simulates older / lower-resolution captures; labels unchanged
photometric (brightness / contrast / saturation / hue, blur, JPEG, sensor noise) untried pixel-wise, projection-agnostic, labels unchanged
horizontal roll (yaw) ✅ untried, demoted rolling along x is a camera yaw: wraps seamlessly, exactly label-preserving (x → (x + Δ) mod 1), free. But see below — expected value is now low
in-plane rotation, perspective / affine warp not a camera motion in this projection. The top and bottom rows are the poles; an image-plane rotation mixes pole with horizon and destroys the equirect mapping
true 3D rotation (sphere pitch / roll) ⚠️ possible, costly correct formulation is rotate-the-sphere-then-re-render: a resampling pass per sample, interpolation-lossy. Low expected value anyway — GSV panos are already gravity-aligned
vertical flip puts sky on the ground, and curb ramps have a hard gravity prior
Why downscale is now the top candidate

The transfer gap looks more like a capture-hardware gap than a colour gap: clovis — the worst split at 0.650 recall — is 2018 GoPro Fusion imagery, a different camera at different resolution. Colour jitter does nothing for resolution. Random downscale→upscale attacks that axis directly, and is equally cheap.

Why yaw-roll got demoted

The original argument was that GSV panos are canonically oriented (vehicle heading centred, hood and stitching seam in fixed columns), so the model could learn a positional prior that fails to transfer. That is still true, but the prior is probably weak: curb ramps already appear at every yaw in training, and equirect distortion is a function of latitude (y) only, so rolling does not change how a ramp looks — only where the surrounding context sits. Keep it as a cheap third arm, not a headline.

Separable, and possibly worth more: circular padding

In equirect, x = 0 and x = 1 are the same meridian, but a convnet zero-pads the horizontal edges — so a ramp straddling the seam is split and sees fabricated boundary context. The targeted fix is circular padding on the horizontal axis, which is an architecture change in rampnet/model.py, not a data change. It addresses the real defect that yaw-roll only papers over. Worth its own issue if this one shows the seam matters.

Proposed experiment

Arms (all at the epoch count #84 selects):

  • (a) baseline, retrained — must be retrained, not the released checkpoint, or run-to-run variance is confounded with the augmentation effect.
  • (b) baseline, second seedthe noise floor, and the single most important arm. Without it, no 1–2 point difference between any other arms is interpretable. Do not skip this to save a slot.
  • (c) + random downscale
  • (d) + photometric jitter
  • (e) + yaw-roll (cheap, low expectation)
  • (f) + best combination, if any single arm clears the noise floor.

Protocol pre-registered before any benchmark eval, same discipline as #71 / PR #80: checkpoint selection on val only, single fixed headline configuration declared up front.

Measure on the existing 7-split benchmark at the #54 operating point (0.30, single-pass), so results drop straight into docs/model_comparison.md and are comparable to everything already there.

Report per-split, never only pooled. The entire hypothesis is about transfer, so richmond (near-in-domain) vs. clovis / budapest (out) is the signal; pooling would average it away. Report the in-domain control (manual_gold) too — an augmentation that buys transfer at some in-domain cost is probably still right under the recall-first policy, but that trade should be visible.

Hypothesis, stated up front so a null result is publishable: downscale and photometric jitter help the low-recall out-of-domain splits (clovis, morgantown, annapolis, budapest) more than the in-domain ones; yaw-roll does little. If nothing clears the noise floor from (b), that is a real result — it would say the transfer gap is about ramp appearance and construction rather than capture nuisance variables, and would redirect effort to #59 and to rubric work.

Cost: one Stage 2 training run per arm. Dataset is already staged on Hyak (/gscratch/scrubbed/jfroehli/rampnet_dataset); no sidewalkcv2 env on klone yet. Wall-clock per epoch is currently unmeasured — see #84.

What changed and why (2026-07-29 revision)

Filed earlier the same day, then revised after working through the training recipe:

  1. Added the 1-epoch caveat and the #84 dependency. The original framing implicitly assumed the multi-epoch mechanism. It does not hold at --epochs 1, and that materially lowers the expected effect size.
  2. Promoted random downscale to top candidate, demoted yaw-roll. The transfer gap tracks capture hardware (clovis = 2018 GoPro Fusion) more than colour, and yaw-roll cannot change ramp appearance in an equirect projection.
  3. Added the second-seed noise-floor arm. The original 4-arm single-seed design could not have distinguished a real 1–2 point effect from run-to-run variance.
  4. Added circular padding as a separable architecture-side idea that addresses the seam defect more directly than yaw-roll.
  5. Dropped the claim that this is straightforwardly "the cheap thing to try first" — #84 is cheaper and is a prerequisite for reading this experiment's result.

🤖 Generated with Claude Code (claude-opus-5[1m])

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read #84 first to determine the epoch count, then inspect stage_two/train.py:202-229 and both stage_one/crop_model/*/train.py:71. Run the pre-registered baseline, second-seed, and augmentation arms on the existing 7-split benchmark at the #54 operating point. Done means per-split results, including manual_gold, are reported in docs/model_comparison.md and compared against the second-seed noise floor.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
computer-vision, machine-learning
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.