Knowhow/Vision

Facescape 모델 displacement map 사용법, detailed mesh 얻어내는 방법

침닦는수건 2024. 8. 28. 18:32
반응형

https://facescape.nju.edu.cn/

 

FaceScape

 

facescape.nju.edu.cn

 

Face mesh 데이터셋 중 오래됐지만 여전히 많이 쓰이는 데이터 중 하나인 facescape 데이터셋. TU model이라는 이름으로 topology를 맞춰놓은 데이터가 특히 사용하기 좋다. 

 

TU model은 근데 생각보다 해상도(mesh vertex, face 수)가 높지 않은데 데이터 자체의 용량을 줄이기 위해서 coarse와 fine을 분리해놨기 때문이다.

 

그냥 obj 파일에 담겨있는 mesh는 coarse 즉 vertex 수도 적고 face도 적은 기본 template mesh를 registration해둔 결과다. 그 이외 fine detail은 displacement map담겨 있다. 

 

displacement map (이하 dpmap)은 texture와 같이 uv 좌표계로 표현되어 있는 하나의 이미지인데, 그 값이 대응되는 mesh vertex의 SDF 값이다. mesh vertex의 uv값을 읽고 해당 dp 값을 얻은 뒤, normal 방향으로 이동시키면 더 정확한 vertex 위치를 얻어낼 수 있다. 

 

이 때 mesh vertex수가 적어서 여전히 아쉬운 점이 있는데, mesh를 subdivision해서 4배, 16배로 부풀려서 dp 값대로 움직이는 행위를 반복하면 엄청 좋은 mesh를 얻어낼 수 있다. 

 

정리해서 말하면 mesh subdivision + dp map 값으로 이동시켜서 최종 mesh를 얻어낼 수 있는게 facescape 데이터셋이다. 

 

위 설명한 방식대로 최종 mesh를 얻어내는 코드는 다음과 같다. 먼저 필요한 함수 및 클래스다.

def MTL(filename):
    contents = {}
    mtl = None
    for line in open(filename, "r"):
        if line.startswith('#'): continue
        values = line.split()
        if not values: continue
        if values[0] == 'newmtl':
            mtl = contents[values[1]] = {}
        elif mtl is None:
            raise ValueError('mtl file doesn\'t start with newmtl stmt')
        elif values[0] == 'map_Kd':
            # load the texture referred to by this declaration
            mtl[values[0]] = values[1]
        else:
            mtl[values[0]] = map(float, values[1:])
    return contents

