保持传播多区域结果为单个遮罩
- 后端传播落库时将同一 seed 在同一目标帧的多个不连通 polygon 保存到同一 annotation - 同步任务传播和兼容同步传播接口的多 polygon 保存逻辑 - 传播结果 bbox 改为覆盖全部不连通 polygon,并保留多 polygon scores 与 holes - 前端回显单条多 polygon annotation 时使用组合 bbox 和真实 polygon 面积 - 补充后端传播 worker 回归测试,验证不连通结果只生成一个 annotation - 补充前端 API 回归测试,验证多 polygon annotation 回显为一个 mask - 更新项目指南和设计冻结文档
This commit is contained in:
@@ -693,6 +693,42 @@ describe('api client contracts', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
it('restores disconnected polygons from one saved annotation as one mask with a combined bbox', async () => {
|
||||
const { annotationToMask } = await import('./api');
|
||||
const frame = { id: '5', projectId: '9', index: 0, url: '/frame.jpg', width: 100, height: 100 };
|
||||
|
||||
const hydrated = annotationToMask({
|
||||
id: 44,
|
||||
project_id: 9,
|
||||
frame_id: 5,
|
||||
template_id: null,
|
||||
mask_data: {
|
||||
polygons: [
|
||||
[[0.1, 0.1], [0.2, 0.1], [0.2, 0.2], [0.1, 0.2]],
|
||||
[[0.7, 0.7], [0.9, 0.7], [0.9, 0.9], [0.7, 0.9]],
|
||||
],
|
||||
label: '多区域',
|
||||
color: '#22c55e',
|
||||
source: 'sam2.1_hiera_tiny_propagation',
|
||||
},
|
||||
points: null,
|
||||
bbox: null,
|
||||
created_at: 'created',
|
||||
updated_at: 'updated',
|
||||
}, frame);
|
||||
|
||||
expect(hydrated).toEqual(expect.objectContaining({
|
||||
id: 'annotation-44',
|
||||
pathData: 'M 10 10 L 20 10 L 20 20 L 10 20 Z M 70 70 L 90 70 L 90 90 L 70 90 Z',
|
||||
segmentation: [
|
||||
[10, 10, 20, 10, 20, 20, 10, 20],
|
||||
[70, 70, 90, 70, 90, 90, 70, 90],
|
||||
],
|
||||
bbox: [10, 10, 80, 80],
|
||||
area: 500,
|
||||
}));
|
||||
});
|
||||
|
||||
it('preserves propagation metadata when saving edited geometry without persisting preview-only smoothing fields', async () => {
|
||||
const { buildAnnotationPayload } = await import('./api');
|
||||
const frame = { id: '5', projectId: '9', index: 0, url: '/frame.jpg', width: 100, height: 50 };
|
||||
|
||||
@@ -592,6 +592,12 @@ function polygonToBbox(points: number[][], width: number, height: number): [numb
|
||||
return [minX, minY, maxX - minX, maxY - minY];
|
||||
}
|
||||
|
||||
function polygonsToBbox(polygons: number[][][], width: number, height: number): [number, number, number, number] {
|
||||
const points = polygons.flat();
|
||||
if (points.length === 0) return [0, 0, 0, 0];
|
||||
return polygonToBbox(points, width, height);
|
||||
}
|
||||
|
||||
function polygonAreaPixels(points: number[][], width: number, height: number): number {
|
||||
if (points.length < 3) return 0;
|
||||
let total = 0;
|
||||
@@ -803,7 +809,7 @@ export function annotationToMask(annotation: SavedAnnotation, frame: Frame): Mas
|
||||
const segmentationPolygons = mergedGeometry.segmentationPolygons;
|
||||
const firstPolygon = segmentationPolygons[0];
|
||||
if (!firstPolygon || firstPolygon.length === 0) return null;
|
||||
const bbox = polygonToBbox(firstPolygon, frame.width, frame.height);
|
||||
const bbox = polygonsToBbox(segmentationPolygons, frame.width, frame.height);
|
||||
const classMetadata = annotation.mask_data?.class;
|
||||
const { polygons: _polygons, holes: _holes, label: _label, color: _color, class: _classMetadata, ...metadata } = annotation.mask_data || {};
|
||||
const restoredMetadata = {
|
||||
@@ -828,7 +834,7 @@ export function annotationToMask(annotation: SavedAnnotation, frame: Frame): Mas
|
||||
segmentation: segmentationPolygons.map((polygon) => polygon.flatMap(([x, y]) => [x * frame.width, y * frame.height])),
|
||||
points: annotation.points?.map(([x, y]) => [x * frame.width, y * frame.height]),
|
||||
bbox,
|
||||
area: bbox[2] * bbox[3],
|
||||
area: segmentationPolygons.reduce((total, polygon) => total + polygonAreaPixels(polygon, frame.width, frame.height), 0),
|
||||
metadata: hasMetadata ? restoredMetadata : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user