yakvrz commited on
Commit
b895e68
·
1 Parent(s): f61de51

Track examples for GitHub; drop HF front-matter; ignore scripts

Browse files
Files changed (3) hide show
  1. .gitignore +1 -1
  2. README.md +6 -11
  3. scripts/precompute_examples.py +0 -269
.gitignore CHANGED
@@ -3,4 +3,4 @@ __pycache__/
3
  .python-version
4
  data/
5
  *.pyc
6
- examples/
 
3
  .python-version
4
  data/
5
  *.pyc
6
+ scripts/
README.md CHANGED
@@ -1,18 +1,13 @@
1
- ---
2
- title: Drone Landing Site Safety
3
- emoji: 🛰️
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 6.0.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
  # Drone Landing Site Safety
13
 
14
  Analyze aerial RGB imagery to detect safe drone landing sites. Combines monocular depth estimation, promptable hazard segmentation, and geometric heuristics to flag flat, obstacle-free areas, with overlays and metrics that show why a spot is safe.
15
 
 
 
 
 
 
 
16
  ## What’s inside
17
  - **Main app (`app.py`)** — runs full inference with adjustable thresholds, overlays, and camera assumptions; requires >8GB VRAM (assuming default 1024 px processing resolution); runtime is ~2000 ms per image.
18
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # Drone Landing Site Safety
2
 
3
  Analyze aerial RGB imagery to detect safe drone landing sites. Combines monocular depth estimation, promptable hazard segmentation, and geometric heuristics to flag flat, obstacle-free areas, with overlays and metrics that show why a spot is safe.
4
 
5
+ <p align="center">
6
+ <img src="examples/build/visloc_03_0001/rgb.jpg" alt="RGB reference" width="80%" />
7
+ <br/>
8
+ <img src="examples/build/visloc_03_0001/composed.png" alt="Safety overlay" width="80%" />
9
+ </p>
10
+
11
  ## What’s inside
12
  - **Main app (`app.py`)** — runs full inference with adjustable thresholds, overlays, and camera assumptions; requires >8GB VRAM (assuming default 1024 px processing resolution); runtime is ~2000 ms per image.
13
 
