☰
PartLLM 3d分割部署踩坑笔记
2026/10/30 2:45:20 网站建设 项目流程

目录

PartLLM简介

环境搭建:

aabb 提示词:

推理加速:

1. 把 VLLM_MAX_MODEL_LEN_DEFAULT 降到实际任务需要的水平

归一化:

结果box可视化:

服务封装:

客户端调用:


PartLLM简介

PartLLM 是一个统一的多模态 3D 部件分割框架,核心思路是把文本引导分割、交互式点击分割、完整物体语义分解这三类任务,统一建模为“由用户意图驱动的自回归语义分解”问题

模型下载:

hf download Czvvd/PartLLM --local-dir ./checkpoints/partllm export MODEL_PATH=$PWD/checkpoints/partllm

环境搭建:

这个代码不是很全

pip install libigl pip install spconv-cu120 pip install torch-scatter -f https://data.pyg.org/whl/torch-2.8.0+cu128.html pip install ray pip install transformers==4.57.6 tensordict pip install tensordict==0.14.3 --no-deps pip install "pyvers>=0.2,<0.3"

aabb 提示词:

提示词是 "head",     "torso",        "arm",        "leg", 我想获取 和glb尺寸一致的各部分aabb框  arm 和leg 一般都是两个,如果 两个arm、leg之间么有相互重叠,接触 返回两个 aabbbox

推理加速:

1. 把VLLM_MAX_MODEL_LEN_DEFAULT降到实际任务需要的水平

当前32768是 32K 上下文。PartLLM 的零件分割推理,输入是网格描述 + 少量零件名称,实际 prompt 长度通常远小于这个值。vLLM 的 KV Cache 池大小直接受max_model_len约束,降低它会线性减少 KV Cache 的显存需求。建议:

  • 先用实际请求在 tokenizer 上统计 prompt 长度的 P99,然后把max_model_len设为该值的1.5~2 倍,给输出留余量。例如 P99 是 2K,可以设4096或8192。

归一化:

根据你上传的代码,prepare_geometry_from_glb() 中明确记录了原始模型到归一化模型的变换:

source_scale = 1.0 / source_diag if source_diag > 0 else 1.0
source_shift = -((source_bbox_min + source_bbox_max) * source_scale / 2.0)
mesh_list = normalize_meshes_diag(mesh_list, norm_diag_len=1.0)

结果box可视化:

