Spaces:
Sleeping
Sleeping
File size: 11,409 Bytes
79d533c 15e9574 79d533c 15e9574 79d533c 99e1f27 15e9574 ac7ea7c d9820a1 79d533c d9820a1 79d533c ac7ea7c 79d533c d9820a1 99e1f27 79d533c d9820a1 99e1f27 79d533c d9820a1 79d533c 99e1f27 79d533c bec7397 79d533c bec7397 79d533c bec7397 79d533c 15e9574 79d533c 15e9574 79d533c 15e9574 79d533c 79b0691 79d533c bec7397 79d533c d9820a1 79d533c d9820a1 70d9e6c 79d533c 70d9e6c 79d533c d9820a1 79d533c 79b0691 99e1f27 79d533c bec7397 79d533c | 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 | """Run change detection and persist results (shared by upload + library paths)."""
from __future__ import annotations
import base64
import json
import logging
import uuid
from datetime import timezone
from pathlib import Path
from typing import Optional
from PIL import Image
from sqlalchemy.orm import Session
from ..auth import get_or_create_guest_user
from ..database import DATA_DIR
from ..models import DetectionRun
from .geo_regions import enrich_regions_geo, resolve_geo_context
logger = logging.getLogger(__name__)
OVERLAYS_DIR = DATA_DIR / "overlays"
THUMB_MAX_SIZE = 200
def _serialize_regions(change_regions) -> list:
return [
{
"id": int(r["id"]),
"area": int(r["area"]),
"center": {"x": round(float(r["center"][0])), "y": round(float(r["center"][1]))},
"bbox": {
"x": int(r["bbox"][0]),
"y": int(r["bbox"][1]),
"w": int(r["bbox"][2]),
"h": int(r["bbox"][3]),
},
"objectType": str(r["object_type"]),
"confidence": float(r["confidence"]),
"severity": r.get("severity", "minor"),
"subType": r.get("sub_type"),
"subTypeConfidence": float(r["sub_type_confidence"])
if r.get("sub_type_confidence") is not None
else None,
"estimatedStories": r.get("estimated_stories"),
"estimatedHeightM": float(r["estimated_height_m"])
if r.get("estimated_height_m") is not None
else None,
"constructionStage": r.get("construction_stage"),
}
for r in change_regions
]
def _isoformat_ist(dt):
from datetime import timedelta
if dt is None:
return None
_IST = timezone(timedelta(hours=5, minutes=30))
if dt.tzinfo is None:
from datetime import timezone as tz
dt = dt.replace(tzinfo=tz.utc)
return dt.astimezone(_IST).isoformat()
def run_detection_and_save(
db: Session,
before_pil: Image.Image,
after_pil: Image.Image,
*,
method: str = "AI-Based Deep Learning",
title: str = "Untitled run",
zone: str = "",
village: str = "",
enable_registration: bool = True,
enable_normalization: bool = True,
detection_sensitivity: float = 0.5,
min_region_area: Optional[int] = None,
notify_email: Optional[str] = None,
max_size: Optional[int] = None,
geo_bounds_path: Optional[Path] = None,
comparison_file: Optional[Path] = None,
base_path: str = "",
user_id: Optional[int] = None,
job_id: Optional[int] = None,
) -> dict:
from ..detection_engine import run_detection
from .job_progress import update_job_progress
def _report(pct: int, stage: str) -> None:
if job_id is not None:
update_job_progress(job_id, pct, stage)
if user_id:
from ..auth import get_user_by_id
user = get_user_by_id(db, user_id)
if not user:
user = get_or_create_guest_user(db)
else:
user = get_or_create_guest_user(db)
detection_sensitivity = max(0.0, min(1.0, float(detection_sensitivity)))
if min_region_area is not None:
min_region_area = int(max(50, min(10000, min_region_area)))
def _on_engine_progress(engine_pct: int, stage: str) -> None:
# Map engine 0–100% into job 15–78%
job_pct = 15 + int(engine_pct * 0.63)
_report(job_pct, stage)
_report(15, "Running detection")
def _geotiff_path(p: Optional[Path]) -> Optional[str]:
if p is not None and str(p).lower().endswith((".tif", ".tiff")):
return str(p)
return None
change_mask, result_image, stats, change_regions = run_detection(
before_pil,
after_pil,
method=method,
enable_registration=enable_registration,
enable_normalization=enable_normalization,
detection_sensitivity=detection_sensitivity,
min_region_area=min_region_area,
max_size=max_size,
on_progress=_on_engine_progress,
before_path=_geotiff_path(geo_bounds_path),
after_path=_geotiff_path(comparison_file),
)
_report(80, "Saving results")
from ..detection_engine import preprocess_image, get_detection_max_size
before_for_slider = Image.fromarray(
preprocess_image(before_pil, max_size=max_size or get_detection_max_size())
)
base_name = f"{user.id}_{uuid.uuid4().hex}"
overlay_filename = base_name + ".png"
overlay_path = OVERLAYS_DIR / overlay_filename
overlay_path.parent.mkdir(parents=True, exist_ok=True)
Image.fromarray(result_image).save(overlay_path)
relative_overlay = f"overlays/{overlay_filename}"
relative_prob = ""
try:
from ..detection_config import get_save_prob_map
score_map = stats.pop("_score_map", None)
if get_save_prob_map() and score_map is not None:
import numpy as _np
prob_u8 = _np.clip(score_map * 255.0, 0, 255).astype("uint8")
prob_img = Image.fromarray(prob_u8)
prob_img.thumbnail((1024, 1024), Image.Resampling.LANCZOS)
prob_file = OVERLAYS_DIR / f"{base_name}_prob.png"
prob_img.save(prob_file)
relative_prob = f"overlays/{base_name}_prob.png"
except Exception as exc:
logger.warning("Probability map export failed: %s", exc)
relative_before_full = ""
relative_before_thumb = ""
relative_after_thumb = ""
relative_after_full = ""
try:
after_for_slider = Image.fromarray(
preprocess_image(after_pil, max_size=max_size or get_detection_max_size())
)
before_full_file = OVERLAYS_DIR / f"{base_name}_before.png"
before_for_slider.save(before_full_file)
relative_before_full = f"overlays/{base_name}_before.png"
after_full_file = OVERLAYS_DIR / f"{base_name}_after.png"
after_for_slider.save(after_full_file)
relative_after_full = f"overlays/{base_name}_after.png"
before_thumb_pil = before_pil.copy()
before_thumb_pil.thumbnail((THUMB_MAX_SIZE, THUMB_MAX_SIZE), Image.Resampling.LANCZOS)
before_thumb_pil.save(OVERLAYS_DIR / f"{base_name}_before_thumb.png")
after_thumb_pil = after_pil.copy()
after_thumb_pil.thumbnail((THUMB_MAX_SIZE, THUMB_MAX_SIZE), Image.Resampling.LANCZOS)
after_thumb_pil.save(OVERLAYS_DIR / f"{base_name}_after_thumb.png")
relative_before_thumb = f"overlays/{base_name}_before_thumb.png"
relative_after_thumb = f"overlays/{base_name}_after_thumb.png"
except Exception as exc:
logger.warning("Failed to save thumbnails: %s", exc)
regions_serializable = _serialize_regions(change_regions)
det_w = int(stats.get("image_width") or 0)
det_h = int(stats.get("image_height") or 0)
if det_w <= 0 or det_h <= 0:
det_w, det_h = before_for_slider.size
geo_ctx = None
bounds = None
if geo_bounds_path:
rel_path = (base_path or "").replace("\\", "/").strip().lstrip("/")
if not rel_path:
try:
from .config import get_storage_root
rel_path = geo_bounds_path.resolve().relative_to(get_storage_root().resolve()).as_posix()
except Exception:
rel_path = geo_bounds_path.name
geo_ctx = resolve_geo_context(db, rel_path, geo_bounds_path)
bounds = geo_ctx.bounds
regions_serializable = enrich_regions_geo(
regions_serializable,
img_width=det_w,
img_height=det_h,
bounds=bounds,
geo=geo_ctx,
)
regions_with_coords = sum(1 for r in regions_serializable if r.get("latLng"))
geo_debug = {
"source": geo_ctx.source if geo_ctx else "none",
"crs": str(geo_ctx.georef.crs) if (geo_ctx and geo_ctx.georef and geo_ctx.georef.crs) else "",
"bounds": list(bounds) if bounds else None,
"georefWidth": geo_ctx.georef_width if geo_ctx else 0,
"georefHeight": geo_ctx.georef_height if geo_ctx else 0,
"detectionWidth": det_w,
"detectionHeight": det_h,
"regionsWithCoords": regions_with_coords,
"regionsTotal": len(regions_serializable),
}
logger.info("Geo debug for run: %s", geo_debug)
total_px = int(stats["total_pixels"])
changed_px = int(stats["changed_pixels"])
change_pct = float(stats["change_percentage"])
run = DetectionRun(
user_id=user.id,
title=title,
method=method,
zone=zone,
village=village,
total_pixels=total_px,
changed_pixels=changed_px,
change_percentage=change_pct,
regions_count=len(change_regions),
overlay_path=relative_overlay,
before_full_path=relative_before_full,
before_thumb_path=relative_before_thumb,
after_thumb_path=relative_after_thumb,
after_full_path=relative_after_full,
regions_json=json.dumps(regions_serializable),
)
db.add(run)
db.commit()
db.refresh(run)
_report(90, "Preparing report")
overlay_b64 = base64.b64encode(overlay_path.read_bytes()).decode("utf-8")
notification_sent = False
notification_error = None
if notify_email and notify_email.strip():
_report(95, "Sending notification")
from .config import IS_DDA_MODE, get_public_base_url
report_url = f"{get_public_base_url()}/dda/reports/{run.id}" if IS_DDA_MODE else ""
notification_sent, notification_error = send_notification(
recipient=notify_email.strip(),
title=title,
method=method,
zone=zone,
village=village,
change_pct=change_pct,
changed_px=changed_px,
total_px=total_px,
regions=regions_serializable,
report_url=report_url,
)
_report(100, "Complete")
return {
"id": run.id,
"title": run.title,
"method": run.method,
"zone": run.zone or "",
"village": run.village or "",
"statistics": {
"totalPixels": total_px,
"changedPixels": changed_px,
"unchangedPixels": int(stats["unchanged_pixels"]),
"changePercentage": change_pct,
"thresholdDebug": stats.get("threshold_debug", {}),
"params": stats.get("params", {}),
"alignmentWarning": stats.get("alignment_warning"),
"registrationOk": stats.get("params", {}).get("registration_ok"),
"geo": geo_debug,
"probabilityMapUrl": f"/api/overlay/{relative_prob}" if relative_prob else None,
},
"regions": regions_serializable,
"overlayBase64Png": overlay_b64,
"overlayUrl": f"/api/overlay/{relative_overlay}",
"beforeFullUrl": f"/api/overlay/{relative_before_full}" if relative_before_full else None,
"beforeThumbUrl": f"/api/overlay/{relative_before_thumb}" if relative_before_thumb else None,
"afterThumbUrl": f"/api/overlay/{relative_after_thumb}" if relative_after_thumb else None,
"afterFullUrl": f"/api/overlay/{relative_after_full}" if relative_after_full else None,
"notificationSent": notification_sent,
"notificationError": notification_error,
"createdAt": _isoformat_ist(run.created_at),
"detectionMaxSide": max_size,
}
|