人体模型部位分割:从 SCHP平面解析 映射到 3D 模型

对人体扫描网格做语义部位分割,常见做法是直接在三维点云或网格上跑分割网络------我试过很多次,效果都不太理想。 最终发现:先把模型「切」成平面渲染图,用成熟的 SCHP 做二维人体解析,再把标签沿视线反射回网格, 稳定、可控。

工程实现:HelixToolkit + SharpDX · SCHP-Pascal-7 ONNX


1. 先 2D 分割,再映射 3D

人体扫描网格(结构光、多视角重建、点云融合)通常具有不规则拓扑、噪声、孔洞、非标准姿态。 若在此类数据上直接做三维语义分割,网络需要同时解决几何噪声、采样密度不均、类别边界模糊三个难题。

相比之下,把网格渲染成与视口一致的 RGB 快照后,问题退化为成熟的「人体解析(Human Parsing)」: 输入是规则图像,SCHP 等模型在 Pascal-Person-Part 等数据集上已充分训练,对头/躯干/四肢的边界极其稳定------ 即便图像分辨率有限、光照不理想,分割质量仍远好于我们在 mesh 上试过的多种方案。

发现窍门: 「部位是什么」交给 2D;「这个部位在模型空间的哪里」交给相机投影 + 标签反射


2. 三维上踩的坑

部位分割直接相关的结论如下:

尝试思路 分割结论
直接在 3D 网格/参数化模型上定位部位 边界抖动大,肩背、腋下错分严重
另一种 3D 标签传播策略 单视角不稳定,融合后仍碎块多
纯二维 SCHP 实验,不涉及映射 验证 SCHP 分割质量极好
引入 SCHP + 单视角反射 可行,但背面与侧面标签缺失
多视角融合 + 穿透补标 + 拓扑后处理 最建议的方案

开源 SMPL 模板顶点索引、3D human parsing 预训练权重也试过------在标准 T-pose 模型上尚可, 一旦换成真实扫描网格,肩线、腰线、臀线对应的顶点簇与视觉语义严重错位。


3. 第一步:从 3D 模型切出平面图片

「切图」并非简单截图------必须保证后续映射时相机外参与截图时刻完全一致 。 人体网格挂载于可旋转的转盘 Transform 上,相机固定; 每次 SCHP 推理前会保存 ViewSnapshot(相机位置、朝向等)。

3.1 多视角采集

「多角度 2D SCHP」流程按预定步进角旋转模型,逐帧渲染视口并送入 ONNX。 每一帧产出:RGB 渲染图、512×512 标签图、对应 ViewState。 这些帧存入 _parsedFrames,供单帧「结果反射」或多帧「加权融合」使用。

3.2 前景掩码

视口底色为纯色(如红底), 按与背景色的色差提取前景,避免把阴影误判为背景的同时,尽量保留暗部身体区域。 SCHP 输入还可做对称扩边(默认四边各扩若干像素),减轻缩放到 512 时的边缘截断效应。

3.3 正交投影导出(为部位点集服务)

分割完成后,按模型包围盒生成两张正交投影图(720×560):

  • 正面图:X--Y 平面,每一列取最靠近观察者的标签(画家算法)
  • 侧面图:Z--Y 平面,同样按深度取可见标签

导出前会对标签做透视,确保侧面图上前缘与后缘都有像素,让 2D 映射结果在投影平面上可读。


4. 第二步:SCHP 二维人体解析

推理管线:Schp → ONNX Runtime → argmax 得逐像素类别 → BlendOverlay 与原图混合供人眼检查。

单帧 SCHP 在当前视角可见表面 上几乎完美;但扫描网格是闭合流形,单帧无法覆盖背侧。 因此 SCHP 输出本身不是终点------它是多视角融合的原材料

融合前质检:

并非每一帧都参与融合。过滤器会剔除明显错误的分割,例如:

  • 前景面积过小(几乎没拍到人)
  • 可见身体类少于 5/6(关键部位大面积缺失)
  • 图像顶部条带以「小腿」为主且头部极少(纵向逻辑崩溃)
  • 中间条带「大腿」占比过高而躯干极少(躯干被腿类霸占)

