File size: 16,194 Bytes
7a58fa9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# Satellite Change Detection β€” Project Workflow Guide

A complete walkthrough of how this project is structured, how a detection request
flows through the system, and how to run it locally. Read this top-to-bottom to get
familiar with the codebase.

---

## 1. What this project does

This is a **standalone web application for satellite image change detection**. A user
uploads two images of the same place taken at different times (a **"before"** and an
**"after"**), and the app:

1. Aligns the two images (registration).
2. Normalizes their colors/brightness.
3. Runs change detection (classical computer vision + a pretrained deep-learning model).
4. Finds and classifies the changed regions (new buildings, vegetation change, water, etc.).
5. Draws the changes on an overlay image and shows statistics.
6. Saves the run to a per-user history (with login/accounts).

It is deployed as a Docker container on **Hugging Face Spaces**, but runs the same way locally.

---

## 2. Tech stack

| Layer | Technology |
|-------|-----------|
| Backend API | **FastAPI** (Python), served by **uvicorn** |
| Image processing | **OpenCV**, **NumPy**, **scikit-learn** |
| Deep learning | **PyTorch** + **Hugging Face Transformers** (AdaptFormer model) |
| Database | **SQLAlchemy** ORM over **SQLite** (default) or **PostgreSQL** |
| Auth | **JWT** tokens (python-jose) + **bcrypt** password hashing (passlib) |
| Frontend | Plain **HTML + CSS + vanilla JavaScript** (single-page app) |
| Email | HTTP email API or SMTP (optional notifications) |
| Deployment | **Docker** β†’ Hugging Face Spaces |

There is **no build step** for the frontend β€” it's static files served directly.

---

## 3. Repository structure

```
change_detection_webapp/
β”œβ”€β”€ app/
β”‚   β”œβ”€β”€ main.py             # FastAPI app + all HTTP routes (entry point)
β”‚   β”œβ”€β”€ detection_engine.py # CORE: preprocessing, registration, detection, classification
β”‚   β”œβ”€β”€ model_inference.py  # AdaptFormer deep-learning model loader + tiled inference
β”‚   β”œβ”€β”€ cd_models/          # Extra DL building blocks
β”‚   β”‚   β”œβ”€β”€ change_model.py  #   Siamese U-Net architecture (optional, needs weights)
β”‚   β”‚   └── model_utils.py   #   tiling, multi-scale, confidence-map helpers
β”‚   β”œβ”€β”€ auth.py             # JWT create/verify, password hashing, current-user lookup
β”‚   β”œβ”€β”€ database.py         # SQLAlchemy engine/session, DATA_DIR resolution
β”‚   β”œβ”€β”€ models.py           # ORM models: User, DetectionRun
β”‚   └── notifier.py         # Email notification sending
β”œβ”€β”€ static/
β”‚   β”œβ”€β”€ css/style.css       # All styles
β”‚   └── js/app.js           # All frontend logic (auth, upload, render results)
β”œβ”€β”€ templates/
β”‚   └── index.html          # Single-page UI shell
β”œβ”€β”€ scripts/
β”‚   └── validate_detection.py  # Sanity checks for the detection pipeline
β”œβ”€β”€ data/                   # Created at runtime: SQLite DB + overlay/thumbnail images
β”œβ”€β”€ requirements.txt        # Python dependencies
β”œβ”€β”€ Dockerfile              # Container build (also pre-downloads the model)
β”œβ”€β”€ README.md               # Short setup notes (Hugging Face front-matter on top)
└── WORKFLOW.md             # This document
```

> Note: the `app/cd_models/` folder is named `cd_models` (not `models`) on purpose β€”
> `app/models.py` already exists for the database models, and a folder named `models`
> would shadow it and break imports.

---

## 4. High-level architecture

```mermaid
flowchart LR
    browser["Browser (index.html + app.js)"] -->|"HTTP / JSON"| api["FastAPI (main.py)"]
    api --> auth["auth.py (JWT)"]
    api --> engine["detection_engine.py"]
    engine --> model["model_inference.py (AdaptFormer)"]
    api --> db["(SQLite / Postgres via models.py)"]
    api --> notifier["notifier.py (email)"]
    api --> files["data/overlays (result images)"]
```

