jonfroehlich commited on
Commit
606a119
·
verified ·
1 Parent(s): 1078bcd

Re-export with transformers-compatible RampNetModel wrapper + corrected card (fixes #19); weights unchanged from v1.0-paper

Browse files
Files changed (7) hide show
  1. README.md +141 -82
  2. config.json +33 -11
  3. configuration_rampnet.py +25 -0
  4. model.safetensors +2 -2
  5. modeling.py +0 -27
  6. modeling_rampnet.py +26 -0
  7. rampnet_model.py +44 -0
README.md CHANGED
@@ -1,82 +1,141 @@
1
- ---
2
- tags:
3
- - curb ramp detection
4
- - accessibility
5
- license: mit
6
- datasets:
7
- - projectsidewalk/rampnet-dataset
8
- base_model:
9
- - timm/convnextv2_base.fcmae_ft_in22k_in1k_384
10
- pipeline_tag: object-detection
11
- ---
12
-
13
- **RampNet** is a two-stage pipeline that addresses the scarcity of curb ramp detection datasets by using government location data to automatically generate over 210,000 annotated Google Street View panoramas. This new dataset is then used to train a state-of-the-art curb ramp detection model that significantly outperforms previous efforts. In this repo, we provide the checkpoint for our curb ramp detection model.
14
-
15
-
16
-
17
- **Usage:**
18
-
19
- *For a step-by-step walkthrough, see our [Google Colab notebook](https://colab.research.google.com/drive/1TOtScud5ac2McXJmg1n_YkOoZBchdn3w?usp=sharing), which includes a visualization in addition to the code below.*
20
- ```py
21
- import torch
22
- from transformers import AutoModel
23
- from PIL import Image
24
- import numpy as np
25
- from torchvision import transforms
26
- from skimage.feature import peak_local_max
27
-
28
- IMAGE_PATH = "example.jpg"
29
- DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
30
-
31
- model = AutoModel.from_pretrained("projectsidewalk/rampnet-model", trust_remote_code=True).to(DEVICE).eval()
32
-
33
- preprocess = transforms.Compose([
34
- transforms.Resize((2048, 4096), interpolation=transforms.InterpolationMode.BILINEAR),
35
- transforms.ToTensor(),
36
- transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
37
- ])
38
-
39
- img = Image.open(IMAGE_PATH).convert("RGB")
40
- img_tensor = preprocess(img).unsqueeze(0).to(DEVICE)
41
-
42
- with torch.no_grad():
43
- heatmap = model(img_tensor).squeeze().cpu().numpy()
44
-
45
- peaks = peak_local_max(np.clip(heatmap, 0, 1), min_distance=10, threshold_abs=0.5)
46
- scale_w = img.width / heatmap.shape[1]
47
- scale_h = img.height / heatmap.shape[0]
48
- coordinates = [(int(c * scale_w), int(r * scale_h)) for r, c in peaks]
49
-
50
- # Coordinates of detected curb ramps
51
- print(coordinates)
52
- ```
53
-
54
- ![Example of Curb Ramp Points](https://i.postimg.cc/66fQm4m5/labeled.png)
55
-
56
-
57
- If you get an error like this (see below), then you need to update your transformers library version.
58
- ```
59
- >>> model = AutoModel.from_pretrained("projectsidewalk/rampnet-model", trust_remote_code=True)
60
- Traceback (most recent call last):
61
- File "<stdin>", line 1, in <module>
62
- File "/gscratch/scrubbed/jsomeara/envs/sidewalk-validator-ai/lib/python3.10/site-packages/transformers/models/auto/auto_factory.py", line 558, in from_pretrained
63
- model_class = add_generation_mixin_to_remote_model(model_class)
64
- File "/gscratch/scrubbed/jsomeara/envs/sidewalk-validator-ai/lib/python3.10/site-packages/transformers/models/auto/auto_factory.py", line 726, in add_generation_mixin_to_remote_model
65
- has_custom_generate = "GenerationMixin" not in str(getattr(model_class, "generate"))
66
- AttributeError: type object 'KeypointModel' has no attribute 'generate'
67
- >>>
68
- ```
69
-
70
- Citation:
71
-
72
- ```bibtex
73
- @inproceedings{omeara2025rampnet,
74
- author = {John S. O'Meara and Jared Hwang and Zeyu Wang and Michael Saugstad and Jon E. Froehlich},
75
- title = {{RampNet: A Two-Stage Pipeline for Bootstrapping Curb Ramp Detection in Streetscape Images from Open Government Metadata}},
76
- booktitle = {{ICCV'25 Workshop on Vision Foundation Models and Generative AI for Accessibility: Challenges and Opportunities (ICCV 2025 Workshop)}},
77
- year = {2025},
78
- doi = {https://doi.org/10.48550/arXiv.2508.09415},
79
- url = {https://cv4a11y.github.io/ICCV2025/index.html},
80
- note = {DOI: forthcoming}
81
- }
82
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ library_name: transformers
4
+ pipeline_tag: keypoint-detection
5
+ tags:
6
+ - curb-ramp-detection
7
+ - accessibility
8
+ - street-view
9
+ - keypoint-heatmap
10
+ datasets:
11
+ - projectsidewalk/rampnet-dataset
12
+ base_model:
13
+ - timm/convnextv2_base.fcmae_ft_in22k_in1k_384
14
+ ---
15
+
16
+ # RampNet Curb Ramp Detection Model
17
+
18
+ Stage-2 model from **RampNet: A Two-Stage Pipeline for Bootstrapping Curb Ramp Detection in
19
+ Streetscape Images from Open Government Metadata** (O'Meara et al., ICCV'25 CV4A11y workshop,
20
+ [arXiv:2508.09415](https://arxiv.org/abs/2508.09415)).
21
+
22
+ Takes a 2048x4096 equirectangular street-view panorama (ImageNet-normalized) and predicts a
23
+ 512x1024 heatmap of curb ramp locations. Extract detections with `skimage.feature.peak_local_max`.
24
+
25
+ ## Provenance
26
+
27
+ | Field | Value |
28
+ | :--- | :--- |
29
+ | Training code | https://github.com/ProjectSidewalk/RampNet @ `842ec33` |
30
+ | Source checkpoint | `epoch_1_step_9378.pth` (sha256 prefix `b0c3ff7a10fc`) |
31
+ | Training dataset | [projectsidewalk/rampnet-dataset](https://huggingface.co/datasets/projectsidewalk/rampnet-dataset) revision `main` |
32
+ | Exported | 2026-07-24 by `scripts/export_hf_model.py` |
33
+
34
+ The model **weights are unchanged** from the originally published artifact — this revision only
35
+ fixes the packaging (a `transformers`-compatible wrapper; see [Requirements](#requirements)) and
36
+ corrects the evaluation numbers (see [Erratum](#erratum-evaluation-metrics)). The prior artifact
37
+ remains addressable at its tagged Hub revision.
38
+
39
+ ## Evaluation (1,000-panorama manually labeled gold set)
40
+
41
+ | Metric | Value |
42
+ | :--- | :--- |
43
+ | Average Precision (interpolated, full sweep) | 0.9205 |
44
+ | Precision @ threshold 0.55 | 0.9492 |
45
+ | Recall @ threshold 0.55 | 0.8727 |
46
+ | Ground-truth points | 3919 |
47
+ | Matching radius (normalized) | 0.022 |
48
+ | Flip TTA | on |
49
+
50
+ **Important:** these numbers were measured **with horizontal-flip test-time augmentation**
51
+ (evaluate the original and mirrored panorama, combine heatmaps with elementwise max). Single-pass
52
+ inference will land somewhat below them; derive your own threshold curve without TTA before
53
+ choosing an operating point. Average Precision is the interpolated area under the full
54
+ precision–recall sweep; precision and recall are reported at the operating threshold.
55
+
56
+ ### Erratum: Evaluation Metrics
57
+
58
+ After publication we found that the evaluation protocol described in §3.3 of the paper (and
59
+ implemented at tag [`v1.0-iccv2025`](https://github.com/ProjectSidewalk/RampNet/tree/v1.0-iccv2025))
60
+ differs from standard detection evaluation in two ways that bias precision and recall upward:
61
+ predictions matching an already-claimed ground-truth point were counted as additional true
62
+ positives rather than false positives, and one prediction could satisfy multiple ground-truth
63
+ points. The numbers above use corrected one-to-one matching. For transparency, the originally
64
+ published figures are kept here alongside the corrected ones:
65
+
66
+ | Metric | Originally published | Corrected (this revision) |
67
+ | :--- | :--- | :--- |
68
+ | Precision @ 0.55 | 0.938 | **0.949** |
69
+ | Recall @ 0.55 | 0.935 | **0.873** |
70
+ | Average Precision | 0.9236 | **0.9205** |
71
+
72
+ Swapping only the matching rule on identical model outputs moves precision by −1.0 points and
73
+ recall by −4.4 points; the remainder is reproduction drift (environment, JPEG re-encoding). The
74
+ comparison with prior work is unaffected (both systems were scored under the same protocol, and the
75
+ gap is far larger than the correction). Full write-up:
76
+ [`docs/eval_protocol_verification.html`](https://github.com/ProjectSidewalk/RampNet/blob/main/docs/eval_protocol_verification.html)
77
+ and the repository README's erratum.
78
+
79
+ ## Choosing a detection threshold
80
+
81
+ - `0.55` is the recommended default operating point (see evaluation above).
82
+ - Sweeping thresholds: run `stage_two/evaluate.py` in the training repo, which emits full
83
+ precision/recall-vs-confidence curves as CSV.
84
+ - Per-city deployments should calibrate on ~100 locally labeled panoramas; see the repo README's
85
+ "Choosing a Detection Threshold" section.
86
+
87
+ ## Requirements
88
+
89
+ Loads via `trust_remote_code`, so a compatible `transformers` is required:
90
+
91
+ - `transformers >= 5.13` — supported (the custom-model auto-class contract this
92
+ version introduced is satisfied; see issue #19).
93
+ - `transformers 5.12.x` — supported.
94
+
95
+ Both are covered by a round-trip load smoke test (`tests/test_hf_load.py` in the
96
+ training repo). Earlier revisions of this model fail to load on
97
+ `transformers >= 5.13` with `AttributeError: ... 'KeypointModel' has no attribute
98
+ 'register_for_auto_class'` (or `... has no attribute 'generate'`) — pull the current revision.
99
+
100
+ ## Usage
101
+
102
+ *For a step-by-step walkthrough, see the [Google Colab notebook](https://colab.research.google.com/drive/1TOtScud5ac2McXJmg1n_YkOoZBchdn3w?usp=sharing),
103
+ which adds a visualization to the code below.*
104
+
105
+ ```python
106
+ import torch
107
+ from transformers import AutoModel
108
+ from PIL import Image
109
+ import numpy as np
110
+ from torchvision import transforms
111
+ from skimage.feature import peak_local_max
112
+
113
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
114
+ model = AutoModel.from_pretrained("projectsidewalk/rampnet-model", trust_remote_code=True).to(DEVICE).eval()
115
+
116
+ preprocess = transforms.Compose([
117
+ transforms.Resize((2048, 4096), interpolation=transforms.InterpolationMode.BILINEAR),
118
+ transforms.ToTensor(),
119
+ transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
120
+ ])
121
+
122
+ img = Image.open("panorama.jpg").convert("RGB")
123
+ with torch.no_grad():
124
+ heatmap = model(preprocess(img).unsqueeze(0).to(DEVICE)).squeeze().cpu().numpy()
125
+
126
+ peaks = peak_local_max(np.clip(heatmap, 0, 1), min_distance=10, threshold_abs=0.55)
127
+ scale_w, scale_h = img.width / heatmap.shape[1], img.height / heatmap.shape[0]
128
+ print([(int(c * scale_w), int(r * scale_h)) for r, c in peaks])
129
+ ```
130
+
131
+ ## Citation
132
+
133
+ ```bibtex
134
+ @inproceedings{omeara2025rampnet,
135
+ author = {John S. O'Meara and Jared Hwang and Zeyu Wang and Michael Saugstad and Jon E. Froehlich},
136
+ title = {{RampNet: A Two-Stage Pipeline for Bootstrapping Curb Ramp Detection in Streetscape Images from Open Government Metadata}},
137
+ booktitle = {{ICCV'25 Workshop on Vision Foundation Models and Generative AI for Accessibility: Challenges and Opportunities (ICCV 2025 Workshop)}},
138
+ year = {2025},
139
+ doi = {https://doi.org/10.48550/arXiv.2508.09415},
140
+ }
141
+ ```
config.json CHANGED
@@ -1,11 +1,33 @@
1
- {
2
- "_name_or_path": "projectsidewalk/rampnet-model",
3
- "architectures": [
4
- "KeypointModel"
5
- ],
6
- "auto_map": {
7
- "AutoModel": "modeling.KeypointModel"
8
- },
9
- "heatmap_size": [ 512, 1024 ],
10
- "model_type": "convnextv2"
11
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "RampNetModel"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_rampnet.RampNetConfig",
7
+ "AutoModel": "modeling_rampnet.RampNetModel"
8
+ },
9
+ "dtype": "float32",
10
+ "heatmap_size": [
11
+ 512,
12
+ 1024
13
+ ],
14
+ "image_mean": [
15
+ 0.485,
16
+ 0.456,
17
+ 0.406
18
+ ],
19
+ "image_std": [
20
+ 0.229,
21
+ 0.224,
22
+ 0.225
23
+ ],
24
+ "input_size": [
25
+ 2048,
26
+ 4096
27
+ ],
28
+ "model_type": "rampnet",
29
+ "recommended_min_distance": 10,
30
+ "recommended_threshold": 0.55,
31
+ "transformers_version": "5.14.1",
32
+ "tta_recommended": true
33
+ }
configuration_rampnet.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class RampNetConfig(PretrainedConfig):
5
+ model_type = "rampnet"
6
+
7
+ def __init__(
8
+ self,
9
+ input_size=(2048, 4096),
10
+ heatmap_size=(512, 1024),
11
+ image_mean=(0.485, 0.456, 0.406),
12
+ image_std=(0.229, 0.224, 0.225),
13
+ recommended_threshold=0.55,
14
+ recommended_min_distance=10,
15
+ tta_recommended=True,
16
+ **kwargs,
17
+ ):
18
+ self.input_size = list(input_size)
19
+ self.heatmap_size = list(heatmap_size)
20
+ self.image_mean = list(image_mean)
21
+ self.image_std = list(image_std)
22
+ self.recommended_threshold = recommended_threshold
23
+ self.recommended_min_distance = recommended_min_distance
24
+ self.tta_recommended = tta_recommended
25
+ super().__init__(**kwargs)
model.safetensors CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:b4122c254fde7de4fce26e61604f3cdce3296b264ab9c2894113a09660d58d29
3
- size 360245468
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f2119e3becb0b551fa1470f7b7ba85b82122a3f73a6ed2a85609dd57617866b5
3
+ size 360247780
modeling.py DELETED
@@ -1,27 +0,0 @@
1
-
2
- import torch
3
- import torch.nn as nn
4
- import timm
5
- from huggingface_hub import PyTorchModelHubMixin
6
-
7
- class KeypointModel(nn.Module, PyTorchModelHubMixin):
8
- def __init__(self, config, **kwargs):
9
- super().__init__()
10
-
11
- upsample_size = config.heatmap_size
12
-
13
- backbone = timm.create_model('convnextv2_base.fcmae_ft_in22k_in1k_384', pretrained=False)
14
-
15
- self.feature_extractor = nn.Sequential(*list(backbone.children())[:-2])
16
- in_channels = backbone.num_features
17
- self.head = nn.Sequential(
18
- nn.Conv2d(in_channels, 256, kernel_size=3, padding=1),
19
- nn.ReLU(inplace=True),
20
- nn.Upsample(size=upsample_size, mode='bilinear', align_corners=False),
21
- nn.Conv2d(256, 1, kernel_size=1)
22
- )
23
-
24
- def forward(self, image):
25
- features = self.feature_extractor(image)
26
- heatmap = self.head(features)
27
- return heatmap
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
modeling_rampnet.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PreTrainedModel
2
+
3
+ from .configuration_rampnet import RampNetConfig
4
+ # rampnet_model.py is copied verbatim from rampnet/model.py by
5
+ # scripts/export_hf_model.py at export time — it is generated, not a fork.
6
+ from .rampnet_model import KeypointModel
7
+
8
+
9
+ class RampNetModel(PreTrainedModel):
10
+ config_class = RampNetConfig
11
+ main_input_name = "pixel_values"
12
+
13
+ def __init__(self, config):
14
+ super().__init__(config)
15
+ self.model = KeypointModel(
16
+ heatmap_size=tuple(config.heatmap_size),
17
+ pretrained_backbone=False,
18
+ )
19
+ # Required under transformers >= 5.x: initializes loading-related state
20
+ # (e.g. all_tied_weights_keys) that from_pretrained expects on every
21
+ # PreTrainedModel. Harmless no-op extras under 4.x.
22
+ self.post_init()
23
+
24
+ def forward(self, pixel_values):
25
+ """Returns the predicted curb ramp keypoint heatmap (B, 1, H, W)."""
26
+ return self.model(pixel_values)
rampnet_model.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import timm
2
+ import torch.nn as nn
3
+
4
+ BACKBONE_NAME = 'convnextv2_base.fcmae_ft_in22k_in1k_384'
5
+
6
+ # Panorama model (stage_two): equirectangular 2048x4096 input -> 512x1024 heatmap.
7
+ PANO_INPUT_SIZE = (2048, 4096)
8
+ PANO_HEATMAP_SIZE = (512, 1024)
9
+
10
+ # Crop model (stage_one): perspective 1024x352 input -> 256x88 heatmap.
11
+ CROP_INPUT_SIZE = (1024, 352)
12
+ CROP_HEATMAP_SIZE = (256, 88)
13
+
14
+ IMAGENET_MEAN = [0.485, 0.456, 0.406]
15
+ IMAGENET_STD = [0.229, 0.224, 0.225]
16
+
17
+
18
+ class KeypointModel(nn.Module):
19
+ """The one canonical RampNet keypoint-heatmap model.
20
+
21
+ The state_dict key layout (feature_extractor as Sequential over
22
+ backbone.children()[:-2], i.e. dropping norm_pre and head) is the layout
23
+ every released checkpoint was trained and saved with — including the
24
+ HuggingFace weights. Do not restructure the modules (e.g. features_only=True
25
+ or named submodules): that changes the keys and breaks strict loading of
26
+ all existing checkpoints.
27
+ """
28
+
29
+ def __init__(self, heatmap_size=PANO_HEATMAP_SIZE, pretrained_backbone=False):
30
+ super().__init__()
31
+ backbone = timm.create_model(BACKBONE_NAME, pretrained=pretrained_backbone)
32
+ self.feature_extractor = nn.Sequential(*list(backbone.children())[:-2])
33
+ in_channels = backbone.num_features
34
+ self.head = nn.Sequential(
35
+ nn.Conv2d(in_channels, 256, kernel_size=3, padding=1),
36
+ nn.ReLU(inplace=True),
37
+ nn.Upsample(size=heatmap_size, mode='bilinear', align_corners=False),
38
+ nn.Conv2d(256, 1, kernel_size=1)
39
+ )
40
+
41
+ def forward(self, image):
42
+ features = self.feature_extractor(image)
43
+ heatmap = self.head(features)
44
+ return heatmap