# Geo-Referenced Skyline Matching & Directed Viewshed

**EE 367 Final Project — Stanford University**
**Author:** Evan Twarog (evantwarog@stanford.edu)

Match horizon profiles extracted from geotagged photographs to a DEM-derived synthetic skyline, determine camera heading, and compute a directed viewshed showing what terrain is visible in each image.

---

## Quick Start

Run scripts in order from the project root. Each step depends on the previous.

```bash
python extract_metadata.py       # 1. EXIF + FOV extraction → metadata.csv
python extract_skyline.py        # 2. Horizon detection → results/skyline_profiles/
python compute_viewshed.py       # 3. Viewshed + synthetic skyline → results/viewshed_maps/
python match_skyline.py          # 4. Skyline matching + directed viewshed → results/skyline_matches/
python summarize_results.py      # 5. Summary tables & plots → results/summary/
python summarize_results.py --measure  # (optional) Interactive angular offset measurement
```

NOTE: WE HAD TO DELETE THE DEM DIRECTORY DUE TO ITS LARGE SIZE (3GB)

---

## Scripts

| # | Script | What it does |
|---|--------|-------------|
| 1 | `extract_metadata.py` | Extract EXIF GPS, focal length, and compute HFOV/VFOV from geotagged photos |
| 2 | `extract_skyline.py` | Detect sky–terrain boundary via gradient segmentation + morphological cleaning |
| 3 | `compute_viewshed.py` | Ray-cast 360° viewshed from DEM; generate synthetic skyline with grid-convergence correction |
| 4 | `match_skyline.py` | Match photo skyline to synthetic via NCC heading search; compute directed (wedge) viewshed |
| 5 | `summarize_results.py` | Generate metrics table, NCC curves, bar charts; optionally measure angular offsets interactively |

---

## Project Structure

```
FinalProject/
├── src/
│   ├── config.py                # Shared constants (paths, parameters)
│   └── utils.py                 # Shared utilities (image loading, compass labels)
├── extract_metadata.py          # Step 1
├── extract_skyline.py           # Step 2
├── compute_viewshed.py          # Step 3
├── match_skyline.py             # Step 4
├── summarize_results.py         # Step 5
├── images/                      # Input photos (landscape/ and portrait/)
├── dem/                         # 2 m IFSAR DEM (EPSG:3413) + Hike.json track
├── results/                     # All outputs
│   ├── skyline_profiles/        #   .npy profiles + overlay PNGs
│   ├── viewshed_maps/           #   4-panel viewshed figures
│   ├── skyline_matches/         #   Match plots, overlays, directed viewsheds
│   ├── summary/                 #   Tables, NCC curves, bar charts
│   └── angular_offsets.csv      #   Manual offset measurements (--measure)
└── references/                  # Reference papers
```

---

## Key Technical Details

- **DEM**: USGS IFSAR Alaska, 2 m resolution, EPSG:3413
- **Grid convergence**: ~104° correction (grid North → true North)
- **Observer height**: 1.7 m | **Max radius**: 15 km | **Synth rays**: 3600 (0.1° steps)
- **Heading search**: NCC over 360° at 0.5° steps → scores of 0.88–0.96
- **Mean angular offset**: ~3.3° between extracted and synthetic skylines

---

## Dependencies

```bash
pip install numpy pandas matplotlib scipy rasterio opencv-python Pillow pillow-heif
```

Requires Python 3.10+.