被否决的帧仍可在 UI 中浏览,但不写入 voteScores,避免污染全局标签。


5. 第三步:结果反射------标签穿透映射到顶点

「结果反射」是单视角下最直接的理解方式:点击 Form1 的「结果反射」按钮, 系统回放该帧保存的 ViewState,调用 ApplySchpLabelReflection

5.1 投影采样规则

对每个网格顶点:

  1. 局部坐标 → 转盘 Transform → 世界坐标
  2. View.Project(world) 得屏幕像素 (sx, sy)
  3. 按 SCHP 标签图分辨率缩放为 (lx, ly),读取 labels[ly * labelW + lx]
  4. 不做法线朝向过滤 ------同一屏幕坐标上的前后表面顶点取相同 2D 标签

第 4 点是有意为之:人体是薄壳,前后投影重叠区域应语义一致;旋转模型后,背面顶点色应与 SCHP 截面图对齐。

核心代码片段:单视角反射

ini 复制代码
// SmplViewport.ApplySchpLabelReflection --- 简化示意
for (int i = 0; i < vertexCount; i++)
{
    var world = meshTransform.Transform(localPosition[i]);
    var sp = viewport.Project(world);

    int lx = (int)(sp.X * labelW / viewportWidth);
    int ly = (int)(sp.Y * labelH / viewportHeight);
    perVertexLab[i] = labels[Clamp(lx, ly)];  // 沿视线穿透,不区分正反面
}

FillParallelPenetrationAlongDepth(perVertexLab, positions);  // 柱穿透补标
RefineSmallLabelIslands(perVertexLab, adjacency);          // 拓扑离岛抑制
if (fillNearest) FillUnclassifiedByNearestRegion(...);     // 可选:未投影区扩散

6. 第四步:多视角加权融合

单视角只能可靠覆盖朝向相机的一半表面。多角度融合为每个顶点维护一个长度为 7 的得分向量 voteScores[vertex, class],每接受一帧便累加权重。

6.1 权重设计

AccumulateWeightedSchpVotesForPrimaryMesh 中:

  • 权重 ∝ max(0, n · v)p --- 法线越朝向相机,该视角标签越可信
  • 背景类额外乘以 backgroundVoteScale ≈ 0.38,降低「背景误投到网格」的影响
  • 低于 minFacingDot 的顶点在该帧不计票(侧对相机时本就看不清)

融合结束后对每个顶点取 argmax 得最终 byte 标签 fused[i]

6.2 手动采样融合

除自动转盘批处理外,还支持操作员手动旋转到「正面 / 侧面 / 背侧」等关键角度, 点击采样加入 _manualFusionSamples,再一键「从采样融合」------ 适合姿态特殊、自动步进角覆盖不足的情况。

核心代码片段:加权投票

ini 复制代码
// 每个顶点、每一帧 SCHP
float facing = Dot(normalize(vertexNormal), normalize(cameraPos - vertexPos));
if (facing < minFacingDot) continue;

float w = Pow(Max(0, facing), normalDotPower);
byte label = SampleSchpLabel(projectedPixel);
if (label == 0) w *= backgroundVoteScale;

voteScores[vertexIndex, label] += w;

// 全部帧结束后
fused[vertexIndex] = ArgMax(voteScores[vertexIndex, *]);

7. 第五步:网格后处理与补全

融合或单帧反射得到的 per-vertex 标签,会统一经过三道后处理。

7.1 正交穿透补标

将顶点按 (X,Y) 量化到柱体(默认 4096 bins),同一柱内所有顶点共享该柱已标签顶点的多数票类别。 作用:多角度融合仅对朝向相机的面加权时,背侧顶点常无票;柱穿透把「前侧已知标签」沿深度 Z 传播到后侧。

7.2 小连通域离岛抑制

在三角网格邻接图上,对每一语义类做连通分量分析。小于阈值的分量(默认 ≥ max(72, 顶点数/900))视为 「2D 误分经投影形成的曲面碎块」------典型如肩背处被错标为头部的粉红色小岛------ 整体改标为环邻域中出现最多的其它类别。默认仅处理头部(1) ,避免误删合理的手臂/腿细条。

