| 1 | """Fetch REAL ALPR camera locations from OpenStreetMap (Overpass API). These are the camera |
| 2 | positions crowdsourced largely by the DeFlock project (deflock.me) and tagged |
| 3 | man_made=surveillance + surveillance:type=ALPR. Flock does not publish its camera locations; |
| 4 | this is the closest publicly-available, verifiable dataset. |
| 5 | |
| 6 | We query per state (by ISO3166-2 boundary) so every camera has a known state, capping per state |
| 7 | to keep the dataset map-renderable. Output is cached to sample_data/real_cameras.json. |
| 8 | |
| 9 | NOTE: only camera *locations/metadata* are public. Plate-read data is NOT public; Sightline |
| 10 | generates realistic synthetic reads against these real camera positions for the POC. |
| 11 | """ |
| 12 | from __future__ import annotations |
| 13 | |
| 14 | import json |
| 15 | import os |
| 16 | import time |
| 17 | |
| 18 | import httpx |
| 19 | |
| 20 | UA = {"User-Agent": "Sightline-ALPR-POC/1.0 (security research; portfolio)"} |
| 21 | ENDPOINTS = [ |
| 22 | "https://overpass-api.de/api/interpreter", |
| 23 | "https://overpass.kumi.systems/api/interpreter", |
| 24 | "https://maps.mail.ru/osm/tools/overpass/api/interpreter", |
| 25 | ] |
| 26 | OUT = os.path.join(os.path.dirname(__file__), "sample_data", "real_cameras.json") |
| 27 | PER_STATE_CAP = 220 |
| 28 | |
| 29 | STATES = ["AL","AK","AZ","AR","CA","CO","CT","DE","FL","GA","HI","ID","IL","IN","IA","KS","KY", |
| 30 | "LA","ME","MD","MA","MI","MN","MS","MO","MT","NE","NV","NH","NJ","NM","NY","NC","ND", |
| 31 | "OH","OK","OR","PA","RI","SC","SD","TN","TX","UT","VT","VA","WA","WV","WI","WY"] |
| 32 | |
| 33 | |
| 34 | def query_state(st: str) -> list[dict]: |
| 35 | q = (f'[out:json][timeout:60];area["ISO3166-2"="US-{st}"]->.s;' |
| 36 | f'(node(area.s)["man_made"="surveillance"]["surveillance:type"="ALPR"];);' |
| 37 | f'out body {PER_STATE_CAP};') |
| 38 | for ep in ENDPOINTS: |
| 39 | for attempt in range(2): |
| 40 | try: |
| 41 | r = httpx.post(ep, data={"data": q}, headers=UA, timeout=90) |
| 42 | if r.status_code == 200: |
| 43 | return r.json().get("elements", []) |
| 44 | if r.status_code in (429, 504): |
| 45 | time.sleep(5) |
| 46 | except Exception: |
| 47 | time.sleep(3) |
| 48 | return [] |
| 49 | |
| 50 | |
| 51 | def main(): |
| 52 | cams: list[dict] = [] |
| 53 | for i, st in enumerate(STATES): |
| 54 | els = query_state(st) |
| 55 | kept = 0 |
| 56 | for e in els: |
| 57 | t = e.get("tags", {}) |
| 58 | op = t.get("operator") or t.get("brand") or t.get("manufacturer") or "Unknown" |
| 59 | cams.append({ |
| 60 | "osm_id": e.get("id"), "state": st, |
| 61 | "lat": e.get("lat"), "lon": e.get("lon"), |
| 62 | "operator": op, |
| 63 | "manufacturer": t.get("manufacturer") or t.get("brand"), |
| 64 | "direction": t.get("direction") or t.get("camera:direction"), |
| 65 | "zone": t.get("surveillance:zone"), |
| 66 | "mount": t.get("camera:mount") or t.get("support"), |
| 67 | "ref": t.get("ref"), |
| 68 | }) |
| 69 | kept += 1 |
| 70 | print(f"[{i+1}/50] {st}: {kept} cameras (running total {len(cams)})", flush=True) |
| 71 | time.sleep(1.0) |
| 72 | |
| 73 | os.makedirs(os.path.dirname(OUT), exist_ok=True) |
| 74 | with open(OUT, "w") as fh: |
| 75 | json.dump(cams, fh) |
| 76 | ops = {} |
| 77 | for c in cams: |
| 78 | ops[c["operator"]] = ops.get(c["operator"], 0) + 1 |
| 79 | top = sorted(ops.items(), key=lambda kv: kv[1], reverse=True)[:8] |
| 80 | print(f"\nDONE: {len(cams)} real ALPR cameras across {len(STATES)} states -> {OUT}") |
| 81 | print("operators:", top) |
| 82 | |
| 83 | |
| 84 | if __name__ == "__main__": |
| 85 | main() |