增加几何校验
This commit is contained in:
+54
-4
@@ -3,6 +3,7 @@ import path from 'node:path';
|
||||
import {projectRoot} from './config.mjs';
|
||||
|
||||
export const partsSchema = JSON.parse(readFileSync(path.join(projectRoot, 'schemas', 'semantic-parts.schema.json'), 'utf8'));
|
||||
const legacySchema = JSON.parse(readFileSync(path.join(projectRoot, 'schemas', 'semantic-parts-v1.schema.json'), 'utf8'));
|
||||
|
||||
// Validate the subset of JSON Schema used by the checked-in contract. Keeping
|
||||
// the validation driven by that contract avoids two independent field lists.
|
||||
@@ -46,13 +47,16 @@ export function parseParts(text) {
|
||||
export function validateParts(data) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
validate(data, partsSchema, '$', errors);
|
||||
const legacy = data?.schema_version === 1;
|
||||
validate(data, legacy ? legacySchema : partsSchema, '$', errors);
|
||||
if (errors.length) return {valid: false, errors, warnings};
|
||||
const ids = new Set(data.parts.map((part) => part.id));
|
||||
const byId = new Map(data.parts.map((part) => [part.id, part]));
|
||||
if (legacy) warnings.push('旧版计划:rig_role、bind_to 和 completion 将按保守规则迁移,需复核');
|
||||
if (ids.size !== data.parts.length) errors.push('部件 ID 重复');
|
||||
for (const part of data.parts) {
|
||||
if (part.id === 'root') errors.push('root 为后续导出保留 ID');
|
||||
for (const ref of [part.parent, ...part.occluded_by].filter((item) => item !== null)) {
|
||||
for (const ref of [part.parent, ...part.occluded_by, ...(!legacy ? [part.bind_to] : [])].filter((item) => item !== null)) {
|
||||
if (!ids.has(ref) || ref === part.id) errors.push(`${part.id}: 引用了不存在的部件或自身 ${ref}`);
|
||||
}
|
||||
if (new Set(part.occluded_by).size !== part.occluded_by.length) errors.push(`${part.id}: occluded_by 重复`);
|
||||
@@ -62,11 +66,19 @@ export function validateParts(data) {
|
||||
warnings.push(`${part.id}: 完全隐藏部件需要人工确认,不能直接生成可见 mask`);
|
||||
} else {
|
||||
if (!box || box[0] >= box[2] || box[1] >= box[3]) errors.push(`${part.id}: 可见 bbox 必须具有正面积`);
|
||||
if (!part.positive_points_norm.length) errors.push(`${part.id}: 可见部件需要至少一个正点`);
|
||||
if (box) for (const [x, y] of part.positive_points_norm) {
|
||||
if (x < box[0] || x > box[2] || y < box[1] || y > box[3]) errors.push(`${part.id}: 正点不在可见框内`);
|
||||
if (x < box[0] || x > box[2] || y < box[1] || y > box[3]) warnings.push(`${part.id}: 模型正点不在可见框内,将丢弃并重新生成候选`);
|
||||
}
|
||||
}
|
||||
if (part.positive_points_norm.length || part.negative_points_norm.length) warnings.push(`${part.id}: 模型提示点仅用于诊断,不直接传给 SAM`);
|
||||
if (!legacy) {
|
||||
if (part.rig_role === 'bone_part' && part.bind_to !== null) errors.push(`${part.id}: bone_part 使用 parent,bind_to 必须为 null`);
|
||||
if (part.bind_to !== null && byId.get(part.bind_to)?.rig_role !== 'bone_part') errors.push(`${part.id}: bind_to 必须引用 bone_part`);
|
||||
if (part.rig_role === 'bone_part' && part.parent !== null && byId.get(part.parent)?.rig_role !== 'bone_part') errors.push(`${part.id}: bone_part 的 parent 必须引用 bone_part`);
|
||||
if (part.needs_completion !== Object.values(part.completion).some(Boolean)) errors.push(`${part.id}: needs_completion 必须等于 completion 各项的逻辑或`);
|
||||
if (part.visibility !== 'visible' && !part.completion.occlusion_fill) errors.push(`${part.id}: partial/hidden 部件需要 occlusion_fill`);
|
||||
if (part.rig_role !== 'bone_part' && (part.completion.joint_extension_parent || part.completion.joint_extension_child)) errors.push(`${part.id}: 附件/面部特征不应声明独立骨骼关节延展`);
|
||||
}
|
||||
if (part.confidence < 0.6) warnings.push(`${part.id}: 模型自报低置信度,需要复核`);
|
||||
}
|
||||
if (!errors.length) {
|
||||
@@ -89,3 +101,41 @@ export function validateParts(data) {
|
||||
}
|
||||
return {valid: errors.length === 0, errors, warnings};
|
||||
}
|
||||
|
||||
// Legacy results remain readable. These inferred rig/completion decisions are
|
||||
// explicit review candidates, not a claim that the model supplied them.
|
||||
export function upgradeParts(data) {
|
||||
if (data.schema_version !== 1) return structuredClone(data);
|
||||
const result = structuredClone(data);
|
||||
result.schema_version = 2;
|
||||
const byId = new Map(result.parts.map((part) => [part.id, part]));
|
||||
for (const part of result.parts) {
|
||||
part.rig_role = /^(eye|eyes|eyebrow|nose|mouth|mustache)(_|$)/u.test(part.id) ? 'facial_feature'
|
||||
: /^(pelvis|torso|neck|head|upper_arm|forearm|hand|thigh|shin|foot)(_|$)/u.test(part.id) ? 'bone_part' : 'attachment';
|
||||
}
|
||||
const nearestBone = (id) => {
|
||||
const seen = new Set();
|
||||
while (id && !seen.has(id)) {
|
||||
seen.add(id);
|
||||
const part = byId.get(id);
|
||||
if (!part) return null;
|
||||
if (part.rig_role === 'bone_part') return id;
|
||||
id = part.parent;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
for (const part of result.parts) {
|
||||
const sleeveBone = part.id.replace(/^sleeve_/u, 'upper_arm_');
|
||||
part.bind_to = part.rig_role === 'bone_part' ? null
|
||||
: part.id.startsWith('sleeve_') && byId.get(sleeveBone)?.rig_role === 'bone_part' ? sleeveBone : nearestBone(part.parent);
|
||||
if (part.rig_role === 'bone_part') part.parent = nearestBone(part.parent);
|
||||
part.completion = {
|
||||
occlusion_fill: part.visibility !== 'visible' || part.needs_completion,
|
||||
joint_extension_parent: part.rig_role === 'bone_part' && part.parent !== null,
|
||||
joint_extension_child: part.rig_role === 'bone_part' && result.parts.some((child) => child.rig_role === 'bone_part' && nearestBone(child.parent) === part.id),
|
||||
};
|
||||
part.needs_completion = Object.values(part.completion).some(Boolean);
|
||||
}
|
||||
result.notes.push('Migrated from schema v1: rig roles/bindings and completion are heuristic review candidates. Original model points are diagnostic only; use segmentation-prompts.json for geometry candidates.');
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user