7.3 未分类点就近归类

投影区外、或始终未被任何帧看到的顶点 label=0。多源 BFS 从已分类顶点向外扩散, 按拓扑距离赋最近有效语义------我们也可启用「未着色点就近归类」。

核心代码片段:柱穿透补标

scss 复制代码
// 同一 (Qx(X), Qy(Y)) 柱内统计类别直方图
long columnKey = Pack(Qx(pos.X), Qy(pos.Y));
columnVotes[columnKey][label]++;

// 柱内每个顶点(含背面)继承该柱多数票
for (int i = 0; i < vertexCount; i++)
{
    long key = Pack(Qx(pos[i].X), Qy(pos[i].Y));
    labels[i] = ArgMax(columnVotes[key]);  // 背面与前面共享语义
}

8. 第六步:正/侧面部位点云导出

顶点级标签就绪后,系统将其再投影 到两张正交平面,形成结构化的 PartPlanePoints 集合------这是第一期与第二期之间的数据接口

8.1 PartPlanePoints 结构

每个 SCHP 部位(头、躯干、上臂...)对应一条记录:

  • FrontPlanePixels[] --- 正面图 X--Y 上该部位所有可见顶点的像素坐标
  • SidePlanePixels[] --- 侧面图 Z--Y 上的对应像素坐标

坐标系与 PNG 一致:X 向右,Y 向下,(0,0) 为左上角。

8.2 PartPlanePointGroups

在正面图上,上臂/大腿按躯干 X 中心拆分为解剖学左/右(图像右侧 = 人体左侧), 供肩-臂连接点、左右上腿范围等几何规则使用------细节见第二期,但点集本身来自第一期的分割质量。


9. 3D 可视化:让分割「长在模型上」

最直观的效果呈现,是带 SCHP 顶点色的旋转人体模型。 将七类标签转为顶点 diffuse 色, 在 HelixToolkit 视口中实时渲染;可从任意角度检查分割。


10. 不同模型与边界情况

场景 分割风险 应对
肥胖 / 宽松衣物 躯干与上臂粘连 多视角融合 + 离岛抑制;手动补采样侧面
消瘦 / 含胸 躯干面积过小被质检拒绝 放宽 SchpMultiViewFusionFrameFilter 或手动融合
非 A-pose 抬臂 上臂遮挡躯干 增加背侧视角帧;柱穿透补全背面
扫描孔洞 / 非流形 BFS 扩散越界 关闭就近归类或降低网格简化程度
长发 / 配饰 头部类向外扩张 头部离岛抑制;第二期颈围算法单独处理


11. 小结

整理流程概括为:

3D 渲染切图→SCHP 2D 解析→视线反射 / 多视角融合→柱穿透 + 拓扑后处理→正/侧面部位点集→3D 顶点色可视化。

基于以上的比较稳定的部位分割,我们就可以进行身体不同部位的测量工作。在侧面轮廓上用手工可控的特征点算法定位颈/胸/腰/臀测量平面, 在 3D 网格上截取倾斜截面并计算围度。我们将在下篇尝试进行3D部位测量处理。

相关推荐
董员外1 小时前
RAG 系统进化论(五):Corrective RAG 与 Self-RAG,让系统发现并纠正错误
人工智能·后端·设计模式
暗黑小白1 小时前
路由策略与引擎可替换性
后端·python·ai agent
zzzll11113 小时前
SpringBoot 入门与实践指南
java·spring boot·后端
newerp3 小时前
Go :结构体嵌入、sort.Interface 与 io.Reader/Writer
后端
SamDeepThinking3 小时前
如何维持缓存的一致性?
后端·面试·程序员
泡海椒3 小时前
还在手动把 Postman 和浏览器的 curl 转成 Java 代码?这个框架让你一键粘贴直接用
后端
用户8181870627463 小时前
第13章 死锁诊断实战
后端
晨曦中的暮雨3 小时前
Redis 有哪些常见数据结构?它们的底层实现和使用场景是什么?
java·后端·八股
枫叶V3 小时前
Prompt Injection 防不住怎么办?从 Source-Sink 模型设计 Agent 安全边界
后端·agent