| | |
| | | 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; isolation_anomaly_pixels: number; agreement_pixels: number; rule_anomaly_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[]; } |
| | |
| | | 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("/")}`; |
| | | |
| | |
| | | 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 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": file.name }, body: file }); |
| | | 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); }); } |