Дано
[point_cloud.ply](<https://prod-files-secure.s3.us-west-2.amazonaws.com/05dec818-e1ed-4b8f-8983-84fb0682f601/2d2b5235-0091-48d0-aa4e-7f73b0390fff/point_cloud.ply>)
Если координаты правильно преобразовать, то получится что-то такое (возможно это другая голова, но смысл такой). Этот файл можно открыть в сплат вьюерах.
Код на питоне для пересчета
import torch
def dot(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
return torch.sum(x * y, -1, keepdim=True)
def length(x: torch.Tensor, eps: float = 1e-20) -> torch.Tensor:
return torch.sqrt(torch.clamp(dot(x, x), min=eps)) # Clamp to avoid nan gradients because grad(sqrt(0)) = NaN
def safe_normalize(x: torch.Tensor, eps: float = 1e-20) -> torch.Tensor:
return x / length(x, eps)
def compute_face_orientation(verts, faces, return_scale=False):
i0 = faces[..., 0].long()
i1 = faces[..., 1].long()
i2 = faces[..., 2].long()
v0 = verts[..., i0, :]
v1 = verts[..., i1, :]
v2 = verts[..., i2, :]
# basis for local coord system
a0 = safe_normalize(v1 - v0)
a1 = safe_normalize(torch.cross(a0, v2 - v0))
a2 = -safe_normalize(torch.cross(a1, a0)) # will have artifacts without negation
orientation = torch.cat([a0[..., None], a1[..., None], a2[..., None]], dim=-1)
if return_scale:
s0 = length(v1 - v0)
s1 = dot(a2, (v2 - v0)).abs()
scale = (s0 + s1) / 2
return orientation, scale
def get_face_centers(verts, face_ids):
triangles = verts[:, faces]
# just average between 3 verts
return triangles.mean(dim=-2)
def rel_to_abs(rel_xyz, face_binding, face_orien_mat, face_scaling, face_centers):
# rotate, bmm=batch mat mul
xyz = torch.bmm( face_orien_mat[face_binding], rel_xyz )
# scale and translate
return xyz * face_scaling[face_binding] + face_centers[face_binding]
ply = read_ply("path/to/ply") # splat
glb = read_glb("path/to/glb") # reference tringle mesh
rel_xyz = ply[['x','y','z']]
face_binding = ply['binding_0']
# prepare
face_centers = get_face_centers(glb.verts, glb.face_ids)
face_orien_mat, face_scaling = compute_face_orientation(glb.verts, glb.faces, return_scale=True)
face_orien_quat = quat_xyzw_to_wxyz(rotmat_to_unitquat(self.face_orien_mat) )
# 1. Now we are ready to compute absolute coordinates of the splats
abs_xyz = rel_to_abs(rel_xyz, face_binding, face_orien_mat, face_scaling, face_centers)
# 2. Recompute scale
rel_scale = ply[['scale_0', 'scale_1', 'scale_2']]
abs_scale= math.exp(rel_scale) * face_scaling[face_binding]
# 3. Recompute splat SH rotation
rel_rotation = ply[['rot_0', 'rot_1', 'rot_2','rot_3' ]] # quaternion, looks like it is in wxyz format
rel_rotation = normalize(rel_rotation)
face_quat = normalize(face_orien_quat[face_binding])
abs_rotation = quat_xyzw_to_wxyz(quat_product(quat_wxyz_to_xyzw(rel_rotation), quat_wxyz_to_xyzw(face_orien_quat)))
если abs_* переменные записать обратно в ply вместо отностительных, то получится обычный ply файл который откроет любой вьюер