- The **browser** talks only to FastAPI over JSON + multipart form uploads.
- **main.py** is the only place that defines routes; it delegates the heavy lifting to
  `detection_engine.run_detection(...)`.
- Results (overlay PNGs, thumbnails) are written to `data/overlays/` and referenced by URL.
- Run metadata is stored in the database.

---

## 5. The detection pipeline (the core workflow)

This is the most important part to understand. Everything happens inside
`run_detection()` in [`app/detection_engine.py`](app/detection_engine.py).

```mermaid
flowchart TD
    start["run_detection(before, after, method, sensitivity, ...)"] --> pre["preprocess_image()  -- RGB, resize, denoise"]
    pre --> reg{"registration enabled?"}
    reg -->|yes| align["register_images()  -- SIFT/ORB + ECC align, returns quality metrics"]
    reg -->|no| norm
    align --> norm{"normalization enabled?"}
    norm -->|yes| radio["normalize_radiometry()  -- LAB color match + CLAHE"]
    norm -->|no| method
    radio --> method["pick detection method"]
    method --> ai["AI-Based Deep Learning (default)"]
    method --> diff["Image Difference"]
    method --> feat["Feature-Based (KMeans)"]
    method --> hyb["Hybrid / Hybrid AI"]
    ai --> fuse["AdaptFormer score + classical score -> confidence-gated fusion"]
    diff --> mask
    feat --> mask
    hyb --> fuse
    fuse --> mask["binary change mask"]
    mask --> regions["analyze_change_regions()  -- connected components + classify + NMS"]
    regions --> viz["visualize_changes()  -- draw boxes on overlay"]
    viz --> out["return mask, overlay, stats, regions"]
```

### Step by step

1. **Preprocess** (`preprocess_image`): convert to RGB, cap the size (default 1600px max
   side) for speed, apply light Gaussian blur (and a bilateral filter only when the image
   is noisy) to reduce sensor noise without destroying edges.

2. **Register / align** (`register_images`): the two screenshots rarely line up perfectly.
   - First tries **SIFT + FLANN** feature matching β†’ homography.
   - Falls back to **ORB** features if SIFT is weak.
   - Refines with **ECC** (sub-pixel alignment).
   - Falls back to **multi-scale ECC** if feature matching fails entirely.
   - Returns an `(img1, img2_aligned, registration_ok, reg_meta)` tuple. `registration_ok`
     and the quality metrics (inlier ratio, NCC) are surfaced to the UI as an
     **alignment warning** when the alignment is weak β€” important for Google Earth
     screenshots that differ in zoom/crop.

3. **Radiometric normalization** (`normalize_radiometry`): match the "after" image's color
   statistics to the "before" image in LAB space, plus symmetric CLAHE on the lightness
   channel, so lighting/season differences don't look like real change.

4. **Detection method** (selected by the `method` argument):
   - **AI-Based Deep Learning** (default): runs the **AdaptFormer** model
     (`model_inference.predict_change_mask`) to get a per-pixel change probability map,
     computes a **classical** multi-channel score map (color Ξ”E, SSIM, texture/LBP, edges,
     change-vector analysis), then **fuses** them with `fuse_dl_and_classical()`. Fusion is
     **confidence-gated** (not a blind union): the model drives structural changes, the
     classical signal + Excess-Green index supports vegetation changes.
   - **Image Difference**: LAB Ξ”E difference with adaptive (Otsu/MAD) thresholding.
   - **Feature-Based**: KMeans clustering of per-pixel difference features.
   - **Hybrid / Hybrid AI**: weighted combinations of the above.

5. **Clean the mask** (`_clean_mask`): morphological open/close, hole filling, remove tiny
   or thin (shadow-like) components.

6. **Find & classify regions** (`analyze_change_regions`): connected-components on the mask
   β†’ bounding boxes β†’ `classify_object_type()` assigns a label (New Construction/Building,
   Vegetation Change, Water Body Change, Road/Pavement, Bare Land, etc.) with a confidence
   and severity β†’ **non-maximum suppression** removes overlapping/duplicate boxes.

