From 404ab607688ce9c02a0b1155406446cf6026d664 Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Mon, 17 Aug 2026 17:59:09 +0800
Subject: [PATCH] feat:变化检测的
---
scripts/serve_workbench_console.py | 163 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 163 insertions(+), 0 deletions(-)
diff --git a/scripts/serve_workbench_console.py b/scripts/serve_workbench_console.py
index 08b333e..9063f7e 100644
--- a/scripts/serve_workbench_console.py
+++ b/scripts/serve_workbench_console.py
@@ -25,9 +25,14 @@
MAX_FILE_BYTES = 96 * 1024 * 1024
MAX_IMAGES_PER_RUN = 12
MAX_SEGMENTATION_IMAGES_PER_RUN = 6
+MAX_MEASUREMENT_RASTERS_PER_RUN = 4
+CHANGE_THRESHOLD_DEFAULT = 0.5
+CHANGE_THRESHOLD_MIN = 0.01
+CHANGE_THRESHOLD_MAX = 0.99
ALLOWED_PATH_PREFIXES = (
"apps/workbench-console",
"shared/outputs",
+ "shared/data/raw/00-change-detection",
"shared/data/raw/01-object-detection",
"shared/data/raw/02-semantic-mapping",
)
@@ -131,6 +136,49 @@
return sorted(records, key=lambda item: (item["createdAt"], item["id"]), reverse=True)
+def change_runs(root: Path) -> list[dict[str, Any]]:
+ output_root = root / "shared" / "outputs" / "00-change-detection"
+ records: list[dict[str, Any]] = []
+ for metadata_path in output_root.rglob("run_metadata.json"):
+ artifact = metadata_path.parent
+ metadata = load_json(metadata_path)
+ artifacts = metadata.get("artifacts")
+ if metadata.get("capability") != "00-change-detection" or metadata.get("schema_version") != 1 or not isinstance(artifacts, dict):
+ continue
+ if not (artifact / str(artifacts.get("overlay") or "")).is_file() or not (artifact / str(artifacts.get("vector") or "")).is_file():
+ continue
+ raw_root_value = str(metadata.get("raw_input_dir") or "shared/data/raw/00-change-detection/validation-20260817")
+ raw_root = root / Path(raw_root_value)
+ input_files = metadata.get("input_files")
+ if not isinstance(input_files, list) or len(input_files) != 2:
+ continue
+ before_value = str(metadata.get("raw_before") or (Path(raw_root_value) / str(input_files[0])).as_posix())
+ after_value = str(metadata.get("raw_after") or (Path(raw_root_value) / str(input_files[1])).as_posix())
+ try:
+ before_path = (root / before_value).resolve()
+ after_path = (root / after_value).resolve()
+ allowed_raw = (root / "shared" / "data" / "raw" / "00-change-detection").resolve()
+ before_path.relative_to(allowed_raw)
+ after_path.relative_to(allowed_raw)
+ except ValueError:
+ continue
+ if not before_path.is_file() or not after_path.is_file():
+ continue
+ run_id = artifact.name
+ records.append(
+ {
+ "id": run_id,
+ "label": run_id,
+ "note": "ChangeStar CPU 变化栅格与 GeoAI 像素坐标图斑;结果需人工复核。",
+ "artifactRoot": relative_path(root, artifact),
+ "beforeImage": relative_path(root, before_path),
+ "afterImage": relative_path(root, after_path),
+ "createdAt": str(metadata.get("created_at") or ""),
+ }
+ )
+ return sorted(records, key=lambda item: (item["createdAt"], item["id"]), reverse=True)
+
+
def semantic_runs(root: Path) -> list[dict[str, Any]]:
output_root = root / "shared" / "outputs" / "02-semantic-mapping"
records: list[dict[str, Any]] = []
@@ -148,6 +196,27 @@
"artifactRoot": relative_path(root, artifact),
"inputRoot": str(metadata.get("input_dir") or "shared/data/processed/02-semantic-mapping"),
"rawInputRoot": str(metadata.get("raw_input_dir") or "shared/data/raw/02-semantic-mapping"),
+ "createdAt": str(metadata.get("created_at") or ""),
+ }
+ )
+ return sorted(records, key=lambda item: (item["createdAt"], item["id"]), reverse=True)
+
+
+def measurement_runs(root: Path) -> list[dict[str, Any]]:
+ output_root = root / "shared" / "outputs" / "04-spatial-measurement"
+ records: list[dict[str, Any]] = []
+ for metadata_path in output_root.rglob("run_metadata.json"):
+ artifact = metadata_path.parent
+ metadata = load_json(metadata_path)
+ if metadata.get("capability") != "04-spatial-measurement" or not isinstance(metadata.get("images"), list):
+ continue
+ run_id = artifact.name
+ records.append(
+ {
+ "id": run_id,
+ "label": run_id,
+ "note": "GeoAI 栅格转矢量后进行对象计数、面积和周长测量。",
+ "artifactRoot": relative_path(root, artifact),
"createdAt": str(metadata.get("created_at") or ""),
}
)
@@ -173,6 +242,9 @@
def do_GET(self) -> None: # noqa: N802 - inherited standard-library method name
path = urlsplit(self.path).path
+ if path == "/api/change-detection/runs":
+ self.send_json(HTTPStatus.OK, {"runs": change_runs(self.root)})
+ return
if path == "/api/trajectory/runs":
self.send_json(HTTPStatus.OK, {"runs": trajectory_runs(self.root)})
return
@@ -185,6 +257,9 @@
if path == "/api/semantic-mapping/tasks":
self.send_json(HTTPStatus.OK, {"tasks": semantic_tasks(self.root)})
return
+ if path == "/api/spatial-measurement/runs":
+ self.send_json(HTTPStatus.OK, {"runs": measurement_runs(self.root)})
+ return
if path == "/":
self.send_response(HTTPStatus.FOUND)
self.send_header("Location", "/apps/workbench-console/")
@@ -196,6 +271,9 @@
path = urlsplit(self.path).path
try:
payload = self.read_json_body()
+ if path == "/api/change-detection/runs":
+ self.send_json(HTTPStatus.CREATED, {"run": self.create_change_run(payload)})
+ return
if path == "/api/trajectory/runs":
self.send_json(HTTPStatus.CREATED, {"run": self.create_trajectory_run(payload)})
return
@@ -204,6 +282,9 @@
return
if path == "/api/semantic-mapping/runs":
self.send_json(HTTPStatus.CREATED, {"run": self.create_semantic_run(payload)})
+ return
+ if path == "/api/spatial-measurement/runs":
+ self.send_json(HTTPStatus.CREATED, {"run": self.create_measurement_run(payload)})
return
self.send_json(HTTPStatus.NOT_FOUND, {"error": "Unknown local API endpoint."})
except ApiError as exc:
@@ -299,6 +380,56 @@
raise ApiError("Detection script finished without the expected result metadata.")
return next(item for item in detection_runs(self.root) if item["id"] == run_id)
+ def create_change_run(self, payload: dict[str, Any]) -> dict[str, Any]:
+ files = payload.get("files")
+ if not isinstance(files, dict):
+ raise ApiError("Change-detection request must contain before and after files.")
+ threshold_value = payload.get("threshold", CHANGE_THRESHOLD_DEFAULT)
+ if isinstance(threshold_value, bool) or not isinstance(threshold_value, (int, float)):
+ raise ApiError("Change-detection threshold must be a number between 0.01 and 0.99.")
+ threshold = float(threshold_value)
+ if not CHANGE_THRESHOLD_MIN <= threshold <= CHANGE_THRESHOLD_MAX:
+ raise ApiError("Change-detection threshold must be between 0.01 and 0.99.")
+ suffixes = {".jpg", ".jpeg", ".png", ".tif", ".tiff"}
+ before = decode_upload(files.get("before"), suffixes)
+ after = decode_upload(files.get("after"), suffixes)
+ run_id = make_run_id("change")
+ raw_root = self.root / "shared" / "data" / "raw" / "00-change-detection" / "runs" / run_id
+ before_path = raw_root / "before" / before[0]
+ after_path = raw_root / "after" / after[0]
+ before_path.parent.mkdir(parents=True, exist_ok=False)
+ after_path.parent.mkdir(parents=True, exist_ok=False)
+ before_path.write_bytes(before[1])
+ after_path.write_bytes(after[1])
+ processed_root = self.root / "shared" / "data" / "processed" / "00-change-detection" / run_id
+ output = self.root / "shared" / "outputs" / "00-change-detection" / "runs" / run_id
+ python = self.root / ".venvs" / "00-change-detection" / "Scripts" / "python.exe"
+ if not python.is_file():
+ raise ApiError("Change-detection virtual environment is unavailable. Run the capability setup first.")
+ with RUN_LOCK:
+ self.run_command(
+ [
+ str(python),
+ str(self.root / "capabilities" / "00-change-detection" / "run_change_detection.py"),
+ "--before", str(before_path),
+ "--after", str(after_path),
+ "--threshold", f"{threshold:.4f}",
+ "--processed-output", str(processed_root),
+ "--output", str(output),
+ ],
+ 1200,
+ )
+ metadata_path = output / "run_metadata.json"
+ if not metadata_path.is_file():
+ raise ApiError("Change-detection script finished without the expected result metadata.")
+ metadata = load_json(metadata_path)
+ metadata["raw_input_dir"] = relative_path(self.root, raw_root)
+ metadata["processed_input_dir"] = relative_path(self.root, processed_root)
+ metadata["raw_before"] = relative_path(self.root, before_path)
+ metadata["raw_after"] = relative_path(self.root, after_path)
+ metadata_path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
+ return next(item for item in change_runs(self.root) if item["id"] == run_id)
+
def create_semantic_run(self, payload: dict[str, Any]) -> dict[str, Any]:
task_id = str(payload.get("taskId") or "color_baseline")
task = next((item for item in semantic_tasks(self.root) if item["id"] == task_id), None)
@@ -339,6 +470,38 @@
metadata_path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
return next(item for item in semantic_runs(self.root) if item["id"] == run_id)
+ def create_measurement_run(self, payload: dict[str, Any]) -> dict[str, Any]:
+ uploads = payload.get("rasters")
+ if not isinstance(uploads, list) or not uploads:
+ raise ApiError("Spatial-measurement request must include at least one label raster.")
+ if len(uploads) > MAX_MEASUREMENT_RASTERS_PER_RUN:
+ raise ApiError(f"A spatial-measurement run accepts at most {MAX_MEASUREMENT_RASTERS_PER_RUN} rasters.")
+ decoded = [decode_upload(item, {".png", ".tif", ".tiff"}) for item in uploads]
+ if len({name.casefold() for name, _ in decoded}) != len(decoded):
+ raise ApiError("Uploaded raster names must be unique within one run.")
+ run_id = make_run_id("measurement")
+ raw_root = self.root / "shared" / "data" / "raw" / "04-spatial-measurement" / "runs" / run_id
+ processed_root = self.root / "shared" / "data" / "processed" / "04-spatial-measurement" / run_id
+ raw_root.mkdir(parents=True, exist_ok=False)
+ processed_root.mkdir(parents=True, exist_ok=False)
+ for name, content in decoded:
+ (raw_root / name).write_bytes(content)
+ (processed_root / name).write_bytes(content)
+ output = self.root / "shared" / "outputs" / "04-spatial-measurement" / "runs" / run_id
+ python = self.root / ".venvs" / "04-spatial-measurement" / "Scripts" / "python.exe"
+ if not python.is_file():
+ raise ApiError("Spatial-measurement virtual environment is unavailable. Run the capability setup first.")
+ with RUN_LOCK:
+ self.run_command([str(python), str(self.root / "capabilities" / "04-spatial-measurement" / "run_spatial_measurement.py"), "--input", str(processed_root), "--output", str(output)], 900)
+ metadata_path = output / "run_metadata.json"
+ if not metadata_path.is_file():
+ raise ApiError("Spatial-measurement script finished without the expected result metadata.")
+ metadata = load_json(metadata_path)
+ metadata["input_dir"] = relative_path(self.root, processed_root)
+ metadata["raw_input_dir"] = relative_path(self.root, raw_root)
+ metadata_path.write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
+ return next(item for item in measurement_runs(self.root) if item["id"] == run_id)
+
def send_json(self, status: HTTPStatus, payload: dict[str, Any]) -> None:
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
--
Gitblit v1.9.3