autofacemonker 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autofacemonker-0.1.0/PKG-INFO +10 -0
- autofacemonker-0.1.0/README.md +92 -0
- autofacemonker-0.1.0/pyproject.toml +25 -0
- autofacemonker-0.1.0/setup.cfg +4 -0
- autofacemonker-0.1.0/src/autofacemonker/__init__.py +6 -0
- autofacemonker-0.1.0/src/autofacemonker/_cli.py +44 -0
- autofacemonker-0.1.0/src/autofacemonker/_register.py +127 -0
- autofacemonker-0.1.0/src/autofacemonker/data/template.ply +0 -0
- autofacemonker-0.1.0/src/autofacemonker.egg-info/PKG-INFO +10 -0
- autofacemonker-0.1.0/src/autofacemonker.egg-info/SOURCES.txt +12 -0
- autofacemonker-0.1.0/src/autofacemonker.egg-info/dependency_links.txt +1 -0
- autofacemonker-0.1.0/src/autofacemonker.egg-info/entry_points.txt +2 -0
- autofacemonker-0.1.0/src/autofacemonker.egg-info/requires.txt +5 -0
- autofacemonker-0.1.0/src/autofacemonker.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autofacemonker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automatic 3D facial template registration via MVMP + MeshMonk
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: embreex>=4.4.0
|
|
7
|
+
Requires-Dist: meshmonk[io]>=0.3.0
|
|
8
|
+
Requires-Dist: mvmp>=1.3.0
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: trimesh
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# AutoFaceMonker
|
|
2
|
+
|
|
3
|
+
Automatic 3D facial template registration using [MVMP](https://github.com/gfacchi-dev/mvmp) landmark detection and [MeshMonk](https://github.com/jsnyde0/meshmonk) nonrigid surface registration.
|
|
4
|
+
|
|
5
|
+
Given a template mesh and a target 3D face scan, AutoFaceMonker detects 478 MediaPipe facial landmarks via MVMP, aligns the template with Procrustes analysis, then refines the fit with MeshMonk nonrigid registration — no manual intervention required.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install autofacemonker
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Python ≥ 3.11.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
autofacemonker subject.obj -o warped.ply
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This uses the bundled template mesh and built-in 7-point anatomical landmark correspondences.
|
|
22
|
+
|
|
23
|
+
## Python API
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from autofacemonker import AutoFaceMonker
|
|
27
|
+
|
|
28
|
+
# Use default template and correspondences
|
|
29
|
+
monker = AutoFaceMonker()
|
|
30
|
+
warped_vertices = monker.register("subject.obj", save_path="warped.ply")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Custom template and correspondences
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
monker = AutoFaceMonker(
|
|
37
|
+
template="my_template.ply",
|
|
38
|
+
correspondences=[
|
|
39
|
+
(0, 3572), # nasion → template vertex 3572
|
|
40
|
+
(4, 3589), # nose tip → template vertex 3589
|
|
41
|
+
(133, 2436), # left eye → template vertex 2436
|
|
42
|
+
(362, 4648), # right eye → template vertex 4648
|
|
43
|
+
(61, 2310), # left mouth → template vertex 2310
|
|
44
|
+
(291, 4849), # right mouth → template vertex 4849
|
|
45
|
+
(152, 3543), # chin → template vertex 3543
|
|
46
|
+
],
|
|
47
|
+
num_iterations=200,
|
|
48
|
+
)
|
|
49
|
+
warped = monker.register("subject.obj")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## CLI
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
usage: autofacemonker <target.obj> [options]
|
|
56
|
+
|
|
57
|
+
positional arguments:
|
|
58
|
+
target Path to target .obj mesh
|
|
59
|
+
|
|
60
|
+
options:
|
|
61
|
+
-t, --template Template mesh path (default: bundled template.ply)
|
|
62
|
+
-c, --correspondences
|
|
63
|
+
JSON file with landmark→vertex mapping
|
|
64
|
+
-o, --out Output PLY path (default: <target>_warped.ply)
|
|
65
|
+
-n, --iterations MeshMonk nonrigid iterations (default: 120)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Correspondence JSON format
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{"0": 3572, "4": 3589, "133": 2436, "362": 4648, "61": 2310, "291": 4849, "152": 3543}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## How It Works
|
|
75
|
+
|
|
76
|
+
1. **MVMP** detects 478 MediaPipe facial landmarks on the target mesh using multi-view 2D projections with 5 zone cameras.
|
|
77
|
+
|
|
78
|
+
2. **Procrustes** rigidly aligns the template using the 7 anatomical landmark correspondences, computing rotation, translation, and uniform scale.
|
|
79
|
+
|
|
80
|
+
3. **MeshMonk nonrigid** refines the fit by deforming the template to match the target surface over 120 iterations.
|
|
81
|
+
|
|
82
|
+
## Requirements
|
|
83
|
+
|
|
84
|
+
- Python ≥ 3.11
|
|
85
|
+
- meshmonk ≥ 0.3.0
|
|
86
|
+
- mvmp ≥ 1.3.0
|
|
87
|
+
- trimesh
|
|
88
|
+
- numpy
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "autofacemonker"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Automatic 3D facial template registration via MVMP + MeshMonk"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"embreex>=4.4.0",
|
|
8
|
+
"meshmonk[io]>=0.3.0",
|
|
9
|
+
"mvmp>=1.3.0",
|
|
10
|
+
"numpy",
|
|
11
|
+
"trimesh",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
autofacemonker = "autofacemonker._cli:main"
|
|
16
|
+
|
|
17
|
+
[build-system]
|
|
18
|
+
requires = ["setuptools>=64"]
|
|
19
|
+
build-backend = "setuptools.build_meta"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.package-data]
|
|
25
|
+
autofacemonker = ["data/*.ply"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""CLI entry point for meshmonker."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from ._register import AutoFaceMonker
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
description="Register a facial template onto a target mesh",
|
|
13
|
+
usage="autofacemonker <target.obj> [options]",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument("target", help="Path to target .obj mesh")
|
|
16
|
+
parser.add_argument("-t", "--template", default=None,
|
|
17
|
+
help="Template mesh path (default: bundled template.ply)")
|
|
18
|
+
parser.add_argument("-c", "--correspondences", default=None,
|
|
19
|
+
help="JSON file with landmark→vertex mapping, e.g. {\"0\": 3572, ...}")
|
|
20
|
+
parser.add_argument("-o", "--out", default=None,
|
|
21
|
+
help="Output PLY path (default: <target>_warped.ply)")
|
|
22
|
+
parser.add_argument("-n", "--iterations", type=int, default=120,
|
|
23
|
+
help="MeshMonk nonrigid iterations (default: 120)")
|
|
24
|
+
args = parser.parse_args()
|
|
25
|
+
|
|
26
|
+
# Load correspondences from JSON if provided
|
|
27
|
+
corr = None
|
|
28
|
+
if args.correspondences:
|
|
29
|
+
with open(args.correspondences) as f:
|
|
30
|
+
raw = json.load(f)
|
|
31
|
+
corr = [(int(k), v) for k, v in raw.items()]
|
|
32
|
+
|
|
33
|
+
out = args.out or args.target.replace(".obj", "_warped.ply")
|
|
34
|
+
|
|
35
|
+
monker = AutoFaceMonker(
|
|
36
|
+
template=args.template,
|
|
37
|
+
correspondences=corr,
|
|
38
|
+
num_iterations=args.iterations,
|
|
39
|
+
)
|
|
40
|
+
monker.register(args.target, save_path=out)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Core: MVMP landmarks → Procrustes → MeshMonk nonrigid."""
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import trimesh
|
|
7
|
+
import meshmonk
|
|
8
|
+
from mvmp import Facemarker
|
|
9
|
+
|
|
10
|
+
# 7 verified anatomical MediaPipe landmark → template vertex correspondences
|
|
11
|
+
DEFAULT_CORRESPONDENCES = [
|
|
12
|
+
(0, 3572), # nasion
|
|
13
|
+
(4, 3589), # nose tip
|
|
14
|
+
(133, 2436), # left eye inner
|
|
15
|
+
(362, 4648), # right eye inner
|
|
16
|
+
(61, 2310), # left mouth
|
|
17
|
+
(291, 4849), # right mouth
|
|
18
|
+
(152, 3543), # chin
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _default_template():
|
|
23
|
+
return str(files("autofacemonker.data").joinpath("template.ply"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AutoFaceMonker:
|
|
27
|
+
"""Register a facial template onto target meshes using MVMP + MeshMonk.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
template : str, pathlib.Path, trimesh.Trimesh, or None
|
|
32
|
+
Template mesh. None (default) uses the bundled template.ply.
|
|
33
|
+
correspondences : list of (lmk_idx, tpl_vert_id) or None
|
|
34
|
+
Manual correspondences. None (default) uses built-in 7-keypoint set.
|
|
35
|
+
num_iterations : int
|
|
36
|
+
MeshMonk nonrigid iterations (default 120).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, template=None, correspondences=None, num_iterations=120):
|
|
40
|
+
tpl_path = template if isinstance(template, (str, type(None))) else None
|
|
41
|
+
if tpl_path is None:
|
|
42
|
+
tpl_path = _default_template()
|
|
43
|
+
self.template = trimesh.load(tpl_path) if tpl_path is not None else template
|
|
44
|
+
|
|
45
|
+
self.correspondences = correspondences or DEFAULT_CORRESPONDENCES
|
|
46
|
+
self.num_iterations = num_iterations
|
|
47
|
+
self._marker = Facemarker()
|
|
48
|
+
|
|
49
|
+
self._lmk_indices = np.array([c[0] for c in self.correspondences])
|
|
50
|
+
self._tpl_vids = np.array([c[1] for c in self.correspondences])
|
|
51
|
+
self._tmpl_lmks = self.template.vertices[self._tpl_vids]
|
|
52
|
+
|
|
53
|
+
def register(self, target_path, save_path=None):
|
|
54
|
+
"""Register the template onto *target_path*.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
target_path : str or pathlib.Path
|
|
59
|
+
Path to the target .obj mesh.
|
|
60
|
+
save_path : str, pathlib.Path, or None
|
|
61
|
+
If provided, export the warped template as PLY.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
warped_vertices : np.ndarray (N, 3)
|
|
66
|
+
"""
|
|
67
|
+
target = trimesh.load(str(target_path))
|
|
68
|
+
|
|
69
|
+
# ── MVMP ──────────────────────────────────────────────────────────
|
|
70
|
+
res = self._marker.predict(str(target_path))
|
|
71
|
+
lmks_full = np.full((478, 3), np.nan)
|
|
72
|
+
for k, v in res.landmarks_3d.items():
|
|
73
|
+
lmks_full[int(k)] = v
|
|
74
|
+
|
|
75
|
+
# Keep only detected landmarks
|
|
76
|
+
mask = ~np.isnan(lmks_full[self._lmk_indices, 0])
|
|
77
|
+
idx = self._lmk_indices[mask]
|
|
78
|
+
vid = self._tpl_vids[mask]
|
|
79
|
+
tmpl = self._tmpl_lmks[mask]
|
|
80
|
+
tgt = lmks_full[idx]
|
|
81
|
+
|
|
82
|
+
# ── Procrustes (rotation + translation + scale) ───────────────────
|
|
83
|
+
tmpl_c = tmpl - tmpl.mean(axis=0)
|
|
84
|
+
tgt_c = tgt - tgt.mean(axis=0)
|
|
85
|
+
H = tmpl_c.T @ tgt_c
|
|
86
|
+
U, _, Vt = np.linalg.svd(H)
|
|
87
|
+
R = Vt.T @ U.T
|
|
88
|
+
if np.linalg.det(R) < 0:
|
|
89
|
+
Vt[-1] *= -1
|
|
90
|
+
R = Vt.T @ U.T
|
|
91
|
+
scale = np.sum(np.linalg.norm(tgt_c, axis=1)) / np.sum(
|
|
92
|
+
np.linalg.norm(tmpl_c, axis=1)
|
|
93
|
+
)
|
|
94
|
+
t = tgt.mean(axis=0) - scale * R @ tmpl.mean(axis=0)
|
|
95
|
+
|
|
96
|
+
aligned = scale * (R @ self.template.vertices.T).T + t
|
|
97
|
+
aligned_n = (R @ self.template.vertex_normals.T).T
|
|
98
|
+
|
|
99
|
+
resids = np.linalg.norm(aligned[self._tpl_vids[mask]] - tgt, axis=1)
|
|
100
|
+
print(
|
|
101
|
+
f"Procrustes: {mask.sum()} lmks scale={scale:.0f}x "
|
|
102
|
+
f"P50={np.median(resids):.1f}mm max={resids.max():.1f}mm"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# ── MeshMonk nonrigid ─────────────────────────────────────────────
|
|
106
|
+
tgt_features = np.column_stack([target.vertices, target.vertex_normals])
|
|
107
|
+
result = meshmonk.nonrigid_register(
|
|
108
|
+
floating_features=np.column_stack([aligned, aligned_n]),
|
|
109
|
+
target_features=tgt_features,
|
|
110
|
+
floating_faces=self.template.faces,
|
|
111
|
+
target_faces=target.faces,
|
|
112
|
+
num_iterations=self.num_iterations,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if save_path:
|
|
116
|
+
out = self.template.copy()
|
|
117
|
+
out.vertices = result.aligned_vertices
|
|
118
|
+
out.export(str(save_path))
|
|
119
|
+
print(f"Saved {save_path}")
|
|
120
|
+
|
|
121
|
+
return result.aligned_vertices
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def register_template(target_path, template=None, correspondences=None, save_path=None):
|
|
125
|
+
"""Convenience function. See ``AutoFaceMonker`` for parameters."""
|
|
126
|
+
m = AutoFaceMonker(template=template, correspondences=correspondences)
|
|
127
|
+
return m.register(target_path, save_path=save_path)
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autofacemonker
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Automatic 3D facial template registration via MVMP + MeshMonk
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: embreex>=4.4.0
|
|
7
|
+
Requires-Dist: meshmonk[io]>=0.3.0
|
|
8
|
+
Requires-Dist: mvmp>=1.3.0
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: trimesh
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/autofacemonker/__init__.py
|
|
4
|
+
src/autofacemonker/_cli.py
|
|
5
|
+
src/autofacemonker/_register.py
|
|
6
|
+
src/autofacemonker.egg-info/PKG-INFO
|
|
7
|
+
src/autofacemonker.egg-info/SOURCES.txt
|
|
8
|
+
src/autofacemonker.egg-info/dependency_links.txt
|
|
9
|
+
src/autofacemonker.egg-info/entry_points.txt
|
|
10
|
+
src/autofacemonker.egg-info/requires.txt
|
|
11
|
+
src/autofacemonker.egg-info/top_level.txt
|
|
12
|
+
src/autofacemonker/data/template.ply
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
autofacemonker
|