Add Gamma SBAS production workflow

This commit is contained in:
2026-05-21 04:53:39 +08:00
parent 9000feeee8
commit cc22b9ac2d
46 changed files with 11386 additions and 332 deletions
+396 -17
View File
@@ -19,7 +19,7 @@ from sqlalchemy import select
from .. import database
from ..config import settings
from ..models import SystemJobORM, DinsarResultORM, HazardPointORM, DinsarTaskItemORM, PsTaskItemORM, RadarDataORM, SARSceneGeoORM, FloodDetectionORM, WaterDetectionORM, GF3ProcessingORM, AiDiagnosisORM
from ..models import SystemJobORM, DinsarResultORM, HazardPointORM, DinsarTaskItemORM, PsTaskItemORM, RadarDataORM, SARSceneGeoORM, FloodDetectionORM, WaterDetectionORM, WaterExtractionORM, GF3ProcessingORM, AiDiagnosisORM
from ..scheduler import scan_data_job
from .data_service import data_service
from .asset_inventory_service import asset_inventory_service
@@ -80,7 +80,10 @@ JOB_TYPE_IDL_RUN_DINSAR = "IDL_RUN_DINSAR"
JOB_TYPE_WATER_GEOCODE = "WATER_GEOCODE"
JOB_TYPE_WATER_FLOOD = "WATER_FLOOD"
JOB_TYPE_WATER_DETECT = "WATER_DETECT"
JOB_TYPE_SAR_SCENE_PREPROCESS = "SAR_SCENE_PREPROCESS"
JOB_TYPE_FLOOD_DETECTION = "FLOOD_DETECTION"
JOB_TYPE_GF3_PROCESS = "GF3_PROCESS"
JOB_TYPE_GF3_UNPACK = "GF3_UNPACK"
JOB_TYPE_GF3_BATCH_PROCESS = "GF3_BATCH_PROCESS"
JOB_TYPE_ISCE2_RUN = "ISCE2_RUN"
JOB_TYPE_PYINT_RUN = "PYINT_RUN"
@@ -88,6 +91,7 @@ JOB_TYPE_PUBLISH_DINSAR_PRODUCTS = "PUBLISH_DINSAR_PRODUCTS"
JOB_TYPE_REBUILD_DINSAR_CATALOG = "REBUILD_DINSAR_CATALOG"
JOB_TYPE_REBUILD_PSINSAR_CATALOG = "REBUILD_PSINSAR_CATALOG"
JOB_TYPE_SCAN_ASSET_INVENTORY = "SCAN_ASSET_INVENTORY"
JOB_TYPE_SBAS_COREGISTRATION = "SBAS_COREGISTRATION"
COPY_ALLOWED_STATUSES = {"PENDING", "IN_PROGRESS", "COMPLETED", "FAILED"}
@@ -3149,6 +3153,106 @@ async def _handle_water_geocode(job: SystemJobORM) -> None:
)
async def _handle_sar_scene_preprocess(job: SystemJobORM) -> None:
"""Build one analysis-ready GeoTIFF for flood/water algorithms."""
payload = job.payload or {}
scene_id = payload.get("scene_id")
radar_data_id = payload.get("radar_data_id")
engine = str(payload.get("engine") or "").strip().lower()
if not scene_id and not radar_data_id:
raise ValueError("SAR_SCENE_PREPROCESS requires scene_id or radar_data_id")
if engine not in {"gf3_gdal", "lt_gamma"}:
raise ValueError(f"Unsupported SAR scene preprocessing engine: {engine}")
await task_service.start_task(job.task_id, message="Preparing analysis-ready SAR GeoTIFF...")
async with AsyncSessionLocal() as db:
scene: SARSceneGeoORM | None = None
if scene_id:
scene = await db.get(SARSceneGeoORM, int(scene_id))
if not scene and radar_data_id:
result = await db.execute(
select(SARSceneGeoORM).where(SARSceneGeoORM.radar_data_id == int(radar_data_id))
)
scene = result.scalar_one_or_none()
if not scene:
scene = SARSceneGeoORM(radar_data_id=int(radar_data_id), status="PENDING")
db.add(scene)
await db.flush()
radar = await db.get(RadarDataORM, int(scene.radar_data_id))
if not radar:
raise ValueError(f"RadarDataORM id={scene.radar_data_id} does not exist")
scene.status = "RUNNING"
scene.error_msg = None
await db.commit()
scene_id = int(scene.id)
radar_data_id = int(radar.id)
try:
if engine == "gf3_gdal":
await task_service.update_task(job.task_id, progress=20, message="Standardizing GF3 L2 GeoTIFF...")
from .sar_analysis_ready_service import standardize_gf3_l2_for_radar
async with AsyncSessionLocal() as db:
manifest = await standardize_gf3_l2_for_radar(
db=db,
radar_id=int(radar_data_id),
l2_path=payload.get("l2_path"),
polarization=payload.get("polarization"),
)
else:
await task_service.update_task(job.task_id, progress=15, message="Running LT Gamma single-scene preprocessing...")
from .lt_gamma_scene_service import run_lt_gamma_scene_preprocess
from .sar_analysis_ready_service import register_analysis_ready_tif
async with AsyncSessionLocal() as db:
scene = await db.get(SARSceneGeoORM, int(scene_id))
radar = await db.get(RadarDataORM, int(radar_data_id))
if not scene or not radar:
raise ValueError("Scene or radar record disappeared before LT Gamma preprocessing")
def _run_lt() -> Dict[str, Any]:
return run_lt_gamma_scene_preprocess(radar=radar, scene=scene, job_id=job.job_id)
lt_manifest = await asyncio.to_thread(_run_lt)
await task_service.update_task(job.task_id, progress=85, message="Registering LT analysis-ready GeoTIFF...")
analysis_tif_path = str(lt_manifest.get("analysis_tif_path") or "").strip()
if not analysis_tif_path:
raise RuntimeError("LT Gamma preprocessing returned no analysis_tif_path")
async with AsyncSessionLocal() as db:
scene = await db.get(SARSceneGeoORM, int(scene_id))
radar = await db.get(RadarDataORM, int(radar_data_id))
if not scene or not radar:
raise ValueError("Scene or radar record disappeared before analysis-ready registration")
manifest = await register_analysis_ready_tif(
db=db,
scene=scene,
radar=radar,
source_tif_path=analysis_tif_path,
engine="lt_gamma",
profile="lt1_gamma_geocoded_mli",
backscatter_unit=str(lt_manifest.get("backscatter_unit") or "gamma_mli_db"),
polarization=radar.polarization,
metadata=lt_manifest,
)
await db.commit()
except Exception as exc:
async with AsyncSessionLocal() as db:
scene = await db.get(SARSceneGeoORM, int(scene_id))
if scene:
scene.status = "FAILED"
scene.error_msg = str(exc)
await db.commit()
raise
await task_service.update_task(
job.task_id,
status="COMPLETED",
progress=100,
message=f"Analysis-ready GeoTIFF ready: {manifest.get('analysis_tif_path')}",
)
async def _handle_water_flood(job: SystemJobORM) -> None:
"""洪涝检测 job handler(灾前 + 灾后配对分类)。"""
from .water_service import run_flood_detection, WATER_RESULTS_DIR
@@ -3171,8 +3275,10 @@ async def _handle_water_flood(job: SystemJobORM) -> None:
raise ValueError("灾前或灾后场景记录不存在")
if pre_scene.status != "DONE" or post_scene.status != "DONE":
raise ValueError("灾前或灾后场景尚未完成地理编码")
pre_geo = pre_scene.geo_path
post_geo = post_scene.geo_path
pre_geo = pre_scene.analysis_tif_path or pre_scene.geo_path
post_geo = post_scene.analysis_tif_path or post_scene.geo_path
if not pre_geo or not post_geo:
raise ValueError("Pre/post scenes must have analysis-ready GeoTIFF paths")
output_dir = os.path.join(WATER_RESULTS_DIR, f"flood_{detection_id}")
os.makedirs(output_dir, exist_ok=True)
@@ -3228,36 +3334,129 @@ async def _handle_water_flood(job: SystemJobORM) -> None:
)
async def _handle_water_detect(job: SystemJobORM) -> None:
"""水体检测 job handlerOtsu + DEM + 形态学 + 连通分量)。"""
from .water_detect_service import run_water_detection
async def _handle_flood_detection(job: SystemJobORM) -> None:
"""Flood-analysis detection job: pure Python GeoTIFF change classification."""
from .flood_detection_service import run_geotiff_flood_detection
payload = job.payload or {}
detection_id = payload.get("detection_id")
if not detection_id:
raise ValueError("WATER_DETECT job 缺少 detection_id")
raise ValueError("FLOOD_DETECTION job requires detection_id")
refine = bool(payload.get("refine", False))
await task_service.start_task(job.task_id, message="Reading flood-detection scene pair...")
async with AsyncSessionLocal() as db:
det = await db.get(FloodDetectionORM, int(detection_id))
if not det:
raise ValueError(f"FloodDetectionORM id={detection_id} does not exist")
pre_scene = await db.get(SARSceneGeoORM, det.pre_scene_id)
post_scene = await db.get(SARSceneGeoORM, det.post_scene_id)
if not pre_scene or not post_scene:
raise ValueError("Pre/post scene records do not exist")
if pre_scene.status != "DONE" or post_scene.status != "DONE":
raise ValueError("Pre/post scenes are not DONE")
pre_tif = pre_scene.analysis_tif_path
post_tif = post_scene.analysis_tif_path
if not pre_tif or not post_tif:
raise ValueError("Flood detection requires analysis-ready GeoTIFF paths for both scenes")
output_dir = os.path.join(settings.WATER_RESULTS_DIR, f"flood_{detection_id}")
os.makedirs(output_dir, exist_ok=True)
await task_service.update_task(job.task_id, progress=10, message="Running GeoTIFF flood classification...")
def _run() -> Dict[str, Any]:
return run_geotiff_flood_detection(
pre_tif_path=pre_tif,
post_tif_path=post_tif,
output_dir=output_dir,
job_id=job.job_id,
refine=refine,
)
try:
result = await asyncio.to_thread(_run)
except Exception as exc:
async with AsyncSessionLocal() as db:
det = await db.get(FloodDetectionORM, int(detection_id))
if det:
det.status = "FAILED"
det.error_msg = str(exc)
await db.commit()
raise
async with AsyncSessionLocal() as db:
det = await db.get(FloodDetectionORM, int(detection_id))
if det:
if result.get("ok"):
det.classified_path = result.get("classified_path")
det.flood_area_km2 = result.get("flood_area_km2")
det.stable_water_area_km2 = result.get("stable_water_area_km2")
det.output_dir = output_dir
det.status = "DONE"
det.error_msg = None
else:
det.status = "FAILED"
det.error_msg = result.get("error", "Unknown error")
await db.commit()
if not result.get("ok"):
raise RuntimeError(f"Flood detection failed: {result.get('error')}")
await task_service.update_task(
job.task_id,
status="COMPLETED",
progress=100,
message=(
f"GeoTIFF flood detection completed: flood_area={result.get('flood_area_km2')} km2, "
f"stable_water={result.get('stable_water_area_km2')} km2"
),
)
async def _handle_water_detect(job: SystemJobORM) -> None:
"""水体检测 job handlerOtsu + DEM + 形态学 + 连通分量)。"""
from .water_extraction_service import run_otsu_water_extraction
payload = job.payload or {}
extraction_id = payload.get("extraction_id")
detection_id = payload.get("detection_id")
record_id = extraction_id or detection_id
if not record_id:
raise ValueError("WATER_DETECT job 缺少 extraction_id/detection_id")
use_extraction_table = extraction_id is not None
await task_service.start_task(job.task_id, message="读取检测任务信息...")
async with AsyncSessionLocal() as db:
det = await db.get(WaterDetectionORM, int(detection_id))
det = await db.get(WaterExtractionORM if use_extraction_table else WaterDetectionORM, int(record_id))
if not det:
raise ValueError(f"WaterDetectionORM id={detection_id} 不存在")
model_name = "WaterExtractionORM" if use_extraction_table else "WaterDetectionORM"
raise ValueError(f"{model_name} id={record_id} 不存在")
input_path = det.input_path
det.status = "RUNNING"
if use_extraction_table and hasattr(det, "task_id"):
det.task_id = job.task_id
if not use_extraction_table:
mirror = await db.get(WaterExtractionORM, int(record_id))
if mirror:
mirror.status = "RUNNING"
mirror.task_id = job.task_id
await db.commit()
if not input_path:
raise ValueError("水体检测缺少输入路径 input_path")
output_dir = os.path.join(os.path.dirname(input_path), f"water_detect_{detection_id}")
output_name = f"water_extraction_{record_id}" if use_extraction_table else f"water_detect_{record_id}"
output_dir = os.path.join(os.path.dirname(input_path), output_name)
os.makedirs(output_dir, exist_ok=True)
await task_service.update_task(job.task_id, progress=10, message="启动水体检测算法...")
def _run() -> Dict[str, Any]:
return run_water_detection(
geo_tiff_path=input_path,
return run_otsu_water_extraction(
input_path=input_path,
output_dir=output_dir,
job_id=job.job_id,
)
@@ -3266,26 +3465,56 @@ async def _handle_water_detect(job: SystemJobORM) -> None:
result = await asyncio.to_thread(_run)
except Exception as exc:
async with AsyncSessionLocal() as db:
det = await db.get(WaterDetectionORM, int(detection_id))
det = await db.get(WaterExtractionORM if use_extraction_table else WaterDetectionORM, int(record_id))
if det:
det.status = "FAILED"
det.error_msg = str(exc)
await db.commit()
if not use_extraction_table:
mirror = await db.get(WaterExtractionORM, int(record_id))
if mirror:
mirror.status = "FAILED"
mirror.error_msg = str(exc)
mirror.task_id = job.task_id
await db.commit()
raise
async with AsyncSessionLocal() as db:
det = await db.get(WaterDetectionORM, int(detection_id))
det = await db.get(WaterExtractionORM if use_extraction_table else WaterDetectionORM, int(record_id))
if det:
if result.get("ok"):
det.output_path = result.get("output_path")
det.water_area_km2 = result.get("water_area_km2")
det.water_pixel_count = result.get("water_pixel_count")
det.otsu_threshold_db = result.get("otsu_threshold_db")
if use_extraction_table:
det.processor = result.get("processor") or det.processor or "otsu"
det.threshold_value = result.get("threshold_value")
det.metadata_json = {
"legacy_otsu_threshold_db": result.get("otsu_threshold_db"),
"job_id": job.job_id,
}
else:
det.otsu_threshold_db = result.get("otsu_threshold_db")
det.status = "DONE"
det.error_msg = None
else:
det.status = "FAILED"
det.error_msg = result.get("error", "Unknown error")
if not use_extraction_table:
mirror = await db.get(WaterExtractionORM, int(record_id))
if mirror:
mirror.output_path = det.output_path
mirror.water_area_km2 = det.water_area_km2
mirror.water_pixel_count = det.water_pixel_count
mirror.threshold_value = result.get("threshold_value") or result.get("otsu_threshold_db")
mirror.processor = result.get("processor") or mirror.processor or "otsu"
mirror.status = det.status
mirror.error_msg = det.error_msg
mirror.task_id = job.task_id
mirror.metadata_json = {
"legacy_otsu_threshold_db": result.get("otsu_threshold_db"),
"legacy_detection_id": int(record_id),
"job_id": job.job_id,
}
await db.commit()
if not result.get("ok"):
@@ -3376,9 +3605,70 @@ async def _handle_gf3_process(job: SystemJobORM) -> None:
)
async def _handle_gf3_unpack(job: SystemJobORM) -> None:
"""GF3 archive inbox -> persistent L1A source pool."""
from .gf3_unpack_service import run_gf3_archive_unpack
if not job.task_id:
raise ValueError("GF3_UNPACK requires task_id for progress tracking.")
payload = job.payload or {}
await task_service.start_task(job.task_id, message="扫描 GF3 压缩包来源目录...")
loop = asyncio.get_running_loop()
def _submit(coro):
try:
future = asyncio.run_coroutine_threadsafe(coro, loop)
except RuntimeError:
return
def _swallow_errors(fut):
try:
fut.result()
except Exception as exc:
logger.warning("[GF3 Unpack] task callback failed: %s", exc)
future.add_done_callback(_swallow_errors)
def _log_cb(level: str, message: str) -> None:
_submit(task_service.add_log(job.task_id, level, message))
def _progress_cb(progress: int, message: str) -> None:
_submit(task_service.update_task(job.task_id, progress=progress, message=message))
try:
result = await asyncio.to_thread(
run_gf3_archive_unpack,
source_dirs=payload.get("source_dirs"),
target_dirs=payload.get("target_dirs"),
archive_exts=payload.get("archive_exts"),
max_files_per_run=payload.get("max_files_per_run"),
delete_archive=payload.get("delete_archive") if "delete_archive" in payload else None,
min_disk_space_gb=payload.get("min_disk_space_gb"),
tmp_suffix=payload.get("tmp_suffix"),
log_callback=_log_cb,
progress_callback=_progress_cb,
)
except Exception as exc:
await task_service.update_task(job.task_id, status="FAILED", progress=100, message=f"GF3 解包失败: {exc}")
raise
message = (
f"GF3 解包完成: 成功 {int(result.get('processed') or 0)}, "
f"跳过 {int(result.get('skipped') or 0)}, "
f"失败 {int(result.get('failed') or 0)}"
)
remaining = int(result.get("remaining") or 0)
if remaining > 0:
message += f", 剩余 {remaining}"
await task_service.update_task(job.task_id, status="COMPLETED", progress=100, message=message)
async def _handle_gf3_batch_process(job: SystemJobORM) -> None:
"""批量 GF3 L1A→L2:扫描来源目录,逐个处理并自动入库到 radar_data。"""
from .gf3_service import run_gf3_l1a_to_l2, register_l2_to_radar_data
from .sar_analysis_ready_service import standardize_gf3_l2_for_radar
payload = job.payload or {}
source_dirs = payload.get("source_dirs") or []
@@ -3458,12 +3748,18 @@ async def _handle_gf3_batch_process(job: SystemJobORM) -> None:
# Auto-register to radar_data
try:
async with AsyncSessionLocal() as db:
await register_l2_to_radar_data(
radar_id = await register_l2_to_radar_data(
l2_dir=result.get("output_dir", output_dir),
input_dir_name=dir_name,
polarizations=result.get("polarizations", []),
db=db,
)
if radar_id:
await standardize_gf3_l2_for_radar(
db=db,
radar_id=int(radar_id),
l2_path=result.get("output_dir", output_dir),
)
except Exception as reg_err:
logger.warning("[GF3 Batch] Auto-register failed for %s: %s", dir_name, reg_err)
else:
@@ -3888,6 +4184,85 @@ async def _handle_rebuild_psinsar_catalog(job: SystemJobORM) -> None:
)
async def _handle_sbas_coregistration(job: SystemJobORM) -> None:
if not job.task_id:
raise ValueError("SBAS_COREGISTRATION requires task_id for progress tracking.")
payload = job.payload or {}
run_id = str(payload.get("run_id") or "").strip()
if not run_id:
raise ValueError("SBAS_COREGISTRATION requires run_id payload.")
rlks = _normalize_positive_int(payload.get("rlks")) or 8
azlks = _normalize_positive_int(payload.get("azlks")) or 8
timeout_seconds = _normalize_positive_int(payload.get("timeout_seconds")) or 43200
await task_service.start_task(job.task_id, message="正在执行 SBAS-InSAR Gamma 共参考配准...")
await task_service.update_task(
job.task_id,
progress=5,
message=f"准备运行 Gamma SLC_coreg.py: run_id={run_id}",
)
await task_service.add_log(
job.task_id,
"INFO",
f"SBAS coregistration queued: run_id={run_id}, rlks={rlks}, azlks={azlks}, timeout={timeout_seconds}s",
)
from .sbas_insar_production_service import sbas_insar_production_service
async def _task_keepalive() -> None:
progress = 12
while True:
await asyncio.sleep(60)
progress = min(88, progress + 2)
await task_service.update_task(
job.task_id,
progress=progress,
message=f"Gamma 共参考配准仍在运行: run_id={run_id}",
)
runner_task = asyncio.create_task(
asyncio.to_thread(
sbas_insar_production_service.execute_coregistration,
run_id,
rlks=rlks,
azlks=azlks,
timeout_seconds=timeout_seconds,
)
)
keepalive_task = asyncio.create_task(_task_keepalive())
try:
result = await runner_task
finally:
keepalive_task.cancel()
try:
await keepalive_task
except asyncio.CancelledError:
pass
manifest = result.get("manifest") or {}
run = result.get("run") or {}
summary = (manifest.get("coregistration") or {}).get("summary") or {}
status = str(run.get("status") or manifest.get("status") or "").strip()
if status != "COREGISTRATION_READY":
raise RuntimeError(
"SBAS coregistration failed: "
f"status={status or 'UNKNOWN'}, "
f"missing_dates={summary.get('missing_dates') or []}, "
f"missing_tabs={summary.get('missing_tabs') or []}"
)
await task_service.update_task(
job.task_id,
status="COMPLETED",
progress=100,
message=(
"SBAS-InSAR 共参考配准完成: "
f"{summary.get('ready_secondary_count', 0)}/{summary.get('expected_secondary_count', 0)} secondary scenes ready"
),
)
_HANDLERS = {
JOB_TYPE_SCAN_DATA: _handle_scan_data,
JOB_TYPE_SCAN_ASSET_INVENTORY: _handle_scan_asset_inventory,
@@ -3918,10 +4293,14 @@ _HANDLERS = {
JOB_TYPE_ISCE2_RUN: _handle_isce2_run,
JOB_TYPE_PYINT_RUN: _handle_pyint_run,
JOB_TYPE_WATER_GEOCODE: _handle_water_geocode,
JOB_TYPE_SAR_SCENE_PREPROCESS: _handle_sar_scene_preprocess,
JOB_TYPE_WATER_FLOOD: _handle_water_flood,
JOB_TYPE_FLOOD_DETECTION: _handle_flood_detection,
JOB_TYPE_WATER_DETECT: _handle_water_detect,
JOB_TYPE_GF3_PROCESS: _handle_gf3_process,
JOB_TYPE_GF3_UNPACK: _handle_gf3_unpack,
JOB_TYPE_GF3_BATCH_PROCESS: _handle_gf3_batch_process,
JOB_TYPE_SBAS_COREGISTRATION: _handle_sbas_coregistration,
}