● Zion Boggan
repos/Sightline/backend/fetch_cameras.py
zionboggan.com ↗
85 lines · python
History for this file →
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()