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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """AutoFaceMonker: automatic 3D facial template registration via MVMP + MeshMonk."""
2
+
3
+ from ._register import AutoFaceMonker, register_template
4
+
5
+ __version__ = "0.1.0"
6
+ __all__ = ["AutoFaceMonker", "register_template"]
@@ -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)
@@ -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,2 @@
1
+ [console_scripts]
2
+ autofacemonker = autofacemonker._cli:main
@@ -0,0 +1,5 @@
1
+ embreex>=4.4.0
2
+ meshmonk[io]>=0.3.0
3
+ mvmp>=1.3.0
4
+ numpy
5
+ trimesh
@@ -0,0 +1 @@
1
+ autofacemonker