From 385be2eca72eb3833efa4be0a0088b34e764788a Mon Sep 17 00:00:00 2001
From: shuishen <1109946754@qq.com>
Date: Mon, 31 Aug 2026 09:04:45 +0800
Subject: [PATCH] feat(pointcloud): complete annotation and result lifecycle workflows
---
apps/workbench-console/src/components/ChangeDetectionPanel.vue | 14 +++++++++-----
1 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/apps/workbench-console/src/components/ChangeDetectionPanel.vue b/apps/workbench-console/src/components/ChangeDetectionPanel.vue
index 24fc253..967298f 100644
--- a/apps/workbench-console/src/components/ChangeDetectionPanel.vue
+++ b/apps/workbench-console/src/components/ChangeDetectionPanel.vue
@@ -2,8 +2,9 @@
import { computed, onMounted, ref } from "vue";
import { DownloadOutlined, FileImageOutlined, FileOutlined, PlayCircleOutlined, UploadOutlined } from "@ant-design/icons-vue";
-import { artifactUrl, createChangeRunFromUploads, createChangeScanFromUploads, getChangeScanJob, loadChangeParameterScans, promoteChangeScan, uploadChangeFile, type ChangeCase, type ChangeFeature, type ChangeParameterScan, type ChangeScanResult } from "@/api/artifacts";
+import { artifactUrl, createChangeRunFromUploads, createChangeScanFromUploads, getChangeScanJob, loadChangeParameterScans, promoteChangeScan, uploadChangeFile, type ChangeCase, type ChangeFeature, type ChangeParameterScan, type ChangeScanResult, type ComputeDevice } from "@/api/artifacts";
import ArtifactState from "@/components/ArtifactState.vue";
+import RunDeletionControl from "@/components/RunDeletionControl.vue";
import { useArtifactStore } from "@/stores/artifacts";
const store = useArtifactStore();
@@ -13,6 +14,7 @@
const threshold = ref(0.5);
const processingMode = ref<"auto" | "image" | "geotiff">("auto");
const maxDimension = ref(0);
+const device = ref<ComputeDevice>("auto");
const showRunForm = ref(false);
const running = ref(false);
const runError = ref<string | null>(null);
@@ -34,6 +36,7 @@
const currentCase = computed<ChangeCase | undefined>(() => store.changeCases[caseId.value] ?? Object.values(store.changeCases)[0]);
const caseOptions = computed(() => Object.values(store.changeCases).map((item) => ({ value: item.id, label: item.label })));
const filteredCaseOptions = computed(() => caseOptions.value.filter((item) => item.label.toLowerCase().includes(searchText.value.trim().toLowerCase())));
+async function removeRun() { await store.loadChange(true); caseId.value = Object.keys(store.changeCases)[0] ?? ""; }
const changePercent = computed(() => ((currentCase.value?.run.changed_pixel_ratio ?? 0) * 100).toFixed(3));
const validPercent = computed(() => ((currentCase.value?.run.valid_pixel_ratio ?? 0) * 100).toFixed(2));
const selectedScan = computed(() => parameterScans.value.find((item) => item.id === selectedScanId.value) ?? parameterScans.value[0]);
@@ -91,7 +94,7 @@
running.value = true; runError.value = null;
try {
const [before, after] = await Promise.all([uploadChangeFile(beforeFile.value, "before"), uploadChangeFile(afterFile.value, "after")]);
- const { run } = await createChangeRunFromUploads({ before, after }, threshold.value, maxDimension.value, processingMode.value);
+ const { run } = await createChangeRunFromUploads({ before, after }, threshold.value, maxDimension.value, processingMode.value, device.value);
await store.loadChange(true); caseId.value = run.id; beforeFile.value = null; afterFile.value = null; showRunForm.value = false;
} catch (error) { runError.value = error instanceof Error ? error.message : "变化检测运行失败"; }
finally { running.value = false; }
@@ -102,7 +105,7 @@
scanRunning.value = true; scanJobError.value = null; scanJobMessage.value = "正在上传影像并排队...";
try {
const [before, after] = await Promise.all([uploadChangeFile(scanBeforeFile.value, "before"), uploadChangeFile(scanAfterFile.value, "after")]);
- const { job: created } = await createChangeScanFromUploads({ before, after }, scanThresholds.value, effectiveScanMinimumAreas.value, scanMaxDimension.value, scanProcessingMode.value);
+ const { job: created } = await createChangeScanFromUploads({ before, after }, scanThresholds.value, effectiveScanMinimumAreas.value, scanMaxDimension.value, scanProcessingMode.value, device.value);
let job = created;
while (job.status === "queued" || job.status === "running") {
scanJobMessage.value = job.phase === "parameter-scan" ? "模型推理完成,正在扫描参数组合..." : job.phase === "vectorization" ? "参数组合完成,正在生成完整 GeoJSON..." : "正在执行 ChangeStar 推理...";
@@ -146,6 +149,7 @@
<div class="resolution-control"><label for="change-mode">处理模式</label><a-select id="change-mode" :value="processingMode" @update:value="updateProcessingMode"><a-select-option value="auto">自动识别</a-select-option><a-select-option value="image">普通图片</a-select-option><a-select-option value="geotiff">GeoTIFF 地理参考</a-select-option></a-select><span>自动模式会识别带 CRS 的 GeoTIFF;普通图片输出像素坐标,GeoTIFF 模式保留空间参考。</span></div>
<div class="threshold-control"><label for="change-threshold">变化阈值</label><div class="threshold-inputs"><a-slider id="change-threshold" :value="threshold" @update:value="updateThreshold" :min="0.01" :max="0.99" :step="0.01" /><a-input-number :value="threshold" @update:value="updateThreshold" :min="0.01" :max="0.99" :step="0.01" :precision="2" /></div><span>本次运行使用 {{ threshold.toFixed(2) }};默认值为 0.50</span></div>
<div class="resolution-control"><label for="change-resolution">处理分辨率</label><a-select id="change-resolution" :value="maxDimension" @update:value="updateMaxDimension"><a-select-option :value="0">GeoTIFF 原始分辨率 / 自动</a-select-option><a-select-option :value="1024">快速预览 · 1024 px</a-select-option><a-select-option :value="1536">标准 · 1536 px</a-select-option><a-select-option :value="2048">小目标优先 · 2048 px</a-select-option><a-select-option :value="3072">高细节 · 3072 px</a-select-option></a-select><span v-if="maxDimension === 0">GeoTIFF 保持原始像素尺寸;普通图片自动使用 1024 px。</span><span v-else>本次运行使用 {{ maxDimension }} px 长边上限;分辨率越高,耗时和显存/内存占用越大。</span></div>
+ <div class="resolution-control"><label for="change-device">计算设备</label><a-select id="change-device" v-model:value="device"><a-select-option value="auto">自动(可用 GPU 否则 CPU)</a-select-option><a-select-option value="cpu">CPU</a-select-option><a-select-option value="cuda">GPU CUDA</a-select-option></a-select><span>模型推理可使用 GPU;参数扫描会沿用此选择。显式 CUDA 探测失败时不回退。</span></div>
<a-alert v-if="runError" type="error" show-icon :message="runError" />
<a-button type="primary" :loading="running" :disabled="!beforeFile || !afterFile" @click="submitRun"><PlayCircleOutlined />开始检测</a-button>
</section>
@@ -169,13 +173,13 @@
<template v-if="currentCase">
<a-row :gutter="[18, 18]" class="change-workspace">
- <a-col :xs="24" :xl="5"><section class="surface-section run-library"><h2>案例库</h2><a-input-search v-model:value="searchText" placeholder="搜索运行" allow-clear /><a-list size="small" :data-source="filteredCaseOptions"><template #renderItem="{ item }"><a-list-item class="run-item" :class="{ active: item.value === currentCase.id }" @click="caseId = item.value">{{ item.label }}</a-list-item></template></a-list></section></a-col>
+ <a-col :xs="24" :xl="5"><section class="surface-section run-library"><h2>案例库</h2><a-input-search v-model:value="searchText" placeholder="搜索运行" allow-clear /><a-list size="small" :data-source="filteredCaseOptions"><template #renderItem="{ item }"><a-list-item class="run-item" :class="{ active: item.value === currentCase.id }" @click="caseId = item.value"><span class="run-item-label">{{ item.label }}</span><RunDeletionControl capability="00-change-detection" :run-id="item.value" :label="item.label" @removed="removeRun" /></a-list-item></template></a-list></section></a-col>
<a-col :xs="24" :xl="19"><section class="surface-section"><div class="section-heading"><div><h2>双期与栅格结果</h2><p>{{ currentCase.run.input_shape[1] }} x {{ currentCase.run.input_shape[0] }} 像素,处理尺寸 {{ currentCase.run.processed_shape[1] }} x {{ currentCase.run.processed_shape[0] }}</p></div><a-tag color="green">{{ currentCase.run.device }} {{ currentCase.run.elapsed_seconds }} 秒</a-tag></div><div class="change-comparison"><figure><figcaption>第一期原图</figcaption><a-image :src="artifactUrl(currentCase.beforeImage)" /></figure><figure><figcaption>第二期原图</figcaption><a-image :src="artifactUrl(currentCase.rawAfterImage || currentCase.afterImage)" /></figure><figure><figcaption>第二期配准图</figcaption><a-image :src="artifactUrl(currentCase.registeredAfterImage || currentCase.afterImage)" /></figure><figure><figcaption>变化叠加:红色 = ChangeStar,青色 = 视觉差异候选</figcaption><a-image :src="artifactUrl(`${currentCase.artifactRoot}/${currentCase.run.artifacts.overlay}`)" /></figure></div></section></a-col>
</a-row>
<a-row :gutter="[18, 18]" class="metric-row change-metrics"><a-col :xs="12" :lg="6"><a-statistic title="变化像素" :value="currentCase.run.changed_pixels" /></a-col><a-col :xs="12" :lg="6"><a-statistic title="变化比例" :value="changePercent" suffix="%" /></a-col><a-col :xs="12" :lg="6"><a-statistic title="变化图斑" :value="currentCase.run.vector_feature_count" /></a-col><a-col :xs="12" :lg="6"><a-statistic title="配准有效区" :value="validPercent" suffix="%" /></a-col></a-row>
- <a-row :gutter="[18, 18]" class="result-band"><a-col :xs="24" :xl="14"><section class="surface-section"><div class="section-heading"><div><h2>矢量图斑</h2><p>像素坐标结果;红线叠加已换算为图像左上角显示坐标。</p></div><a-tag>{{ currentCase.run.vector_feature_count }} 个</a-tag></div><div class="change-vector"><img :src="artifactUrl(currentCase.afterImage)" alt="第二期影像" /><svg v-if="vectorPaths.length" :viewBox="`0 0 ${currentCase.run.input_shape[1]} ${currentCase.run.input_shape[0]}`" preserveAspectRatio="xMidYMid meet"><path v-for="(path, index) in vectorPaths" :key="index" :d="path" /></svg><a-empty v-else description="当前阈值下没有变化图斑" /></div></section></a-col><a-col :xs="24" :xl="10"><section class="surface-section"><h2>图斑明细</h2><a-table :data-source="currentCase.features.map((item) => item.properties)" :pagination="false" row-key="feature_id" size="small" :scroll="{ x: 480 }"><a-table-column title="编号" data-index="feature_id" key="feature_id" /><a-table-column title="面积 (px)" data-index="area_pixels" key="area_pixels" align="right" /><a-table-column title="平均概率" data-index="mean_probability" key="mean_probability" align="right" /><a-table-column title="最大概率" data-index="max_probability" key="max_probability" align="right" /></a-table><a-divider /><a-descriptions size="small" :column="1"><a-descriptions-item label="模型">{{ currentCase.run.model }}</a-descriptions-item><a-descriptions-item label="阈值">{{ currentCase.run.thresholds.change_probability }}</a-descriptions-item><a-descriptions-item label="模式">{{ currentCase.run.processing_mode === "geotiff" ? "GeoTIFF 地理参考" : "普通图片像素坐标" }}</a-descriptions-item><a-descriptions-item label="配准">{{ currentCase.run.registration.method }} / {{ currentCase.run.registration.inliers }} 内点</a-descriptions-item><a-descriptions-item label="坐标">{{ currentCase.run.georeferenced ? `${currentCase.run.crs} 地图坐标` : "无 CRS,像素坐标" }}</a-descriptions-item></a-descriptions></section></a-col></a-row>
+ <a-row :gutter="[18, 18]" class="result-band"><a-col :xs="24" :xl="14"><section class="surface-section"><div class="section-heading"><div><h2>矢量图斑</h2><p>像素坐标结果;红线叠加已换算为图像左上角显示坐标。</p></div><a-tag>{{ currentCase.run.vector_feature_count }} 个</a-tag></div><div class="change-vector"><img :src="artifactUrl(currentCase.afterImage)" alt="第二期影像" /><svg v-if="vectorPaths.length" :viewBox="`0 0 ${currentCase.run.input_shape[1]} ${currentCase.run.input_shape[0]}`" preserveAspectRatio="xMidYMid meet"><path v-for="(path, index) in vectorPaths" :key="index" :d="path" /></svg><a-empty v-else description="当前阈值下没有变化图斑" /></div></section></a-col><a-col :xs="24" :xl="10"><section class="surface-section"><h2>图斑明细</h2><a-table :data-source="currentCase.features.map((item) => item.properties)" :pagination="false" row-key="feature_id" size="small" :scroll="{ x: 480 }"><a-table-column title="编号" data-index="feature_id" key="feature_id" /><a-table-column title="面积 (px)" data-index="area_pixels" key="area_pixels" align="right" /><a-table-column title="平均概率" data-index="mean_probability" key="mean_probability" align="right" /><a-table-column title="最大概率" data-index="max_probability" key="max_probability" align="right" /></a-table><a-divider /><a-descriptions size="small" :column="1"><a-descriptions-item label="模型">{{ currentCase.run.model }}</a-descriptions-item><a-descriptions-item label="请求 / 实际设备">{{ currentCase.run.requested_device ?? "既有记录" }} / {{ currentCase.run.device }}</a-descriptions-item><a-descriptions-item v-if="currentCase.run.execution?.fallback_used" label="CPU 回退原因">{{ currentCase.run.execution.fallback_reason }}</a-descriptions-item><a-descriptions-item label="阈值">{{ currentCase.run.thresholds.change_probability }}</a-descriptions-item><a-descriptions-item label="模式">{{ currentCase.run.processing_mode === "geotiff" ? "GeoTIFF 地理参考" : "普通图片像素坐标" }}</a-descriptions-item><a-descriptions-item label="配准">{{ currentCase.run.registration.method }} / {{ currentCase.run.registration.inliers }} 内点</a-descriptions-item><a-descriptions-item label="坐标">{{ currentCase.run.georeferenced ? `${currentCase.run.crs} 地图坐标` : "无 CRS,像素坐标" }}</a-descriptions-item></a-descriptions></section></a-col></a-row>
<section class="surface-section result-files change-files"><a-space wrap><a-button :href="artifactUrl(`${currentCase.artifactRoot}/${currentCase.run.artifacts.probability_raster}`)" download><DownloadOutlined />概率 GeoTIFF</a-button><a-button :href="artifactUrl(`${currentCase.artifactRoot}/${currentCase.run.artifacts.mask_raster}`)" download><FileImageOutlined />ChangeStar 变化栅格</a-button><a-button v-if="currentCase.run.artifacts.generic_difference_mask" :href="artifactUrl(`${currentCase.artifactRoot}/${currentCase.run.artifacts.generic_difference_mask}`)" download><FileImageOutlined />视觉差异候选栅格</a-button><a-button type="primary" v-if="currentCase.run.artifacts.rectangle_vector" :href="artifactUrl(`${currentCase.artifactRoot}/${currentCase.run.artifacts.rectangle_vector}`)" download><DownloadOutlined />规则四边形</a-button><a-button v-if="currentCase.run.artifacts.rectangle_vector_wgs84" :href="artifactUrl(`${currentCase.artifactRoot}/${currentCase.run.artifacts.rectangle_vector_wgs84}`)" download><DownloadOutlined />经纬度 GeoJSON</a-button><a-button :href="artifactUrl(`${currentCase.artifactRoot}/${currentCase.run.artifacts.vector}`)" download><FileOutlined />原始图斑 GeoJSON</a-button><a-button :href="artifactUrl(`${currentCase.artifactRoot}/run_metadata.json`)" download><FileOutlined />运行元数据</a-button></a-space></section>
<a-alert class="change-limit" type="warning" show-icon message="当前近景边坡样本不在 ChangeStar 建筑变化权重的验证分布内;结果只能用于工作流与人工复核,不能直接形成工程结论。" />
--
Gitblit v1.9.3