Accessible pedestrian routing for the UTSA Downtown Campus (San Antonio, TX).
Navigation apps give the shortest route. For a wheelchair user or a low-vision pedestrian, that route may be impassable: a corner without a curb ramp, a bike blocking the sidewalk, or a crossing with no signal. RowdyAccess combines OpenStreetMap sidewalk data with computer vision run on street photos we took ourselves (YOLO + RampNet). It then shows the shortest route next to the best route for your profile and explains the difference in plain language.
Scope: about 700 m around 501 W César E. Chávez Blvd.
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# CPU-only laptop? Install the much smaller CPU build of torch first:
# pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
python backend/main.py # → http://localhost:8000
On first start the server downloads YOLO11n (models/yolo11n.pt) and RampNet (Hugging Face cache).
After that everything works offline except the map tiles:
HF_HUB_OFFLINE=1 python backend/main.py # demo day: no network calls to Hugging Face
PATHABLE_PORT=8765 python backend/main.py # other port
PATHABLE_DEVICE=cpu python backend/main.py # force CPU
PATHABLE_NO_DETECTOR=1 python backend/main.py # routing only (no torch needed at runtime)
Basemap: the app uses OpenStreetMap standard raster tiles and needs no map-provider API key. Without internet, routes and markers still draw on a plain background.
The OSM graph is saved per campus (data/utsa_downtown_walk.graphml, data/utsa_main_walk.graphml) and is
never re-downloaded. Delete a file to rebuild it (needs internet).
The data the app changes while it runs is kept in a SQLite database, data/rowdyaccess.db (backend/store.py):
the map points (live set and the "Restore demo data" baseline), share links and the street-view photo index.
Every change is one transaction.
The database file is part of the repository; there are no JSON copies of this data.
python backend/store.py info # what is in the database
python backend/store.py export # write it as JSON files into data/export/ (to read or diff; not tracked)
python backend/store.py import # load data/export/ back into the database (replaces its contents)
PATHABLE_DB=/tmp/test.db python backend/main.py # use another database file
Reference data that the app only reads stays in JSON: presets.json, entrances.json, streets.json,
osm_features.json, via_link_stops.json.
The app covers the campuses listed under campuses in data/presets.json (Downtown Campus, Main Campus). The
user picks one first; its buildings, parking lots, service area and map view are then loaded. Each place in
buildings belongs to a campus ("campus": "main"; without the field: downtown). To add or refresh a campus:
python -c "import sys; sys.path.insert(0, 'backend'); import graph_builder as gb; gb.download_graph('main')"
python backend/entrances.py main # entrances of its places → data/entrances.json
python backend/streets.py # street names → data/streets.json
python backend/osm_features.py main # mapped crossings, steps, curb ramps → data/osm_features.json
Walking routes stay within one campus.
# put your keys in .env first (see .env.example); it is copied into the image
docker compose up --build # → http://localhost:8000 (admin: http://localhost:8000/admin)
The image builds the React frontend, installs the CPU build of PyTorch and bundles YOLO11n, so the
container starts without downloading anything. ./data is mounted into the container: uploads, admin
edits and share links stay on the host and survive rebuilds.
.env is copied into the image when it is built (/app/.env), so the server and the scripts
inside it (docker compose exec app python backend/mapillary_scan.py --campus main) have your keys, and the image
runs on another machine without the file. That means the image contains your API keys: keep it private and do
not push it to a public registry. With compose, .env is also passed at start, so after editing it
docker compose up -d is enough; for a plain docker run, rebuild.id -u prints something else, add
PATHABLE_UID=<id -u> and PATHABLE_GID=<id -g> to .env before building, otherwise it cannot write ./data.PATHABLE_ADMIN_TOKEN in .env, or read the random one from docker compose logs app.ports in docker-compose.yml ("8080:8000"). A Google Maps key
restricted to http://localhost:8000/* then needs the new address added.docker compose --profile gpu up --build app-gpu. This installs CUDA PyTorch (a much larger image) and
runs RampNet on uploaded photos; RampNet is downloaded on first start and kept in the hf-cache volume.PATHABLE_UPLOAD_RAMPNET in
docker-compose.yml to turn it on.Without compose: docker build -t rowdyaccess . && docker run --rm -p 8000:8000 -v "$PWD/data:/app/data" rowdyaccess
The UI lives in web/ (React 19, Material Web components, dynamic Material You color, MapLibre GL).
A built copy is in frontend/dist/, which FastAPI serves at /, so the demo needs no Node.js.
To change the UI (needs Node.js 20+; on our dev server: conda activate pathable-node):
cd web
npm install
npm run dev # http://localhost:5173, API calls are proxied to the backend on :8000
npm run build # writes ../frontend/dist (commit it)
Notes for the browser:
http://localhost:8000 or HTTPS) and location permission. Away from campus it
says how far you are and offers Use campus location for demos.| Step | In the UI |
|---|---|
| 1. Campus map with detected features | Open the page (3D map; 3D button bottom-right toggles 2D). Click any marker to see its photo with the YOLO box, type, confidence and a Verified map information badge when it was added manually. |
| 2. Wheelchair: shortest vs accessible | Load sample trip (Durango Building → Casa Navarro). The Routes list shows every path we compute (shortest, step-free, signalized-crossing), each rated for the selected profile. The shortest is shown first, in red, marked Not accessible with its barriers. Click Step-free route · Recommended to switch: "This route is 108 m (2 min) longer but avoids 1 corner without a curb ramp, 1 blocked sidewalk, …" Only the selected route is drawn. |
| 3. Low vision | Click the Low vision tab, then the recommended Signalized-crossing route: "…uses 4 signalized crossings instead of 2." |
| 4. Add map information | Switch back to Wheelchair and select the recommended route, click the camera button (Add map info), select a photo of an obstacle on the blue route, then Analyze photo. The new feature appears on the map and the route recalculates around it. If YOLO misses it, choose Obstacle and Add to map. |
| Multi-destination | Add destination (or click the map) to add stops B, C, D… (up to 8). Each leg is routed separately and listed with its distance and time. Reverse flips the order. |
| Reset | The reset button (Restore demo data) restores the baseline map points in the database. |
The service area (shaded outside, dashed outline) is the bounding box of the campus buildings in data/presets.json plus 150 m (BOUNDARY_PAD_M in backend/main.py); stops outside it are rejected.
The sample trip is set in data/presets.json ("demo": start/end, or a "stops" list of building names). The building coordinates come from OSM.
python backend/osm_features.py # → data/osm_features.json
Imports what OpenStreetMap already maps inside the service area: crossings (signalized with marking style, audible signals and push buttons where tagged; marked; unmarked), curb types, and steps. They show on the map with a "From OpenStreetMap" link and are merged into the map points by the detector (step 3 below); a team label at the same spot (within 5 m) overrides them. Currently: 53 signalized crossings, 7 marked crosswalks, 5 unmarked crossings, 2 sets of steps.
OpenStreetMap has no curb-ramp data here, but Mapillary has thousands of street photos downtown (2015–2021,
mostly dashcam; no 360° panoramas around campus). With a Mapillary client token in .env (MAPILLARY_TOKEN="MLY|..."):
python backend/mapillary_scan.py --limit 5 # try a few crossings first
python backend/mapillary_scan.py # every mapped crossing (~5 min on GPU)
For each crossing it picks the 2 nearest photos whose camera faces it, runs RampNet (and YOLO), turns each ramp's position in the photo into a compass direction, and snaps it to the crossing corner in that direction. RampNet was trained on 360° panoramas, so on ordinary photos its peaks are weaker (0.3–0.5): the scan uses threshold 0.3 and keeps only ramps that line up with a mapped crossing corner. YOLO's objects are logged but not saved (the photos are years old). Results replace earlier scan results in the database (live points and the reset baseline). Restart the server afterwards. Last run: 73 photos at 65 crossings → 13 curb ramps (confidence 0.35–0.82), each with a link to its Mapillary photo.
Street View for map points: python backend/mapillary_scan.py --streetview picks, for every feature in
the database, the nearest Mapillary photo that faces it (else the closest within 60 m), saves it as
data/photos/sv_<id>.jpg and indexes it in the database. Popups then show that photo with
Open Street View, an interactive Mapillary viewer (needs MAPILLARY_TOKEN). Last run: 67 of 80 features.
pillow-heif), but JPEG is safer.corner_*.jpg. If RampNet finds no ramp in a corner photo, it records a no_curb_ramp.data/photos/ and run:
python backend/detector.py --photos data/photos
# --no-rampnet YOLO only (e.g. on a CPU laptop; RampNet takes ~30 s per photo on CPU)
# --rampnet-mode stretch README-style resize instead of the panorama embedding (compare both!)
Photos without GPS are skipped and logged. Annotated copies go to data/photos/annotated/.
The results go to the database (live points and the reset baseline).data/manual_labels.json, which uses the same format; id, photo and bbox are optional:
[{"type": "curb_ramp", "lat": 29.42359, "lon": -98.50051, "label": "ramp, NW corner Nueva/Pecos"}]
A manual label replaces any model label of the same kind (ramp / no ramp count as one kind) within 5 m. Re-run step 3, then restart the server.Shared detection format (every source outputs this):
{"id": "det_0001", "type": "obstacle", "label": "bicycle", "lat": 29.4229, "lon": -98.5012,
"confidence": 0.91, "photo": "IMG_0412.jpg", "bbox": [x1, y1, x2, y2], "model": "yolo-coco", "verified": false}
All weights are in one dictionary: PROFILES in backend/profiles.py.
Penalties are "extra meters"; INF means blocked. To see how routes change, run:
python backend/graph_builder.py 29.422982 -98.502770 29.423082 -98.497637
| Method | Path | |
|---|---|---|
| POST | /route | {"start": [lat, lon], "end": [lat, lon], "profile": "wheelchair"}, or {"stops": [[lat, lon], …], "profile": …} for 2–8 stops (adds legs); also returns options (every distinct path, rated for the profile, with recommended, blocked, explanation); returns shortest and accessible (GeoJSON LineString, per-segment known/unknown, length, time, features), explanation, and message if no route exists |
| GET | /features | all detections as GeoJSON points (type, confidence, photo, verified, snap info) |
| POST | /upload | multipart file (+ optional lat, lon if EXIF has no GPS). Runs detection, saves, re-attaches |
| POST | /label | {"type", "lat", "lon"} adds verified map information |
| POST | /reset | restores the baseline map points in the database |
| GET | /photos/{name} | original photo; ?annotated=1 for the version with boxes |
| GET | /presets, /health | buildings + demo route; status |
phone photos (GPS EXIF) OpenStreetMap (osmnx, downloaded once)
│ │
▼ ▼
detector.py graph_builder.py
├ EXIF GPS (Pillow) ├ walk graph → data/utsa_downtown_walk.graphml
├ YOLO11n COCO → obstacle, ├ crossings = groups of footway=crossing edges
│ traffic light (weak signal hint) │ + OSM tags: crossing, kerb, tactile_paving, surface
├ RampNet → curb_ramp / no_curb_ramp ├ attach detections (≤ 25 m):
├ crosswalk.pt (optional) │ corner types → nearest crossing node
├ manual_labels.json override (5 m) │ obstacle / damaged → nearest edge
└ annotated JPEGs └ per-profile edge costs (profiles.py)
│ │
└────────► data/rowdyaccess.db ◄────┘
│
main.py (FastAPI, all in memory)
/route /features /upload /label /reset /photos
│
web/ → frontend/dist (React + Material You, MapLibre GL 3D map)
multi-stop directions · markers + photos · route comparison · explanation · upload
How the cost model works:
length × multipliers + penalties.no_curb_ramp from a manual label, a verified label, or OSM kerb=raised blocks the crossing. One that comes only from RampNet adds a large penalty instead, because a model miss shouldn't cut off a corner.| What | License | Used for |
|---|---|---|
| OpenStreetMap contributors | ODbL | sidewalks, crossings, tags, map tiles, building locations |
| OSMnx | MIT | downloading and simplifying the walk graph |
| NetworkX | BSD-3 | Dijkstra routing |
| Shapely | BSD-3 | geometry / snapping |
| scikit-learn | BSD-3 | nearest-node search (via OSMnx) |
| Ultralytics YOLO11n, pretrained on COCO | AGPL-3.0 (model), CC BY 4.0 (COCO) | obstacle / traffic light detection |
RampNet projectsidewalk/rampnet-model (Project Sidewalk, University of Washington) | MIT | curb ramp detection |
| PyTorch, torchvision, timm, Transformers | BSD / Apache-2.0 | model inference |
| scikit-image | BSD-3 | RampNet heatmap peaks |
| Pillow, pillow-heif | MIT-CMU / BSD-3 | EXIF GPS, drawing boxes, HEIC |
| FastAPI, Uvicorn | MIT / BSD-3 | API server |
| OpenStreetMap standard tiles | ODbL | basemap |
| MapLibre GL JS 5.24 | BSD-3 | 3D map |
| React 19 + Vite | MIT | frontend app and build |
| Material Web + material-color-utilities | Apache-2.0 | Material You (Material Design 3) components and dynamic color |
| Material Symbols, Roboto (bundled via npm) | Apache-2.0 / OFL | UI icons and font |
| Temaki 5.13 (OpenStreetMap editor icons) | CC0 | map icons: curb ramp, missing curb ramp, crosswalk, pedestrian signal |
| Font Awesome Free 7.3 | Icons CC BY 4.0 | icons: obstacle, damaged sidewalk, steps, profiles (bundled in frontend/vendor/icons/icons.js) |
| Nominatim (OSM) | ODbL | geocoded the campus address once (hardcoded afterwards) |
All application code was written during the event.
COUNT_CARS_AS_OBSTACLES = False in detector.py); otherwise every street photo would block its sidewalk.wheelchair and incline tags are absent here and kerb / tactile_paving are rare, so much of each route shows as "no data". Crossing tags (signals, markings) are well mapped.PATHABLE_UPLOAD_RAMPNET=1.Accessible pedestrian routing for the UTSA Downtown Campus (San Antonio, TX).
Navigation apps give the shortest route. For a wheelchair user or a low-vision pedestrian, that route may be impassable: a corner without a curb ramp, a bike blocking the sidewalk, or a crossing with no signal. RowdyAccess combines OpenStreetMap sidewalk data with computer vision run on street photos we took ourselves (YOLO + RampNet). It then shows the shortest route next to the best route for your profile and explains the difference in plain language.
Scope: about 700 m around 501 W César E. Chávez Blvd.
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# CPU-only laptop? Install the much smaller CPU build of torch first:
# pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
python backend/main.py # → http://localhost:8000
On first start the server downloads YOLO11n (models/yolo11n.pt) and RampNet (Hugging Face cache).
After that everything works offline except the map tiles:
HF_HUB_OFFLINE=1 python backend/main.py # demo day: no network calls to Hugging Face
PATHABLE_PORT=8765 python backend/main.py # other port
PATHABLE_DEVICE=cpu python backend/main.py # force CPU
PATHABLE_NO_DETECTOR=1 python backend/main.py # routing only (no torch needed at runtime)
Basemap: the app uses OpenStreetMap standard raster tiles and needs no map-provider API key. Without internet, routes and markers still draw on a plain background.
The OSM graph is saved per campus (data/utsa_downtown_walk.graphml, data/utsa_main_walk.graphml) and is
never re-downloaded. Delete a file to rebuild it (needs internet).
The data the app changes while it runs is kept in a SQLite database, data/rowdyaccess.db (backend/store.py):
the map points (live set and the "Restore demo data" baseline), share links and the street-view photo index.
Every change is one transaction.
The database file is part of the repository; there are no JSON copies of this data.
python backend/store.py info # what is in the database
python backend/store.py export # write it as JSON files into data/export/ (to read or diff; not tracked)
python backend/store.py import # load data/export/ back into the database (replaces its contents)
PATHABLE_DB=/tmp/test.db python backend/main.py # use another database file
Reference data that the app only reads stays in JSON: presets.json, entrances.json, streets.json,
osm_features.json, via_link_stops.json.
The app covers the campuses listed under campuses in data/presets.json (Downtown Campus, Main Campus). The
user picks one first; its buildings, parking lots, service area and map view are then loaded. Each place in
buildings belongs to a campus ("campus": "main"; without the field: downtown). To add or refresh a campus:
python -c "import sys; sys.path.insert(0, 'backend'); import graph_builder as gb; gb.download_graph('main')"
python backend/entrances.py main # entrances of its places → data/entrances.json
python backend/streets.py # street names → data/streets.json
python backend/osm_features.py main # mapped crossings, steps, curb ramps → data/osm_features.json
Walking routes stay within one campus.
# put your keys in .env first (see .env.example); it is copied into the image
docker compose up --build # → http://localhost:8000 (admin: http://localhost:8000/admin)
The image builds the React frontend, installs the CPU build of PyTorch and bundles YOLO11n, so the
container starts without downloading anything. ./data is mounted into the container: uploads, admin
edits and share links stay on the host and survive rebuilds.
.env is copied into the image when it is built (/app/.env), so the server and the scripts
inside it (docker compose exec app python backend/mapillary_scan.py --campus main) have your keys, and the image
runs on another machine without the file. That means the image contains your API keys: keep it private and do
not push it to a public registry. With compose, .env is also passed at start, so after editing it
docker compose up -d is enough; for a plain docker run, rebuild.id -u prints something else, add
PATHABLE_UID=<id -u> and PATHABLE_GID=<id -g> to .env before building, otherwise it cannot write ./data.PATHABLE_ADMIN_TOKEN in .env, or read the random one from docker compose logs app.ports in docker-compose.yml ("8080:8000"). A Google Maps key
restricted to http://localhost:8000/* then needs the new address added.docker compose --profile gpu up --build app-gpu. This installs CUDA PyTorch (a much larger image) and
runs RampNet on uploaded photos; RampNet is downloaded on first start and kept in the hf-cache volume.PATHABLE_UPLOAD_RAMPNET in
docker-compose.yml to turn it on.Without compose: docker build -t rowdyaccess . && docker run --rm -p 8000:8000 -v "$PWD/data:/app/data" rowdyaccess
The UI lives in web/ (React 19, Material Web components, dynamic Material You color, MapLibre GL).
A built copy is in frontend/dist/, which FastAPI serves at /, so the demo needs no Node.js.
To change the UI (needs Node.js 20+; on our dev server: conda activate pathable-node):
cd web
npm install
npm run dev # http://localhost:5173, API calls are proxied to the backend on :8000
npm run build # writes ../frontend/dist (commit it)
Notes for the browser:
http://localhost:8000 or HTTPS) and location permission. Away from campus it
says how far you are and offers Use campus location for demos.| Step | In the UI |
|---|---|
| 1. Campus map with detected features | Open the page (3D map; 3D button bottom-right toggles 2D). Click any marker to see its photo with the YOLO box, type, confidence and a Verified map information badge when it was added manually. |
| 2. Wheelchair: shortest vs accessible | Load sample trip (Durango Building → Casa Navarro). The Routes list shows every path we compute (shortest, step-free, signalized-crossing), each rated for the selected profile. The shortest is shown first, in red, marked Not accessible with its barriers. Click Step-free route · Recommended to switch: "This route is 108 m (2 min) longer but avoids 1 corner without a curb ramp, 1 blocked sidewalk, …" Only the selected route is drawn. |
| 3. Low vision | Click the Low vision tab, then the recommended Signalized-crossing route: "…uses 4 signalized crossings instead of 2." |
| 4. Add map information | Switch back to Wheelchair and select the recommended route, click the camera button (Add map info), select a photo of an obstacle on the blue route, then Analyze photo. The new feature appears on the map and the route recalculates around it. If YOLO misses it, choose Obstacle and Add to map. |
| Multi-destination | Add destination (or click the map) to add stops B, C, D… (up to 8). Each leg is routed separately and listed with its distance and time. Reverse flips the order. |
| Reset | The reset button (Restore demo data) restores the baseline map points in the database. |
The service area (shaded outside, dashed outline) is the bounding box of the campus buildings in data/presets.json plus 150 m (BOUNDARY_PAD_M in backend/main.py); stops outside it are rejected.
The sample trip is set in data/presets.json ("demo": start/end, or a "stops" list of building names). The building coordinates come from OSM.
python backend/osm_features.py # → data/osm_features.json
Imports what OpenStreetMap already maps inside the service area: crossings (signalized with marking style, audible signals and push buttons where tagged; marked; unmarked), curb types, and steps. They show on the map with a "From OpenStreetMap" link and are merged into the map points by the detector (step 3 below); a team label at the same spot (within 5 m) overrides them. Currently: 53 signalized crossings, 7 marked crosswalks, 5 unmarked crossings, 2 sets of steps.
OpenStreetMap has no curb-ramp data here, but Mapillary has thousands of street photos downtown (2015–2021,
mostly dashcam; no 360° panoramas around campus). With a Mapillary client token in .env (MAPILLARY_TOKEN="MLY|..."):
python backend/mapillary_scan.py --limit 5 # try a few crossings first
python backend/mapillary_scan.py # every mapped crossing (~5 min on GPU)
For each crossing it picks the 2 nearest photos whose camera faces it, runs RampNet (and YOLO), turns each ramp's position in the photo into a compass direction, and snaps it to the crossing corner in that direction. RampNet was trained on 360° panoramas, so on ordinary photos its peaks are weaker (0.3–0.5): the scan uses threshold 0.3 and keeps only ramps that line up with a mapped crossing corner. YOLO's objects are logged but not saved (the photos are years old). Results replace earlier scan results in the database (live points and the reset baseline). Restart the server afterwards. Last run: 73 photos at 65 crossings → 13 curb ramps (confidence 0.35–0.82), each with a link to its Mapillary photo.
Street View for map points: python backend/mapillary_scan.py --streetview picks, for every feature in
the database, the nearest Mapillary photo that faces it (else the closest within 60 m), saves it as
data/photos/sv_<id>.jpg and indexes it in the database. Popups then show that photo with
Open Street View, an interactive Mapillary viewer (needs MAPILLARY_TOKEN). Last run: 67 of 80 features.
pillow-heif), but JPEG is safer.corner_*.jpg. If RampNet finds no ramp in a corner photo, it records a no_curb_ramp.data/photos/ and run:
python backend/detector.py --photos data/photos
# --no-rampnet YOLO only (e.g. on a CPU laptop; RampNet takes ~30 s per photo on CPU)
# --rampnet-mode stretch README-style resize instead of the panorama embedding (compare both!)
Photos without GPS are skipped and logged. Annotated copies go to data/photos/annotated/.
The results go to the database (live points and the reset baseline).data/manual_labels.json, which uses the same format; id, photo and bbox are optional:
[{"type": "curb_ramp", "lat": 29.42359, "lon": -98.50051, "label": "ramp, NW corner Nueva/Pecos"}]
A manual label replaces any model label of the same kind (ramp / no ramp count as one kind) within 5 m. Re-run step 3, then restart the server.Shared detection format (every source outputs this):
{"id": "det_0001", "type": "obstacle", "label": "bicycle", "lat": 29.4229, "lon": -98.5012,
"confidence": 0.91, "photo": "IMG_0412.jpg", "bbox": [x1, y1, x2, y2], "model": "yolo-coco", "verified": false}
All weights are in one dictionary: PROFILES in backend/profiles.py.
Penalties are "extra meters"; INF means blocked. To see how routes change, run:
python backend/graph_builder.py 29.422982 -98.502770 29.423082 -98.497637
| Method | Path | |
|---|---|---|
| POST | /route | {"start": [lat, lon], "end": [lat, lon], "profile": "wheelchair"}, or {"stops": [[lat, lon], …], "profile": …} for 2–8 stops (adds legs); also returns options (every distinct path, rated for the profile, with recommended, blocked, explanation); returns shortest and accessible (GeoJSON LineString, per-segment known/unknown, length, time, features), explanation, and message if no route exists |
| GET | /features | all detections as GeoJSON points (type, confidence, photo, verified, snap info) |
| POST | /upload | multipart file (+ optional lat, lon if EXIF has no GPS). Runs detection, saves, re-attaches |
| POST | /label | {"type", "lat", "lon"} adds verified map information |
| POST | /reset | restores the baseline map points in the database |
| GET | /photos/{name} | original photo; ?annotated=1 for the version with boxes |
| GET | /presets, /health | buildings + demo route; status |
phone photos (GPS EXIF) OpenStreetMap (osmnx, downloaded once)
│ │
▼ ▼
detector.py graph_builder.py
├ EXIF GPS (Pillow) ├ walk graph → data/utsa_downtown_walk.graphml
├ YOLO11n COCO → obstacle, ├ crossings = groups of footway=crossing edges
│ traffic light (weak signal hint) │ + OSM tags: crossing, kerb, tactile_paving, surface
├ RampNet → curb_ramp / no_curb_ramp ├ attach detections (≤ 25 m):
├ crosswalk.pt (optional) │ corner types → nearest crossing node
├ manual_labels.json override (5 m) │ obstacle / damaged → nearest edge
└ annotated JPEGs └ per-profile edge costs (profiles.py)
│ │
└────────► data/rowdyaccess.db ◄────┘
│
main.py (FastAPI, all in memory)
/route /features /upload /label /reset /photos
│
web/ → frontend/dist (React + Material You, MapLibre GL 3D map)
multi-stop directions · markers + photos · route comparison · explanation · upload
How the cost model works:
length × multipliers + penalties.no_curb_ramp from a manual label, a verified label, or OSM kerb=raised blocks the crossing. One that comes only from RampNet adds a large penalty instead, because a model miss shouldn't cut off a corner.| What | License | Used for |
|---|---|---|
| OpenStreetMap contributors | ODbL | sidewalks, crossings, tags, map tiles, building locations |
| OSMnx | MIT | downloading and simplifying the walk graph |
| NetworkX | BSD-3 | Dijkstra routing |
| Shapely | BSD-3 | geometry / snapping |
| scikit-learn | BSD-3 | nearest-node search (via OSMnx) |
| Ultralytics YOLO11n, pretrained on COCO | AGPL-3.0 (model), CC BY 4.0 (COCO) | obstacle / traffic light detection |
RampNet projectsidewalk/rampnet-model (Project Sidewalk, University of Washington) | MIT | curb ramp detection |
| PyTorch, torchvision, timm, Transformers | BSD / Apache-2.0 | model inference |
| scikit-image | BSD-3 | RampNet heatmap peaks |
| Pillow, pillow-heif | MIT-CMU / BSD-3 | EXIF GPS, drawing boxes, HEIC |
| FastAPI, Uvicorn | MIT / BSD-3 | API server |
| OpenStreetMap standard tiles | ODbL | basemap |
| MapLibre GL JS 5.24 | BSD-3 | 3D map |
| React 19 + Vite | MIT | frontend app and build |
| Material Web + material-color-utilities | Apache-2.0 | Material You (Material Design 3) components and dynamic color |
| Material Symbols, Roboto (bundled via npm) | Apache-2.0 / OFL | UI icons and font |
| Temaki 5.13 (OpenStreetMap editor icons) | CC0 | map icons: curb ramp, missing curb ramp, crosswalk, pedestrian signal |
| Font Awesome Free 7.3 | Icons CC BY 4.0 | icons: obstacle, damaged sidewalk, steps, profiles (bundled in frontend/vendor/icons/icons.js) |
| Nominatim (OSM) | ODbL | geocoded the campus address once (hardcoded afterwards) |
All application code was written during the event.
COUNT_CARS_AS_OBSTACLES = False in detector.py); otherwise every street photo would block its sidewalk.wheelchair and incline tags are absent here and kerb / tactile_paving are rare, so much of each route shows as "no data". Crossing tags (signals, markings) are well mapped.PATHABLE_UPLOAD_RAMPNET=1.