| | |
| | | POINTCLOUD_INFERENCE_JOBS_LOCK = threading.Lock() |
| | | MAX_ANNOTATION_LABELS = 400_000 |
| | | POINTCLOUD_CLASS_CODES = {1, 2, 5, 6, 15, 16} |
| | | POINTCLOUD_CPU_ENVIRONMENT = "05-3d-pointcloud" |
| | | POINTCLOUD_GPU_ENVIRONMENT = "05-3d-pointcloud-gpu" |
| | | |
| | | |
| | | class ApiError(ValueError): |
| | |
| | | for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b""): |
| | | digest.update(chunk) |
| | | return digest.hexdigest() |
| | | |
| | | |
| | | def pointcloud_execution_environment(root: Path, requested_device: str = "auto") -> dict[str, str]: |
| | | """Select only a fixed point-cloud interpreter after a short CUDA probe. |
| | | |
| | | The console never accepts a browser-supplied Python path. A failed or |
| | | unavailable GPU environment is an expected condition for ``auto`` and |
| | | falls back to the retained CPU environment. |
| | | """ |
| | | if requested_device not in {"auto", "cpu", "cuda"}: |
| | | raise ApiError("Point-cloud device must be auto, cpu, or cuda.") |
| | | cpu_python = root / ".venvs" / POINTCLOUD_CPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | gpu_python = root / ".venvs" / POINTCLOUD_GPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | if requested_device != "cpu" and gpu_python.is_file(): |
| | | try: |
| | | probe = subprocess.run( |
| | | [str(gpu_python), "-c", "import json, torch; print(json.dumps({'cuda': bool(torch.cuda.is_available()), 'torch': torch.__version__}))"], |
| | | cwd=root, |
| | | capture_output=True, |
| | | text=True, |
| | | timeout=20, |
| | | check=False, |
| | | ) |
| | | payload = json.loads(probe.stdout.strip().splitlines()[-1]) if probe.returncode == 0 and probe.stdout.strip() else {} |
| | | if payload.get("cuda") is True and isinstance(payload.get("torch"), str): |
| | | return {"python": str(gpu_python), "device": "cuda", "environment": POINTCLOUD_GPU_ENVIRONMENT, "torchVersion": payload["torch"]} |
| | | except (OSError, subprocess.SubprocessError, json.JSONDecodeError, IndexError): |
| | | pass |
| | | if requested_device == "cuda": |
| | | raise ApiError("CUDA was requested, but the fixed point-cloud GPU environment is unavailable.") |
| | | if not cpu_python.is_file(): |
| | | raise ApiError("3D point-cloud CPU virtual environment is unavailable. Run the capability setup first.") |
| | | return {"python": str(cpu_python), "device": "cpu", "environment": POINTCLOUD_CPU_ENVIRONMENT, "torchVersion": "unknown"} |
| | | |
| | | |
| | | def trajectory_runs(root: Path) -> list[dict[str, Any]]: |
| | |
| | | return dict(value) if value else None |
| | | |
| | | |
| | | def execute_pointcloud_training_job(root: Path, job_id: str, annotation: Path, output: Path, device: str) -> None: |
| | | def execute_pointcloud_training_job(root: Path, job_id: str, annotation: Path, output: Path, execution: dict[str, str]) -> None: |
| | | with POINTCLOUD_TRAINING_JOBS_LOCK: |
| | | POINTCLOUD_TRAINING_JOBS[job_id].update({"status": "running", "stage": "training", "startedAt": datetime.now(UTC).isoformat()}) |
| | | python = root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe" |
| | | command = [str(python), str(root / "capabilities" / "05-3d-pointcloud" / "train_pointcloud_semantic_model.py"), "--annotation", str(annotation), "--output", str(output), "--device", device] |
| | | command = [execution["python"], str(root / "capabilities" / "05-3d-pointcloud" / "train_pointcloud_semantic_model.py"), "--annotation", str(annotation), "--output", str(output), "--device", execution["device"]] |
| | | try: |
| | | with RUN_LOCK: |
| | | completed = subprocess.run(command, cwd=root, capture_output=True, text=True, timeout=14_400, check=False) |
| | |
| | | raise ApiError("LAS/LAZ 文件没有点记录。请选择包含实际 RGB 点位的完整点云文件,而不是空分块。") |
| | | |
| | | |
| | | def execute_pointcloud_inference_job(root: Path, job_id: str, model: Path, source: Path, output: Path) -> None: |
| | | def execute_pointcloud_inference_job(root: Path, job_id: str, model: Path, source: Path, output: Path, execution: dict[str, str]) -> None: |
| | | with POINTCLOUD_INFERENCE_JOBS_LOCK: |
| | | POINTCLOUD_INFERENCE_JOBS[job_id].update({"status": "running", "stage": "inference", "startedAt": datetime.now(UTC).isoformat()}) |
| | | python = root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe" |
| | | command = [str(python), str(root / "capabilities" / "05-3d-pointcloud" / "apply_pointcloud_semantic_model.py"), "--model", str(model), "--input", str(source), "--output", str(output), "--device", "cpu"] |
| | | command = [execution["python"], str(root / "capabilities" / "05-3d-pointcloud" / "apply_pointcloud_semantic_model.py"), "--model", str(model), "--input", str(source), "--output", str(output), "--device", execution["device"]] |
| | | try: |
| | | with RUN_LOCK: |
| | | completed = subprocess.run(command, cwd=root, capture_output=True, text=True, timeout=14_400, check=False) |
| | |
| | | source_bytes[name] = raw_path.stat().st_size |
| | | shutil.copyfile(raw_path, processed_root / name) |
| | | output = self.root / "shared" / "outputs" / "05-3d-pointcloud" / "runs" / run_id |
| | | python = self.root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe" |
| | | python = self.root / ".venvs" / POINTCLOUD_CPU_ENVIRONMENT / "Scripts" / "python.exe" |
| | | if not python.is_file(): |
| | | raise ApiError("3D point-cloud virtual environment is unavailable. Run the capability setup first.") |
| | | raise ApiError("3D point-cloud CPU virtual environment is unavailable. Run the capability setup first.") |
| | | command = [str(python), str(self.root / "capabilities" / "05-3d-pointcloud" / "run_pointcloud_understanding.py"), "--input", str(processed_root), "--output", str(output), "--ground-up-axis", "z"] |
| | | with RUN_LOCK: |
| | | self.run_command(command, 900) |
| | |
| | | record = load_json(annotation) |
| | | if record.get("schema_version") != 1: |
| | | raise ApiError("The selected annotation revision is unavailable.") |
| | | python = self.root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe" |
| | | if not python.is_file(): |
| | | raise ApiError("3D point-cloud virtual environment is unavailable. Run the capability setup first.") |
| | | execution = pointcloud_execution_environment(self.root, device) |
| | | job_id = uuid4().hex |
| | | output = self.root / "shared" / "outputs" / "05-3d-pointcloud" / "training-runs" / make_run_id("semantic-model") |
| | | job = {"id": job_id, "annotationId": annotation_id, "status": "queued", "stage": "queued", "device": device, "createdAt": datetime.now(UTC).isoformat()} |
| | | job = {"id": job_id, "annotationId": annotation_id, "status": "queued", "stage": "queued", "requestedDevice": device, "device": execution["device"], "environment": execution["environment"], "torchVersion": execution["torchVersion"], "createdAt": datetime.now(UTC).isoformat()} |
| | | with POINTCLOUD_TRAINING_JOBS_LOCK: |
| | | POINTCLOUD_TRAINING_JOBS[job_id] = job |
| | | thread = threading.Thread(target=execute_pointcloud_training_job, args=(self.root, job_id, annotation, output, device), daemon=True, name=f"pointcloud-training-{job_id[:8]}") |
| | | thread = threading.Thread(target=execute_pointcloud_training_job, args=(self.root, job_id, annotation, output, execution), daemon=True, name=f"pointcloud-training-{job_id[:8]}") |
| | | thread.start() |
| | | return dict(job) |
| | | |
| | |
| | | if Path(name).suffix.lower() not in suffixes: |
| | | raise ApiError("Model inference requires a PLY, PCD, XYZ, LAS, or LAZ point cloud.") |
| | | validate_pointcloud_model_input(staged_path) |
| | | python = self.root / ".venvs" / "05-3d-pointcloud" / "Scripts" / "python.exe" |
| | | if not python.is_file(): |
| | | raise ApiError("3D point-cloud virtual environment is unavailable. Run the capability setup first.") |
| | | execution = pointcloud_execution_environment(self.root) |
| | | run_id = make_run_id("semantic-inference") |
| | | raw_root = self.root / "shared" / "data" / "raw" / "05-3d-pointcloud" / "model-inference-runs" / run_id |
| | | processed_root = self.root / "shared" / "data" / "processed" / "05-3d-pointcloud" / "model-inference-runs" / run_id |
| | |
| | | except ValueError as exc: |
| | | raise ApiError("Selected model is outside the allowed training output directory.") from exc |
| | | job_id = uuid4().hex |
| | | job = {"id": job_id, "runId": run_id, "modelId": model_id, "inputName": name, "status": "queued", "stage": "queued", "device": "cpu", "createdAt": datetime.now(UTC).isoformat(), "sourceSha256": actual_sha256, "rawInput": relative_path(self.root, raw_path), "processedInput": relative_path(self.root, processed_path)} |
| | | job = {"id": job_id, "runId": run_id, "modelId": model_id, "inputName": name, "status": "queued", "stage": "queued", "requestedDevice": "auto", "device": execution["device"], "environment": execution["environment"], "torchVersion": execution["torchVersion"], "createdAt": datetime.now(UTC).isoformat(), "sourceSha256": actual_sha256, "rawInput": relative_path(self.root, raw_path), "processedInput": relative_path(self.root, processed_path)} |
| | | with POINTCLOUD_INFERENCE_JOBS_LOCK: |
| | | POINTCLOUD_INFERENCE_JOBS[job_id] = job |
| | | thread = threading.Thread(target=execute_pointcloud_inference_job, args=(self.root, job_id, model_path, processed_path, output), daemon=True, name=f"pointcloud-inference-{job_id[:8]}") |
| | | thread = threading.Thread(target=execute_pointcloud_inference_job, args=(self.root, job_id, model_path, processed_path, output, execution), daemon=True, name=f"pointcloud-inference-{job_id[:8]}") |
| | | thread.start() |
| | | return dict(job) |
| | | |