Automated analysis of coverage and visibility zones using QGIS and Python.
This repository provides a script-based solution to:
- Generate viewsheds (visibility zones) from high points
- Normalize and combine them
- Analyze coverage and overlaps
- Visualize everything within QGIS using real DEM data and basemaps
.
├── data/ # Input & output data (CSV, DEM, results)
│ ├── sdis-67/
│ │ ├── sites.csv # Input coordinates
│ │ ├── dem_l93.tif # Projected DEM (EPSG:2154)
│ │ └── output/ # Generated outputs (viewsheds, metrics)
├── analysis_shape/ # Core Python modules
│ ├── viewshed.py # Viewshed creation
│ ├── area_analysis.py # Coverage & overlap analysis
│ └── utils.py # Helper functions
├── generate_dem.py # Script to download and reproject DEM
├── main.py # Main QGIS execution script
├── export.py # Export viewsheds as KMZ overlays + GeoPackage polygons
├── visualize.py # Streamlit app to explore viewsheds + per-site/total area stats
└── README.md
pip install -r requirements.txtOr use the provided .venv.
You can now automatically download and project a DEM using:
python generate_dem.pyThis script:
- Parses station coordinates from
sites.csv - Computes a bounding box with buffer
- Downloads SRTM tiles using the
eioCLI - Reprojects to
EPSG:2154 - Saves the result to
data/sdis-67/dem_l93.tif
- Open QGIS
- Create a new project
- Open the Python Console → Show Editor
- Load and run
main.py
This will:
- Load your DEM and OpenStreetMap as background
- Generate and normalize viewsheds
- Compute overlaps and total coverage
- Save results to
output.csv
- Reads
sites.csvwithName,Latitude,Longitude,Height - Reprojects points to
EPSG:2154 - Generates one
.tifviewshed per point
normalize_create()replaces no-data with zerosdisplay_tif()adds raster layer with stylefusion_or()andfusion_and()combine rasters
- Uses
rasterioto compute:- Area covered per viewshed
- % of total coverage
- Pairwise overlaps between viewsheds
After running main.py, you’ll find:
- Individual viewsheds:
data/sdis-67/output/viewsheds_geotiff/ - Normalized viewsheds:
data/sdis-67/output/normalized/ - Combined viewsheds:
data/sdis-67/output/fusion/ - Coverage metrics:
data/sdis-67/output/output.csv
Once main.py has produced the raw viewsheds, two helper scripts package them
for delivery and quick inspection. Both read the same folder = "<region>"
constant at the top of the file — change it to switch region.
python export.pyFor each viewshed_<site>.tif in data/<region>/output/viewsheds_geotiff/:
- KMZ — reprojects to WGS84, renders the visible pixels as a colored
semi-transparent PNG ground overlay, and writes one
<site>.kmzper site (Google Earth ready). Output:kmz_output_<region>/. - GeoPackage — polygonizes each binary raster into a single dissolved
(Multi)Polygonper site (vectorization in source CRS for accuracy, then reprojected to WGS84), and writes them all to a singlekmz_output_<region>/viewsheds_<region>.gpkgfile with one layer (viewsheds) and asite_nameattribute.
Recommended (zero-setup, isolated env via uv):
uv run --with-requirements requirements.txt streamlit run visualize.pyOr with the project venv directly (after pip install -r requirements.txt):
streamlit run visualize.py
⚠️ If you have a system/homebrewstreamliton yourPATH, plainstreamlit runmay pick that binary and fail withModuleNotFoundError: No module named 'geopandas'. Theuv runform avoids this entirely.
Interactive web app to inspect the GeoPackage produced by export.py.
Sidebar — Sites
- Auto-select — pick a number
Nand click "Pick best N (max coverage)": a greedy max-coverage approximation chooses the N sites whose combined viewsheds cover the largest area (each step adds the site that grows the union the most, so complementary sites win over near-duplicates). Greedy is provably ≥ (1 − 1/e) ≈ 63 % of optimum and is essentially optimal at this scale. - Manual — All / None bulk toggles, then a vertical checkbox list with every site visible at once.
Main panel
- Top-of-page metrics: number of sites selected, total covered area (geometric union — overlap deduplicated), and the overlap surface (sum of per-site areas minus the union).
- A folium map of the selected viewsheds (CartoDB Positron basemap, colored by site, hover tooltip with site name + area).
- A sortable per-site area table (km²).
Areas are computed by reprojecting to a local UTM zone via
gdf.estimate_utm_crs() so they are accurate regardless of region.
Requires streamlit, streamlit-folium, folium, matplotlib,
mapclassify (all in requirements.txt).
# Step 1: Generate DEM
python generate_dem.pyThen:
- Open QGIS
- Create a new project
- Open the Python Console → Show Editor
- Load and run
main.py - After the script finishes, go to
Menu: Project → Properties → CRS and set it toEPSG:2154(Lambert-93) - Back in a regular shell, run
python export.pyto produce KMZ + GeoPackage deliverables, thenstreamlit run visualize.pyto explore the result with per-site/total-area stats.
ℹ️ Note: Due to QGIS limitations, the project CRS might not fully apply during script execution. Manually setting it ensures all layers are correctly reprojected.
- This project uses the QGIS Visibility Analysis processing tool under the hood.
- Basemap is OpenStreetMap (via XYZ tiles), aligned with EPSG:2154.
- All coordinates are reprojected from
EPSG:4326(lat/lon) toEPSG:2154. - Output rasters use LZW compression for performance.