aq3d-instance-segmentation / superpoints.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
9550667 verified
Raw
History Blame Contribute Delete
5.39 kB
"""Superpoint (over-segmentation) extraction.
Faithful Python/Numba port of ``segment_mesh`` from
https://github.com/kenomo/segmentator (itself the ScanNet ``Segmentator``
Felzenszwalb-Huttenlocher graph segmentation). AQ3D's ScanNet preprocessing
calls it with ``kThresh=0.01, segMinVerts=20``; the network consumes those
superpoints directly, so reproducing this exactly matters far more than it
looks.
The original is a small C++/PyTorch extension that has to be built with CMake
against libtorch, which is not practical inside a Space image, hence the port.
"""
import numpy as np
from numba import njit
@njit(cache=True, nogil=True)
def _segment_graph(ea, eb, w, order, num_vertices, c, seg_min_verts):
parent = np.arange(num_vertices, dtype=np.int64)
rank = np.zeros(num_vertices, dtype=np.int64)
size = np.ones(num_vertices, dtype=np.int64)
threshold = np.full(num_vertices, c, dtype=np.float32)
# --- pass 1: Felzenszwalb-Huttenlocher merging over ascending weights ---
for i in range(order.shape[0]):
e = order[i]
# find(a)
x = ea[e]
y = x
while y != parent[y]:
y = parent[y]
parent[x] = y
a = y
# find(b)
x = eb[e]
y = x
while y != parent[y]:
y = parent[y]
parent[x] = y
b = y
if a != b:
ww = w[e]
if ww <= threshold[a] and ww <= threshold[b]:
# join(a, b)
if rank[a] > rank[b]:
parent[b] = a
size[a] += size[b]
root = a
else:
parent[a] = b
size[b] += size[a]
if rank[a] == rank[b]:
rank[b] += 1
root = b
threshold[root] = ww + c / size[root]
# --- pass 2: absorb segments smaller than seg_min_verts ---
for i in range(order.shape[0]):
e = order[i]
x = ea[e]
y = x
while y != parent[y]:
y = parent[y]
parent[x] = y
a = y
x = eb[e]
y = x
while y != parent[y]:
y = parent[y]
parent[x] = y
b = y
if a != b and (size[a] < seg_min_verts or size[b] < seg_min_verts):
if rank[a] > rank[b]:
parent[b] = a
size[a] += size[b]
else:
parent[a] = b
size[b] += size[a]
if rank[a] == rank[b]:
rank[b] += 1
out = np.empty(num_vertices, dtype=np.int64)
for q in range(num_vertices):
y = q
while y != parent[y]:
y = parent[y]
parent[q] = y
out[q] = y
return out
def unit_face_normals(vertices: np.ndarray, faces: np.ndarray) -> np.ndarray:
v = vertices.astype(np.float64)
n = np.cross(v[faces[:, 1]] - v[faces[:, 0]], v[faces[:, 2]] - v[faces[:, 0]])
length = np.linalg.norm(n, axis=1, keepdims=True)
return np.divide(n, length, out=np.zeros_like(n), where=length > 1e-20)
def segmentator_vertex_normals(vertices: np.ndarray, faces: np.ndarray) -> np.ndarray:
"""Mean of the incident *unit* face normals (the C++ code's running lerp)."""
nf = unit_face_normals(vertices, faces)
idx = faces.reshape(-1)
vals = np.repeat(nf, 3, axis=0)
nv = np.zeros((vertices.shape[0], 3), dtype=np.float64)
for a in range(3):
nv[:, a] = np.bincount(idx, weights=vals[:, a], minlength=vertices.shape[0])
counts = np.bincount(idx, minlength=vertices.shape[0])
nv /= np.maximum(counts, 1)[:, None]
return nv
def segment_mesh(vertices: np.ndarray, faces: np.ndarray, k_thresh: float = 0.01,
seg_min_verts: int = 20) -> np.ndarray:
"""Return a superpoint id per vertex, relabelled to ``0..S-1``."""
vertices = np.ascontiguousarray(vertices, dtype=np.float32)
faces = np.ascontiguousarray(faces, dtype=np.int64)
num_vertices = vertices.shape[0]
normals = segmentator_vertex_normals(vertices, faces)
# edges (i1,i2), (i1,i3), (i3,i2) per face -- exactly as in the C++ source
ea = np.empty(faces.shape[0] * 3, dtype=np.int64)
eb = np.empty(faces.shape[0] * 3, dtype=np.int64)
ea[0::3], eb[0::3] = faces[:, 0], faces[:, 1]
ea[1::3], eb[1::3] = faces[:, 0], faces[:, 2]
ea[2::3], eb[2::3] = faces[:, 2], faces[:, 1]
p = vertices.astype(np.float64)
d = p[eb] - p[ea]
dd = np.linalg.norm(d, axis=1, keepdims=True)
d = np.divide(d, dd, out=np.zeros_like(d), where=dd > 1e-20)
n1, n2 = normals[ea], normals[eb]
dot = (n1 * n2).sum(1)
dot2 = (n2 * d).sum(1)
w = 1.0 - dot
w = np.where(dot2 > 0, w * w, w).astype(np.float32)
order = np.argsort(w, kind="stable").astype(np.int64)
comps = _segment_graph(ea, eb, w, order, num_vertices,
np.float32(k_thresh), np.int64(seg_min_verts))
# remap component ids to 0..S-1 (upstream does the same)
_, remapped = np.unique(comps, return_inverse=True)
return remapped.astype(np.int64).reshape(-1)
def warmup() -> None:
"""Trigger Numba JIT compilation once, at import time."""
v = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]], dtype=np.float32)
f = np.array([[0, 1, 2], [1, 3, 2]], dtype=np.int64)
segment_mesh(v, f)