Skip to content

Publish the paper's Stage 1 inputs, and plan the rest onto Hugging Face - #109

Open
jonfroehlich wants to merge 15 commits into
mainfrom
feat/publish-stage1-inputs
Open

Publish the paper's Stage 1 inputs, and plan the rest onto Hugging Face#109
jonfroehlich wants to merge 15 commits into
mainfrom
feat/publish-stage1-inputs

Conversation

@jonfroehlich

@jonfroehlich jonfroehlich commented Aug 4, 2026

Copy link
Copy Markdown
Member

v1.0-iccv2025 shipped the Stage 1 code but not the data it consumes. This publishes what can go
in git, proves what the published files became, and writes down where the rest goes.

The gap this closes

The three open-government curb ramp inventories the paper ran on were excluded from git by a
location_data/* line in stage_one/dataset_generation/.gitignore. That line is how the omission
happened, and it is not a gap re-downloading can close: the city portals serve current data
and it drifts (Bend +8.7%), so a download made today reproduces a different experiment.

All three are now committed (71.8 MB) and hash-pinned in docs/data_provenance.md §3, with a new
.gitattributes marking them binary so no contributor's core.autocrlf can silently invalidate
those hashes.

Which government records became training labels

combine_location_data.py reduces the inventories to (latitude, longitude, date) and shuffles,
discarding NYC's RampID/CornerID, the geojson OBJECTID/FacilityID/NonAssetID, and even
the city label — so all_locations.csv cannot answer "is this ramp in the training set?".

scripts/analysis/gov_provenance.py rebuilds the mapping and verifies it: 276,071 / 276,071 rows
resolved, 0 unmatched.

Bend Portland NYC total
government records 13,357 45,035 217,679 276,071
consumed by a generated panorama 5,110 21,075 130,527 156,712
consumption rate 38.26% 46.80% 59.96% 56.77%

43.23% of the government records never became a training label. That belongs next to any claim
about how much open-government data this pipeline converts.

Three findings that constrain any Stage 1 re-run

  1. The paper-era shuffle was unseeded. random.seed(42) was added later, so the published row
    order is unrecoverable. My first implementation replayed the seeded shuffle; verification
    caught it on row 0, and the working method is a coordinate join instead. This is the Stage 1
    counterpart of the split-seeding caveat in data_provenance.md §4.
  2. convert_date changed after the paper. Unknown install dates were "2000-01-01", now "".
    Measured: 276,066/276,071 dates reproduce under paper-era logic vs 252,983/276,071 (91.64%)
    under current main. The 23,088-row gap is exactly the records with no install date
    8.36% of the corpus, every one silently admitted to the installed-before-captured check.
  3. 8 coordinates are shared by two government records (16 rows), and that is necessarily the
    whole of the residual date disagreement.

Street centrelines: 801.6 MB → 18.7 MB, proven equivalent

street_data/ could never live in git — New York - Streets.geojson alone is 669 MB. But its only
consumer, generate_negative_panos.py, reads the geometry plus one name field used solely as
an emptiness test. Route numbers, ZIP, MSAG, ESN, road class and county are never touched.

scripts/build_street_derivative.py strips the rest: 42.9× smaller, New York alone 67.5×. The
name field is kept rather than dropped because Portland has 4,192 features with an empty
FULL_NAME that load_city_streets skips — a pure-geometry file would silently re-admit them.

Equivalence is proved, not asserted. verify computes a consumer fingerprint — sha256 over the
ordered (name, geometry) pairs of every feature surviving the filter, which is exactly what the
length-weighted sampling index is built from. All three match, and reproduce identically on Windows
and on the cluster. generate_negative_panos.py prefers a full download when present, so existing
checkouts are unaffected.

The Hugging Face plan

docs/replication.md now carries the full plan and README.md a "Published Artifacts" table, so
no input is discoverable only by asking us. Ordering is driven by what blocks what, and the top
item was not the obvious one:

  • rampnet-crop-model (720.7 MB) first. inference_isolator.py hardcodes a path to the
    round-2 crop checkpoint — the model that turns every government coordinate into a pixel keypoint
    — and it exists only on lab storage. Publishing location_data/ without it hands someone the
    inputs to a pipeline they still cannot run.
  • rampnet-stage1-inputs (1.06 GB), manifests first. finaldataset.jsonl and
    negativepanosSHORTENED.jsonl are the only way to reproduce the paper's negatives, because the
    sampler is unseeded.
  • rampnet-benchmark (~12 GB, Publish deployment validation ground-truth as a HuggingFace dataset (Bend GSV + Richmond Mapillary) #21) incrementally. Benchmark imagery is immutable once fetched
    and the revisable parts live in git, so the set only grows and a new city costs one folder.
    Folders named panos_native/ and panos_4096x2048/ — by resolution, not by consumer, so the
    name cannot rot when the model's input size changes.

Checked the two live repos against the July 2025 terms rather than assuming we had followed them:
rampnet-dataset is Parquet in 128 shards per split as advised; rampnet-crop-model-dataset is
1,214 loose JPEGs, under the per-folder limit but not the recommended shape.

Also checked the whole plan against HF's current limits, and nothing is within an order of
magnitude of one: largest file 669 MB (limit 500 GB), largest folder 125 entries (limit 10,000),
largest repo ~2,550 files (recommended <100k). The per-city subdirectory layout is what keeps the
folder count trivial. The real consideration is the storage quota — projectsidewalk is a free
org, whose public storage is "best-effort", and the existing 463 GB rests on the July 2025 human
approval rather than an entitlement. Our +29 GB is +6%, but a courtesy note on that thread belongs
in the publish steps.

Published to Hugging Face

All three planned repos are live, in the RampNet collection:

repo size notes
rampnet-crop-model 720.7 MB rounds 1+2; Stage 1 could not run without round 2
rampnet-stage1-inputs 1.03 GB inventories, raw street data, manifests
rampnet-benchmark 11.41 GB Parquet; configs native / 4096x2048 / galleries — the imagery half of #21

Uploads were verified rather than trusted: HF stores each LFS object under its sha256, so oids were compared directly against local hashes (8/8, then 27/27). Files too small for LFS were re-downloaded and hashed. For the benchmark, load_dataset() against the Hub returns bytes whose sha256 matches both the row and the committed imagery_manifest.json.

All three human review passes are now redoable by someone outside the lab. GT verification and the #55 A/B previously needed imagery that existed only on lab machines. In particular the second-rater pass on Budapest — the top outstanding follow-up in the model comparison — is now blocked on a person, not an artifact.

Remaining: the 15 GB round-1 Project Sidewalk crop set, planned for rampnet-crop-model-dataset/round1_ps/.

Verification

  • pytest -q501 passed, at each commit.
  • gov_provenance.py re-run under both paper-era and current convert_date; both figures quoted
    above come from that run.
  • build_street_derivative.py verify — 3/3 fingerprints match, on two platforms.
  • The three inventories' sha256 confirmed identical to the cluster copies after transfer.

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

jonfroehlich and others added 4 commits August 4, 2026 06:04
… rows they became

v1.0-iccv2025 shipped the code that consumes the three open-government curb ramp
inventories but not the files themselves: stage_one/dataset_generation/.gitignore
carried a location_data/* line. That gap is not fixable by re-downloading -- the city
portals serve current data and it drifts (Bend +8.7%) -- so the paper's actual inputs
existed only on a cluster scratch directory.

Commits all three (71.8 MB, hash-pinned in docs/data_provenance.md section 3) and marks
them binary in .gitattributes, so no contributor's core.autocrlf can quietly invalidate
those hashes.

Adds scripts/analysis/gov_provenance.py, which answers the question all_locations.csv
cannot. combine_location_data.py reduces the inventories to (latitude, longitude, date)
and shuffles, discarding NYC's RampID/CornerID, the geojson OBJECTID/FacilityID/
NonAssetID, and even the city label. The script recovers the mapping by coordinate join
and verifies it: 276,071/276,071 rows resolved, 0 unmatched, 156,712 (56.77%) consumed
by a generated panorama. So 43.23% of the government records never became a label.

Three findings that constrain any Stage 1 re-run, documented beside the numbers:

- The paper-era shuffle was UNSEEDED -- random.seed(42) was added later -- so the
  published row order is gone for good. Hence a coordinate join, not a replay.
- 8 coordinates are shared by two government records, making 16 rows ambiguous, and
  that is necessarily the whole of the residual date disagreement.
- convert_date changed after the paper: an unknown date was "2000-01-01", now "".
  Measured, that is 23,088 records (8.36%) with no install date, every one of which the
  paper's run silently admitted to the installed-before-capture check.

street_data/ (801 MB) and the Stage 1 manifests stay out -- "New York - Streets.geojson"
alone is 669 MB, past GitHub's 100 MB hard limit. Both are now logged as explicit
blockers in docs/replication.md for #21.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
street_data/ was the last Stage 1 input that could not go in git: 801.6 MB, and
"New York - Streets.geojson" alone is 669 MB, past GitHub's 100 MB hard limit.

It turns out almost none of that is used. Its only consumer, generate_negative_panos.py,
reads the LineString geometry plus exactly ONE name property, and only as an emptiness
test -- FULLNAME (Bend), FULL_NAME (Portland), Street (New York). Route numbers, ZIP,
MSAG, ESN, one-way direction, road class and county are never touched.

scripts/build_street_derivative.py strips the rest:

    Bend        8,934,761 ->    553,314   16.1x
    Portland  123,616,787 ->  8,238,233   15.0x
    New York  669,049,016 ->  9,908,856   67.5x
    TOTAL     801,600,564 -> 18,700,403   42.9x

The name field is kept rather than dropped, because Portland has 4,192 features whose
FULL_NAME is empty and which load_city_streets skips -- a pure-geometry file would
silently re-admit them and change the sampled network.

Equivalence is proved, not asserted. `verify` computes a consumer fingerprint: sha256
over the ordered (name, geometry) pairs of every feature surviving the filter, which is
exactly what the length-weighted sampling index is built from. All three match, and the
same fingerprints reproduce on Windows and on klone.

generate_negative_panos.py prefers a full download when present and falls back to the
derivative, so existing checkouts behave identically.

Priority note recorded in docs/replication.md: street data is NOT on the critical path
for reproducing the paper. generate_negative_panos.py is unseeded, so the negatives
cannot be regenerated from any street file. What reproduces them is the manifest --
negativepanosSHORTENED.jsonl (5.2 MB) and finaldataset.jsonl (64 MB). Those are the
smaller, higher-value publish and should lead #21.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… lives

Answers "what goes on HF, and in what order" with measured sizes, plus a README table
so no input is discoverable only by asking us.

Everything lands in the projectsidewalk/rampnet collection: three repos exist, three are
new, and one existing repo gains a folder. Under 29 GB of new upload against the 463 GB
already hosted, so capacity is not a consideration.

Ordering is driven by what blocks what, and the top item was not the obvious one:

- rampnet-crop-model (720.7 MB) FIRST. inference_isolator.py hardcodes a path to the
  round-2 crop checkpoint -- it is what turns every government GPS coordinate into a
  pixel keypoint -- and it exists only on lab storage. Publishing location_data without
  it hands someone the inputs to a pipeline they still cannot run. Both rounds go up,
  since round 1 initialises round 2. Verified by hash while inventorying: ps_model.pth
  is byte-identical to the round-1 checkpoint, so the README's "copy it here, renamed"
  step is now confirmed rather than assumed, and only two distinct files need uploading.
- rampnet-stage1-inputs (1.06 GB), manifests first. finaldataset.jsonl and
  negativepanosSHORTENED.jsonl are the ONLY way to reproduce the paper's negatives,
  because the sampler is unseeded. The source files are archive; the manifests are the
  reproduction path.
- rampnet-benchmark (~12 GB, #21) incrementally, not held back for more cities.
  Benchmark imagery is immutable once fetched and the revisable parts live in git, so
  the set only grows; a new city is one folder and hf upload skips matching hashes.
- The 15 GB round-1 Project Sidewalk crop set joins rampnet-crop-model-dataset as
  Parquet under round1_ps/. Parquet is forced, not chosen: 27,710 entries is past the
  10,000-per-folder hard limit. The 1,212 round-2 crops stay loose -- converting them
  would orphan LFS versions and break existing paths for a cosmetic gain.

Checked the two live repos against the terms we agreed in July 2025 rather than assuming
we had followed them: rampnet-dataset is Parquet in 128 shards per split, exactly as
advised; rampnet-crop-model-dataset is 1,214 loose JPEGs, which is under the per-folder
limit but not the recommended shape. That is what surfaced the constraint above.

Renames the imagery folders panos_native/ and panos_4096x2048/. The earlier
panos_model_res/ is a poor public name twice over: "res" is unexplained, and "model
resolution" is relative, so it silently goes wrong when the input size changes -- a live
risk with #25 and #20 open. Layout is the one thing worth settling before the first
upload, since restructuring means replacing large blobs, the one expensive HF operation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… of estimating it

Two loose ends from the publishing plan.

scripts/export_crop_model.py packages the Stage 1 crop checkpoints for HF, mirroring
scripts/export_hf_model.py. Nothing uploads without --push; expected sha256 values are
asserted before anything is copied, and the copies are re-hashed afterwards so a push
cannot ship bytes that were never verified. It also refuses to run if both inputs hash
the same, which is the easy mistake here -- ps_model.pth sits beside the round-2
checkpoint and is a byte-identical copy of round 1.

The panos_4096x2048 size was the one number in the plan that was an estimate. Rendered
all 1,109 panoramas across the 9 splits at 4096x2048 / q82 / BILINEAR -- byte-for-byte
what gt_gallery.py applies, deliberately NOT using PIL draft(), which is much faster on
the 16384px natives but yields different pixels and would defeat the purpose.

Measured 1.02 GB, against 10.89 GB native. The estimate said 1-2 GB, so the benchmark
repo lands at 12.1 GB.

The per-split spread is the more useful result, and it corrects how I had been
describing this folder:

    gainesville/paterson/sao_paulo/bend/clovis   16-18x smaller  (13312-16384 px)
    annapolis/richmond                           3.6-3.8x        (8000-12288 px)
    budapest_district5                           1.6x            (4096-5760 px)
    morgantown                                   0.9x            (4096-5760 px)

Morgantown's derivative is LARGER than its native archive -- those panoramas are already
at model resolution and heavily compressed, so re-encoding costs a JPEG generation and
buys nothing. That is not a reason to drop it, since gt_gallery.py re-encodes at q82
regardless and this is therefore still what the reviewer saw. But it does mean the
folder is a review-fidelity artifact, not "the small one", and the docs now say so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jonfroehlich
jonfroehlich force-pushed the feat/publish-stage1-inputs branch from 10f1359 to a4373c1 Compare August 4, 2026 17:44
…constraint

Nothing in the publishing plan is within an order of magnitude of a limit:

    single file      669 MB   vs 500 GB hard / 200 GB recommended
    entries/folder      125   vs 10,000 hard
    files per repo   ~2,550   vs <100k recommended
    commit size      auto-split by upload_folder

The per-city subdirectory layout is what keeps the folder count trivial, which is the
one decision that could have gone wrong.

Corrects a claim I made earlier: I said 27,710 round-1 crop files "exceed the 10k hard
limit, so loose files are not an option". The limit is per FOLDER, not per repo, and the
crop set already has train/val/test x class subdirectories, so loose files would in fact
be legal. Parquet is still right -- HF requires well-integrated formats for large
datasets, it is what makes the viewer work, and 27,710 loose files would eat a quarter of
the <100k recommendation for nothing -- but it is a strong recommendation here, not a
wall, and the doc now says which.

Records the consideration that IS real, and it is not file sizes: projectsidewalk is a
free organisation, and free orgs get "best-effort" public storage. The existing 463 GB
rests on a human approval from July 2025 rather than an entitlement, and HF states they
run mitigations against abuse of free public storage. Our +29 GB is +6% and squarely the
kind of artifact that approval was for, but the arrangement is informal, so a courtesy
note on the existing thread belongs in the publish steps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jonfroehlich
jonfroehlich force-pushed the feat/publish-stage1-inputs branch from a4373c1 to 799e3e5 Compare August 4, 2026 17:44
jonfroehlich and others added 10 commits August 4, 2026 10:52
Two of the three planned repos are live, both byte-verified end to end.

  https://huggingface.co/projectsidewalk/rampnet-crop-model          720.7 MB
  https://huggingface.co/datasets/projectsidewalk/rampnet-stage1-inputs  1.03 GB

The crop model was the binding blocker: inference_isolator.py loads the round-2
checkpoint by a hardcoded path, so until now the committed government inventories were
inputs to a pipeline nobody outside the lab could execute. Both rounds are up, since
round 1 initialises round 2.

rampnet-stage1-inputs carries location_data/ (mirroring the committed copy),
street_data/ (the full 801 MB downloads behind the committed 18.7 MB derivative), and
manifests/ -- which is the part that actually matters. generate_negative_panos.py samples
with an unseeded RNG, so negativepanosSHORTENED.jsonl is the ONLY record of which 43,834
negatives the paper used; they cannot be regenerated, only downloaded.

Adds scripts/export_stage1_inputs.py and its card template, alongside the crop-model
exporter. It asserts location_data sha256 against the values pinned in
docs/data_provenance.md before staging anything, so the exporter cannot publish
inventories that drifted from the ones the paper ran on.

Verification, rather than trusting the upload: HF stores each LFS object under its
sha256, so the oids were compared directly against the local hashes -- 8/8 match. The
three files small enough to land as plain git blobs were re-downloaded and hashed
instead. All 11 files confirmed identical to what was staged.

Both cards state the limits beside the data: unseeded shuffle, unseeded negative
sampler, and the convert_date change that affects 8.36% of records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…twice

HF asked for Parquet or WebDataset rather than loose image folders when they approved
hosting rampnet-dataset in July 2025. Loose per-city folders would have been legal (125
entries against a 10k limit) but it is the shape they steered us away from, the viewer
would not work, and restructuring later means replacing large blobs -- the one expensive
operation on HF. So: done properly the first time.

scripts/export_benchmark.py streams each split into one Parquet per (config, city) with
pyarrow, which keeps memory flat across 11.41 GB instead of materialising 2 GB splits.
Three configs, named by resolution rather than by consumer:

    native      1,109 panoramas as fetched, 4096-16384 px      9.99 GB
    4096x2048   the same at the model input size               1.00 GB
    galleries     314 incremental-FP crops from the #55 A/B    0.40 GB

The image bytes are embedded verbatim -- rows carry image as {bytes, path}, so nothing is
re-encoded on write -- and each row also carries the sha256 of its own bytes. Confirmed
that datasets reads the column back as a real Image feature, which is what makes the
viewer render rather than showing an opaque struct.

Two DIFFERENT things are verified, because they are different claims:

  verify        re-hashes every embedded image straight out of the Parquet.
                2,532/2,532 round-trip byte-identical. Proves Parquet preserved whatever
                we put in.
  cross-check   compares the packaged native bytes against the sha256 pinned in each
                split's committed imagery_manifest.json, written at review time.
                1,109/1,109 match. Proves what we put in is what the ground-truth
                reviewers actually judged -- the claim that matters to a second rater,
                and one the first check cannot make.

Labels stay in git on purpose. records.jsonl and verdicts.json get revised; imagery does
not, which is what makes this repo purely additive and safe to grow one city at a time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
  https://huggingface.co/datasets/projectsidewalk/rampnet-benchmark   11.41 GB

27 parquet files uploaded and LFS-oid-verified against local hashes. Then the check that
actually matters, from a clean client: load_dataset() against the Hub returns all three
configs and nine splits, decodes the image column as a real PIL image, and the sha256 of
the bytes that came back off the Hub matches the value recorded in the row -- which in
turn matched each split committed imagery_manifest.json. The chain from a reviewer eyes
to a stranger download is now closed and checkable at every hop.

Updates the ledger, including three claims that were true this morning and are not now:

- "benchmark/*/panos/ -- 9.0 GB, git-ignored, HF #21 pending, BLOCKER" -> published.
- "Crop-model checkpoints -- lab storage only, BLOCKER" -> published.
- The #46 run-book no longer opens by naming two blockers; everything it needs is
  obtainable.