scripts/precompute_examples.py DELETED
@@ -1,269 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Precompute example outputs for static distribution.
3
-
4
- Given a sample manifest, this script runs the Landing Site Safety Analyzer on each
5
- image, saves the composed preview and RGB thumbnail, and writes an index.json for browsing.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- import argparse
11
- import dataclasses
12
- import json
13
- import sys
14
- from datetime import datetime
15
- from pathlib import Path
16
- from typing import Any, Dict, List
17
-
18
- import numpy as np
19
-
20
- try:
21
- import yaml # type: ignore
22
- except ImportError as exc: # pragma: no cover - dependency shim
23
- raise SystemExit("pyyaml is required for example manifest parsing (pip install pyyaml).") from exc
24
- from PIL import Image
25
-
26
- # Ensure repository root is on the path so `app` imports work when running the script directly
27
- ROOT = Path(__file__).resolve().parents[1]
28
- if str(ROOT) not in sys.path:
29
- sys.path.append(str(ROOT))
30
-
31
- from app.config import DEFAULT_ANALYZER_SETTINGS, AnalyzerSettings, IMAGE_EXTS # type: ignore # noqa: E402
32
- from app.safety import AnalysisRequest, SafetyAnalyzer # type: ignore # noqa: E402
33
- from app.visualization import compose_view # type: ignore # noqa: E402
34
-
35
-
36
- def _load_manifest(path: Path) -> List[Dict[str, Any]]:
37
- if not path.exists():
38
- raise FileNotFoundError(f"Manifest not found: {path}")
39
- with path.open("r") as f:
40
- data = yaml.safe_load(f)
41
- samples = data.get("samples") if isinstance(data, dict) else None
42
- if not samples:
43
- raise ValueError(f"No samples found in manifest: {path}")
44
- entries: List[Dict[str, Any]] = []
45
- for item in samples:
46
- if not isinstance(item, dict):
47
- continue
48
- if "id" not in item or "path" not in item:
49
- continue
50
- entries.append(item)
51
- if not entries:
52
- raise ValueError(f"Manifest contained no usable entries: {path}")
53
- return entries
54
-
55
-
56
- def _analysis_request_from_args(args: argparse.Namespace, source_path: Path) -> AnalysisRequest:
57
- defaults = DEFAULT_ANALYZER_SETTINGS
58
- resolve = lambda value, default: default if value is None else value # noqa: E731
59
-
60
- process_res_cap = int(resolve(args.process_res_cap, defaults.process_res_cap))
61
- segmentation_max_side = int(resolve(args.segmentation_max_side, defaults.segmentation_max_side))
62
- return AnalysisRequest(
63
- footprint_m=float(resolve(args.footprint_m, defaults.footprint_m)),
64
- std_thresh=float(resolve(args.std_thresh, defaults.std_thresh)),
65
- grad_thresh=float(resolve(args.grad_thresh, defaults.grad_thresh)),
66
- use_water_mask=bool(args.use_water_mask),
67
- use_road_mask=bool(args.use_road_mask),
68
- use_roof_mask=bool(args.use_roof_mask),
69
- use_tree_mask=True,
70
- water_prompt=resolve(args.water_prompt, defaults.water_prompt),
71
- road_prompt=resolve(args.road_prompt, defaults.road_prompt),
72
- roof_prompt=resolve(getattr(args, "roof_prompt", None), defaults.roof_prompt),
73
- tree_prompt=resolve(getattr(args, "tree_prompt", None), defaults.tree_prompt),
74
- altitude_m=float(resolve(args.altitude_m, defaults.altitude_m)),
75
- fov_deg=float(resolve(args.fov_deg, defaults.fov_deg)),
76
- clearance_factor=float(resolve(args.clearance_factor, defaults.clearance_factor)),
77
- process_res_cap=process_res_cap,
78
- depth_smoothing_base=float(resolve(args.depth_smoothing_base, defaults.depth_smoothing_base)),
79
- segmentation_model_id=resolve(args.segmentation_model_id, defaults.segmentation_model_id),
80
- segmentation_max_side=segmentation_max_side,
81
- segmentation_score_thresh=float(resolve(args.segmentation_score_thresh, defaults.segmentation_score_thresh)),
82
- segmentation_mask_thresh=float(resolve(args.segmentation_mask_thresh, defaults.segmentation_mask_thresh)),
83
- coverage_strictness=float(resolve(args.coverage_strictness, defaults.coverage_strictness)),
84
- model_id=resolve(args.model_id, defaults.model_id),
85
- openness_weight=float(resolve(args.openness_weight, defaults.openness_weight)),
86
- texture_threshold=float(resolve(args.texture_threshold, defaults.texture_threshold)),
87
- source_path=str(source_path),
88
- )
89
-
90
-
91
- def _ensure_image(path: Path) -> Path:
92
- if not path.exists():
93
- raise FileNotFoundError(f"Sample image missing: {path}")
94
- if path.suffix.lower() not in IMAGE_EXTS:
95
- raise ValueError(f"Unsupported image type for example sample: {path.name}")
96
- return path
97
-
98
-
99
- def _save_image(img: Image.Image, path: Path, quality: int = 95) -> None:
100
- path.parent.mkdir(parents=True, exist_ok=True)
101
- save_kwargs: Dict[str, Any] = {}
102
- if path.suffix.lower() in (".jpg", ".jpeg"):
103
- save_kwargs["quality"] = quality
104
- save_kwargs["optimize"] = True
105
- img.save(path, **save_kwargs)
106
-
107
-
108
- def _relative_to_base(path: Path, base: Path) -> str:
109
- try:
110
- return path.relative_to(base).as_posix()
111
- except ValueError:
112
- return path.as_posix()
113
-
114
-
115
- def _to_builtin(obj: Any) -> Any:
116
- """Recursively convert numpy/scalar types to JSON-friendly Python types."""
117
- if isinstance(obj, np.generic):
118
- return obj.item()
119
- if isinstance(obj, dict):
120
- return {k: _to_builtin(v) for k, v in obj.items()}
121
- if isinstance(obj, (list, tuple)):
122
- return [_to_builtin(v) for v in obj]
123
- return obj
124
-
125
-
126
- def precompute_examples(
127
- manifest_path: Path,
128
- output_root: Path,
129
- args: argparse.Namespace,
130
- base_view: str = "RGB",
131
- heat_opacity: float = 0.2,
132
- hazard_opacity: float = 0.2,
133
- ) -> Path:
134
- manifest_entries = _load_manifest(manifest_path)
135
- output_root.mkdir(parents=True, exist_ok=True)
136
- analyzer = SafetyAnalyzer()
137
- index_entries: List[Dict[str, Any]] = []
138
-
139
- for item in manifest_entries:
140
- sample_id = item.get("id")
141
- source_path = _ensure_image(Path(item.get("path")))
142
- title = item.get("title") or sample_id
143
- description = item.get("description") or ""
144
- tags = item.get("tags") or []
145
-
146
- request = _analysis_request_from_args(args, source_path)
147
- print(f"[INFO] Processing {sample_id} -> {source_path}")
148
- result = analyzer.process_path(source_path, request)
149
- composed = compose_view(
150
- result.images,
151
- base_view=base_view,
152
- heat_on=True,
153
- heat_alpha=float(heat_opacity),
154
- risk_on=True,
155
- risk_alpha=float(hazard_opacity),
156
- hazards_on=True,
157
- grad_on=False,
158
- flat_on=False,
159
- flat_heat_on=False,
160
- spot_on=True,
161
- )
162
-
163
- sample_dir = output_root / sample_id
164
- rgb_path = sample_dir / "rgb.jpg"
165
- composed_path = sample_dir / "composed.png"
166
- summary_path = sample_dir / "summary.json"
167
-
168
- summary_dict = _to_builtin(dataclasses.asdict(result.summary))
169
-
170
- _save_image(result.images["RGB"], rgb_path)
171
- _save_image(composed, composed_path, quality=98)
172
- with summary_path.open("w") as f:
173
- json.dump(summary_dict, f, indent=2)
174
-
175
- entry = {
176
- "id": sample_id,
177
- "title": title,
178
- "description": description,
179
- "tags": tags,
180
- "source_path": str(source_path),
181
- "artifacts": {
182
- "rgb": _relative_to_base(rgb_path, output_root),
183
- "composed": _relative_to_base(composed_path, output_root),
184
- "summary": _relative_to_base(summary_path, output_root),
185
- },
186
- "summary": summary_dict,
187
- "request": _to_builtin(dataclasses.asdict(request)),
188
- }
189
- index_entries.append(entry)
190
-
191
- index = {
192
- "generated_at": datetime.utcnow().isoformat(timespec="seconds") + "Z",
193
- "num_samples": len(index_entries),
194
- "output_root": output_root.as_posix(),
195
- "manifest": manifest_path.as_posix(),
196
- "samples": index_entries,
197
- }
198
- index_path = output_root / "index.json"
199
- with index_path.open("w") as f:
200
- json.dump(index, f, indent=2)
201
- print(f"[DONE] Wrote examples index: {index_path}")
202
- return index_path
203
-
204
-
205
- def build_parser() -> argparse.ArgumentParser:
206
- p = argparse.ArgumentParser(description="Precompute example outputs for distribution.")
207
- p.add_argument(
208
- "--manifest",
209
- type=Path,
210
- required=True,
211
- help="YAML manifest with example sample definitions.",
212
- )
213
- p.add_argument(
214
- "--output-dir",
215
- type=Path,
216
- default=Path("examples/build"),
217
- help="Directory to store example outputs and index.json.",
218
- )
219
- # Analysis controls
220
- p.add_argument("--model-id", type=str, help="DepthAnything3 model id to use.")
221
- p.add_argument("--footprint-m", type=float, help="Landing footprint size in meters.")
222
- p.add_argument("--std-thresh", type=float, help="Flatness threshold.")
223
- p.add_argument("--grad-thresh", type=float, help="Gradient threshold.")
224
- p.add_argument("--coverage-strictness", type=float, help="Coverage strictness for safe areas.")
225
- p.add_argument("--openness-weight", type=float, help="Weight for distance-from-hazards when scoring.")
226
- p.add_argument("--texture-threshold", type=float, help="Texture tolerance.")
227
- p.add_argument("--clearance-factor", type=float, help="Clearance dilation multiplier.")
228
- p.add_argument("--process-res-cap", type=int, help="Depth max resolution (long side).")
229
- p.add_argument("--depth-smoothing-base", type=float, help="Base sigma for depth smoothing.")
230
- p.add_argument("--segmentation-max-side", type=int, help="Segmentation max side.")
231
- p.add_argument("--segmentation-model-id", type=str, help="Segmentation model id (e.g., facebook/sam3 or maskformer).")
232
- p.add_argument("--segmentation-score-thresh", type=float, help="Segmentation score threshold.")
233
- p.add_argument("--segmentation-mask-thresh", type=float, help="Segmentation mask threshold.")
234
- p.add_argument("--altitude-m", type=float, help="Camera altitude in meters.")
235
- p.add_argument("--fov-deg", type=float, help="Camera FOV in degrees.")
236
- p.add_argument("--water-prompt", type=str, help="Water segmentation prompt.")
237
- p.add_argument("--road-prompt", type=str, help="Road segmentation prompt.")
238
- p.add_argument("--roof-prompt", type=str, help="Roof segmentation prompt.")
239
- p.add_argument("--tree-prompt", type=str, help="Tree segmentation prompt.")
240
- p.add_argument("--use-water-mask", action="store_true", dest="use_water_mask", help="Enable water mask.")
241
- p.add_argument("--no-water-mask", action="store_false", dest="use_water_mask", help="Disable water mask.")
242
- p.add_argument("--use-road-mask", action="store_true", dest="use_road_mask", help="Enable road mask.")
243
- p.add_argument("--no-road-mask", action="store_false", dest="use_road_mask", help="Disable road mask.")
244
- p.add_argument("--use-roof-mask", action="store_true", dest="use_roof_mask", default=True, help="Enable roof mask.")
245
- p.add_argument("--no-roof-mask", action="store_false", dest="use_roof_mask", help="Disable roof mask.")
246
- p.set_defaults(use_water_mask=True, use_road_mask=True)
247
- p.add_argument("--cpu", action="store_true", help="Force CPU inference to avoid CUDA OOM.")
248
- # View controls
249
- p.add_argument("--base-view", type=str, default="RGB", help="Base view to compose (RGB/Depth/etc).")
250
- p.add_argument("--heat-opacity", type=float, default=0.2, help="Safety overlay opacity.")
251
- p.add_argument("--hazard-opacity", type=float, default=0.2, help="Hazard overlay opacity.")
252
- return p
253
-
254
-
255
- if __name__ == "__main__":
256
- parser = build_parser()
257
- args = parser.parse_args()
258
- if args.cpu:
259
- import os
260
-
261
- os.environ["CUDA_VISIBLE_DEVICES"] = ""
262
- precompute_examples(
263
- manifest_path=Path(args.manifest),
264
- output_root=Path(args.output_dir),
265
- args=args,
266
- base_view=args.base_view,
267
- heat_opacity=args.heat_opacity,
268
- hazard_opacity=args.hazard_opacity,
269
- )