7. **Visualize** (`visualize_changes`): draw a subtle tint on changed pixels and numbered,
   color-coded boxes for the top regions.

8. **Return** `change_mask, result_image, stats, change_regions` back to `main.py`.

### Sensitivity

The `detection_sensitivity` slider (0–1, default 0.5) shifts thresholds: higher = detect
more (more recall, more false positives), lower = stricter (fewer, higher-confidence
detections).

---

## 6. The deep-learning model

- File: [`app/model_inference.py`](app/model_inference.py)
- Model: **`deepang/adaptformer-LEVIR-CD`** (a change-detection transformer trained on the
  LEVIR-CD building dataset), pulled from the Hugging Face Hub.
- It is **pre-downloaded at Docker build time** (see the Dockerfile) so cold starts are fast,
  and **preloaded on app startup** so the first request isn't slow.
- Inference is **tile-based**: the image is split into overlapping 256Γ—256 tiles (the model's
  native size), each tile is predicted, and the results are stitched back with a
  raised-cosine blend to avoid visible seams.
- If PyTorch/Transformers aren't installed or the model fails to load, the engine **falls
  back gracefully** to the classical-only path β€” the app still works.

`app/cd_models/change_model.py` contains a from-scratch **Siamese U-Net** as an alternative
DL backbone. It only activates if a trained weights file exists at
`app/cd_models/weights/siamese_unet_cd.pt` (none is shipped), so it's currently dormant.

---

## 7. Request lifecycle (auth + detect)

```mermaid
sequenceDiagram
    participant U as Browser
    participant A as FastAPI (main.py)
    participant Au as auth.py
    participant E as detection_engine.py
    participant D as Database

    U->>A: POST /api/auth/login {email, password}
    A->>Au: verify_password + create_access_token
    Au-->>A: JWT
    A-->>U: token (also set as httpOnly cookie)

    U->>A: POST /api/detect (before, after, method, ...) + token
    A->>Au: resolve user from token/cookie
    A->>E: run_detection(...)
    E-->>A: mask, overlay, stats, regions
    A->>D: save DetectionRun row
    A->>A: write overlay + thumbnails to data/overlays/
    A-->>U: JSON {statistics, regions, overlayBase64Png, ...}
    U->>U: app.js renders overlay + regions table
```

---

## 8. Data model

Defined in [`app/models.py`](app/models.py):

- **User**: `id, email (unique), hashed_password, full_name, created_at`
- **DetectionRun**: `id, user_id, title, method, total_pixels, changed_pixels,
  change_percentage, regions_count, overlay_path, before/after thumbnail paths, zone,
  village, regions_json (serialized regions), created_at`

The database file lives at `data/satellite_app.db` (SQLite) and is created automatically on
first run. Set `DATABASE_URL` to use PostgreSQL instead.

---

## 9. API reference

All routes are in [`app/main.py`](app/main.py).

| Method & path | Purpose |
|---------------|---------|
| `POST /api/auth/register` | Create account `{email, password, full_name}` β†’ token |
| `POST /api/auth/login` | Log in `{email, password}` β†’ token (+ cookie) |
| `POST /api/auth/logout` | Clear auth cookie |
| `POST /api/auth/reset-password` | Set a new password `{email, new_password}` |
| `GET /api/me` | Current user (requires auth) |
| `POST /api/detect` | **Main endpoint.** multipart: `before`, `after` files + `method`, `title`, `zone`, `village`, `enable_registration`, `enable_normalization`, `detection_sensitivity`, `min_region_area`, `notify_email` |
| `GET /api/history` | List the current user's past runs |
| `POST /api/notify/test` | Send a test email |
| `GET /api/overlay/<path>` | Serve a saved overlay / thumbnail image |
| `GET /health` | Lightweight health check (no DB) |
| `GET /` | Serves the single-page UI |

`POST /api/detect` returns: `statistics` (pixels, change %, threshold debug, alignment
warning), `regions` (list with type/confidence/severity/bbox), and the overlay as base64 PNG.

---

## 10. Frontend

