From 7cc239cee1a9af4e2e8a0f3d5b7a00a074b17214 Mon Sep 17 00:00:00 2001
From: 罗广辉 <guanghui.luo@foxmail.com>
Date: Wed, 19 Aug 2026 15:43:13 +0800
Subject: [PATCH] feat: 复用检测
---
apps/workbench-console/src/api/artifacts.ts | 201 ++++++++++++++++++++++++++++----------------------
1 files changed, 112 insertions(+), 89 deletions(-)
diff --git a/apps/workbench-console/src/api/artifacts.ts b/apps/workbench-console/src/api/artifacts.ts
index 47de970..1b348d1 100644
--- a/apps/workbench-console/src/api/artifacts.ts
+++ b/apps/workbench-console/src/api/artifacts.ts
@@ -1,101 +1,124 @@
-export interface Detection {
- class_id: number;
- class_name: string;
- confidence: number;
- bbox_xyxy: number[];
-}
-
-export interface DetectionImage {
- file: string;
- annotated_file: string;
- width: number;
- height: number;
- detections: Detection[];
-}
-
-export interface DetectionRun {
- detection_count: number;
- processed_images: number;
- elapsed_seconds: number;
- device: string;
- model: string;
- confidence: number;
- input_dir: string;
- notes: string[];
-}
-
-export interface TrajectoryEvent {
- event_id: string;
- event_type: string;
- track_ids: string[];
- start_time: string;
- end_time: string;
- duration_seconds: number;
-}
-
-export interface TrajectoryRun {
- case_count: number;
- track_count: number;
- event_count: number;
- dropped_duplicate_observations: number;
- elapsed_seconds: number;
- device: string;
- input: string;
- thresholds_by_case: Record<string, Record<string, number>>;
-}
-
-export interface TrajectorySummary {
- track_id: string;
- entity_type: string;
- point_count: string;
- distance_m: string;
- average_speed_mps: string;
- behavior_labels: string;
-}
+export interface Detection { class_id: number; class_name: string; confidence: number; bbox_xyxy: number[]; }
+export interface DetectionImage { file: string; annotated_file: string; width: number; height: number; detections: Detection[]; }
+export interface DetectionRun { detection_count: number; processed_images: number; elapsed_seconds: number; device: string; model: string; confidence: number; input_dir: string; notes: string[]; created_at?: string; }
+export interface DetectionCase { id: string; label: string; note: string; artifactRoot: string; inputRoot: string; createdAt: string; run: DetectionRun; images: DetectionImage[]; }
+export interface SemanticImage { file: string; width: number; height: number; mask_file: string; overlay_file: string; raster_file: string; vector_file: string; class_pixel_counts: Record<string, number>; georeferenced: boolean; vectorizer: string; elapsed_seconds: number; }
+export interface SemanticRun { capability: string; classification: string; task_id?: string; task_name?: string; created_at?: string; geoai_version: string; method: string; model: string; device: string; thresholds: Record<string, number>; input_count: number; processed_images: number; elapsed_seconds: number; images: SemanticImage[]; classes: Array<{ id: number; key: string; label: string; color: number[] }>; limitations: string[]; input_dir?: string; raw_input_dir?: string; }
+export interface SemanticCase { id: string; label: string; note: string; artifactRoot: string; inputRoot: string; rawInputRoot: string; createdAt: string; run: SemanticRun; }
+export interface SemanticTask { id: string; name: string; status: string; selectable: boolean; input_sensor: string[]; classes: string[]; project_use?: string; required_evidence?: string[]; prohibited_claim?: string; }
+export interface MeasurementImage { file: string; width: number; height: number; raster_file: string; preview_file: string; vector_file: string; csv_file: string; object_count: number; class_counts: Record<string, number>; total_area: number; total_perimeter: number; area_unit: string; length_unit: string; measurement_basis: string; crs: string | null; georeferenced: boolean; vectorizer: string; elapsed_seconds: number; }
+export interface MeasurementRun { capability: string; classification: string; created_at?: string; geoai_version: string; method: string; model: string; device: string; input_count: number; processed_images: number; elapsed_seconds: number; images: MeasurementImage[]; limitations: string[]; input_dir?: string; raw_input_dir?: string; }
+export interface MeasurementCase { id: string; label: string; note: string; artifactRoot: string; createdAt: string; run: MeasurementRun; }
+export interface AnomalyCandidate { type: "Feature"; properties: { feature_id?: number; mask_code?: number; method?: string; rule_flag?: boolean; isolation_flag?: boolean; agreement?: boolean; max_rule_score?: number; max_isolation_score?: number; reason_feature?: string; area_pixels?: number }; geometry: { type: string; coordinates: unknown }; }
+export interface AnomalyImage { file: string; width: number; height: number; tile_count: number; rule_heatmap_file: string; isolation_heatmap_file: string; overlay_file: string; mask_file: string; vector_file: string; tiles_file: string; candidate_count: number; rule_anomaly_pixels: number; local_change_pixels?: number; isolation_anomaly_pixels: number; agreement_pixels: number; rule_anomaly_coverage: number; local_change_coverage?: number; isolation_anomaly_coverage: number; agreement_coverage: number; georeferenced: boolean; crs: string | null; coordinate_basis: string; vectorizer: string; elapsed_seconds: number; }
+export interface AnomalyRun { capability: string; classification: string; created_at?: string; versions: Record<string, string>; method: string; model: string; device: string; reference_count: number; reference_tile_count: number; input_count: number; reference_images: Array<{ file: string; tile_count: number }>; feature_names: string[]; parameters: { tile_size: number; stride: number; threshold_quantile: number; random_state: number; spatial_mode_requested?: "auto" | "global" | "aligned"; rule_mode_selected?: "global" | "aligned" }; thresholds: { rule_score: number; isolation_score: number }; images: AnomalyImage[]; elapsed_seconds: number; limitations: string[]; }
+export interface AnomalyCase { id: string; label: string; note: string; artifactRoot: string; inputRoot: string; referenceRoot: string; createdAt: string; run: AnomalyRun; candidates: Record<string, AnomalyCandidate[]>; }
+export interface ChangeFeature { type: "Feature"; properties: { feature_id?: number; area_pixels?: number; mean_probability?: number; max_probability?: number; bounds_pixel?: number[]; confidence?: number; class?: number }; geometry: { type: string; coordinates: unknown }; }
+export interface ChangeRun { schema_version: number; capability: string; classification: string; geoai_version: string; method: string; model: string; device: string; processing_mode?: "image" | "geotiff"; requested_processing_mode?: "auto" | "image" | "geotiff"; thresholds: Record<string, number>; tile_size: number; overlap: number; max_dimension?: number; effective_max_dimension?: number; input_files: string[]; input_shape: number[]; processed_shape: number[]; registration: { method: string; matches: number; inliers: number; inlier_ratio: number; valid_ratio: number }; valid_pixel_ratio: number; raw_changed_pixels: number; changed_pixels: number; changed_pixel_ratio: number; vector_feature_count: number; georeferenced: boolean; coordinate_basis: string; crs: string | null; elapsed_seconds: number; limitations: string[]; artifacts: { probability_raster: string; raw_mask_raster: string; mask_raster: string; overlay: string; vector: string; features: string }; created_at?: string; }
+export interface ChangeCase { id: string; label: string; note: string; artifactRoot: string; beforeImage: string; afterImage: string; createdAt: string; run: ChangeRun; features: ChangeFeature[]; }
+export interface TrajectoryEvent { event_id: string; event_type: string; track_ids: string[]; start_time: string; end_time: string; duration_seconds: number; }
+export interface TrajectoryCaseRun { case_id: string; input_count: number; track_count: number; event_count: number; dropped_duplicate_observations: number; elapsed_seconds: number; device: string; input: string; thresholds: Record<string, number>; created_at?: string; }
+export interface TrajectorySummary { track_id: string; entity_type: string; point_count: string; distance_m: string; average_speed_mps: string; behavior_labels: string; }
+export interface TrajectoryCase { id: string; label: string; note: string; artifactRoot: string; showSpatialContext: boolean; showFlyableZones: boolean; createdAt: string; run: TrajectoryCaseRun; events: TrajectoryEvent[]; summary: TrajectorySummary[]; }
+interface CaseDefinition { id: string; label: string; note: string; artifactRoot: string; createdAt: string; }
+interface TrajectoryDefinition extends CaseDefinition { showSpatialContext: boolean; showFlyableZones: boolean; }
+interface DetectionDefinition extends CaseDefinition { inputRoot: string; }
+interface SemanticDefinition extends CaseDefinition { inputRoot: string; rawInputRoot: string; }
+interface MeasurementDefinition extends CaseDefinition {}
+interface ChangeDefinition extends CaseDefinition { beforeImage: string; afterImage: string; }
+interface AnomalyDefinition extends CaseDefinition { inputRoot: string; referenceRoot: string; }
+export interface UploadFilePayload { name: string; content: string; }
+export interface ChangeUploadRef { uploadId: string; role: "before" | "after"; name: string; size: number; }
+export interface AnomalyUploadRef { uploadId: string; role: "reference" | "input"; name: string; size: number; sha256: string; }
+export interface AnomalyJob { id: string; runId: string; status: "queued" | "running" | "complete" | "failed"; createdAt: string; finishedAt?: string; error?: string; run?: AnomalyDefinition; }
export const artifactUrl = (path: string) => `/${path.replace(/\\/g, "/").split("/").map(encodeURIComponent).join("/")}`;
-async function getJson<T>(path: string): Promise<T> {
- const response = await fetch(artifactUrl(path), { cache: "no-store" });
- if (!response.ok) throw new Error(`无法读取 ${path} (${response.status})`);
- return response.json() as Promise<T>;
+async function responseJson<T>(response: Response): Promise<T> {
+ const payload = await response.json().catch(() => ({})) as { error?: string } & T;
+ if (!response.ok) throw new Error(payload.error || `本地服务请求失败 (${response.status})`);
+ return payload;
+}
+async function getJson<T>(path: string): Promise<T> { return responseJson<T>(await fetch(artifactUrl(path), { cache: "no-store" })); }
+async function getText(path: string): Promise<string> { const response = await fetch(artifactUrl(path), { cache: "no-store" }); if (!response.ok) throw new Error(`无法读取 ${path} (${response.status})`); return response.text(); }
+function parseCsv(text: string): TrajectorySummary[] { const lines = text.trim().split(/\r?\n/); const headers = lines.shift()?.replace(/^\uFEFF/, "").split(",") ?? []; return lines.filter(Boolean).map((line) => { const values = line.split(","); return Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""])) as unknown as TrajectorySummary; }); }
+
+export async function loadTrajectoryArtifacts(): Promise<Record<string, TrajectoryCase>> {
+ const { runs } = await getJson<{ runs: TrajectoryDefinition[] }>("api/trajectory/runs");
+ const cases = await Promise.all(runs.map(async (definition) => {
+ const [run, eventPayload, summary] = await Promise.all([getJson<TrajectoryCaseRun>(`${definition.artifactRoot}/run_metadata.json`), getJson<{ events: TrajectoryEvent[] }>(`${definition.artifactRoot}/events.json`), getText(`${definition.artifactRoot}/trajectory_summary.csv`)]);
+ return { ...definition, run, events: eventPayload.events, summary: parseCsv(summary) } satisfies TrajectoryCase;
+ }));
+ return Object.fromEntries(cases.map((item) => [item.id, item]));
}
-async function getText(path: string): Promise<string> {
- const response = await fetch(artifactUrl(path), { cache: "no-store" });
- if (!response.ok) throw new Error(`无法读取 ${path} (${response.status})`);
- return response.text();
+export async function loadDetectionArtifacts(): Promise<Record<string, DetectionCase>> {
+ const { runs } = await getJson<{ runs: DetectionDefinition[] }>("api/object-detection/runs");
+ const cases = await Promise.all(runs.map(async (definition) => {
+ const [run, payload] = await Promise.all([getJson<DetectionRun>(`${definition.artifactRoot}/run_metadata.json`), getJson<{ images: DetectionImage[] }>(`${definition.artifactRoot}/detections.json`)]);
+ return { ...definition, run, images: payload.images } satisfies DetectionCase;
+ }));
+ return Object.fromEntries(cases.map((item) => [item.id, item]));
}
-function parseCsv(text: string): TrajectorySummary[] {
- const lines = text.trim().split(/\r?\n/);
- const headers = lines.shift()?.replace(/^\uFEFF/, "").split(",") ?? [];
- return lines.filter(Boolean).map((line) => {
- const values = line.split(",");
- return Object.fromEntries(headers.map((header, index) => [header, values[index] ?? ""])) as unknown as TrajectorySummary;
- });
+export async function loadSemanticArtifacts(): Promise<Record<string, SemanticCase>> {
+ const { runs } = await getJson<{ runs: SemanticDefinition[] }>("api/semantic-mapping/runs");
+ const cases = await Promise.all(runs.map(async (definition) => ({ ...definition, run: await getJson<SemanticRun>(`${definition.artifactRoot}/run_metadata.json`) } satisfies SemanticCase)));
+ return Object.fromEntries(cases.map((item) => [item.id, item]));
}
-export async function loadDetectionArtifacts() {
- const [run, payload] = await Promise.all([
- getJson<DetectionRun>("shared/outputs/01-object-detection/run_metadata.json"),
- getJson<{ images: DetectionImage[] }>("shared/outputs/01-object-detection/detections.json")
- ]);
- return { run, images: payload.images };
+export async function loadSemanticTasks(): Promise<SemanticTask[]> {
+ return (await getJson<{ tasks: SemanticTask[] }>("api/semantic-mapping/tasks")).tasks;
}
-export async function loadTrajectoryArtifacts() {
- const [run, difficultEvents, difficultSummary, normalSummary] = await Promise.all([
- getJson<TrajectoryRun>("shared/outputs/15-trajectory-analysis/run_metadata.json"),
- getJson<{ events: TrajectoryEvent[] }>("shared/outputs/15-trajectory-analysis/difficult/events.json"),
- getText("shared/outputs/15-trajectory-analysis/difficult/trajectory_summary.csv"),
- getText("shared/outputs/15-trajectory-analysis/normal/trajectory_summary.csv")
- ]);
- return {
- run,
- cases: {
- difficult: { events: difficultEvents.events, summary: parseCsv(difficultSummary) },
- normal: { events: [], summary: parseCsv(normalSummary) }
- }
- };
+export async function loadMeasurementArtifacts(): Promise<Record<string, MeasurementCase>> {
+ const { runs } = await getJson<{ runs: MeasurementDefinition[] }>("api/spatial-measurement/runs");
+ const cases = await Promise.all(runs.map(async (definition) => ({ ...definition, run: await getJson<MeasurementRun>(`${definition.artifactRoot}/run_metadata.json`) } satisfies MeasurementCase)));
+ return Object.fromEntries(cases.map((item) => [item.id, item]));
}
+
+export async function loadChangeArtifacts(): Promise<Record<string, ChangeCase>> {
+ const { runs } = await getJson<{ runs: ChangeDefinition[] }>("api/change-detection/runs");
+ const cases = await Promise.all(runs.map(async (definition) => {
+ const run = await getJson<ChangeRun>(`${definition.artifactRoot}/run_metadata.json`);
+ const vector = await getJson<{ features?: ChangeFeature[] }>(`${definition.artifactRoot}/${run.artifacts.vector}`);
+ return { ...definition, run, features: vector.features ?? [] } satisfies ChangeCase;
+ }));
+ return Object.fromEntries(cases.map((item) => [item.id, item]));
+}
+
+export async function loadAnomalyArtifacts(): Promise<Record<string, AnomalyCase>> {
+ const { runs } = await getJson<{ runs: AnomalyDefinition[] }>("api/anomaly-detection/runs");
+ const cases = await Promise.all(runs.map(async (definition) => {
+ const run = await getJson<AnomalyRun>(`${definition.artifactRoot}/run_metadata.json`);
+ const entries = await Promise.all(run.images.map(async (image) => {
+ const payload = await getJson<{ features?: AnomalyCandidate[] }>(`${definition.artifactRoot}/${image.vector_file}`);
+ return [image.file, payload.features ?? []] as const;
+ }));
+ return { ...definition, run, candidates: Object.fromEntries(entries) } satisfies AnomalyCase;
+ }));
+ return Object.fromEntries(cases.map((item) => [item.id, item]));
+}
+
+async function postRun<T>(path: string, body: object): Promise<T> { return responseJson<T>(await fetch(artifactUrl(path), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) })); }
+export async function createTrajectoryRun(files: { flight: UploadFilePayload; route: UploadFilePayload; restricted: UploadFilePayload; flyable?: UploadFilePayload }) { return postRun<{ run: TrajectoryDefinition }>("api/trajectory/runs", { files }); }
+export async function createDetectionRun(images: UploadFilePayload[]) { return postRun<{ run: DetectionDefinition }>("api/object-detection/runs", { images }); }
+export async function createSemanticRun(images: UploadFilePayload[], taskId: string) { return postRun<{ run: SemanticDefinition }>("api/semantic-mapping/runs", { images, taskId }); }
+export async function createMeasurementRun(rasters: UploadFilePayload[]) { return postRun<{ run: MeasurementDefinition }>("api/spatial-measurement/runs", { rasters }); }
+export async function createChangeRun(files: { before: UploadFilePayload; after: UploadFilePayload }, threshold = 0.5, maxDimension = 0, processingMode: "auto" | "image" | "geotiff" = "auto") { return postRun<{ run: ChangeDefinition }>("api/change-detection/runs", { files, threshold, maxDimension, processingMode }); }
+export async function uploadChangeFile(file: File, role: "before" | "after"): Promise<ChangeUploadRef> {
+ const uploadId = globalThis.crypto.randomUUID().replaceAll("-", "");
+ const query = new URLSearchParams({ role });
+ const response = await fetch(`/api/change-detection/uploads/${uploadId}?${query.toString()}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": encodeURIComponent(file.name) }, body: file });
+ return responseJson<ChangeUploadRef>(response);
+}
+export async function createChangeRunFromUploads(uploads: { before: ChangeUploadRef; after: ChangeUploadRef }, threshold = 0.5, maxDimension = 0, processingMode: "auto" | "image" | "geotiff" = "auto") { return postRun<{ run: ChangeDefinition }>("api/change-detection/runs", { uploads, threshold, maxDimension, processingMode }); }
+export async function uploadAnomalyFile(file: File, role: "reference" | "input"): Promise<AnomalyUploadRef> {
+ const uploadId = globalThis.crypto.randomUUID().replaceAll("-", "");
+ const query = new URLSearchParams({ role });
+ const response = await fetch(`/api/anomaly-detection/uploads/${uploadId}?${query.toString()}`, { method: "PUT", headers: { "Content-Type": "application/octet-stream", "X-Upload-Name": encodeURIComponent(file.name) }, body: file });
+ return responseJson<AnomalyUploadRef>(response);
+}
+export async function createAnomalyRun(uploads: { reference: AnomalyUploadRef[]; input: AnomalyUploadRef[] }, parameters: { tileSize: number; stride: number; thresholdQuantile: number; randomState: number }) { return postRun<{ job: AnomalyJob }>("api/anomaly-detection/runs", { uploads, ...parameters }); }
+export async function loadAnomalyJob(jobId: string) { return (await getJson<{ job: AnomalyJob }>(`api/anomaly-detection/jobs/${jobId}`)).job; }
+export function readFileAsPayload(file: File): Promise<UploadFilePayload> { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onerror = () => reject(new Error(`无法读取 ${file.name}`)); reader.onload = () => { const value = String(reader.result ?? ""); resolve({ name: file.name, content: value.slice(value.indexOf(",") + 1) }); }; reader.readAsDataURL(file); }); }
--
Gitblit v1.9.3