satdetect-dev / app /detection_config.py
coderuday21's picture
Improve detection accuracy with full-res tiling, config flags, and evaluation.
99e1f27
Raw
History Blame Contribute Delete
7.66 kB
"""Centralized, env-driven detection configuration.
Single source of truth for detection tuning knobs so the engine, model
inference, and tiling utilities stay consistent. Every value is read from the
environment with conservative defaults that preserve the previous behavior
(``downscaled`` inference, ``smart_union`` fusion, no multi-scale).
Env vars
--------
DETECTION_MAX_SIDE Max pixel dimension for the legacy downscaled path.
DETECTION_INFERENCE_MODE ``downscaled`` (default) | ``fullres_tiled``.
DETECTION_FULLRES_MAX_SIDE Cap for full-resolution tiled mode (0 = native).
DETECTION_TILE_SIZE Tile size for full-res scoring (256..2048).
DETECTION_TILE_OVERLAP Fractional tile overlap (0.0..0.5).
DETECTION_MULTISCALE ``off`` | comma list e.g. ``0.5,1.0,1.5``.
DETECTION_FUSION ``smart_union`` (default) | ``hysteresis``.
DETECTION_SKIP_PREBLUR ``true`` | ``false`` (auto: on for fullres_tiled).
DETECTION_TTA ``off`` | ``hflip`` | ``full`` | ``auto`` (model_inference).
"""
from __future__ import annotations
import logging
import os
from typing import List
_log = logging.getLogger(__name__)
def _env(name: str, default: str = "") -> str:
return os.environ.get(name, default).strip()
def get_detection_max_side() -> int:
"""Max pixel dimension for image load + the downscaled detection path."""
hosted = bool(_env("SPACE_ID"))
default = "2048" if hosted else "4096"
try:
value = int(os.environ.get("DETECTION_MAX_SIDE", default))
except ValueError:
value = int(default)
return max(1024, min(8192, value))
def get_inference_mode() -> str:
"""Detection inference mode: ``downscaled`` or ``fullres_tiled``."""
mode = _env("DETECTION_INFERENCE_MODE", "downscaled").lower()
return mode if mode in ("downscaled", "fullres_tiled") else "downscaled"
def get_fullres_max_side() -> int:
"""Upper bound for full-res tiled mode. 0 means use native resolution.
Defaults to 8192 so very large GeoTIFFs stay bounded in RAM while still
running detection at far higher detail than the 2048/4096 downscaled cap.
"""
raw = _env("DETECTION_FULLRES_MAX_SIDE", "8192")
try:
value = int(raw)
except ValueError:
value = 8192
if value <= 0:
return 0 # native resolution, no cap
return max(2048, min(20000, value))
def get_load_max_side() -> int:
"""Effective image-load / inference pixel cap for the active mode.
In ``fullres_tiled`` mode this returns the (much larger) full-res cap so
images keep their detail; otherwise it returns the standard downscale cap.
Always a concrete positive int so callers can pass it straight to loaders.
"""
if get_inference_mode() == "fullres_tiled":
cap = get_fullres_max_side()
return cap if cap > 0 else 20000
return get_detection_max_side()
def get_windowed_threshold() -> int:
"""Native max-side above which GeoTIFF inference streams via rasterio windows.
Keeps peak RAM bounded for very large rasters: when a GeoTIFF's native
longest side exceeds this, the deep score map is built tile-by-tile from
disk instead of loading the whole array. 0 disables windowed streaming.
"""
raw = _env("DETECTION_WINDOWED_THRESHOLD", "8192")
try:
value = int(raw)
except ValueError:
value = 8192
return max(0, value)
def get_tile_size() -> int:
"""Tile size (px) for full-resolution scoring."""
try:
value = int(os.environ.get("DETECTION_TILE_SIZE", "512"))
except ValueError:
value = 512
return max(256, min(2048, value))
def get_tile_overlap() -> float:
"""Fractional overlap between adjacent tiles (0.0..0.5)."""
try:
value = float(os.environ.get("DETECTION_TILE_OVERLAP", "0.25"))
except ValueError:
value = 0.25
return min(0.5, max(0.0, value))
def get_multiscale_scales() -> List[float]:
"""Scales for multi-scale fusion. Empty list disables it."""
raw = _env("DETECTION_MULTISCALE", "off").lower()
if raw in ("off", "", "none", "0", "false"):
return []
scales: List[float] = []
for part in raw.split(","):
part = part.strip()
if not part:
continue
try:
scale = float(part)
except ValueError:
continue
if 0.1 <= scale <= 4.0:
scales.append(scale)
# Always include native scale so recall never drops below single-scale
if scales and 1.0 not in scales:
scales.append(1.0)
return sorted(set(scales))
def get_fusion_mode() -> str:
"""DL + classical fusion strategy: ``smart_union`` or ``hysteresis``."""
mode = _env("DETECTION_FUSION", "smart_union").lower()
return mode if mode in ("smart_union", "hysteresis") else "smart_union"
def get_skip_preblur() -> bool:
"""Whether to skip the preprocessing Gaussian/bilateral blur.
Explicit env wins; otherwise auto-skip in full-res mode to preserve the
fine detail that motivates running detection at native resolution.
"""
raw = _env("DETECTION_SKIP_PREBLUR", "").lower()
if raw in ("1", "true", "yes", "on"):
return True
if raw in ("0", "false", "no", "off"):
return False
return get_inference_mode() == "fullres_tiled"
def _flag(name: str, default: bool) -> bool:
raw = _env(name, "").lower()
if raw in ("1", "true", "yes", "on"):
return True
if raw in ("0", "false", "no", "off"):
return False
return default
def get_enable_clahe() -> bool:
"""CLAHE contrast equalization during radiometric normalization (default on)."""
return _flag("DETECTION_CLAHE", True)
def get_hist_match() -> bool:
"""Optional histogram matching of the after image to the before (default off)."""
return _flag("DETECTION_HIST_MATCH", False)
def get_tile_batch() -> int:
"""Tiles per model forward pass (GPU throughput). 1 = no batching (default)."""
try:
value = int(os.environ.get("DETECTION_TILE_BATCH", "1"))
except ValueError:
value = 1
return max(1, min(64, value))
def get_tile_memory_budget_mb() -> int:
"""Soft RAM budget (MB) for a single in-memory detection array.
When loading both timestamps at the full-res cap would exceed this, large
GeoTIFFs switch to disk-windowed streaming so peak memory stays bounded
(lets 15 GB rasters run without OOM). 0 disables the budget check.
"""
raw = _env("DETECTION_TILE_MEMORY_MB", "1536")
try:
value = int(raw)
except ValueError:
value = 1536
return max(0, value)
def get_save_prob_map() -> bool:
"""Save a downsampled probability-map PNG per run for debugging (default off)."""
return _flag("DETECTION_SAVE_PROB_MAP", False)
def get_border_margin() -> int:
"""Border pixels zeroed in mask cleanup. Smaller in full-res mode."""
raw = _env("DETECTION_BORDER_MARGIN", "")
if raw:
try:
return max(0, min(64, int(raw)))
except ValueError:
pass
return 4 if get_inference_mode() == "fullres_tiled" else 12
def summary() -> dict:
"""Snapshot of effective detection config (for logs and debug output)."""
return {
"maxSide": get_detection_max_side(),
"inferenceMode": get_inference_mode(),
"fullresMaxSide": get_fullres_max_side(),
"tileSize": get_tile_size(),
"tileOverlap": get_tile_overlap(),
"multiscale": get_multiscale_scales(),
"fusion": get_fusion_mode(),
"skipPreblur": get_skip_preblur(),
"borderMargin": get_border_margin(),
}