- [`templates/index.html`](templates/index.html) is the shell: login/register forms, the
  upload form, history table, and the result modal.
- [`static/js/app.js`](static/js/app.js) handles everything client-side: calling the API,
  storing the token, submitting the detect form, and rendering results (the before/after
  slider, the regions table, alignment warnings, and fusion stats).
- [`static/css/style.css`](static/css/style.css) holds all styling.
- Cache-busting: the `?v=NN` suffixes on the CSS/JS `<link>`/`<script>` tags are bumped when
  those files change so browsers don't serve stale copies.

---

## 11. Running locally

### Option A β€” Python virtual environment (recommended for development)

```bash
cd change_detection_webapp

# 1. Create and activate a virtual environment
python -m venv venv
# Windows (PowerShell):
venv\Scripts\Activate.ps1
# macOS / Linux:
source venv/bin/activate

# 2. Install dependencies (this pulls CPU PyTorch + Transformers, ~couple GB)
pip install -r requirements.txt

# 3. Run the dev server (auto-reload on code changes)
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```

Open **http://localhost:8000**, register an account, then upload a before/after pair.

> First detection run downloads the AdaptFormer model from Hugging Face (one time).
> If you don't have PyTorch installed or want a lighter setup, the app still runs using
> the classical detection path only.

### Option B β€” Docker (mirrors production)

```bash
cd change_detection_webapp
docker build -t change-detection .
docker run -p 7860:7860 change-detection
```

Open **http://localhost:7860**. The Docker build pre-downloads the model, so the first
request is fast.

### Validate the pipeline

```bash
python scripts/validate_detection.py
```

This runs registration/fusion/end-to-end sanity checks on synthetic images.

---

## 12. Configuration (environment variables)

| Variable | Default | Purpose |
|----------|---------|---------|
| `DATABASE_URL` | `sqlite:///data/satellite_app.db` | Use PostgreSQL by setting this |
| `SECRET_KEY` | (in `auth.py`) | JWT signing key β€” **set this in production** |
| `HF_HOME` | `/app/.hf_cache` (Docker) | Where the model is cached |
| `EMAIL_API_URL` | manager's API | Email backend; empty + `SMTP_USER`/`SMTP_PASS` for SMTP |
| `PORT` | `7860` | Port uvicorn binds to in the container |
| `SPACE_ID` | (set by HF) | When present, data is written to `~/data` |

---

## 13. Deployment (Hugging Face Spaces)

- The Space is a **Docker** Space. Pushing to its git remote triggers a rebuild.
- Hugging Face builds from the **`main`** branch. (This repo's working branch is `master`,
  so deploys push `master` β†’ both `master` and `main` on the HF remote.)
- The `ARG APP_BUILD=NN` line in the Dockerfile is a cache-buster: bumping it forces pip to
  reinstall and the model to re-download on the next build.
- The YAML front-matter at the top of `README.md` (title, emoji, `sdk: docker`,
  `app_port: 7860`) is what Hugging Face reads to configure the Space.

---

## 14. Common tasks & gotchas

- **"It's detecting too much / too little"** β†’ adjust the sensitivity slider, and make sure
  the before/after images are the **same location, zoom, and crop**. Weak alignment shows an
  alignment warning in the result panel.
- **Changed CSS/JS but nothing updates** β†’ bump the `?v=NN` query string in `index.html`.
- **Adding a DB column** β†’ update `app/models.py`; the app creates tables on startup
  (there's lightweight migration handling in `main.py`).
- **Don't create a folder named `app/models/`** β†’ it shadows `app/models.py`.
- **Model not loading** β†’ the app logs a warning and continues with classical detection;
  check the logs for the AdaptFormer load message.

---

## 15. Where to start reading the code

1. [`app/main.py`](app/main.py) β€” see the routes, especially `POST /api/detect`.
2. [`app/detection_engine.py`](app/detection_engine.py) β€” start at `run_detection()` at the
   bottom and follow the calls upward.
3. [`app/model_inference.py`](app/model_inference.py) β€” how the DL model is loaded and run.
4. [`static/js/app.js`](static/js/app.js) β€” how the frontend calls the API and renders results.

Welcome to the project!