Face motion to picture
Stage 4 gives a moving 3D face with no skin, hair or light. This stage learns to paint it as the creator, from their own video frames, so that whatever the mesh does, the picture looks like them doing it.
How a face is drawn: blobs stuck to a mesh
The renderer is STAvatar (a published method; credits). It draws the head as a cloud of about 80,000 tiny soft coloured blobs (3D Gaussians: each a fuzzy ellipsoid with a colour and a transparency). Each blob is attached to a triangle of the face mesh, so when the mesh moves, the blobs ride along. A small network adds a nudge per blob for each expression: the crease that appears in a smile, the inside of the mouth.
How it learns
What was changed from STAvatar as published
| change | why |
|---|---|
| the mesh is the MetaHuman rig, with teeth and eyeballs added, not FLAME | so stage 4's output plugs straight in; the teeth make an open mouth read as a mouth |
| many short clips merged into one training set | STAvatar expects one long, steady video; Dr K's training set is 162 chunks |
| only frames where they face the camera | turned heads smeared, and the live app shows them facing forward |
| blink amount measured from the real eyes | the sound cannot say when someone blinks |
Our changes are kept as one patch (env/patches/stavatar.patch) applied on a fixed STAvatar commit by
env/setup_externals.sh.
Putting the face on their real body
A floating head looks wrong. So the live app pastes the drawn head into real footage of the creator, and only the head is new: shoulders, hands and room are the original video.
How it is built
| step | does |
|---|---|
| export | each tracked chunk into the renderer's input format: frames, masks, cameras |
| head placements | where the skull is in every frame, so the rig's face can be put there |
| render settings | three values worked out from rules, not guessed (derive.py) |
| blinks | how open the real eyes are, every frame |
| cook meshes | stage 4's model run over every chunk's sound: the predicted mesh per frame, with teeth |
| merge, front-facing only | one training set, holding back the validation and test clips |
| a person: the joins | a sheet with the mesh drawn on the photos, and a two-second video with sound. Is the mesh on the face, and in time? |
| train | STAvatar, 3-4 hours |
| render and look | a held-back clip, with sound |
| a person: promote | to checkpoints/<creator>/face/render/, writing down what is still wrong |
Commands
./alive build <creator> --stage render runs it. By hand, from engines/audio2face/:
~/miniconda3/envs/stavatar/bin/python gauss/recipes/mesh-to-render/tools/status.py --profile <name>
# render a held-back clip, on its real body, with sound:
~/miniconda3/envs/stavatar/bin/python gauss/recipes/mesh-to-render/tools/infer.py --profile <name> --run <run> \
--clip <chunk> --camera clip --pose replay:<chunk> --composite
The profile has the same name as the creator's motion profile and reads most of what it needs from it. The full
recipe is engines/audio2face/gauss/recipes/mesh-to-render/RECIPE.md.
Where the decisions are kept
| decision | stored in |
|---|---|
| the joins sheet was looked at | creators/<id>/build/render/joins_render_acked.json (Dr K only) |
| release the deployed run, and what is still wrong with it | creators/<id>/build.json, render.deployed and render.wrong |
The profile is gauss/recipes/mesh-to-render/profiles/<id>.py, copied from _template.py in the same folder.
Things that go wrong
| you see | why, and the fix |
|---|---|
| dark specks or lines beside the eyes or mouth | a render path that let the network nudge the teeth and eyeball blobs. Every path must freeze them, as training does |
| the face smears when they turn | trained on front-facing frames on purpose; the body footage is front-facing too |
| a hand or microphone vanishes behind the face | known; separating the face from what is in front of it needs a face-parsing model not yet added |
| the mouth runs ahead of the voice | Huberman: about 0.2 s, because his source video's sound is late. Open |
| training will not start | the live app holds the card. ./alive down |