class OBJ:
    def __init__(self, filename, swapyz=False):
        """Loads a Wavefront OBJ file. """
        self.vertices = []
        self.normals = []
        self.texcoords = []
        self.faces = []
        self.adjacent_list = []

        material = None
        for line in open(filename, "r"):
            if line.startswith('#'): continue
            values = line.split()
            if not values: continue
            if values[0] == 'v':
                v = list(map(float, values[1:4]))
                if swapyz:
                    v = v[0], v[2], v[1]
                self.vertices.append(v)
            elif values[0] == 'vn':
                v = list(map(float, values[1:4]))
                if swapyz:
                    v = v[0], v[2], v[1]
                self.normals.append(v)
            elif values[0] == 'vt':
                self.texcoords.append(list(map(float, values[1:3])))
            elif values[0] in ('usemtl', 'usemat'):
                material = values[1]
            # elif values[0] == 'mtllib':
            #    self.mtl = MTL(os.path.dirname(filename) + '/' + os.path.basename(values[1]))
            elif values[0] == 'f':
                face = []
                texcoords = []
                norms = []
                for v in values[1:]:
                    w = v.split('/')
                    face.append(int(w[0]))
                    if len(w) >= 2 and len(w[1]) > 0:
                        texcoords.append(int(w[1]))
                    else:
                        texcoords.append(0)
                    if len(w) >= 3 and len(w[2]) > 0:
                        norms.append(int(w[2]))
                    else:
                        norms.append(0)
                self.faces.append((face, norms, texcoords, material))

    def get_adjacent(self, index):
        if not self.adjacent_list:
            adjacent_list = [[] for i in range(len(self.vertices))]
            for face in self.faces:
                face_vertices, face_normals, face_texture_coords, material = face
                adjacent_list[face_vertices[0] - 1].append(face_vertices[1] - 1)
                adjacent_list[face_vertices[0] - 1].append(face_vertices[2] - 1)
                adjacent_list[face_vertices[1] - 1].append(face_vertices[0] - 1)
                adjacent_list[face_vertices[1] - 1].append(face_vertices[2] - 1)
                adjacent_list[face_vertices[2] - 1].append(face_vertices[0] - 1)
                adjacent_list[face_vertices[2] - 1].append(face_vertices[1] - 1)

            adjacent_list = list(map(set, adjacent_list))
            self.adjacent_list = list(map(list, adjacent_list))
        return self.adjacent_list[index]
        
 def subdiv(verts, tris, texcoords=None, face_index=None):
    if face_index is None:
        face_index = np.arange(len(tris))
    else:
        face_index = np.asanyarray(face_index)

    # the (c, 3) int array of vertex indices
    tris_subset = tris[face_index]

    # find the unique edges of our faces subset
    edges = np.sort(trimesh.remesh.faces_to_edges(tris_subset), axis=1)
    unique, inverse = trimesh.remesh.grouping.unique_rows(edges)
    # then only produce one midpoint per unique edge
    mid = verts[edges[unique]].mean(axis=1)
    mid_idx = inverse.reshape((-1, 3)) + len(verts)

    # the new faces_subset with correct winding
    f = np.column_stack([tris_subset[:, 0],
                         mid_idx[:, 0],
                         mid_idx[:, 2],
                         mid_idx[:, 0],
                         tris_subset[:, 1],
                         mid_idx[:, 1],
                         mid_idx[:, 2],
                         mid_idx[:, 1],
                         tris_subset[:, 2],
                         mid_idx[:, 0],
                         mid_idx[:, 1],
                         mid_idx[:, 2]]).reshape((-1, 3))
    # add the 3 new faces_subset per old face
    new_faces = np.vstack((tris, f[len(face_index):]))
    # replace the old face with a smaller face
    new_faces[face_index] = f[:len(face_index)]

    new_vertices = np.vstack((verts, mid))

    if texcoords is not None:
        texcoords_mid = texcoords[edges[unique]].mean(axis=1)
        new_texcoords = np.vstack((texcoords, texcoords_mid))
        return new_vertices, new_faces, new_texcoords

    return new_vertices, new_faces


def dpmap2verts(verts, tris, texcoords, dpmap, scale=0.914):
    dpmap = np.array(dpmap).astype(int)
    normals = np.zeros(verts.shape)
    test = np.zeros(verts.shape)
    tri_verts = verts[tris]
    n0 = np.cross(tri_verts[::, 1] - tri_verts[::, 0], tri_verts[::, 2] - tri_verts[::, 0])
    n0 = n0 / np.linalg.norm(n0, axis=1)[:, np.newaxis]
    for i in range(tris.shape[0]):
        normals[tris[i, 0]] += n0[i]
        normals[tris[i, 1]] += n0[i]
        normals[tris[i, 2]] += n0[i]

    normals = normals / np.linalg.norm(normals, axis=1)[:, np.newaxis]

    pos_u = dpmap.shape[0] - (texcoords[:, 1] * dpmap.shape[0]).astype(int)
    pos_v = (texcoords[:, 0] * dpmap.shape[1]).astype(int)
    pos_u[pos_u >= dpmap.shape[0]] = dpmap.shape[0] - 1
    pos_v[pos_v >= dpmap.shape[1]] = dpmap.shape[1] - 1
    verts += normals * (dpmap[pos_u, pos_v] - 32768)[:, np.newaxis] / 32768 * scale
    return verts

 

그 다음 아래처럼 움직여주면 된다.

mesh = OBJ(obj_path)
 
tris = []
vert_texcoords = np.zeros((len(mesh.vertices), 2))
for face in mesh.faces:
     vertices, normals, texture_coords, material = face
     tris.append([vertices[0] - 1, vertices[1] - 1, vertices[2] - 1])
     for i in range(len(vertices)):
         vert_texcoords[vertices[i] - 1] = mesh.texcoords[texture_coords[i] - 1]
tris = np.array(tris)

verts = np.array(mesh.vertices)
raw_verts = verts.copy()
verts = verts / np.linalg.norm(verts[12280] - verts[12320]) * 60
     
### CHANGE THIS NUMBER
num_subdiv = 1
for _ in range(num_subdiv):
   verts, tris, vert_texcoords = subdiv(verts, tris, vert_texcoords)

if os.path.exists(dp_path):
   dpmap = Image.open(dp_path)
   dpmap = np.array(dpmap)
   verts = dpmap2verts(verts, tris, vert_texcoords, dpmap)
verts = verts / 60 * np.linalg.norm(raw_verts[12280] - raw_verts[12320])

 

반응형