And the part worth stating plainly: ALL THREE human review passes are now redoable by
someone outside the lab. Two of them (GT verification, the #55 A/B) previously needed
imagery that existed only on lab machines. The judgments and rubrics were committed all
along; publishing the pixels is what closed the loop. In particular the second-rater pass
on Budapest -- whose ground truth was reviewed at LOW confidence and which is the top
outstanding follow-up in the model comparison -- is blocked on a PERSON now, not on an
artifact.

Remaining: the 15 GB round-1 Project Sidewalk crop set, planned for
rampnet-crop-model-dataset/round1_ps/ as Parquet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…step

Creating a HF repo does NOT add it to a collection -- that is a separate API call, and
none of the exporters make it. All three repos published today were reachable only by
direct URL or by scrolling the org listing, which is precisely the discoverability gap
publishing was supposed to close. Caught by Jon looking at the collection page.

The RampNet collection now holds six items: rampnet-model, rampnet-dataset,
rampnet-crop-model-dataset, and the three added today.

Documents the step in docs/replication.md with the exact call, including the detail that
cost a minute: the collection slug carries an id suffix
(projectsidewalk/rampnet-6871b77b1add07bdfecfcd5c), so the bare .../collections/
projectsidewalk/rampnet URL does not resolve through the API. list_collections finds it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s visible

The 9-city benchmark is NOT part of the RampNet paper, and nothing said so. The card I
published yesterday was actively misleading -- it described the panoramas as "from
RampNet: A Two-Stage Pipeline... (ICCV 25)", which reads as the paper own evaluation set.
benchmark/README.md never mentioned it either.

The boundary is cleanly provable, not a judgement call: v1.0-iccv2025 (2025-08-15)
contains no benchmark/ directory at all. Its tree is manual_labels, stage_one, stage_two
and support files. The splits were added 2026-07-22 through 2026-07-31, eleven months
later. The paper evaluated on the 1,000-panorama gold set in manual_labels/, sampled from
its own three training cities; the benchmark exists to measure the published model OUT of
domain -- non-US cities, Mapillary as well as GSV -- and against VLM detectors.

So numbers from the benchmark are not comparable with the published ones: different
ground truth, different cities, different matching protocol.

Corrected in four places, three of them public:

- the rampnet-benchmark card, now opening with a warning block (pushed; card-only update,
  no blob churn)
- the HF collection description and the per-item note
- benchmark/README.md
- the README Published Artifacts table, which now marks the benchmark row and states that
  every other row is paper-era

Adds a `card` mode to export_benchmark.py so a card can be re-rendered and pushed on its
own. Rebuilding 11.41 GB of Parquet to fix a sentence would rewrite every blob, which is
the one expensive operation on HF -- and cards get revised far more often than data does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tually asked for

I had shipped pixels and left labels in git, which was a deviation from #21 rather than a
fulfilment of it. The issue title is "Publish deployment validation GROUND-TRUTH as a
HuggingFace dataset", and it also specifies per-pano source, capture date, camera heading
and per-record source attribution. None of that was in the dataset.

Adds a fourth config, `records`: one row per reviewed panorama with

    source, capture_date, lat, lng, camera_heading, width, height, copyright,
    detections[{x_normalized, y_normalized, confidence, verdict}],
    missed[{x_normalized, y_normalized, unsure}], no_missed,
    model_id, model_training_date, label_type

1,109 panoramas, 2,061 detections, 1,191 reviewer-marked missed ramps -- in 179 KB. The
dataset is now self-contained: a stranger can score a model against this benchmark without
cloning the repo, and `source` makes the cross-domain slice the one-line filter #21 asked
for.

verdicts.json stores judgments as a bool/string mix; normalised to correct / incorrect /
unsure / duplicate, with the mapping documented on the card so nothing is lost.

Cross-checked against the source BEFORE pushing: every pano, every detection index,
coordinate equality to 1e-12, and missed-ramp counts. 1,109 panos, verdict distribution
1833/96/116/16 matching a direct survey of the committed files, 0 discrepancies. Index
alignment between detections and dets[] is the thing that could have been silently wrong,
so it is checked explicitly rather than assumed.

Adds `records` and `card` modes that touch only what they change. A verdict correction now
costs ~180 KB of upload instead of replacing 11.41 GB of image blobs -- and verdicts do get
revised, which is why labels are a separate config rather than extra columns on the imagery.

Labels remain DERIVED: benchmark/<city>/{records.jsonl,verdicts.json} stay the source of
truth and the config regenerates from them. Rubrics, reviewer confidence and review notes
stay in git, and the card says to read them before quoting a number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ce anchor

The crop checkpoints went up as raw torch.save pickle archives -- they literally begin
PK\x03\x04 -- while projectsidewalk/rampnet-model ships model.safetensors, because
converting it was a deliberate item on the July re-publish. Publishing two 360 MB pickles
into the same org, linked from the same collection, was inconsistent with a decision the
project had already made.

Calibrating the risk honestly: torch 2.6 defaults torch.load to weights_only=True, so this
was not a live exploit path for most users. The reasons to fix it anyway are that HF runs
a pickle scanner over model repos (scansDone was still false on ours), and that the
inconsistency is indefensible if anyone looks.

The fix is purely ADDITIVE, which is why it was worth doing to an already-published repo:
the .pth files stay exactly where they are with their sha256 unchanged, nothing is
orphaned, no link breaks. The "already up there, live with it" argument applies to
REPLACING blobs, not to adding a file.

Both rounds converted, 380 tensors each, with every tensor compared for dtype and exact
equality after the round trip -- the converter refuses to write on any mismatch. The .pth
is kept deliberately: its hash is what ties this artifact to the paper's run, and it is
what inference_isolator.py loads unmodified. The card now leads with safetensors and says
plainly why both formats are there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…made a real column

The round-1 Project Sidewalk crop set (27,704 crops, ~15 GB) is the last unpublished
paper-era input. Parquet because HF asks for it on large datasets and it is what makes the
viewer work -- not because loose files would be illegal. The 10k limit is per FOLDER, and
train/val/test subdirectories would have kept loose JPEGs legal; I said otherwise earlier
and the docstring now records the correction.

The interesting part is that the labels were hiding in the filenames.
`007mz25c_-_118_596_-_478_611.jpg` is panorama 007mz25c with keypoints (118, 596) and
(478, 611), parsed at load time by train.py. This export makes them a `keypoints` column,
so the dataset is usable without reimplementing a filename parser.

Coordinates are stored VERBATIM in the stored crop pixel space (683x2048) and deliberately
NOT normalised. The loader multiplies them by exactly 0.5 while the image is resized
683 -> 352 on x, a factor of 0.515. That ~2% discrepancy is in the original code; helpfully
normalising here would silently commit to one reading of it. The card says so and points
at train.py.

Two things that cannot be regenerated, both documented beside the data rather than as
instructions someone would follow and get different results from:

- download_data.py reads live from Project Sidewalk servers with no snapshot pinning, and
  those databases keep growing, so re-running builds a DIFFERENT training set.
- splititup.sh makes the 70/15/15 partition with `shuf` and no seed, so even the split is
  not reproducible. (That is now the fourth unseeded step found in this pipeline, after
  the all_locations shuffle, the negative sampler, and the dataset split.)

Ships as its own repo rather than a folder inside rampnet-crop-model-dataset: that repo
serves its 1,214 loose JPEGs through imagefolder auto-detection, and adding an explicit
configs block risks breaking a published viewer with 22 likes to gain tidiness. Both land
in the RampNet collection, so discoverability does not depend on them sharing a repo.

Train shards at ~1.5 GB so no single parquet is awkward to resume.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… instead

Building the export surfaced a wrong claim in a card I wrote before seeing the data.
`n_keypoints` was documented as "0 is possible -- negative crops". There are none. All
27,704 crops carry at least one keypoint, 35,757 between them: 21,751 have exactly one and
the tail runs to nine. A model trained on this set alone never sees an empty example, which
is exactly the sort of thing you want to know before you use it.

So the card no longer asserts a distribution -- it renders one, reading n_keypoints back
out of the built shards. If a future export does contain negatives it will say so, with the
count. It cannot drift from the data it ships with.

write_card is split out of build and exposed as a `card` mode, because correcting one
sentence should not mean rebuilding 13 GB of Parquet.

The tests exist because parse_name is the entire value-add of this export and nothing else
checked it. The byte-level verify pass is not a guard here: it re-hashes image bytes, so it
stays green even if every keypoint were silently dropped. Eight tests cover multi-keypoint
names, verbatim negative coordinates, the no-keypoint case, a segment that is not a
coordinate pair, and both branches of the summary. pyarrow is declared in
requirements-dev.txt rather than borrowed from datasets.

What the build measured, for the record:

- the 13,389,588,480-byte archive hashes 7fd446c1c9fd...8ec9 on klone and identically after
  transfer, so the crops that went into Parquet are the rescued paper-era files unchanged
- 27,704 crops in, 27,704 rows out, split 19,392 / 4,155 / 4,157, 11 shards, 13.37 GB
- every crop re-hashed against its stored sha256: all round-trip byte-identical
- an independent re-parse of the filenames, deliberately not importing the exporter, agrees
  exactly -- 35,757 keypoints, zero unparseable segments, x in [0, 682] and y in [256, 1612]

One correction to 0080473's message: rampnet-crop-model-dataset has 22 DOWNLOADS, not 22
likes; it has one like. The reason to leave that repo alone still holds -- its viewer works
off imagefolder auto-detection and an added configs block would risk it -- but the argument
is "do not break something that works", not "it is popular".

The docstring said ~15 GB from an estimate made before the tar existed. It is 13.4 GB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plan in replication.md §4 -- put the round-1 crop set into rampnet-crop-model-dataset
under round1_ps/, leaving the 1,212 round-2 JPEGs loose -- does not work. Worse, it does
not work SILENTLY. I dry-ran it against throwaway repos before touching the published one,
which is the only reason this is a commit rather than a broken dataset.

`datasets` infers ONE builder module per repository, from the default config, and applies
it to every config in that repo:

- round 2 default: both configs get Imagefolder. The round-1 Parquet patterns resolve
  correctly, the shards are found, and then the imagefolder builder looks inside them for
  images, finds none, and yields 0 rows. No error until you ask for a split, at which point
  it is `Instruction "train" corresponds to no data!`
- round 1 default: both configs get Parquet, and the JPEG config breaks instead.

There is no arrangement that works. A parquet-only control with the identical config block
loads fine, which is what isolates the cause to the mixing rather than the pattern.
scripts/analysis/hf_config_mixing_check.py reproduces all three against throwaway repos it
deletes afterwards.

Unifying would mean converting the 1,212 round-2 JPEGs and deleting the originals --
replacing a published layout, not adding to one. b569c59 already drew that line for this
project, and it still holds. The cost is a real naming wart, since the unmarked repo is
round 2 and the marked one is round 1; the mitigation is the two-row table both cards carry,
which is additive and breaks nothing.

Two label problems the build surfaced, both of which would have shipped:

- crop_uid, not pano_id. I had published the leading filename token as a panorama id.
  download_data.py:277 builds it with random.choices(alphabet, k=8) -- it is opaque, and the
  source panorama is not in this artifact at all. The measurement that caught it: 27,704
  distinct tokens for 27,704 crops, where a real pano id would repeat (the paper's own table
  says these crops come from 20,698 panoramas). Naming it pano_id would have invited a join
  against rampnet-dataset that silently returns nothing. A test now asserts the schema has
  no pano_id field.
- The x-axis mismatch is 3.1%, not the ~2% I wrote, and it is x-only. train.py scales both
  keypoint axes by 0.5, but Resize((1024, 352)) scales the image by 0.5 on y and
  352/683 = 0.5154 on x. So y is exactly consistent and x is under-scaled, drifting a label
  left of its ramp in proportion to x, up to ~10.5 px at the right edge. The card now shows
  the per-axis table and tells you to scale x by 352/683 if you are not reproducing the paper.

Rebuilt on the corrected schema and re-verified: 27,704 crops, 11 shards, 13.37 GB, every
crop round-tripping byte-identical, 35,757 keypoints. Also measured while checking the card's
other claims: all 27,704 crops are exactly 683x2048, so that claim was right.

510 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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