import json import numpy as np import trimesh GLB_PATH = r"E:\pro_math\math_image\3d_print\chai_person\glb_part\0715_1649_0.glb" JSON_STR = r""" { "model_id": "mesh", "prompt_mode": "text_guided", "granularity": "semantic", "user_prompt": "Please segment all the head; torso; left arm; right arm; left leg; right leg in <point_cloud>.", "num_parts_predicted": 6, "response": "{\"bbox_aabb\": [-0.243, 0.023, -0.198, 0.243, 0.364, 0.198], \"label\": \"Head\"}<|SEG|>\n{\"bbox_aabb\": [-0.14, -0.201, -0.134, 0.14, 0.028, 0.122], \"label\": \"Torso\"}<|SEG|>\n{\"bbox_aabb\": [-0.28, -0.234, -0.072, -0.091, 0.02, 0.06], \"label\": \"Left Arm\"}<|SEG|>\n{\"bbox_aabb\": [0.091, -0.234, -0.072, 0.28, 0.02, 0.06], \"label\": \"Right Arm\"}<|SEG|>\n{\"bbox_aabb\": [-0.184, -0.364, -0.097, -0.024, -0.145, 0.106], \"label\": \"Left Leg\"}<|SEG|>\n{\"bbox_aabb\": [0.013, -0.364, -0.097, 0.18, -0.145, 0.106], \"label\": \"Right Leg\"}<|SEG|>\n<|BG|>", "mesh_space": "normalized_model_space", "metrics": { "num_parts_predicted": 6 } } """ # ========== 1. 读取 GLB,计算归一化参数 ========== def load_meshes_and_norm(glb_path): """读取 GLB,返回有效网格列表和归一化参数(scale / shift)。""" scene = trimesh.load(glb_path, force="scene") meshes = [ g for g in scene.dump() if isinstance(g, trimesh.Trimesh) and len(g.vertices) > 0 ] if not meshes: raise RuntimeError("GLB 中没有有效网格") vertices = np.vstack([np.asarray(m.vertices, dtype=np.float32) for m in meshes]) bbox_min = vertices.min(axis=0) bbox_max = vertices.max(axis=0) diag = float(np.linalg.norm(bbox_max - bbox_min)) scale = 1.0 / diag if diag > 0 else 1.0 shift = -(bbox_min + bbox_max) * scale / 2.0 return meshes, bbox_min, bbox_max, scale, shift # ========== 2. 根据 JSON 获取 AABB(还原到原始坐标) ========== def get_aabbs_from_json(json_str, scale, shift): """ 解析 JSON 字符串中的 response,把归一化空间 bbox_aabb 还原到原始坐标。 参数: json_str: 含 response 的 JSON 字符串 scale: 归一化 scale(1 / 原始包围盒对角线) shift: 归一化平移量 返回: results: [{"label": str, "bbox_aabb_original": [minx,miny,minz,maxx,maxy,maxz]}, ...] """ data = json.loads(json_str) response = data["response"] shift = np.asarray(shift, dtype=np.float64) scale = float(scale) results = [] for line in response.split("<|SEG|>"): line = line.strip() if not line.startswith("{"): continue item = json.loads(line) bbox = np.asarray(item["bbox_aabb"], dtype=np.float64) norm_min, norm_max = bbox[:3], bbox[3:] corners = np.array([ [x, y, z] for x in (norm_min[0], norm_max[0]) for y in (norm_min[1], norm_max[1]) for z in (norm_min[2], norm_max[2]) ]) original_corners = (corners - shift) / scale original_min = original_corners.min(axis=0) original_max = original_corners.max(axis=0) results.append({ "label": item["label"], "bbox_aabb_original": [*original_min.tolist(), *original_max.tolist()], }) return results # ========== 3. 线框立方体 ========== def make_wireframe_box(mn, mx, radius): """用 12 根细圆柱拼出一个线框立方体。""" mn = np.asarray(mn, dtype=np.float64) mx = np.asarray(mx, dtype=np.float64) corners = np.array([ [x, y, z] for x in (mn[0], mx[0]) for y in (mn[1], mx[1]) for z in (mn[2], mx[2]) ]) # 12 条棱的端点索引(立方体 8 顶点) edges = [ (0, 1), (0, 2), (0, 4), (1, 3), (1, 5), (2, 3), (2, 6), (3, 7), (4, 5), (4, 6), (5, 7), (6, 7), ] parts = [] for a, b in edges: p0, p1 = corners[a], corners[b] seg = p1 - p0 length = float(np.linalg.norm(seg)) if length < 1e-9: continue cyl = trimesh.creation.cylinder(radius=radius, height=length, sections=8) # 把圆柱从 +Z 方向旋到 seg 方向 z = np.array([0.0, 0.0, 1.0]) direction = seg / length if np.allclose(direction, z): rot = np.eye(4) elif np.allclose(direction, -z): rot = trimesh.transformations.rotation_matrix(np.pi, [1, 0, 0]) else: axis = np.cross(z, direction) axis /= np.linalg.norm(axis) angle = np.arccos(np.clip(np.dot(z, direction), -1.0, 1.0)) rot = trimesh.transformations.rotation_matrix(angle, axis) cyl.apply_transform(rot) cyl.apply_translation((p0 + p1) / 2.0) parts.append(cyl) return trimesh.util.concatenate(parts) # ========== 主流程 ========== if __name__ == "__main__": # 1. 读取 GLB meshes, bbox_min, bbox_max, source_scale, source_shift = load_meshes_and_norm(GLB_PATH) source_diag

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询