aleph65 commited on
Commit
da5afad
·
verified ·
1 Parent(s): a593adc

Remove paper folders — centralized in private aleph65/Papers

Browse files
3dgazenet/README.md DELETED
@@ -1,72 +0,0 @@
1
- # 3dgazenet — 3DGazeNet single-photo → 3D gaze + dense eye meshes
2
-
3
- One photo in, per-face 3D gaze direction plus dense 3D eye reconstructions
4
- out, using [3DGazeNet](https://github.com/eververas/3DGazeNet) (Ververas et
5
- al., ECCV 2024) — gaze estimation as dense 3D eye-mesh regression,
6
- generalizing to new environments without adaptation. Uses the authors' demo
7
- model (resnet18, 128×128 input) trained on ETH-XGaze + GazeCapture + Gaze360 +
8
- MPIIFaceGaze + VFHQ. Runs on the native python of the `plx1029/comfyui-qwen`
9
- RunPod image (python 3.12, torch 2.8+cu128) — **no venv, no conda, torch is
10
- not reinstalled**.
11
-
12
- ## Install (once per pod, ~2 min + ~1.6 GB downloads)
13
-
14
- ```bash
15
- ./gazenet_install.sh
16
- ```
17
-
18
- Everything lands in `$GAZENET_HOME` (default `/workspace/3dgazenet`).
19
- Re-running is safe; completed steps are skipped. The demo data zip (3 gaze
20
- checkpoints, `eyes3d.pkl` dense-eye template, test media) comes from the
21
- authors' official Google Drive via gdown. The insightface `buffalo_l` face
22
- detector is **reused from the p3dm install** if present.
23
-
24
- ## Run (~10 s per photo)
25
-
26
- ```bash
27
- ./gazenet_run.sh photo.jpg # -> /workspace/outputs/3dgazenet/photo/
28
- ./gazenet_run.sh photo.jpg -o /some/dir --det-size 640 --force
29
- ```
30
-
31
- All faces in the image are processed (files get an `_f<i>` suffix when more
32
- than one). Outputs: `<name>_gaze.jpg` (drawn eye contours, iris circles, gaze
33
- direction), `<name>_facedet.jpg` (detection check), `<name>_gaze.json` (gaze
34
- vectors — direct regression, per-eye from mesh, combined — plus pitch/yaw in
35
- rad and deg, iris centers in px, bbox, 5-pt landmarks), `<name>_eye_left.obj`
36
- / `<name>_eye_right.obj` (dense 481-vertex 3D eye meshes incl. eyeball + iris
37
- in image-space coords, from the paper's core dense-regression output), and
38
- `<name>_raw.pkl` (the complete model output dict).
39
-
40
- Pipeline: insightface `buffalo_l` face detection (5-pt landmarks) → roll
41
- normalization → per-element crops (left eye, right eye, face) → resnet18
42
- regression of 2×481 eye vertices + gaze vector → gaze = combination of direct
43
- vector and per-eye mesh directions (`gaze_out` in the json).
44
-
45
- The wrapper `gazenet_infer.py` drives the authors' demo `GazeNetInference`
46
- class and exports all of the above (the stock demo only writes the drawn jpg).
47
-
48
- ## Porting notes (vs. upstream repo)
49
-
50
- Upstream targets python 3.9 / torch 2.5 / numpy 1.26 via conda. On the native
51
- py3.12 / torch 2.8 / numpy 2.1 env **zero source patches were needed** — the
52
- only additions are:
53
-
54
- - `easydict` (tiny config dep) — everything else (insightface, onnxruntime,
55
- opencv, matplotlib) is already in the native env (p3dm installed
56
- insightface/onnxruntime)
57
- - `TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1` at runtime (checkpoint predates torch
58
- 2.6 `weights_only` default)
59
- - pip runs against a constraints file pinning the already-installed
60
- numpy/torch/opencv/… so the ComfyUI env is never disturbed
61
-
62
- The main repo also ships a research pipeline (`preprocess_inference.py` +
63
- `inference.py` with a fitting optimizer, separate GDrive data bundle); the
64
- demo model is the recommended, most robust entry point for in-the-wild
65
- images and is what these scripts use.
66
-
67
- ## Licenses
68
-
69
- 3DGazeNet code and the demo checkpoints are downloaded from the authors'
70
- official sources at install time and are **not** redistributed here. The
71
- model is trained on datasets with research-only terms (ETH-XGaze,
72
- GazeCapture, Gaze360, MPIIFaceGaze, VFHQ) — treat outputs accordingly.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3dgazenet/gazenet_infer.py DELETED
@@ -1,120 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Single-image 3DGazeNet inference with full result export.
3
-
4
- Wraps the demo's GazeNetInference to save, per detected face: gaze vectors
5
- (json), dense 3D eye meshes (obj, image-space coords), iris centers, the
6
- drawn visualization, the face-detection visualization, and a raw pickle of
7
- everything the model returns.
8
-
9
- Usage: python3 gazenet_infer.py --image photo.jpg --out outdir --name photo
10
- """
11
- import argparse
12
- import json
13
- import os
14
- import pickle
15
- import sys
16
-
17
- import cv2
18
- import numpy as np
19
-
20
- PATH_DEMO = os.path.join(os.path.dirname(os.path.abspath(__file__)), '3DGazeNet', 'demo')
21
- sys.path.insert(0, PATH_DEMO)
22
- os.environ.setdefault('TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD', '1')
23
-
24
- from inference import GazeNetInference # noqa: E402
25
- from utils import load_eyes3d, draw_results # noqa: E402
26
-
27
-
28
- def pitchyaw_from_vector(g):
29
- # model convention: x right, y down, z out of the image towards the
30
- # camera (z ~ +1 when looking straight at the camera); pitch positive up,
31
- # yaw positive to the image left, (0, 0) = looking at the camera
32
- x, y, z = g / np.linalg.norm(g)
33
- return float(np.arcsin(-y)), float(np.arctan2(-x, z))
34
-
35
-
36
- def write_eye_obj(path, verts, trilist):
37
- with open(path, 'w') as f:
38
- f.write('# 3DGazeNet eye mesh, image-space coords (px)\n')
39
- for v in verts:
40
- f.write(f'v {v[0]:.4f} {v[1]:.4f} {v[2]:.4f}\n')
41
- for t in trilist:
42
- f.write(f'f {t[0]+1} {t[1]+1} {t[2]+1}\n')
43
-
44
-
45
- def main():
46
- ap = argparse.ArgumentParser()
47
- ap.add_argument('--image', required=True)
48
- ap.add_argument('--out', required=True)
49
- ap.add_argument('--name', default=None)
50
- ap.add_argument('--det-thresh', type=float, default=0.5)
51
- ap.add_argument('--det-size', type=int, default=224)
52
- args = ap.parse_args()
53
-
54
- name = args.name or os.path.splitext(os.path.basename(args.image))[0]
55
- os.makedirs(args.out, exist_ok=True)
56
-
57
- image = cv2.imread(args.image)
58
- assert image is not None, f'cannot read {args.image}'
59
-
60
- gazenet = GazeNetInference(args.det_thresh, args.det_size)
61
- bboxs, kpts, faces = gazenet.face_detector.run(image)
62
- assert kpts is not None and len(kpts) > 0, 'no face detected'
63
-
64
- # face detection visualization
65
- cv2.imwrite(os.path.join(args.out, f'{name}_facedet.jpg'),
66
- gazenet.face_detector.draw_bbox(image, faces))
67
-
68
- trilist = load_eyes3d()['trilist_eye']
69
- results, out_img = [], image
70
- for i, kpt in enumerate(kpts):
71
- res = gazenet.gaze_predictor(image, kpt, undo_roll=True)
72
- assert res is not None, f'gaze prediction failed for face {i}'
73
- results.append(res)
74
- out_img = draw_results(out_img, kpts[i], res)
75
-
76
- cv2.imwrite(os.path.join(args.out, f'{name}_gaze.jpg'), out_img)
77
-
78
- summary = []
79
- for i, (res, bbox, kpt) in enumerate(zip(results, bboxs, kpts)):
80
- sfx = '' if len(results) == 1 else f'_f{i}'
81
- for side in ('left', 'right'):
82
- write_eye_obj(os.path.join(args.out, f'{name}{sfx}_eye_{side}.obj'),
83
- res['verts_eyes'][side], trilist)
84
- gaze = np.asarray(res['gaze_out'], dtype=float)
85
- pitch, yaw = pitchyaw_from_vector(gaze)
86
- summary.append({
87
- 'face_index': i,
88
- 'bbox_xyxy': np.asarray(bbox).tolist(),
89
- 'kpt5': np.asarray(kpt).tolist(),
90
- 'gaze_out': gaze.tolist(),
91
- 'gaze_pitch_yaw_rad': [pitch, yaw],
92
- 'gaze_pitch_yaw_deg': [np.degrees(pitch), np.degrees(yaw)],
93
- 'gaze_direct': np.asarray(res['gaze']).tolist(),
94
- 'gaze_from_eyes': {k: np.asarray(v).tolist()
95
- for k, v in res['gaze_from_eyes'].items()},
96
- 'gaze_combined': np.asarray(res['gaze_combined']).tolist(),
97
- 'iris_center_px': {k: np.asarray(v).tolist()
98
- for k, v in res['centers_iris'].items()},
99
- })
100
-
101
- with open(os.path.join(args.out, f'{name}_gaze.json'), 'w') as f:
102
- json.dump({'image': os.path.basename(args.image),
103
- 'n_faces': len(results),
104
- 'convention': 'gaze vectors: x right, y down, z towards camera '
105
- '(z ~ +1 when looking at the camera); pitch positive '
106
- 'up, yaw positive to image left, (0,0) = at camera',
107
- 'faces': summary}, f, indent=2)
108
-
109
- with open(os.path.join(args.out, f'{name}_raw.pkl'), 'wb') as f:
110
- pickle.dump({'bboxs': bboxs, 'kpts': kpts, 'results': results,
111
- 'trilist_eye': trilist}, f)
112
-
113
- for s in summary:
114
- p, y = s['gaze_pitch_yaw_deg']
115
- print(f"face {s['face_index']}: gaze={np.round(s['gaze_out'], 4).tolist()} "
116
- f"pitch={p:.1f}deg yaw={y:.1f}deg")
117
-
118
-
119
- if __name__ == '__main__':
120
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3dgazenet/gazenet_install.sh DELETED
@@ -1,110 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # gazenet_install.sh — one-shot installer for 3DGazeNet (single photo ->
4
- # 3D gaze vector + dense 3D eye meshes)
5
- #
6
- # Installs https://github.com/eververas/3DGazeNet (ECCV 2024, demo pipeline)
7
- # into the NATIVE python environment (no venv, no conda, no torch reinstall).
8
- # Tested on: Ubuntu 24.04, python 3.12, torch 2.8.0+cu128, H100 (RunPod image
9
- # plx1029/comfyui-qwen).
10
- #
11
- # Idempotent: safe to re-run; finished steps are skipped.
12
- #
13
- # Requirements:
14
- # - torch with CUDA already installed (the script will not install torch)
15
- # - ~1.6 GB disk (demo data zip from the authors' Google Drive: 3 gaze
16
- # checkpoints + eyes3d template + test media)
17
- #
18
- # Usage:
19
- # ./gazenet_install.sh
20
- #
21
- # Layout (override with GAZENET_HOME):
22
- # $GAZENET_HOME/3DGazeNet code (+ demo/data with weights)
23
- # $GAZENET_HOME/gazenet_infer.py single-image inference wrapper
24
- # =============================================================================
25
- set -euo pipefail
26
-
27
- GAZENET_HOME="${GAZENET_HOME:-/workspace/3dgazenet}"
28
- REPO="$GAZENET_HOME/3DGazeNet"
29
-
30
- log() { echo -e "\n\033[1;36m[gazenet-install]\033[0m $*"; }
31
-
32
- mkdir -p "$GAZENET_HOME"
33
- cd "$GAZENET_HOME"
34
-
35
- # ---------------------------------------------------------------- sanity checks
36
- log "checking python/torch"
37
- python3 - <<'EOF'
38
- import torch
39
- assert torch.cuda.is_available(), "CUDA torch required"
40
- print(f" torch {torch.__version__} cuda {torch.version.cuda} ok")
41
- EOF
42
-
43
- # ---------------------------------------------------- constraints (protect env)
44
- log "generating pip constraints from current env"
45
- python3 - > "$GAZENET_HOME/constraints.txt" <<'EOF'
46
- from importlib.metadata import version, PackageNotFoundError
47
- for p in ["numpy","torch","torchvision","pillow","scipy","opencv-python",
48
- "matplotlib","onnxruntime","insightface","requests"]:
49
- try: print(f"{p}=={version(p)}")
50
- except PackageNotFoundError: pass
51
- EOF
52
- CONS="-c $GAZENET_HOME/constraints.txt"
53
-
54
- # ------------------------------------------------------------------ pip deps
55
- # insightface + onnxruntime are already in the native env (from
56
- # p3dm_install.sh); the demo additionally needs only easydict (+ gdown for the
57
- # data download). No source patches are required at all on py3.12/torch2.8.
58
- log "installing python dependencies (native env, constrained)"
59
- pip install -q $CONS easydict gdown
60
- python3 -c "import insightface, onnxruntime" 2>/dev/null || \
61
- pip install -q $CONS insightface onnxruntime
62
-
63
- # ------------------------------------------------------------------ 3DGazeNet
64
- if [ ! -d "$REPO" ]; then
65
- log "cloning 3DGazeNet"
66
- git clone --depth 1 https://github.com/eververas/3DGazeNet.git "$REPO"
67
- fi
68
-
69
- # ------------------------------------------------------------- model weights
70
- # Official demo data zip (Google Drive, ~760 MB): res18 gaze checkpoints
71
- # (ALL, ALL+VFHQ, Gaze360), eyes3d.pkl dense-eye template, test media.
72
- if [ ! -s "$REPO/demo/data/checkpoints/res18_x128_all_vfhq_vert.pth" ] || \
73
- [ ! -s "$REPO/demo/data/eyes3d.pkl" ]; then
74
- log "downloading demo data (~760 MB, official Google Drive)"
75
- gdown 1aVbPD51-8EqpJ89TqiTr40pmrpk6iESl -O "$GAZENET_HOME/gazenet_data.zip" -q
76
- unzip -o -q "$GAZENET_HOME/gazenet_data.zip" -d "$GAZENET_HOME/gazenet_data_tmp"
77
- rm -rf "$REPO/demo/data"
78
- mv "$GAZENET_HOME/gazenet_data_tmp/data" "$REPO/demo/data"
79
- rm -rf "$GAZENET_HOME/gazenet_data.zip" "$GAZENET_HOME/gazenet_data_tmp"
80
- fi
81
-
82
- # insightface buffalo_l detector (auto-downloaded by insightface on first use;
83
- # fetched explicitly here for determinism, same source as p3dm_install.sh)
84
- if [ ! -f ~/.insightface/models/buffalo_l/det_10g.onnx ] && \
85
- [ ! -d ~/.insightface/models/buffalo_l/buffalo_l ]; then
86
- log "downloading insightface buffalo_l detector"
87
- mkdir -p ~/.insightface/models
88
- gdown 1navJMy0DTr1_DHjLWu1i48owCPvXWfYc -O ~/.insightface/models/buffalo_l.zip -q
89
- unzip -o -q ~/.insightface/models/buffalo_l.zip -d ~/.insightface/models/buffalo_l
90
- rm ~/.insightface/models/buffalo_l.zip
91
- fi
92
-
93
- # ------------------------------------------------------------------ verify
94
- log "verifying imports + files"
95
- REPO_DIR="$REPO" python3 - <<'EOF'
96
- import os, sys, warnings
97
- warnings.filterwarnings("ignore")
98
- import easydict, insightface, onnxruntime, cv2, matplotlib
99
- repo = os.environ["REPO_DIR"]
100
- sys.path.insert(0, os.path.join(repo, "demo"))
101
- from utils import load_eyes3d
102
- eyes3d = load_eyes3d()
103
- assert eyes3d["trilist_eye"].shape[1] == 3
104
- for f in ["demo/data/checkpoints/res18_x128_all_vfhq_vert.pth",
105
- "demo/data/eyes3d.pkl"]:
106
- assert os.path.getsize(os.path.join(repo, f)) > 0, f
107
- print(" all good")
108
- EOF
109
-
110
- log "install complete. run: ./gazenet_run.sh <photo.jpg>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3dgazenet/gazenet_run.sh DELETED
@@ -1,60 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # gazenet_run.sh — single photo in -> 3D gaze + dense 3D eye meshes (3DGazeNet)
4
- #
5
- # Usage:
6
- # ./gazenet_run.sh <photo.(jpg|png)> [-o <output_dir>] [--det-size N] [--force]
7
- #
8
- # -o DIR output directory (default: /workspace/outputs/3dgazenet/<name>)
9
- # --det-size N face detection input size (default 224; try 640 for small faces)
10
- # --force redo even if cached results exist
11
- #
12
- # Outputs (in <output_dir>), per detected face (suffix _f<i> if several):
13
- # <name>_gaze.jpg input with drawn eye meshes, iris circles + gaze
14
- # <name>_facedet.jpg face detection boxes + 5-point landmarks
15
- # <name>_gaze.json gaze vectors (direct, per-eye, combined), pitch/yaw
16
- # (rad + deg), iris centers (px), bbox, landmarks
17
- # <name>_eye_left.obj dense 3D eye mesh, 481 verts (image-space px)
18
- # <name>_eye_right.obj dense 3D eye mesh, 481 verts (image-space px)
19
- # <name>_raw.pkl everything the model returns (verts, vectors, tris)
20
- #
21
- # Requires gazenet_install.sh to have been run first.
22
- # =============================================================================
23
- set -euo pipefail
24
-
25
- GAZENET_HOME="${GAZENET_HOME:-/workspace/3dgazenet}"
26
- REPO="$GAZENET_HOME/3DGazeNet"
27
-
28
- IMG="" ; OUT="" ; DET_SIZE=224 ; FORCE=0
29
- while [ $# -gt 0 ]; do
30
- case "$1" in
31
- -o) OUT="$2"; shift 2;;
32
- --det-size) DET_SIZE="$2"; shift 2;;
33
- --force) FORCE=1; shift;;
34
- -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
35
- *) IMG="$1"; shift;;
36
- esac
37
- done
38
- [ -n "$IMG" ] && [ -f "$IMG" ] || { echo "usage: $0 <photo.jpg> [-o outdir] [--det-size N] [--force]"; exit 1; }
39
- [ -f "$REPO/demo/data/eyes3d.pkl" ] || { echo "3DGazeNet not installed - run gazenet_install.sh first"; exit 1; }
40
-
41
- # sanitized job name from the file stem
42
- STEM="$(basename "$IMG")"; STEM="${STEM%.*}"
43
- NAME="$(echo "$STEM" | tr -c 'a-zA-Z0-9' '_' | sed 's/_*$//;s/^_*//' | cut -c1-60)"
44
- OUT="${OUT:-/workspace/outputs/3dgazenet/$NAME}"
45
-
46
- export TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1 # repo predates torch>=2.6 weights_only default
47
-
48
- log() { echo -e "\033[1;35m[3dgazenet]\033[0m $*"; }
49
- T0=$(date +%s)
50
-
51
- if [ "$FORCE" = 1 ]; then rm -rf "$OUT"; fi
52
- if [ ! -f "$OUT/${NAME}_gaze.json" ]; then
53
- log "running 3DGazeNet (insightface detection -> res18 dense eye regression)"
54
- python3 "$GAZENET_HOME/gazenet_infer.py" --image "$IMG" --out "$OUT" --name "$NAME" \
55
- --det-size "$DET_SIZE" 2> >(grep -viE "warning|provider|set det-size|model ignore|find model" >&2 || true)
56
- fi
57
- [ -f "$OUT/${NAME}_gaze.json" ] || { echo "ERROR: inference failed for $IMG"; exit 1; }
58
-
59
- log "done in $(( $(date +%s) - T0 ))s -> $OUT"
60
- ls -la "$OUT"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deca/README.md DELETED
@@ -1,79 +0,0 @@
1
- # deca — DECA single-photo → detailed 3D FLAME face mesh
2
-
3
- One photo in, detailed 3D face reconstruction out, using
4
- [DECA](https://github.com/yfeng95/DECA) (Feng et al., SIGGRAPH 2021):
5
- a coarse FLAME mesh plus a person-specific detail (wrinkle) displacement map
6
- that can be re-posed with new expressions. Runs on the native python of the
7
- `plx1029/comfyui-qwen` RunPod image (python 3.12, torch 2.8+cu128) —
8
- **no venv, no conda, torch is not reinstalled**.
9
-
10
- ## Install (once per pod, ~2 min + ~450 MB downloads)
11
-
12
- ```bash
13
- ./deca_install.sh
14
- ```
15
-
16
- Everything lands in `$DECA_HOME` (default `/workspace/deca`). Re-running is
17
- safe; completed steps are skipped. The FLAME 2020 model is **reused from the
18
- p3dm install** if present (same file MICA uses); otherwise it is downloaded
19
- with FLAME account creds (`FLAME_USERNAME`/`FLAME_PASSWORD` env vars, free
20
- account at https://flame.is.tue.mpg.de). `deca_model.tar` comes from the
21
- authors' official Google Drive via gdown.
22
-
23
- ## Run (~15 s per photo on H100; first run +30 s for CUDA JIT compile and the
24
- face-alignment s3fd detector weights download)
25
-
26
- ```bash
27
- ./deca_run.sh photo.jpg # -> /workspace/outputs/deca/photo/
28
- ./deca_run.sh photo.jpg -o /some/dir --force
29
- ```
30
-
31
- Outputs: `<name>.obj` (coarse FLAME mesh, 5023 verts, with UV texture
32
- extracted from the photo — obj/mtl/png opens textured in Blender),
33
- `<name>_detail.obj` (dense mesh with wrinkles baked in), `<name>_normals.png`
34
- (uv-space detail normal map, referenced by the mtl as displacement),
35
- `<name>_vis.jpg` + `<name>_vis_original_size.jpg` (input | 2d/3d landmarks |
36
- coarse shape | detail shape | depth), `<name>_depth.jpg`.
37
-
38
- Plus `raw/` with the machine-readable results: `<name>.mat` (all regressed
39
- FLAME codes — shape 100, expression 50, pose 6, camera, spherical-harmonics
40
- lighting 27, detail latent 128 — plus vertices, transformed vertices, 2d/3d
41
- landmarks, uv texture/displacement maps), `kpt2d`/`kpt3d` txt, and every
42
- per-part image (cropped and original-resolution renders).
43
-
44
- Pipeline: FAN (face-alignment) crop → ResNet50 encoder → FLAME decoder +
45
- detail displacement decoder → textured render with DECA's own CUDA
46
- "standard" rasterizer (JIT-compiled; enables original-resolution rendering).
47
-
48
- ## What the installer patches (vs. upstream repo)
49
-
50
- Upstream targets python 3.7 / torch 1.6 / numpy 1.x / cuda 10.2 + gcc-7. To
51
- run on the native py3.12 / torch 2.8 / numpy 2.1 env:
52
-
53
- - `face_alignment.LandmarksType._2D` → `.TWO_D` (face-alignment ≥ 1.4)
54
- - numpy 2: `np.int` alias removed → `np.int64` (decalib/utils/util.py)
55
- - standard rasterizer CUDA source: `Tensor::data<T>()` → `data_ptr<T>()`,
56
- `.type()` → `.scalar_type()` in `AT_DISPATCH`, build flags `-std=c++14
57
- -ccbin=gcc-7` → `-std=c++17` (torch 2.x requirement)
58
- - **chumpy** installed `--no-deps` and patched (`getargspec` →
59
- `getfullargspec`, numpy alias imports) — only needed to unpickle FLAME
60
- (already done identically by p3dm_install.sh; skipped if present)
61
- - `TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1` at runtime (repo predates torch 2.6
62
- `weights_only` default)
63
- - pip runs against a constraints file pinning the already-installed
64
- numpy/torch/opencv/kornia/… so the ComfyUI env is never disturbed; the only
65
- new packages are `face-alignment`, `yacs` (and `chumpy` if p3dm absent) —
66
- pytorch3d is NOT required (DECA's own rasterizer is used)
67
-
68
- Optional not installed: `FLAME_albedo_from_BFM.npz` (needs a Basel Face Model
69
- registration) would enable `--useTex` FLAME albedo output; instead the run
70
- uses `--extractTex`, which extracts the UV texture directly from the photo
71
- (higher fidelity for the visible area, mean-texture fill elsewhere).
72
-
73
- ## Licenses
74
-
75
- DECA code is MIT, but the FLAME model (registration required) and
76
- `deca_model.tar` are downloaded from their official sources at install time
77
- and are **not** redistributed in this repo. Check the
78
- [DECA license](https://github.com/yfeng95/DECA/blob/master/LICENSE) for
79
- usage terms of the pretrained model.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deca/deca_install.sh DELETED
@@ -1,144 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # deca_install.sh — one-shot installer for DECA (single photo -> detailed
4
- # 3D FLAME face mesh with animatable expression wrinkles)
5
- #
6
- # Installs https://github.com/yfeng95/DECA into the NATIVE python environment
7
- # (no venv, no conda, no torch reinstall). Tested on: Ubuntu 24.04,
8
- # python 3.12, torch 2.8.0+cu128, H100 (RunPod image plx1029/comfyui-qwen).
9
- #
10
- # Idempotent: safe to re-run; finished steps are skipped.
11
- #
12
- # Requirements:
13
- # - torch with CUDA already installed (the script will not install torch)
14
- # - nvcc in PATH (needed once for the standard-rasterizer JIT compile)
15
- # - FLAME 2020 model: reused from an existing p3dm install if present,
16
- # otherwise downloaded with FLAME account creds (https://flame.is.tue.mpg.de)
17
- # via env vars FLAME_USERNAME / FLAME_PASSWORD (prompts if unset).
18
- #
19
- # Usage:
20
- # ./deca_install.sh
21
- #
22
- # Layout (override with DECA_HOME):
23
- # $DECA_HOME/DECA code + weights (deca_model.tar, FLAME 2020)
24
- # $DECA_HOME/staging per-image input staging
25
- # $DECA_HOME/results per-image raw demo outputs
26
- # =============================================================================
27
- set -euo pipefail
28
-
29
- DECA_HOME="${DECA_HOME:-/workspace/deca}"
30
- REPO="$DECA_HOME/DECA"
31
-
32
- log() { echo -e "\n\033[1;36m[deca-install]\033[0m $*"; }
33
-
34
- mkdir -p "$DECA_HOME" "$DECA_HOME/staging" "$DECA_HOME/results"
35
- cd "$DECA_HOME"
36
-
37
- # ---------------------------------------------------------------- sanity checks
38
- log "checking python/torch/nvcc"
39
- python3 - <<'EOF'
40
- import torch
41
- assert torch.cuda.is_available(), "CUDA torch required"
42
- print(f" torch {torch.__version__} cuda {torch.version.cuda} ok")
43
- EOF
44
- command -v nvcc >/dev/null || echo " WARNING: nvcc not found - standard rasterizer JIT compile will fail at runtime"
45
-
46
- # ---------------------------------------------------- constraints (protect env)
47
- # Pin the currently-installed versions of shared heavyweight packages so pip
48
- # cannot up/downgrade them while installing DECA deps (keeps ComfyUI safe).
49
- log "generating pip constraints from current env"
50
- python3 - > "$DECA_HOME/constraints.txt" <<'EOF'
51
- from importlib.metadata import version, PackageNotFoundError
52
- for p in ["numpy","torch","torchvision","pillow","scipy","scikit-image",
53
- "opencv-python","kornia","imageio","requests"]:
54
- try: print(f"{p}=={version(p)}")
55
- except PackageNotFoundError: pass
56
- EOF
57
- CONS="-c $DECA_HOME/constraints.txt"
58
-
59
- # ------------------------------------------------------------------ pip deps
60
- # Most deps (kornia, scikit-image, opencv, imageio, ninja) are already in the
61
- # native env (or from p3dm_install.sh). face-alignment provides the FAN
62
- # detector used for cropping; yacs the config system.
63
- log "installing python dependencies (native env, constrained)"
64
- pip install -q $CONS face-alignment yacs ninja gdown
65
-
66
- # chumpy: only needed to unpickle FLAME. Needs patching for py>=3.11 / numpy>=2.
67
- if ! python3 -c "import chumpy" 2>/dev/null; then
68
- log "installing + patching chumpy for py3.12/numpy2"
69
- pip install -q --no-deps --no-build-isolation chumpy
70
- CHDIR=$(python3 -c "import importlib.util,os; print(os.path.dirname(importlib.util.find_spec('chumpy').origin))")
71
- grep -rl "inspect.getargspec" "$CHDIR"/*.py | xargs -r sed -i "s/inspect\.getargspec/inspect.getfullargspec/g"
72
- sed -i "s/^from numpy import bool, int, float, complex, object, unicode, str, nan, inf$/from numpy import nan, inf/" "$CHDIR/__init__.py"
73
- python3 -c "import chumpy" || { echo "chumpy patch failed"; exit 1; }
74
- fi
75
-
76
- # ------------------------------------------------------------------ DECA
77
- if [ ! -d "$REPO" ]; then
78
- log "cloning DECA"
79
- git clone --depth 1 https://github.com/yfeng95/DECA.git "$REPO"
80
- fi
81
-
82
- # ------------------------------------------- py3.12 / numpy2 / torch2.8 patches
83
- log "applying compat patches"
84
- # face-alignment >=1.4 renamed the enum
85
- sed -i 's/face_alignment\.LandmarksType\._2D/face_alignment.LandmarksType.TWO_D/' \
86
- "$REPO/decalib/datasets/detectors.py"
87
- # numpy 2 removed the np.int alias
88
- sed -i 's/\.astype(np\.int)/.astype(np.int64)/g' "$REPO/decalib/utils/util.py"
89
- # torch 2.x removed Tensor::data<T>() and .type() in AT_DISPATCH; old flags
90
- # assumed cuda 10.2 + gcc-7
91
- sed -i 's/\.data<scalar_t>()/.data_ptr<scalar_t>()/g; s/\.data<int>()/.data_ptr<int>()/g; s/\.data<float>()/.data_ptr<float>()/g' \
92
- "$REPO/decalib/utils/rasterizer/standard_rasterize_cuda_kernel.cu" \
93
- "$REPO/decalib/utils/rasterizer/standard_rasterize_cuda.cpp"
94
- sed -i '/AT_DISPATCH/s/\.type()/.scalar_type()/' \
95
- "$REPO/decalib/utils/rasterizer/standard_rasterize_cuda_kernel.cu"
96
- sed -i "s/extra_cuda_cflags = \['-std=c++14', '-ccbin=\\\$\\\$(which gcc-7)'\])/extra_cuda_cflags = ['-std=c++17'])/" \
97
- "$REPO/decalib/utils/renderer.py"
98
- grep -q "std=c++17" "$REPO/decalib/utils/renderer.py" || { echo "renderer patch failed"; exit 1; }
99
-
100
- # ------------------------------------------------------------------ FLAME 2020
101
- if [ ! -f "$REPO/data/generic_model.pkl" ]; then
102
- P3DM_FLAME="/workspace/p3dm/pixel3dmm/src/pixel3dmm/preprocessing/MICA/data/FLAME2020/generic_model.pkl"
103
- if [ -f "$P3DM_FLAME" ]; then
104
- log "reusing FLAME 2020 from p3dm install"
105
- cp "$P3DM_FLAME" "$REPO/data/generic_model.pkl"
106
- else
107
- log "downloading FLAME 2020 (requires FLAME account)"
108
- if [ -z "${FLAME_USERNAME:-}" ]; then read -rp "FLAME username: " FLAME_USERNAME; fi
109
- if [ -z "${FLAME_PASSWORD:-}" ]; then read -rsp "FLAME password: " FLAME_PASSWORD; echo; fi
110
- urle () { local LANG=C i x; for (( i = 0; i < ${#1}; i++ )); do x="${1:i:1}"; [[ "${x}" == [a-zA-Z0-9.~-] ]] && echo -n "${x}" || printf '%%%02X' "'${x}"; done; }
111
- U=$(urle "$FLAME_USERNAME"); P=$(urle "$FLAME_PASSWORD")
112
- wget -q --post-data "username=$U&password=$P" 'https://download.is.tue.mpg.de/download.php?domain=flame&sfile=FLAME2020.zip&resume=1' -O "$REPO/data/FLAME2020.zip" --no-check-certificate --continue
113
- unzip -o -q "$REPO/data/FLAME2020.zip" -d "$REPO/data/FLAME2020_tmp"
114
- mv "$REPO/data/FLAME2020_tmp/generic_model.pkl" "$REPO/data/generic_model.pkl"
115
- rm -rf "$REPO/data/FLAME2020.zip" "$REPO/data/FLAME2020_tmp"
116
- fi
117
- [ -f "$REPO/data/generic_model.pkl" ] || { echo "FLAME download failed"; exit 1; }
118
- fi
119
-
120
- # ------------------------------------------------------------- model weights
121
- if [ ! -s "$REPO/data/deca_model.tar" ]; then
122
- log "downloading deca_model.tar (~430 MB, official Google Drive)"
123
- gdown 1rp8kdyLPvErw2dTmqtjISRVvQLj6Yzje -O "$REPO/data/deca_model.tar" -q
124
- fi
125
- [ -s "$REPO/data/deca_model.tar" ] || { echo "deca_model.tar download failed"; exit 1; }
126
-
127
- # ------------------------------------------------------------------ verify
128
- log "verifying imports + files"
129
- REPO_DIR="$REPO" python3 - <<'EOF'
130
- import os, sys, warnings
131
- warnings.filterwarnings("ignore")
132
- import chumpy, face_alignment, yacs, kornia
133
- repo = os.environ["REPO_DIR"]
134
- sys.path.insert(0, repo)
135
- from decalib.utils.config import cfg
136
- for f in ["data/deca_model.tar", "data/generic_model.pkl",
137
- "data/head_template.obj", "data/landmark_embedding.npy",
138
- "data/fixed_displacement_256.npy", "data/texture_data_256.npy"]:
139
- p = os.path.join(repo, f)
140
- assert os.path.getsize(p) > 0, p
141
- print(" all good")
142
- EOF
143
-
144
- log "install complete. run: ./deca_run.sh <photo.jpg>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
deca/deca_run.sh DELETED
@@ -1,96 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # deca_run.sh — single photo in -> detailed 3D FLAME face mesh out (DECA)
4
- #
5
- # Usage:
6
- # ./deca_run.sh <photo.(jpg|png)> [-o <output_dir>] [--force]
7
- #
8
- # -o DIR output directory (default: /workspace/outputs/deca/<name>)
9
- # --force redo reconstruction even if cached results exist
10
- #
11
- # Outputs (in <output_dir>):
12
- # <name>.obj coarse FLAME mesh with UV texture (obj/mtl/png
13
- # triplet + normal map, opens textured in Blender)
14
- # <name>_detail.obj detailed mesh (expression wrinkles as displacement)
15
- # <name>.mtl, <name>.png, <name>_normals.png texture + uv normal map
16
- # <name>_vis.jpg input | landmarks 2d/3d | coarse | detail | depth
17
- # <name>_vis_original_size.jpg same, rendered at input resolution
18
- # <name>_depth.jpg rendered depth map
19
- # raw/ machine-readable results: <name>.mat (all FLAME
20
- # codes: shape/exp/pose/cam/light/detail + verts,
21
- # landmarks, uv maps), kpt2d/kpt3d txt, per-part
22
- # images (crop + original-size renders)
23
- #
24
- # Requires deca_install.sh to have been run first.
25
- # =============================================================================
26
- set -euo pipefail
27
-
28
- DECA_HOME="${DECA_HOME:-/workspace/deca}"
29
- REPO="$DECA_HOME/DECA"
30
-
31
- IMG="" ; OUT="" ; FORCE=0
32
- while [ $# -gt 0 ]; do
33
- case "$1" in
34
- -o) OUT="$2"; shift 2;;
35
- --force) FORCE=1; shift;;
36
- -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
37
- *) IMG="$1"; shift;;
38
- esac
39
- done
40
- [ -n "$IMG" ] && [ -f "$IMG" ] || { echo "usage: $0 <photo.jpg> [-o outdir] [--force]"; exit 1; }
41
- [ -f "$REPO/data/deca_model.tar" ] || { echo "DECA not installed - run deca_install.sh first"; exit 1; }
42
-
43
- # sanitized job name from the file stem (used as folder + file identifier)
44
- STEM="$(basename "$IMG")"; STEM="${STEM%.*}"
45
- NAME="$(echo "$STEM" | tr -c 'a-zA-Z0-9' '_' | sed 's/_*$//;s/^_*//' | cut -c1-60)"
46
- OUT="${OUT:-/workspace/outputs/deca/$NAME}"
47
-
48
- export TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1 # repo predates torch>=2.6 weights_only default
49
- if [ -z "${TORCH_CUDA_ARCH_LIST:-}" ]; then
50
- export TORCH_CUDA_ARCH_LIST="$(python3 -c "import torch; print('%d.%d+PTX' % torch.cuda.get_device_capability())")"
51
- fi
52
-
53
- log() { echo -e "\033[1;35m[deca]\033[0m $*"; }
54
- T0=$(date +%s)
55
-
56
- # stage the photo in a folder named after the job (demo processes a folder)
57
- STAGE="$DECA_HOME/staging/$NAME"
58
- RES="$DECA_HOME/results/$NAME"
59
- if [ "$FORCE" = 1 ]; then rm -rf "$STAGE" "$RES"; fi
60
- mkdir -p "$STAGE"
61
- cp -f "$IMG" "$STAGE/$NAME.${IMG##*.}"
62
-
63
- cd "$REPO"
64
- log "reconstructing (FAN crop -> DECA encode/decode -> standard rasterizer render)"
65
- if [ ! -f "$RES/$NAME/$NAME.obj" ]; then
66
- python3 demos/demo_reconstruct.py -i "$STAGE" -s "$RES" \
67
- --rasterizer_type standard --render_orig 1 \
68
- --saveDepth 1 --saveKpt 1 --saveObj 1 --saveMat 1 --saveImages 1 --saveVis 1 \
69
- > /dev/null 2>&1 \
70
- || { echo "reconstruction failed, rerunning verbosely:"; \
71
- python3 demos/demo_reconstruct.py -i "$STAGE" -s "$RES" \
72
- --rasterizer_type standard --render_orig 1 \
73
- --saveDepth 1 --saveKpt 1 --saveObj 1 --saveMat 1 --saveImages 1 --saveVis 1; exit 1; }
74
- fi
75
- [ -f "$RES/$NAME/$NAME.obj" ] || { echo "ERROR: no face detected / reconstruction failed for $IMG"; exit 1; }
76
-
77
- # --------------------------------------------------------------- final outputs
78
- mkdir -p "$OUT" "$OUT/raw"
79
- cp "$RES/$NAME/$NAME.obj" "$OUT/$NAME.obj"
80
- cp "$RES/$NAME/${NAME}_detail.obj" "$OUT/${NAME}_detail.obj"
81
- cp "$RES/$NAME/$NAME.png" "$OUT/$NAME.png"
82
- cp "$RES/$NAME/${NAME}_normals.png" "$OUT/${NAME}_normals.png"
83
- # the generated mtl embeds an absolute path for the displacement map -> relative
84
- sed "s|disp .*${NAME}_normals\.png|disp ${NAME}_normals.png|" "$RES/$NAME/$NAME.mtl" > "$OUT/$NAME.mtl"
85
- cp "$RES/${NAME}_vis.jpg" "$OUT/${NAME}_vis.jpg"
86
- [ -f "$RES/${NAME}_vis_original_size.jpg" ] && cp "$RES/${NAME}_vis_original_size.jpg" "$OUT/${NAME}_vis_original_size.jpg"
87
- cp "$RES/$NAME/${NAME}_depth.jpg" "$OUT/${NAME}_depth.jpg"
88
-
89
- # raw machine-readable results (FLAME codes, landmarks, per-part images)
90
- cp "$RES/$NAME/$NAME.mat" "$OUT/raw/"
91
- cp "$RES/$NAME/${NAME}_kpt2d.txt" "$OUT/raw/"
92
- cp "$RES/$NAME/${NAME}_kpt3d.txt" "$OUT/raw/"
93
- cp "$RES/$NAME/"*.jpg "$OUT/raw/" 2>/dev/null || true
94
-
95
- log "done in $(( $(date +%s) - T0 ))s -> $OUT"
96
- ls -la "$OUT"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
facelift/README.md DELETED
@@ -1,115 +0,0 @@
1
- # facelift — FaceLift single photo → 3D Gaussian-splat head
2
-
3
- One photo in, a full 3D head as a Gaussian splat out, using
4
- [FaceLift](https://github.com/weijielyu/FaceLift) (Lyu et al., ICCV 2025,
5
- UC Merced / Adobe Research): an Era3D-style multi-view diffusion model
6
- generates 6 consistent head views (incl. the unseen back), then a GS-LRM
7
- transformer regresses a 3D Gaussian splat from them. Runs against the native
8
- python of the `plx1029/comfyui-qwen` RunPod image (python 3.12, torch
9
- 2.8+cu128) — **torch is never reinstalled**.
10
-
11
- ## Environment: venv with `--system-site-packages` (why not fully native)
12
-
13
- FaceLift's custom multi-view UNet subclasses diffusers-0.30-era internals
14
- (`LoRACompatibleConv`, `ImagePositionalEmbeddings`, private `load_state_dict`)
15
- and imports `CLIPFeatureExtractor`, which the image's transformers 5.x
16
- removed. Pinning those in the native env would downgrade
17
- transformers 5.12 → 4.44 and huggingface-hub 1.x → 0.x globally and risk
18
- breaking ComfyUI. So the installer creates
19
- `/workspace/facelift/venv` with `--system-site-packages`: torch, torchvision,
20
- numpy, pillow, scipy, einops etc. are reused from the image, and only the
21
- pinned diffusion stack (diffusers 0.30.3, transformers 4.44.2, tokenizers
22
- 0.19, hub 0.36) plus small pure-python deps are layered venv-locally. The
23
- native env is untouched. A constraints file pinning the native versions
24
- guards every pip call from shadowing numpy/torch/….
25
-
26
- ## Install (once per pod, ~5 min + ~10 GB downloads, no accounts needed)
27
-
28
- ```bash
29
- ./facelift_install.sh
30
- ```
31
-
32
- Everything lands in `$FACELIFT_HOME` (default `/workspace/facelift`).
33
- Re-running is safe; completed steps are skipped. Weights come from the
34
- authors' public HF repo [wlyu/OpenFaceLift](https://huggingface.co/wlyu/OpenFaceLift)
35
- (~9.3 GB: 6-view unCLIP SD2.1 pipeline + GS-LRM checkpoint), plus VGG19
36
- matconvnet weights (~548 MB, loss init), u2net.onnx (~176 MB, rembg) and
37
- MTCNN weights — all cached persistently under
38
- `/workspace/facelift/weights_cache`.
39
-
40
- ## Run (~2.5 min per photo on H100: ~35 s model load + diffusion + orbit export)
41
-
42
- ```bash
43
- ./facelift_run.sh photo.jpg # -> /workspace/outputs/facelift/photo/
44
- ./facelift_run.sh photo.jpg -o /some/root # root dir; per-image subfolder inside
45
- ./facelift_run.sh photo.jpg --seed 7 --steps 75 --guidance 3.0
46
- ./facelift_run.sh photo.jpg --render_res 2048 --n_orbit_views 16
47
- ./facelift_run.sh photo.jpg --no-crop # input already a centered head shot
48
- ```
49
-
50
- Inference + export pass live in `facelift_infer.py` (a companion script that
51
- wraps upstream `inference.py`'s model init and adds the export stage — the
52
- upstream repo stays unpatched). Output folders are **never overwritten**: an
53
- existing `<stem>/` gets `_v2`, `_v3`, … suffixes.
54
-
55
- Per-image output layout (`<root>/<stem>[_vN]/`):
56
-
57
- - `orbit/` — **primary output, for KeenTools FaceBuilder / mesh fitting**:
58
- ~36 orbit renders of the splat (34 unique — the two frontal extras at ±45°
59
- coincide with the 22.5° ring and are deduplicated): eye-level ring every
60
- 22.5° (16), +20° ring every 45° (8), −15° ring every 45° (8), frontal
61
- extras at yaw ±15°/±45°. Rendered natively at 2048² (no upscaling) as
62
- lossless sRGB-tagged RGBA PNG with clean alpha (black/white two-pass
63
- composite difference, un-premultiplied). `orbit/gray/` holds the same
64
- frames flattened on neutral 18 % gray (FaceBuilder dislikes hard
65
- white/black edges). Filenames `orbit_e{+EL}_y{YAW:03d}.png`, sortable.
66
- - `opencv_cameras.json` — intrinsics + extrinsics (OpenCV `w2c`, same schema
67
- as `data_sample/gslrm/*/opencv_cameras.json`) for every orbit view, so the
68
- renders are directly usable for 2DGS/GOF/COLMAP-style reconstruction.
69
- - `mvdiff/cam_000..005.png` — the 6 diffusion-generated views
70
- - `gaussians.ply` — filtered splat, standard 3DGS PLY (SuperSplat/Blender)
71
- - `input_processed.png` — the cropped/aligned image the model consumed
72
- - `turntable.mp4` — eye-level 360°, 60 frames @ 30 fps, QC eyeballing only
73
- - `run_meta.json` — seed, guidance, steps, render args, FaceLift git commit,
74
- torch/diffusers/transformers versions, elapsed time (determinism record)
75
-
76
- Pipeline: rembg background removal + MTCNN face crop → 6-view unCLIP
77
- diffusion (512², 75 steps default) → GS-LRM splat regression (fp16 autocast)
78
- → opacity/scale/floater filtering → orbit export + turntable via the repo's
79
- own diff-gaussian-rasterization renderer (`render_opencv_cam`) — no extra
80
- renderer dependency. Orbit cameras reuse the repo's rig convention
81
- (radius 2.7, hfov 50°, azimuth 270 = frontal).
82
-
83
- ## What the installer does differently vs. upstream `setup_env.sh`
84
-
85
- Upstream force-reinstalls torch 2.4/cu124 into the env. Instead:
86
-
87
- - venv `--system-site-packages`; torch/torchvision/numpy/… reused (see above)
88
- - `accelerate>=1.2` instead of upstream's `accelerate==0.33.0`, which pins
89
- numpy<2 (image has numpy 2.1); 1.x is API-compatible here
90
- - `xformers==0.0.32.post2` installed `--no-deps` — the prebuilt wheel matching
91
- torch 2.8.0+cu128 (upstream's 0.0.27 targets torch 2.4). Both the MV-UNet's
92
- custom XFormers attention processors and GS-LRM's flash attention use it
93
- - `facenet-pytorch` installed `--no-deps` (it pins its own torch)
94
- - **diff-gaussian-rasterization** built from source with `#include <cstdint>`
95
- prepended to `cuda_rasterizer/rasterizer_impl.h` (upstream fails to compile
96
- with gcc-13), `--no-build-isolation` so it compiles against the native torch
97
- - `opencv-python-headless` (no GUI libs) instead of `opencv-python`
98
- - `scikit-image` latest instead of upstream's 0.21 pin (py3.8-era, won't
99
- build on 3.12); `lpips`, `pytorch-msssim`, `easydict`, `plyfile`,
100
- `videoio`, `matplotlib`, `jaxtyping`, `rembg`+`onnxruntime` as upstream
101
- - no wandb/gradio/ffmpeg-python (training/webui-only; ffmpeg binary is native)
102
-
103
- Runtime (in `facelift_run.sh`): `TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1` (repo
104
- predates the torch 2.6 `weights_only` default), `VGG19_WEIGHTS_PATH` /
105
- `U2NET_HOME` / `TORCH_HOME` pointed at the persistent `weights_cache/`.
106
- No source patches to FaceLift itself. The run script calls `inference.main()`
107
- programmatically because the CLI's `--auto_crop` flag is `store_true` with
108
- `default=True` and can never be disabled from the command line.
109
-
110
- ## Licenses
111
-
112
- Code is Apache-2.0 (Adobe Inc.). The model weights are under the
113
- **Adobe Research License v1.2 (research / non-commercial use only)** and are
114
- downloaded from the authors' official HF repo at install time, not
115
- redistributed here.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
facelift/facelift_infer.py DELETED
@@ -1,312 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- facelift_infer.py — FaceLift single-image inference + export pass.
4
-
5
- Wraps the upstream FaceLift inference.py (model init + preprocessing +
6
- 6-view diffusion + GS-LRM) and adds an automatic export pass per image:
7
-
8
- outputs/{image_stem}[_vN]/
9
- orbit/ orbit renders of the splat, RGBA PNG + 18%-gray
10
- orbit/gray/ flattened copies (same filenames)
11
- mvdiff/ the 6 diffusion-generated views (cam_000..005.png)
12
- gaussians.ply filtered 3D Gaussian splat (standard 3DGS layout)
13
- opencv_cameras.json intrinsics + extrinsics of every orbit view
14
- input_processed.png the cropped/aligned image the model consumed
15
- turntable.mp4 eye-level 360°, 60 frames @ 30 fps (QC only)
16
- run_meta.json seed / guidance / steps / git commit / versions
17
-
18
- Orbit rig (~36 views, deduplicated): eye-level ring every 22.5° (16 views),
19
- +20° ring every 45° (8), -15° ring every 45° (8), plus frontal extras at
20
- yaw ±15°/±45° eye-level. Yaw 0 = frontal; azimuth convention matches the
21
- repo's own turntable (azimuth 270 = front). Renders use the repo's
22
- gslrm renderer (render_opencv_cam) — no extra renderer dependency; alpha
23
- comes from a black/white two-pass composite difference.
24
-
25
- Never overwrites: an existing output folder gets _v2, _v3, ... suffixes.
26
- """
27
-
28
- import argparse
29
- import json
30
- import os
31
- import subprocess
32
- import sys
33
- import time
34
- import warnings
35
-
36
- FACELIFT_HOME = os.environ.get("FACELIFT_HOME", "/workspace/facelift")
37
- REPO = os.environ.get("FACELIFT_REPO", os.path.join(FACELIFT_HOME, "FaceLift"))
38
- sys.path.insert(0, REPO)
39
-
40
- import numpy as np
41
- import torch
42
- from PIL import Image
43
- from easydict import EasyDict as edict
44
- from einops import rearrange
45
-
46
- from inference import ( # upstream FaceLift
47
- DEFAULT_IMG_SIZE,
48
- initialize_face_detector,
49
- initialize_gslrm_model,
50
- initialize_mvdiffusion_pipeline,
51
- get_model_paths,
52
- setup_camera_parameters,
53
- )
54
- from gslrm.model.gaussians_renderer import (
55
- imageseq2video,
56
- render_opencv_cam,
57
- render_turntable,
58
- )
59
- from utils_folder.face_utils import preprocess_image, preprocess_image_without_cropping
60
-
61
- warnings.filterwarnings("ignore")
62
-
63
- ORBIT_RADIUS = 2.7 # matches the repo's input cameras + turntable
64
- ORBIT_HFOV = 50.0 # deg, matches fx=548.99@512
65
- FRONT_AZIMUTH = 270.0 # repo convention: azimuth 270 = frontal view
66
- GRAY_18 = 118.0 / 255.0 # neutral 18% gray in sRGB
67
-
68
- try: # sRGB ICC tag for every PNG (lossless; FaceBuilder-friendly)
69
- from PIL import ImageCms
70
- _SRGB = ImageCms.ImageCmsProfile(ImageCms.createProfile("sRGB")).tobytes()
71
- except Exception:
72
- _SRGB = None
73
-
74
-
75
- def save_png(array_uint8: np.ndarray, path: str) -> None:
76
- img = Image.fromarray(array_uint8)
77
- if _SRGB:
78
- img.save(path, format="PNG", icc_profile=_SRGB)
79
- else:
80
- img.save(path, format="PNG")
81
-
82
-
83
- def build_orbit_views(n_eye: int) -> list:
84
- """(elevation, yaw) pairs; yaw 0 = front. Duplicates (by rounded degree) removed."""
85
- views = []
86
- for yaw in np.linspace(0, 360, n_eye, endpoint=False):
87
- views.append((0.0, float(yaw)))
88
- for elev in (20.0, -15.0):
89
- for yaw in np.linspace(0, 360, max(n_eye // 2, 1), endpoint=False):
90
- views.append((elev, float(yaw)))
91
- for yaw in (15.0, -15.0, 45.0, -45.0): # frontal-hemisphere extras
92
- views.append((0.0, float(yaw) % 360.0))
93
- seen, out = set(), []
94
- for elev, yaw in views:
95
- key = (int(round(elev)), int(round(yaw)) % 360)
96
- if key not in seen:
97
- seen.add(key)
98
- out.append((elev, yaw))
99
- return out
100
-
101
-
102
- def orbit_camera(elev_deg: float, yaw_deg: float, res: int):
103
- """OpenCV-convention c2w + pinhole intrinsics for one orbit view.
104
-
105
- Same math as the repo's get_turntable_cameras, plus elevation."""
106
- fx = res / (2.0 * np.tan(np.deg2rad(ORBIT_HFOV) / 2.0))
107
- fxfycxcy = np.array([fx, fx, res / 2.0, res / 2.0], dtype=np.float64)
108
- azim = np.deg2rad(FRONT_AZIMUTH + yaw_deg)
109
- elev = np.deg2rad(elev_deg)
110
- cam_pos = np.array([
111
- ORBIT_RADIUS * np.cos(elev) * np.cos(azim),
112
- ORBIT_RADIUS * np.cos(elev) * np.sin(azim),
113
- ORBIT_RADIUS * np.sin(elev),
114
- ])
115
- forward = -cam_pos / np.linalg.norm(cam_pos)
116
- up_vector = np.array([0.0, 0.0, 1.0])
117
- right = np.cross(forward, up_vector)
118
- right = right / np.linalg.norm(right)
119
- up = np.cross(right, forward)
120
- up = up / np.linalg.norm(up)
121
- c2w = np.eye(4)
122
- c2w[:3, :3] = np.stack((right, -up, forward), axis=1)
123
- c2w[:3, 3] = cam_pos
124
- return c2w, fxfycxcy
125
-
126
-
127
- def render_rgba(pc, res: int, c2w: torch.Tensor, fxfycxcy: torch.Tensor):
128
- """Two-pass render -> (rgb premultiplied, alpha), both [*, H, W] float in [0,1].
129
-
130
- black-bg pass gives premultiplied color C0; white-bg pass C1 = C0 + (1-A),
131
- so A = 1 - (C1 - C0)."""
132
- with torch.no_grad():
133
- c0 = render_opencv_cam(pc, res, res, c2w, fxfycxcy, bg_color=(0.0, 0.0, 0.0))["render"]
134
- c1 = render_opencv_cam(pc, res, res, c2w, fxfycxcy, bg_color=(1.0, 1.0, 1.0))["render"]
135
- alpha = (1.0 - (c1 - c0).clamp(0.0, 1.0).mean(dim=0, keepdim=True)).clamp(0.0, 1.0)
136
- return c0.clamp(0.0, 1.0), alpha
137
-
138
-
139
- def export_orbit_pass(pc, out_dir: str, render_res: int, n_orbit_views: int) -> list:
140
- """Render the orbit rig, save RGBA + gray PNGs, return camera frames."""
141
- orbit_dir = os.path.join(out_dir, "orbit")
142
- os.makedirs(os.path.join(orbit_dir, "gray"), exist_ok=True)
143
- device = pc._xyz.device
144
- frames = []
145
- for elev, yaw in build_orbit_views(n_orbit_views):
146
- c2w_np, fxfycxcy_np = orbit_camera(elev, yaw, render_res)
147
- c2w = torch.from_numpy(c2w_np).float().to(device)
148
- fxfycxcy = torch.from_numpy(fxfycxcy_np).float().to(device)
149
- premult, alpha = render_rgba(pc, render_res, c2w, fxfycxcy)
150
-
151
- # un-premultiply for a clean-alpha RGBA (straight color under the matte)
152
- straight = torch.where(alpha > 1e-4, premult / alpha.clamp(min=1e-4),
153
- torch.zeros_like(premult)).clamp(0.0, 1.0)
154
- rgba = torch.cat([straight, alpha], dim=0)
155
- rgba = (rearrange(rgba, "c h w -> h w c").cpu().numpy() * 255.0).round().astype(np.uint8)
156
- gray = premult + (1.0 - alpha) * GRAY_18
157
- gray = (rearrange(gray.clamp(0, 1), "c h w -> h w c").cpu().numpy() * 255.0).round().astype(np.uint8)
158
-
159
- name = f"orbit_e{int(round(elev)):+03d}_y{int(round(yaw)) % 360:03d}.png"
160
- save_png(rgba, os.path.join(orbit_dir, name))
161
- save_png(gray, os.path.join(orbit_dir, "gray", name))
162
-
163
- w2c = np.linalg.inv(c2w_np)
164
- frames.append({
165
- "w": render_res, "h": render_res,
166
- "fx": float(fxfycxcy_np[0]), "fy": float(fxfycxcy_np[1]),
167
- "cx": float(fxfycxcy_np[2]), "cy": float(fxfycxcy_np[3]),
168
- "w2c": w2c.tolist(),
169
- "file_path": f"orbit/{name}",
170
- })
171
- torch.cuda.empty_cache()
172
- return frames
173
-
174
-
175
- def versioned_dir(root: str, stem: str) -> str:
176
- """outputs/{stem}, else _v2, _v3, ... — never overwrite."""
177
- out = os.path.join(root, stem)
178
- version = 2
179
- while os.path.exists(out):
180
- out = os.path.join(root, f"{stem}_v{version}")
181
- version += 1
182
- return out
183
-
184
-
185
- def git_commit(path: str) -> str:
186
- try:
187
- return subprocess.check_output(
188
- ["git", "-C", path, "rev-parse", "HEAD"], text=True).strip()
189
- except Exception:
190
- return "unknown"
191
-
192
-
193
- def main():
194
- parser = argparse.ArgumentParser(description="FaceLift inference + export pass")
195
- parser.add_argument("--input", "-i", required=True, help="input photo (jpg/png)")
196
- parser.add_argument("--output_root", "-o", default="/workspace/outputs/facelift",
197
- help="parent output dir; per-image subfolder is created inside")
198
- parser.add_argument("--seed", type=int, default=4)
199
- parser.add_argument("--guidance_scale_2D", type=float, default=3.0)
200
- parser.add_argument("--step_2D", type=int, default=75)
201
- parser.add_argument("--render_res", type=int, default=2048,
202
- help="orbit render resolution (splats are resolution-independent)")
203
- parser.add_argument("--n_orbit_views", type=int, default=16,
204
- help="views in the eye-level ring; +20/-15 rings get half each")
205
- parser.add_argument("--no_crop", action="store_true", help="skip MTCNN auto-crop")
206
- args = parser.parse_args()
207
-
208
- t0 = time.time()
209
- device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
210
- stem = os.path.splitext(os.path.basename(args.input))[0]
211
- out_dir = versioned_dir(args.output_root, stem)
212
- os.makedirs(os.path.join(out_dir, "mvdiff"), exist_ok=True)
213
-
214
- # ------------------------------------------------------------- models
215
- mvdiff_ckpt, gslrm_ckpt, gslrm_cfg = get_model_paths()
216
- pipeline, generator, prompt_embeds = initialize_mvdiffusion_pipeline(mvdiff_ckpt, device)
217
- gslrm_model = initialize_gslrm_model(gslrm_ckpt, gslrm_cfg, device)
218
- fxfycxcy_in, c2w_in = setup_camera_parameters(device)
219
- torch.manual_seed(args.seed)
220
- generator.manual_seed(args.seed)
221
-
222
- # ------------------------------------------------------- preprocess
223
- image_np = np.array(Image.open(args.input))
224
- if args.no_crop:
225
- input_image = preprocess_image_without_cropping(image_np)
226
- else:
227
- try:
228
- input_image = preprocess_image(image_np)
229
- except Exception as exc:
230
- print(f"auto-crop failed ({exc}); falling back to no-crop preprocessing")
231
- input_image = preprocess_image_without_cropping(image_np)
232
- input_image.save(os.path.join(out_dir, "input_processed.png"),
233
- format="PNG", **({"icc_profile": _SRGB} if _SRGB else {}))
234
-
235
- # ------------------------------------------------- 6-view diffusion
236
- mv_imgs = pipeline(
237
- input_image, None,
238
- prompt_embeds=prompt_embeds,
239
- guidance_scale=args.guidance_scale_2D,
240
- num_images_per_prompt=1,
241
- num_inference_steps=args.step_2D,
242
- generator=generator,
243
- eta=1.0,
244
- ).images
245
- if len(mv_imgs) == 7:
246
- views = [mv_imgs[i] for i in (1, 2, 3, 4, 5, 6)]
247
- elif len(mv_imgs) == 6:
248
- views = list(mv_imgs)
249
- else:
250
- raise ValueError(f"Unexpected number of views: {len(mv_imgs)}")
251
- for i, view in enumerate(views):
252
- view.save(os.path.join(out_dir, "mvdiff", f"cam_{i:03d}.png"),
253
- format="PNG", **({"icc_profile": _SRGB} if _SRGB else {}))
254
-
255
- # ------------------------------------------------------------ GS-LRM
256
- lrm_input = np.stack([np.array(v) for v in views], axis=0)
257
- lrm_input = torch.from_numpy(lrm_input).float()[None].to(device) / 255.0
258
- lrm_input = rearrange(lrm_input, "b v h w c -> b v c h w")
259
- index = torch.stack([
260
- torch.zeros(lrm_input.size(1)).long(),
261
- torch.arange(lrm_input.size(1)).long(),
262
- ], dim=-1)[None].to(device)
263
- batch = edict({"image": lrm_input, "c2w": c2w_in, "fxfycxcy": fxfycxcy_in, "index": index})
264
- with torch.autocast(enabled=True, device_type="cuda", dtype=torch.float16):
265
- result = gslrm_model.forward(batch, create_visual=False, split_data=True)
266
-
267
- # same filtering as upstream inference.py, then splat + all renders share it
268
- pc = result.gaussians[0].apply_all_filters(
269
- opacity_thres=0.04,
270
- scaling_thres=0.1,
271
- floater_thres=0.6,
272
- crop_bbx=[-0.91, 0.91, -0.91, 0.91, -1.0, 1.0],
273
- cam_origins=None,
274
- nearfar_percent=(0.0001, 1.0),
275
- )
276
- pc.save_ply(os.path.join(out_dir, "gaussians.ply"))
277
-
278
- # ------------------------------------------------------- export pass
279
- print(f"orbit export: {args.render_res}x{args.render_res} RGBA")
280
- frames = export_orbit_pass(pc, out_dir, args.render_res, args.n_orbit_views)
281
- with open(os.path.join(out_dir, "opencv_cameras.json"), "w") as f:
282
- json.dump({"id": os.path.basename(out_dir), "frames": frames}, f, indent=2)
283
-
284
- # eye-level 360° turntable, QC only
285
- strip = render_turntable(pc, rendering_resolution=DEFAULT_IMG_SIZE, num_views=60)
286
- video = np.ascontiguousarray(rearrange(strip, "h (v w) c -> v h w c", v=60))
287
- imageseq2video(video, os.path.join(out_dir, "turntable.mp4"), fps=30)
288
-
289
- with open(os.path.join(out_dir, "run_meta.json"), "w") as f:
290
- json.dump({
291
- "input": os.path.abspath(args.input),
292
- "seed": args.seed,
293
- "guidance_scale_2D": args.guidance_scale_2D,
294
- "step_2D": args.step_2D,
295
- "render_res": args.render_res,
296
- "n_orbit_views": args.n_orbit_views,
297
- "num_orbit_renders": len(frames),
298
- "auto_crop": not args.no_crop,
299
- "facelift_git_commit": git_commit(REPO),
300
- "versions": {
301
- "torch": torch.__version__,
302
- "diffusers": __import__("diffusers").__version__,
303
- "transformers": __import__("transformers").__version__,
304
- },
305
- "elapsed_seconds": round(time.time() - t0, 1),
306
- }, f, indent=2)
307
-
308
- print(f"done in {time.time() - t0:.0f}s -> {out_dir}")
309
-
310
-
311
- if __name__ == "__main__":
312
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
facelift/facelift_install.sh DELETED
@@ -1,190 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # facelift_install.sh — one-shot installer for FaceLift (single photo ->
4
- # 3D Gaussian-splat head + turntable video)
5
- #
6
- # Installs https://github.com/weijielyu/FaceLift (ICCV 2025, UC Merced/Adobe).
7
- # Pipeline: multi-view diffusion (Era3D-style unCLIP SD2.1) generates 6 head
8
- # views -> GS-LRM transformer regresses a 3D Gaussian splat of the full head.
9
- #
10
- # Env strategy: a venv with --system-site-packages. The native image packages
11
- # (torch 2.8.0+cu128, torchvision, numpy 2.1, pillow, scipy, einops, ...) are
12
- # REUSED — torch is never reinstalled. Only the version-pinned diffusion stack
13
- # (diffusers 0.30.3, transformers 4.44.2, hub 0.x) is layered inside the venv,
14
- # because FaceLift's custom UNet/pipeline hooks diffusers-0.30-era internals
15
- # and the image's transformers 5.x removed CLIPFeatureExtractor. The native
16
- # env is left 100% untouched (ComfyUI safe).
17
- #
18
- # Tested on: Ubuntu 24.04, python 3.12, torch 2.8.0+cu128, H100
19
- # (RunPod image plx1029/comfyui-qwen).
20
- #
21
- # Idempotent: safe to re-run; finished steps are skipped.
22
- #
23
- # Requirements:
24
- # - torch with CUDA already installed (never reinstalled)
25
- # - nvcc in PATH (compiles diff-gaussian-rasterization once)
26
- # - ~11 GB disk for weights (public downloads, no account needed)
27
- #
28
- # Usage:
29
- # ./facelift_install.sh
30
- #
31
- # Layout (override with FACELIFT_HOME):
32
- # $FACELIFT_HOME/FaceLift code + checkpoints (~9.3 GB, wlyu/OpenFaceLift)
33
- # $FACELIFT_HOME/venv --system-site-packages venv
34
- # $FACELIFT_HOME/weights_cache VGG19 .mat, u2net.onnx, MTCNN weights
35
- # $FACELIFT_HOME/staging|results per-image staging / raw outputs
36
- # =============================================================================
37
- set -euo pipefail
38
-
39
- FACELIFT_HOME="${FACELIFT_HOME:-/workspace/facelift}"
40
- REPO="$FACELIFT_HOME/FaceLift"
41
- VENV="$FACELIFT_HOME/venv"
42
- VPY="$VENV/bin/python"
43
- VPIP="$VENV/bin/pip"
44
-
45
- log() { echo -e "\n\033[1;36m[facelift-install]\033[0m $*"; }
46
-
47
- mkdir -p "$FACELIFT_HOME" "$FACELIFT_HOME/staging" "$FACELIFT_HOME/results" \
48
- "$FACELIFT_HOME/weights_cache/u2net" "$FACELIFT_HOME/weights_cache/torch"
49
- cd "$FACELIFT_HOME"
50
-
51
- # ---------------------------------------------------------------- sanity checks
52
- log "checking python/torch/nvcc"
53
- python3 - <<'EOF'
54
- import torch
55
- assert torch.cuda.is_available(), "CUDA torch required"
56
- print(f" torch {torch.__version__} cuda {torch.version.cuda} ok")
57
- EOF
58
- command -v nvcc >/dev/null || { echo "ERROR: nvcc not found (needed to compile diff-gaussian-rasterization)"; exit 1; }
59
-
60
- # ------------------------------------------------------------------ repo
61
- if [ ! -d "$REPO" ]; then
62
- log "cloning FaceLift"
63
- git clone --depth 1 https://github.com/weijielyu/FaceLift "$REPO"
64
- fi
65
-
66
- # ------------------------------------------------------------------ venv
67
- # --system-site-packages: torch/torchvision/numpy/pillow/scipy/einops etc. come
68
- # from the native image; venv-local installs shadow only what must differ.
69
- if [ ! -x "$VPY" ]; then
70
- log "creating venv (system-site-packages, reuses native torch 2.8)"
71
- python3 -m venv --system-site-packages "$VENV"
72
- fi
73
-
74
- # ---------------------------------------------------- constraints (protect env)
75
- # Pin the native versions of shared packages so pip can never pull a different
76
- # (venv-local) numpy/torch/... that would shadow the native ones.
77
- log "generating pip constraints from native env"
78
- python3 - > "$FACELIFT_HOME/constraints.txt" <<'EOF'
79
- from importlib.metadata import version, PackageNotFoundError
80
- for p in ["numpy","torch","torchvision","pillow","scipy","einops","kornia",
81
- "safetensors","regex","requests","rich","tqdm","psutil","pyyaml",
82
- "typing-extensions","packaging","sympy","networkx","jinja2",
83
- "filelock","fsspec","charset-normalizer","idna","urllib3","certifi"]:
84
- try: print(f"{p}=={version(p)}")
85
- except PackageNotFoundError: pass
86
- EOF
87
- CONS="-c $FACELIFT_HOME/constraints.txt"
88
-
89
- # ------------------------------------------------------------------ pip deps
90
- # Pinned diffusion stack: FaceLift's custom MV-UNet imports diffusers-0.30
91
- # internals (LoRACompatibleConv, ImagePositionalEmbeddings, private
92
- # load_state_dict) and transformers-4.x CLIPFeatureExtractor — both gone in
93
- # the image's diffusers-latest/transformers-5. transformers 4.44.2 pulls a
94
- # matching tokenizers 0.19 + huggingface-hub 0.x into the venv. accelerate
95
- # is NOT the upstream 0.33 pin (that needs numpy<2): any >=1.2 works.
96
- log "installing pinned diffusion stack into venv"
97
- "$VPIP" install -q $CONS "diffusers==0.30.3" "transformers==4.44.2" "accelerate>=1.2"
98
-
99
- log "installing remaining python deps (native versions reused where possible)"
100
- "$VPIP" install -q $CONS "opencv-python-headless==4.10.0.84" "lpips==0.1.4" \
101
- pytorch-msssim easydict plyfile matplotlib scikit-image jaxtyping ninja \
102
- "videoio==0.3.0"
103
- "$VPIP" install -q $CONS rembg onnxruntime
104
- # facenet-pytorch pins its own torch -> --no-deps (runtime deps already present)
105
- "$VPIP" install -q --no-deps facenet-pytorch
106
- # xformers: prebuilt wheel matching torch 2.8.0+cu128; --no-deps so pip cannot
107
- # touch torch. If the native torch changes minor version, bump this pin.
108
- "$VPY" -c "import xformers" 2>/dev/null || "$VPIP" install -q --no-deps "xformers==0.0.32.post2"
109
-
110
- # ---------------------------------------- diff-gaussian-rasterization (CUDA)
111
- # Upstream source misses <cstdint> includes -> fails with gcc-13; patch + build.
112
- if ! "$VPY" -c "import diff_gaussian_rasterization" 2>/dev/null; then
113
- log "building diff-gaussian-rasterization (one-time CUDA compile)"
114
- if [ ! -d "$FACELIFT_HOME/diff-gaussian-rasterization" ]; then
115
- git clone --recursive --depth 1 https://github.com/graphdeco-inria/diff-gaussian-rasterization \
116
- "$FACELIFT_HOME/diff-gaussian-rasterization"
117
- fi
118
- DGR="$FACELIFT_HOME/diff-gaussian-rasterization"
119
- grep -q "#include <cstdint>" "$DGR/cuda_rasterizer/rasterizer_impl.h" \
120
- || sed -i '1i #include <cstdint>' "$DGR/cuda_rasterizer/rasterizer_impl.h"
121
- ARCH="$(python3 -c "import torch; print('%d.%d+PTX' % torch.cuda.get_device_capability())")"
122
- TORCH_CUDA_ARCH_LIST="${TORCH_CUDA_ARCH_LIST:-$ARCH}" \
123
- "$VPIP" install -q --no-build-isolation "$DGR"
124
- fi
125
-
126
- # ------------------------------------------------------------- model weights
127
- # ~9.3 GB from the authors' public HF repo (Adobe Research License).
128
- if [ ! -s "$REPO/checkpoints/gslrm/ckpt_0000000000021125.pt" ] \
129
- || [ ! -d "$REPO/checkpoints/mvdiffusion/pipeckpts" ]; then
130
- log "downloading FaceLift checkpoints (~9.3 GB, wlyu/OpenFaceLift)"
131
- "$VPY" - <<EOF
132
- from huggingface_hub import snapshot_download
133
- snapshot_download(repo_id="wlyu/OpenFaceLift",
134
- local_dir="$REPO/checkpoints",
135
- local_dir_use_symlinks=False)
136
- EOF
137
- fi
138
-
139
- # VGG19 matconvnet weights (~548 MB): GSLRM's perceptual loss downloads these
140
- # at model init even for inference; cache them persistently in /workspace.
141
- VGG="$FACELIFT_HOME/weights_cache/imagenet-vgg-verydeep-19.mat"
142
- if [ ! -s "$VGG" ]; then
143
- log "downloading VGG19 weights (~548 MB)"
144
- wget -q "https://www.vlfeat.org/matconvnet/models/imagenet-vgg-verydeep-19.mat" -O "$VGG"
145
- fi
146
-
147
- # u2net.onnx (~176 MB, rembg background removal) + MTCNN face-detector weights:
148
- # prefetch into the persistent cache so runs work without re-downloading.
149
- if [ ! -s "$FACELIFT_HOME/weights_cache/u2net/u2net.onnx" ]; then
150
- log "prefetching u2net.onnx (rembg)"
151
- U2NET_HOME="$FACELIFT_HOME/weights_cache/u2net" "$VPY" -c "from rembg import new_session; new_session('u2net')"
152
- fi
153
- TORCH_HOME="$FACELIFT_HOME/weights_cache/torch" "$VPY" -c "
154
- import warnings; warnings.filterwarnings('ignore')
155
- from facenet_pytorch import MTCNN; MTCNN()" >/dev/null 2>&1 || true
156
-
157
- # ------------------------------------------------- companion inference script
158
- # facelift_infer.py (inference + orbit-render export pass) ships alongside
159
- # these scripts; copy it in from the download location or fetch from the repo.
160
- if [ ! -s "$FACELIFT_HOME/facelift_infer.py" ]; then
161
- SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
162
- if [ -s "$SRC_DIR/facelift_infer.py" ]; then
163
- cp "$SRC_DIR/facelift_infer.py" "$FACELIFT_HOME/"
164
- else
165
- log "fetching facelift_infer.py from HF"
166
- wget -q "https://huggingface.co/aleph65/ComfyUI/resolve/main/facelift/facelift_infer.py" \
167
- -O "$FACELIFT_HOME/facelift_infer.py"
168
- fi
169
- fi
170
-
171
- # ------------------------------------------------------------------ verify
172
- log "verifying imports + files"
173
- cd "$REPO"
174
- TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1 TORCH_HOME="$FACELIFT_HOME/weights_cache/torch" "$VPY" - <<'EOF'
175
- import os, warnings
176
- warnings.filterwarnings("ignore")
177
- import diffusers, transformers, xformers, diff_gaussian_rasterization, cv2, rembg
178
- from mvdiffusion.pipelines.pipeline_mvdiffusion_unclip import StableUnCLIPImg2ImgPipeline
179
- from gslrm.model.gaussians_renderer import render_turntable, imageseq2video
180
- from utils_folder.face_utils import preprocess_image
181
- from facenet_pytorch import MTCNN
182
- for f in ["checkpoints/gslrm/ckpt_0000000000021125.pt",
183
- "checkpoints/mvdiffusion/pipeckpts/unet/diffusion_pytorch_model.safetensors",
184
- "mvdiffusion/data/fixed_prompt_embeds_6view/clr_embeds.pt"]:
185
- assert os.path.getsize(f) > 0, f
186
- print(f" diffusers {diffusers.__version__} transformers {transformers.__version__} xformers {xformers.__version__}")
187
- print(" all good")
188
- EOF
189
-
190
- log "install complete. run: ./facelift_run.sh <photo.jpg>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
facelift/facelift_run.sh DELETED
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # facelift_run.sh — single photo in -> 3D Gaussian-splat head + export pass
4
- #
5
- # Usage:
6
- # ./facelift_run.sh <photo.(jpg|png)> [-o <output_root>] [options]
7
- #
8
- # -o DIR output ROOT (default: /workspace/outputs/facelift);
9
- # results land in <root>/<image-stem>/ — never overwritten,
10
- # an existing folder gets a _v2, _v3, ... suffix
11
- # --seed N diffusion seed (default: 4)
12
- # --steps N diffusion steps (default: 75)
13
- # --guidance F classifier-free guidance scale (default: 3.0)
14
- # --render_res N orbit render resolution (default: 2048)
15
- # --n_orbit_views N eye-level ring view count (default: 16)
16
- # --no-crop skip MTCNN auto-crop (input already a centered head shot)
17
- #
18
- # Outputs (in <root>/<stem>[_vN]/):
19
- # orbit/ ~36 orbit renders of the splat for mesh fitting
20
- # (KeenTools FaceBuilder etc.): eye-level ring every 22.5°,
21
- # +20° and -15° rings every 45°, frontal extras at ±15°/±45°.
22
- # 2048² RGBA PNG, clean alpha, sRGB tagged;
23
- # orbit/gray/ = same frames flattened on neutral 18% gray
24
- # opencv_cameras.json intrinsics + extrinsics (OpenCV w2c) of every orbit
25
- # view — directly usable for 2DGS/GOF/COLMAP-style recon
26
- # mvdiff/ the 6 diffusion-generated views (cam_000..005.png)
27
- # gaussians.ply filtered 3D Gaussian splat (standard 3DGS PLY)
28
- # input_processed.png the cropped/aligned image the model consumed
29
- # turntable.mp4 eye-level 360°, 60 frames @ 30 fps (QC eyeballing)
30
- # run_meta.json seed/guidance/steps/render args/git commit/versions
31
- #
32
- # Requires facelift_install.sh to have been run first.
33
- # ~2.5 min per photo on H100 (~35 s model load + diffusion + orbit export).
34
- # =============================================================================
35
- set -euo pipefail
36
-
37
- FACELIFT_HOME="${FACELIFT_HOME:-/workspace/facelift}"
38
- REPO="$FACELIFT_HOME/FaceLift"
39
- VPY="$FACELIFT_HOME/venv/bin/python"
40
- INFER="$FACELIFT_HOME/facelift_infer.py"
41
-
42
- IMG="" ; OUT="/workspace/outputs/facelift" ; SEED=4 ; STEPS=75 ; GUIDANCE=3.0
43
- RENDER_RES=2048 ; N_ORBIT=16 ; CROP_FLAG=""
44
- while [ $# -gt 0 ]; do
45
- case "$1" in
46
- -o) OUT="$2"; shift 2;;
47
- --seed) SEED="$2"; shift 2;;
48
- --steps) STEPS="$2"; shift 2;;
49
- --guidance) GUIDANCE="$2"; shift 2;;
50
- --render_res) RENDER_RES="$2"; shift 2;;
51
- --n_orbit_views) N_ORBIT="$2"; shift 2;;
52
- --no-crop) CROP_FLAG="--no_crop"; shift;;
53
- -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
54
- *) IMG="$1"; shift;;
55
- esac
56
- done
57
- [ -n "$IMG" ] && [ -f "$IMG" ] || { echo "usage: $0 <photo.jpg> [-o outroot] [--seed N] [--steps N] [--guidance F] [--render_res N] [--n_orbit_views N] [--no-crop]"; exit 1; }
58
- [ -x "$VPY" ] && [ -s "$REPO/checkpoints/gslrm/ckpt_0000000000021125.pt" ] \
59
- || { echo "FaceLift not installed - run facelift_install.sh first"; exit 1; }
60
- [ -s "$INFER" ] || { echo "missing $INFER - download it alongside these scripts"; exit 1; }
61
-
62
- export TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1 # repo predates torch>=2.6 weights_only default
63
- export VGG19_WEIGHTS_PATH="$FACELIFT_HOME/weights_cache/imagenet-vgg-verydeep-19.mat"
64
- export U2NET_HOME="$FACELIFT_HOME/weights_cache/u2net"
65
- export TORCH_HOME="$FACELIFT_HOME/weights_cache/torch"
66
- export FACELIFT_HOME
67
-
68
- log() { echo -e "\033[1;35m[facelift]\033[0m $*"; }
69
- T0=$(date +%s)
70
-
71
- mkdir -p "$OUT"
72
- log "reconstructing (rembg+MTCNN -> 6-view diffusion -> GS-LRM splat -> orbit export)"
73
- cd "$REPO"
74
- "$VPY" "$INFER" -i "$IMG" -o "$OUT" --seed "$SEED" --step_2D "$STEPS" \
75
- --guidance_scale_2D "$GUIDANCE" --render_res "$RENDER_RES" \
76
- --n_orbit_views "$N_ORBIT" $CROP_FLAG
77
-
78
- # newest matching output folder (the infer script versions _v2, _v3, ...)
79
- STEM="$(basename "$IMG")"; STEM="${STEM%.*}"
80
- FINAL="$(ls -dt "$OUT/$STEM" "$OUT/$STEM"_v* 2>/dev/null | head -1)"
81
- [ -n "$FINAL" ] && [ -f "$FINAL/gaussians.ply" ] \
82
- || { echo "ERROR: reconstruction failed for $IMG (no face detected?)"; exit 1; }
83
-
84
- log "done in $(( $(date +%s) - T0 ))s -> $FINAL"
85
- ls -la "$FINAL"; echo "orbit renders: $(ls "$FINAL/orbit" | grep -c '^orbit_')"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ffhq-uv/README.md DELETED
@@ -1,77 +0,0 @@
1
- # ffhq-uv — FFHQ-UV single-photo → 3D face mesh + UV texture
2
-
3
- One photo in, fitted 3D face mesh (HiFi3D++ topology) plus a 1024×1024 facial
4
- UV-texture map out, using [FFHQ-UV](https://github.com/csbhr/FFHQ-UV)
5
- (Bai et al., CVPR 2023) RGB fitting. Runs on the native python of the
6
- `plx1029/comfyui-qwen` RunPod image (python 3.12, torch 2.8+cu128) —
7
- **no venv, no conda, torch is not reinstalled**.
8
-
9
- ## Install (once per pod, ~3 min + ~2.5 GB downloads)
10
-
11
- ```bash
12
- ./ffhq_uv_install.sh
13
- ```
14
-
15
- Everything lands in `$FFHQUV_HOME` (default `/workspace/ffhq-uv`). Re-running
16
- is safe; completed steps are skipped. Checkpoints and topology assets are
17
- fetched from the **official Hugging Face mirror** (`csbhr/FFHQ-UV`) — no
18
- Baidu/OneDrive account needed, and no registration (unlike FLAME/p3dm).
19
-
20
- ## Run (~2 min per photo on H100)
21
-
22
- ```bash
23
- ./ffhq_uv_run.sh photo.jpg # -> /workspace/outputs/ffhq-uv/photo/
24
- ./ffhq_uv_run.sh photo.jpg -o /some/dir --force
25
- ```
26
-
27
- Outputs: `<name>_uv.png` (1024×1024 UV-texture map — the headline output),
28
- `<name>_id.obj` (neutral/identity head mesh, HiFi3D++ topology, 20481 verts),
29
- `<name>_exp.obj` (mesh with the photo's expression), `<name>.mtl` (links the
30
- meshes to the texture — the obj/mtl/png triplet opens textured in Blender or
31
- any obj viewer), `<name>_vis.png` (input | skin mask | parsing | landmarks |
32
- fitted render | uv), `<name>_input_vis.png` (preprocessing check).
33
-
34
- Plus a `raw/` subfolder with the complete machine-readable fitting results:
35
- all three stages' meshes/coeffs (`stage*_coeffs.pt`: HiFi3D++ id/exp/tex
36
- coefficients, pose, lighting, predicted 68 landmarks; stages 2-3 also GAN
37
- latents `latents_z`/`latents_w`), per-step optimization visuals, fitting log,
38
- tensorboard events, the aligned 224×224 input tensors (`input_data.pt`,
39
- `preprocess/<name>_processed.pt`: image, skin mask, parse mask, landmarks,
40
- arcface warp matrix).
41
-
42
- Pipeline: MTCNN 5-pt + 68-landmark detection (TF frozen graphs, CPU) → skin
43
- mask + face parsing → similarity alignment to 224×224 → Deep3D (HiFi3D++
44
- basis) coefficient init → StyleGAN2-based texture-GAN latent search (spherical
45
- surface, fixed shape, 100 steps) → joint texture+shape optimization (200
46
- steps, arcface id loss + VGG/LPIPS + photometric + landmark losses, rendered
47
- with nvdiffrast CUDA rasterizer).
48
-
49
- ## What the installer does differently (vs. upstream repo)
50
-
51
- Upstream targets python 3.7 / torch 1.7.1 / TF 1.15 + conda. To run on the
52
- native py3.12 / torch 2.8 / numpy 2.1 env:
53
-
54
- - **checkpoints from the official HF mirror** (`csbhr/FFHQ-UV` dataset repo)
55
- instead of Baidu Netdisk/OneDrive; only the ~2.5 GB the RGB-fitting
56
- pipeline needs (not the 300+ GB dataset, not the StyleGAN/e4e/DPR editing
57
- checkpoints used only for dataset creation)
58
- - **tensorflow-cpu** (modern 2.x) instead of tensorflow-gpu 1.15; the two TF1
59
- frozen-graph detectors (MTCNN, 68-landmarks) are run through the
60
- `tf.compat.v1` API — the only source patch needed. CPU on purpose: seconds
61
- of inference, and no CUDA/cuDNN conflicts with torch
62
- - **no pytorch3d needed** (RGB fitting only uses nvdiffrast, already present
63
- from p3dm; the installer installs it if missing); stylegan2 custom CUDA ops
64
- JIT-compile on first use and fall back to reference implementations if the
65
- build fails
66
- - `TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1` at runtime (repo predates torch 2.6
67
- `weights_only` default)
68
- - pip runs against a constraints file pinning the already-installed
69
- numpy/torch/opencv/kornia/… so the ComfyUI env is never disturbed; only two
70
- new packages are actually installed (`dominate`, `tensorflow-cpu`)
71
-
72
- ## Licenses
73
-
74
- FFHQ-UV code and checkpoints are for **non-commercial research** use (see the
75
- [upstream repo](https://github.com/csbhr/FFHQ-UV)); checkpoints are downloaded
76
- from the authors' official Hugging Face mirror at install time and are not
77
- redistributed here. HiFi3D++ topology © Tencent AI Lab (REALY).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ffhq-uv/ffhq_uv_install.sh DELETED
@@ -1,151 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # ffhq_uv_install.sh — one-shot installer for FFHQ-UV RGB fitting
4
- # (single photo -> 3D face mesh + 1024x1024 UV texture)
5
- #
6
- # Installs https://github.com/csbhr/FFHQ-UV (RGB_Fitting pipeline, HiFi3D++
7
- # topology + GAN texture decoder) into the NATIVE python environment (no venv,
8
- # no conda, no torch reinstall). Tested on: Ubuntu 24.04, python 3.12,
9
- # torch 2.8.0+cu128, H100 (RunPod image plx1029/comfyui-qwen).
10
- #
11
- # Idempotent: safe to re-run; finished steps are skipped.
12
- #
13
- # Requirements:
14
- # - torch with CUDA already installed (the script will not install torch)
15
- # - ~2.5 GB disk for checkpoints (downloaded from the official HF mirror
16
- # csbhr/FFHQ-UV — no Baidu/OneDrive account needed)
17
- #
18
- # Usage:
19
- # ./ffhq_uv_install.sh
20
- #
21
- # Layout (override with FFHQUV_HOME):
22
- # $FFHQUV_HOME/FFHQ-UV code + checkpoints + topo assets
23
- # $FFHQUV_HOME/staging per-image input staging
24
- # $FFHQUV_HOME/fitting_output per-image fitting results
25
- # =============================================================================
26
- set -euo pipefail
27
-
28
- FFHQUV_HOME="${FFHQUV_HOME:-/workspace/ffhq-uv}"
29
- REPO="$FFHQUV_HOME/FFHQ-UV"
30
-
31
- log() { echo -e "\n\033[1;36m[ffhq-uv-install]\033[0m $*"; }
32
-
33
- mkdir -p "$FFHQUV_HOME" "$FFHQUV_HOME/staging" "$FFHQUV_HOME/fitting_output"
34
- cd "$FFHQUV_HOME"
35
-
36
- # ---------------------------------------------------------------- sanity checks
37
- log "checking python/torch"
38
- python3 - <<'EOF'
39
- import torch
40
- assert torch.cuda.is_available(), "CUDA torch required"
41
- print(f" torch {torch.__version__} cuda {torch.version.cuda} ok")
42
- EOF
43
-
44
- # ---------------------------------------------------- constraints (protect env)
45
- # Pin the currently-installed versions of shared heavyweight packages so pip
46
- # cannot up/downgrade them while installing FFHQ-UV deps (keeps ComfyUI safe).
47
- log "generating pip constraints from current env"
48
- python3 - > "$FFHQUV_HOME/constraints.txt" <<'EOF'
49
- from importlib.metadata import version, PackageNotFoundError
50
- for p in ["numpy","torch","torchvision","pillow","scipy","scikit-image",
51
- "opencv-python","kornia","tensorboard","huggingface_hub",
52
- "transformers","safetensors","requests"]:
53
- try: print(f"{p}=={version(p)}")
54
- except PackageNotFoundError: pass
55
- EOF
56
- CONS="-c $FFHQUV_HOME/constraints.txt"
57
-
58
- # ------------------------------------------------------------------ pip deps
59
- # Almost everything (opencv, kornia, scikit-image, tensorboard, nvdiffrast) is
60
- # already in the native env. Only two new packages are needed:
61
- # dominate - tiny, HTML logging used by the fitting logger
62
- # tensorflow-cpu - runs the two frozen-graph detectors (MTCNN + 68 landmarks)
63
- # in step1 preprocessing; CPU build on purpose (a few sec of
64
- # inference, avoids CUDA/cuDNN clashes with torch)
65
- log "installing python dependencies (native env, constrained)"
66
- python3 -c "import dominate" 2>/dev/null || pip install -q $CONS dominate
67
- python3 -c "import tensorflow" 2>/dev/null || pip install -q $CONS tensorflow-cpu
68
-
69
- # nvdiffrast: normally already present (e.g. from p3dm_install.sh)
70
- python3 -c "import nvdiffrast" 2>/dev/null || {
71
- log "installing nvdiffrast"
72
- pip install -q $CONS ninja
73
- pip install -q $CONS --no-build-isolation "git+https://github.com/NVlabs/nvdiffrast.git"
74
- }
75
-
76
- # ------------------------------------------------------------------ FFHQ-UV
77
- if [ ! -d "$REPO" ]; then
78
- log "cloning FFHQ-UV"
79
- git clone --depth 1 https://github.com/csbhr/FFHQ-UV.git "$REPO"
80
- fi
81
-
82
- # ------------------------------------------------------- numpy 2 compat patch
83
- # numpy 2 moved VisibleDeprecationWarning into np.exceptions, and no longer
84
- # builds ragged arrays implicitly (t[0]/t[1] are 1-element arrays)
85
- sed -i 's/category=np\.VisibleDeprecationWarning/category=np.exceptions.VisibleDeprecationWarning/;
86
- s/trans_params = np\.array(\[w0, h0, s, t\[0\], t\[1\]\])/trans_params = np.array([w0, h0, s, float(t[0]), float(t[1])])/' \
87
- "$REPO/RGB_Fitting/utils/preprocess_utils.py"
88
-
89
- # --------------------------------------------------- tf1 -> tf2 compat patches
90
- # The MTCNN + 68-landmark detectors are TF1 frozen graphs; modern tensorflow
91
- # runs them through the compat.v1 API with eager execution disabled.
92
- log "applying tensorflow compat patches"
93
- for f in "$REPO/RGB_Fitting/third_party/mtcnn/detect_face_with_mtcnn.py" \
94
- "$REPO/RGB_Fitting/third_party/landmark68/detect_lm68.py"; do
95
- grep -q "tensorflow.compat.v1" "$f" || \
96
- sed -i 's/^import tensorflow as tf$/import tensorflow.compat.v1 as tf; tf.disable_eager_execution()/' "$f"
97
- grep -q "tensorflow.compat.v1" "$f" || { echo "tf compat patch failed: $f"; exit 1; }
98
- done
99
-
100
- # ------------------------------------------------- checkpoints + topo assets
101
- # Official HF mirror of the project (csbhr/FFHQ-UV) hosts all checkpoints and
102
- # topology assets — only the files the RGB-fitting pipeline needs are fetched.
103
- log "downloading checkpoints + topo assets (~2.5 GB, official HF mirror)"
104
- REPO_DIR="$REPO" python3 - <<'EOF'
105
- import os
106
- from huggingface_hub import snapshot_download
107
- snapshot_download(
108
- repo_id="csbhr/FFHQ-UV", repo_type="dataset",
109
- local_dir=os.environ["REPO_DIR"],
110
- allow_patterns=[
111
- "checkpoints/lm_model/68lm_detector.pb",
112
- "checkpoints/mtcnn_model/mtcnn_model.pb",
113
- "checkpoints/parsing_model/79999_iter.pth",
114
- "checkpoints/resnet_model/resnet18-5c106cde.pth",
115
- "checkpoints/deep3d_model/epoch_latest.pth",
116
- "checkpoints/arcface_model/ms1mv3_arcface_r50_fp16_backbone.pth",
117
- "checkpoints/texgan_model/texgan_ffhq_uv.pth",
118
- "checkpoints/vgg_model/vgg16.pt",
119
- "topo_assets/*",
120
- ],
121
- )
122
- print(" checkpoints ok")
123
- EOF
124
-
125
- # ------------------------------------------------------------------ verify
126
- log "verifying imports + files"
127
- REPO_DIR="$REPO" python3 - <<'EOF'
128
- import os, warnings
129
- warnings.filterwarnings("ignore")
130
- os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "3")
131
- import nvdiffrast, kornia, dominate, cv2, skimage
132
- import tensorflow.compat.v1 as tf
133
- assert hasattr(tf, "GraphDef") and hasattr(tf, "InteractiveSession")
134
- repo = os.environ["REPO_DIR"]
135
- for f in ["checkpoints/lm_model/68lm_detector.pb",
136
- "checkpoints/mtcnn_model/mtcnn_model.pb",
137
- "checkpoints/parsing_model/79999_iter.pth",
138
- "checkpoints/resnet_model/resnet18-5c106cde.pth",
139
- "checkpoints/deep3d_model/epoch_latest.pth",
140
- "checkpoints/arcface_model/ms1mv3_arcface_r50_fp16_backbone.pth",
141
- "checkpoints/texgan_model/texgan_ffhq_uv.pth",
142
- "checkpoints/vgg_model/vgg16.pt",
143
- "topo_assets/hifi3dpp_model_info.mat",
144
- "topo_assets/unwrap_1024_info.mat",
145
- "topo_assets/similarity_Lm3D_all.mat"]:
146
- p = os.path.join(repo, f)
147
- assert os.path.getsize(p) > 0, p
148
- print(" all good")
149
- EOF
150
-
151
- log "install complete. run: ./ffhq_uv_run.sh <photo.jpg>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ffhq-uv/ffhq_uv_run.sh DELETED
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # ffhq_uv_run.sh — single photo in -> 3D face mesh + UV texture out (FFHQ-UV)
4
- #
5
- # Usage:
6
- # ./ffhq_uv_run.sh <photo.(jpg|png)> [-o <output_dir>] [--force]
7
- #
8
- # -o DIR output directory (default: /workspace/outputs/ffhq-uv/<name>)
9
- # --force redo preprocessing/fitting even if cached results exist
10
- #
11
- # Outputs (in <output_dir>):
12
- # <name>_uv.png 1024x1024 facial UV-texture map (headline output)
13
- # <name>_id.obj fitted head mesh, identity/neutral (HiFi3D++ topology)
14
- # <name>_exp.obj fitted head mesh with expression
15
- # <name>.mtl material linking the meshes to the UV texture
16
- # <name>_vis.png input | skin mask | parsing | landmarks | render | uv
17
- # <name>_input_vis.png preprocessing visualization (step1)
18
- # raw/ full fitting results (all stages, coeffs, latents,
19
- # logs, per-step visuals) + preprocessed input data
20
- #
21
- # Requires ffhq_uv_install.sh to have been run first.
22
- # =============================================================================
23
- set -euo pipefail
24
-
25
- FFHQUV_HOME="${FFHQUV_HOME:-/workspace/ffhq-uv}"
26
- REPO="$FFHQUV_HOME/FFHQ-UV"
27
- TEXGAN="texgan_ffhq_uv.pth"
28
-
29
- IMG="" ; OUT="" ; FORCE=0
30
- while [ $# -gt 0 ]; do
31
- case "$1" in
32
- -o) OUT="$2"; shift 2;;
33
- --force) FORCE=1; shift;;
34
- -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
35
- *) IMG="$1"; shift;;
36
- esac
37
- done
38
- [ -n "$IMG" ] && [ -f "$IMG" ] || { echo "usage: $0 <photo.jpg> [-o outdir] [--force]"; exit 1; }
39
- [ -d "$REPO" ] || { echo "FFHQ-UV not installed - run ffhq_uv_install.sh first"; exit 1; }
40
-
41
- # sanitized job name from the file stem (used as folder + file identifier)
42
- STEM="$(basename "$IMG")"; STEM="${STEM%.*}"
43
- NAME="$(echo "$STEM" | tr -c 'a-zA-Z0-9' '_' | sed 's/_*$//;s/^_*//' | cut -c1-60)"
44
- OUT="${OUT:-/workspace/outputs/ffhq-uv/$NAME}"
45
-
46
- export TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1 # repo predates torch>=2.6 weights_only default
47
- export TF_CPP_MIN_LOG_LEVEL=3 # silence tensorflow banner
48
- if [ -z "${TORCH_CUDA_ARCH_LIST:-}" ]; then
49
- export TORCH_CUDA_ARCH_LIST="$(python3 -c "import torch; print('%d.%d+PTX' % torch.cuda.get_device_capability())")"
50
- fi
51
-
52
- log() { echo -e "\033[1;35m[ffhq-uv]\033[0m $*"; }
53
- T0=$(date +%s)
54
-
55
- # stage the photo in a folder named after the job (step1 processes a folder)
56
- STAGE="$FFHQUV_HOME/staging/$NAME"
57
- PROC="$STAGE/processed_data"
58
- FITOUT="$FFHQUV_HOME/fitting_output/$NAME"
59
- if [ "$FORCE" = 1 ]; then rm -rf "$STAGE" "$FITOUT"; fi
60
- mkdir -p "$STAGE"
61
- cp -f "$IMG" "$STAGE/$NAME.${IMG##*.}"
62
-
63
- cd "$REPO/RGB_Fitting"
64
-
65
- log "1/2 preprocessing (MTCNN + 68 landmarks, skin mask, face parsing, alignment)"
66
- if [ ! -f "$PROC/$NAME.pt" ]; then
67
- python3 step1_process_data.py --input_dir "$STAGE" --output_dir "$PROC" \
68
- --checkpoints_dir ../checkpoints --topo_dir ../topo_assets > /dev/null 2>&1 \
69
- || { echo "preprocessing failed, rerunning verbosely:"; \
70
- python3 step1_process_data.py --input_dir "$STAGE" --output_dir "$PROC" \
71
- --checkpoints_dir ../checkpoints --topo_dir ../topo_assets; exit 1; }
72
- fi
73
- [ -f "$PROC/$NAME.pt" ] || { echo "ERROR: no face detected / preprocessing failed for $IMG"; exit 1; }
74
-
75
- log "2/2 RGB fitting (deep3d init -> texture search -> joint tex+shape optimization)"
76
- if [ ! -f "$FITOUT/$NAME/stage3_mesh_id.obj" ]; then
77
- python3 step2_fit_processed_data.py --input_dir "$PROC" --output_dir "$FITOUT" \
78
- --checkpoints_dir ../checkpoints --topo_dir ../topo_assets \
79
- --texgan_model_name "$TEXGAN" 2>&1 | grep -E "Stage|stage [0-9]|Fit image|Error|Traceback" || true
80
- fi
81
- RES="$FITOUT/$NAME"
82
- [ -f "$RES/stage3_mesh_id.obj" ] || { echo "ERROR: fitting produced no mesh"; exit 1; }
83
-
84
- # --------------------------------------------------------------- final outputs
85
- mkdir -p "$OUT" "$OUT/raw"
86
- cp "$RES/stage3_uv.png" "$OUT/${NAME}_uv.png"
87
- # meshes + material, renamed with internal references fixed up
88
- sed "s/stage3_mesh\.mlt/${NAME}.mtl/" "$RES/stage3_mesh_id.obj" > "$OUT/${NAME}_id.obj"
89
- sed "s/stage3_mesh\.mlt/${NAME}.mtl/" "$RES/stage3_mesh_exp.obj" > "$OUT/${NAME}_exp.obj"
90
- sed "s/stage3_uv\.png/${NAME}_uv.png/" "$RES/stage3_mesh.mlt" > "$OUT/${NAME}.mtl"
91
- cp "$RES/stage3_vis.png" "$OUT/${NAME}_vis.png"
92
- [ -f "${PROC}_vis/$NAME.png" ] && cp "${PROC}_vis/$NAME.png" "$OUT/${NAME}_input_vis.png"
93
-
94
- # raw fitting + preprocessing data (all stages, coeffs, GAN latents, logs,
95
- # per-step visuals, aligned input tensors) for downstream use
96
- cp -r "$RES/." "$OUT/raw/"
97
- mkdir -p "$OUT/raw/preprocess"
98
- cp "$PROC/$NAME.pt" "$OUT/raw/preprocess/${NAME}_processed.pt"
99
-
100
- log "done in $(( $(date +%s) - T0 ))s -> $OUT"
101
- ls -la "$OUT"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
makeup-extract/README.md DELETED
@@ -1,102 +0,0 @@
1
- # makeup-extract — 3D makeup extraction from a single photo
2
-
3
- Support scripts for [YangXingchao/makeup-extract](https://github.com/YangXingchao/makeup-extract)
4
- (*Makeup Extraction of 3D Representation via Illumination-Aware Image
5
- Decomposition*, Yang, Taketomi, Kanamori — Computer Graphics Forum / Eurographics 2023).
6
-
7
- From one face photo the pipeline reconstructs a coarse FLAME face, unwraps and
8
- completes the UV texture, decomposes it into materials (diffuse albedo, normal,
9
- diffuse/specular shading) and extracts the **makeup as a separate layer**:
10
-
11
- | output | meaning |
12
- |---|---|
13
- | `bare_skin.png` | UV albedo with makeup removed |
14
- | `make_base.png` | makeup base color (UV) |
15
- | `make_alpha.png` | makeup alpha/opacity matte (UV, white = no makeup) |
16
- | `make_blend.png` | premultiplied makeup layer (`base * alpha`, UV) |
17
- | `albe_refine.png`, `norm_refine.png`, `diff_refine.png`, `rnsr_refine.png`, `tex_refine.png`, `completion.png` | refined UV materials / completed texture |
18
- | `img_compose.png`, `img_compose_with_bg.png` | face re-rendered from the decomposed materials (sanity check — should resemble the aligned input) |
19
-
20
- ## Install
21
-
22
- ```bash
23
- ./makeup_extract_install.sh
24
- ```
25
-
26
- One-shot, idempotent, installs into the **native** python env (no venv; a pip
27
- constraints file protects torch/numpy/etc. from being touched). Tested on
28
- Ubuntu 24.04, python 3.12, torch 2.8.0+cu128, H100 (image `plx1029/comfyui-qwen`).
29
-
30
- What it does:
31
-
32
- - apt-installs GL/EGL dev headers (`libglvnd-dev`, mesa dev packages) as a
33
- safety net for nvdiffrast's GL path. Note: current nvdiffrast (2026)
34
- deprecates `RasterizeGLContext` and silently maps it to the CUDA
35
- rasterizer, so the upstream `dr.RasterizeGLContext()` calls run headless
36
- with no OpenGL involved at all.
37
- - pip-installs `ninja`, `gdown`, `nvdiffrast` (from GitHub,
38
- `--no-build-isolation` so it compiles against native torch), and a patched
39
- `chumpy` (py3.12/numpy2 fixes; only needed to unpickle FLAME).
40
- - clones the upstream repo to `$MAKEUP_EXTRACT_HOME/makeup-extract`
41
- (default `/workspace/makeup-extract/makeup-extract`).
42
- - downloads weights:
43
- - pipeline checkpoints (`coarse_reconstruction.pkl`, `uv_completion.pkl`,
44
- `makeup_extraction.pkl`) from the paper's official Google Drive folder;
45
- - FLAME assets `generic_model.pkl` (FLAME2020), `FLAME_masks.pkl`,
46
- `FLAME_texture.npz` — from the **official** site when
47
- `FLAME_USERNAME`/`FLAME_PASSWORD` are set, otherwise from the public HF
48
- mirror `camenduru/show`. Either way the
49
- [FLAME license](https://flame.is.tue.mpg.de/modellicense.html)
50
- (non-commercial scientific research) applies — register at
51
- flame.is.tue.mpg.de;
52
- - `albedoModel2020_FLAME_albedoPart.npz` from the official
53
- [AlbedoMM](https://github.com/waps101/AlbedoMM) GitHub release (its own
54
- non-commercial license applies).
55
-
56
- ## Run
57
-
58
- ```bash
59
- ./makeup_extract_run.sh <photo.jpg> [out_dir]
60
- # e.g.
61
- ./makeup_extract_run.sh /workspace/face2.jpg
62
- ```
63
-
64
- Runs all six upstream steps (preprocess → coarse reconstruction → UV
65
- completion → material refinement → makeup extraction → re-render). Curated
66
- outputs land in `/workspace/outputs/makeup-extract/<stem>/`; raw per-step
67
- outputs are cached in `$MAKEUP_EXTRACT_HOME/results/<stem>/` (delete to
68
- re-run). ~1–2 min per image on an H100 (step 3 is a 500-iteration
69
- optimization; first run adds nvdiffrast JIT compile + VGG19/resnet18 aux
70
- downloads, cached under `weights_cache/torch`).
71
-
72
- ## Deviations from upstream
73
-
74
- - **No upstream file is modified.** All adaptation is done via environment and
75
- wrapper:
76
- - `TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1` at runtime (repo predates the
77
- torch ≥2.6 `torch.load(weights_only=True)` default; checkpoints contain
78
- non-tensor objects).
79
- - `TORCH_HOME` redirected to `weights_cache/torch` so auto-downloaded VGG19 /
80
- resnet18 weights survive pod restarts (`/root` is ephemeral).
81
- - Non-square inputs are padded to square by the run script before step 0
82
- (upstream's landmark scaling assumes square input).
83
- - Dependency versions differ from upstream's `requirements.txt` pins
84
- (numpy 1.23 → 2.1, kornia 0.6 → 0.8, opencv 4.7 → 5.0, scipy native): the
85
- code is compatible as-is; no `np.int`-era aliases are used. `chumpy` is
86
- patched for py3.12/numpy2 (`getargspec` → `getfullargspec`, removed-alias
87
- import) as in the `deca/` implementation.
88
- - FLAME assets fall back to a public HF mirror when no FLAME credentials are
89
- provided (see Install above).
90
-
91
- ## Layout
92
-
93
- ```
94
- /workspace/makeup-extract/
95
- makeup_extract_install.sh this installer
96
- makeup_extract_run.sh single-image runner
97
- makeup-extract/ upstream clone + checkpoints/ + resources/
98
- staging/<stem>/ squared input copy
99
- results/<stem>/ raw per-step outputs (cache)
100
- weights_cache/torch/ VGG19 / resnet18 hub cache
101
- /workspace/outputs/makeup-extract/<stem>/ curated outputs
102
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
makeup-extract/makeup_extract_install.sh DELETED
@@ -1,227 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # makeup_extract_install.sh — one-shot installer for makeup-extract
4
- # (Eurographics 2023: "Makeup Extraction of 3D Representation via
5
- # Illumination-Aware Image Decomposition", Yang et al.)
6
- # single face photo -> FLAME-based UV materials (albedo / normal / shading)
7
- # + extracted makeup layers (bare skin, makeup base, alpha, blend)
8
- # + re-rendered composite.
9
- #
10
- # Installs https://github.com/YangXingchao/makeup-extract into the NATIVE
11
- # python environment (no venv, no conda, no torch reinstall). Tested on:
12
- # Ubuntu 24.04, python 3.12, torch 2.8.0+cu128, H100
13
- # (RunPod image plx1029/comfyui-qwen).
14
- #
15
- # Idempotent: safe to re-run; finished steps are skipped.
16
- #
17
- # Requirements:
18
- # - torch with CUDA already installed (this script will not install torch)
19
- # - nvcc + gcc (nvdiffrast CUDA/GL plugins JIT-compile on first use)
20
- # - NVIDIA EGL driver libs (libEGL_nvidia.so.0) for the headless GL
21
- # rasterizer context — present on this image
22
- # - FLAME assets (see below): official downloads need FLAME account creds
23
- # via env vars FLAME_USERNAME / FLAME_PASSWORD
24
- # (https://flame.is.tue.mpg.de). Without creds the script falls back to
25
- # a public Hugging Face mirror (camenduru/show). Either way you must
26
- # comply with the FLAME license (non-commercial scientific research).
27
- #
28
- # Downloads:
29
- # - pretrained pipeline checkpoints (~1 GB, official Google Drive folder)
30
- # - FLAME2020 generic_model.pkl (~54 MB), FLAME_masks.pkl (~0.2 MB),
31
- # FLAME_texture.npz (~1.3 GB)
32
- # - AlbedoMM albedoModel2020_FLAME_albedoPart.npz (~1.8 GB, official
33
- # GitHub release, CC BY 4.0 non-commercial per AlbedoMM README)
34
- #
35
- # Usage:
36
- # ./makeup_extract_install.sh
37
- #
38
- # Layout (override with MAKEUP_EXTRACT_HOME):
39
- # $MAKEUP_EXTRACT_HOME/makeup-extract upstream clone + weights
40
- # $MAKEUP_EXTRACT_HOME/staging per-image input staging
41
- # $MAKEUP_EXTRACT_HOME/results per-image raw outputs
42
- # $MAKEUP_EXTRACT_HOME/weights_cache torch hub cache (VGG19, resnet18)
43
- # =============================================================================
44
- set -euo pipefail
45
-
46
- if [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0; fi
47
-
48
- M_HOME="${MAKEUP_EXTRACT_HOME:-/workspace/makeup-extract}"
49
- REPO="$M_HOME/makeup-extract"
50
-
51
- log() { echo -e "\n\033[1;36m[makeup-extract-install]\033[0m $*"; }
52
-
53
- mkdir -p "$M_HOME" "$M_HOME/staging" "$M_HOME/results" "$M_HOME/weights_cache"
54
- cd "$M_HOME"
55
-
56
- # ---------------------------------------------------------------- sanity checks
57
- log "checking python/torch/nvcc"
58
- python3 - <<'EOF'
59
- import torch
60
- assert torch.cuda.is_available(), "CUDA torch required"
61
- print(f" torch {torch.__version__} cuda {torch.version.cuda} ok")
62
- EOF
63
- command -v nvcc >/dev/null || echo " WARNING: nvcc not found - nvdiffrast JIT compile will fail at runtime"
64
-
65
- # ---------------------------------------------------- constraints (protect env)
66
- # Pin currently-installed shared heavyweights so pip cannot up/downgrade them
67
- # while installing deps (keeps the ComfyUI env safe).
68
- log "generating pip constraints from current env"
69
- python3 - > "$M_HOME/constraints.txt" <<'EOF'
70
- from importlib.metadata import version, PackageNotFoundError
71
- for p in ["numpy","torch","torchvision","pillow","scipy","kornia",
72
- "opencv-python","opencv-python-headless","requests","einops",
73
- "safetensors","huggingface-hub"]:
74
- try: print(f"{p}=={version(p)}")
75
- except PackageNotFoundError: pass
76
- EOF
77
- CONS="-c $M_HOME/constraints.txt"
78
-
79
- # ------------------------------------------------- system GL/EGL dev packages
80
- # nvdiffrast's GL rasterizer plugin JIT-compiles against EGL/GLES headers.
81
- # The NVIDIA EGL driver (libEGL_nvidia.so.0) is already on the image; we only
82
- # add the glvnd dispatcher + headers.
83
- if ! dpkg -s libglvnd-dev >/dev/null 2>&1; then
84
- log "installing GL/EGL dev packages (apt)"
85
- apt-get update -qq
86
- DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
87
- pkg-config libglvnd0 libgl1 libglx0 libegl1 libgles2 \
88
- libglvnd-dev libgl1-mesa-dev libegl1-mesa-dev libgles2-mesa-dev
89
- else
90
- log "GL/EGL dev packages already installed"
91
- fi
92
-
93
- # ------------------------------------------------------------------ pip deps
94
- log "installing python dependencies (native env, constrained)"
95
- pip install -q $CONS ninja gdown
96
-
97
- if ! python3 -c "import nvdiffrast" 2>/dev/null; then
98
- log "installing nvdiffrast from GitHub (compiles CUDA extension against native torch)"
99
- TORCH_CUDA_ARCH_LIST="$(python3 -c 'import torch; print("%d.%d" % torch.cuda.get_device_capability())')" \
100
- pip install -q $CONS --no-build-isolation "git+https://github.com/NVlabs/nvdiffrast.git"
101
- fi
102
-
103
- # chumpy: only needed to unpickle FLAME. Needs patching for py>=3.11 / numpy>=2.
104
- if ! python3 -c "import chumpy" 2>/dev/null; then
105
- log "installing + patching chumpy for py3.12/numpy2"
106
- pip install -q --no-deps --no-build-isolation chumpy
107
- CHDIR=$(python3 -c "import importlib.util,os; print(os.path.dirname(importlib.util.find_spec('chumpy').origin))")
108
- grep -rl "inspect.getargspec" "$CHDIR"/*.py | xargs -r sed -i "s/inspect\.getargspec/inspect.getfullargspec/g"
109
- sed -i "s/^from numpy import bool, int, float, complex, object, unicode, str, nan, inf$/from numpy import nan, inf/" "$CHDIR/__init__.py"
110
- python3 -c "import chumpy" || { echo "chumpy patch failed"; exit 1; }
111
- fi
112
-
113
- # ------------------------------------------------------------------ repo
114
- if [ ! -d "$REPO" ]; then
115
- log "cloning makeup-extract"
116
- git clone --depth 1 https://github.com/YangXingchao/makeup-extract.git "$REPO"
117
- fi
118
-
119
- # -------------------------------------------------------- pipeline checkpoints
120
- # Official Google Drive folder from the README.
121
- CKPTS="coarse_reconstruction.pkl uv_completion.pkl makeup_extraction.pkl"
122
- need_ckpt=0
123
- for c in $CKPTS; do [ -s "$REPO/checkpoints/$c" ] || need_ckpt=1; done
124
- if [ "$need_ckpt" = 1 ]; then
125
- log "downloading pretrained checkpoints (official Google Drive)"
126
- gdown --folder 1lwkR9JcrbZ7fNylTSJQQEiGnt3s2LQYq -O "$REPO/checkpoints" -q
127
- fi
128
- for c in $CKPTS; do
129
- [ -s "$REPO/checkpoints/$c" ] || { echo "checkpoint $c missing after download"; exit 1; }
130
- done
131
-
132
- # ------------------------------------------------------------------ FLAME assets
133
- RES="$REPO/resources"
134
-
135
- flame_official() { # $1 = sfile, $2 = out zip
136
- urle () { local LANG=C i x; for (( i = 0; i < ${#1}; i++ )); do x="${1:i:1}"; [[ "${x}" == [a-zA-Z0-9.~-] ]] && echo -n "${x}" || printf '%%%02X' "'${x}"; done; }
137
- local U P; U=$(urle "$FLAME_USERNAME"); P=$(urle "$FLAME_PASSWORD")
138
- wget -q --post-data "username=$U&password=$P" \
139
- "https://download.is.tue.mpg.de/download.php?domain=flame&sfile=$1&resume=1" \
140
- -O "$2" --no-check-certificate --continue
141
- }
142
-
143
- hf_mirror() { # $1 = path in camenduru/show, $2 = dest file
144
- python3 - "$1" "$2" <<'EOF'
145
- import shutil, sys
146
- from huggingface_hub import hf_hub_download
147
- src = hf_hub_download("camenduru/show", sys.argv[1])
148
- shutil.copyfile(src, sys.argv[2])
149
- EOF
150
- }
151
-
152
- HAVE_CREDS=0
153
- [ -n "${FLAME_USERNAME:-}" ] && [ -n "${FLAME_PASSWORD:-}" ] && HAVE_CREDS=1
154
-
155
- if [ ! -s "$RES/generic_model.pkl" ]; then
156
- P3DM_FLAME="/workspace/p3dm/pixel3dmm/src/pixel3dmm/preprocessing/MICA/data/FLAME2020/generic_model.pkl"
157
- if [ -f "$P3DM_FLAME" ]; then
158
- log "reusing FLAME 2020 from p3dm install"
159
- cp "$P3DM_FLAME" "$RES/generic_model.pkl"
160
- elif [ "$HAVE_CREDS" = 1 ]; then
161
- log "downloading FLAME 2020 (official, needs FLAME account)"
162
- flame_official "FLAME2020.zip" "$RES/FLAME2020.zip"
163
- unzip -o -q "$RES/FLAME2020.zip" -d "$RES/FLAME2020_tmp"
164
- mv "$RES/FLAME2020_tmp/generic_model.pkl" "$RES/generic_model.pkl"
165
- rm -rf "$RES/FLAME2020.zip" "$RES/FLAME2020_tmp"
166
- else
167
- log "downloading FLAME 2020 generic_model.pkl (HF mirror camenduru/show; set FLAME_USERNAME/FLAME_PASSWORD for official source)"
168
- hf_mirror "models/models_MICA/FLAME2020/generic_model.pkl" "$RES/generic_model.pkl"
169
- fi
170
- fi
171
-
172
- if [ ! -s "$RES/FLAME_masks.pkl" ]; then
173
- if [ "$HAVE_CREDS" = 1 ]; then
174
- log "downloading FLAME vertex masks (official)"
175
- flame_official "FLAME_masks.zip" "$RES/FLAME_masks.zip"
176
- unzip -o -q "$RES/FLAME_masks.zip" -d "$RES/FLAME_masks_tmp"
177
- mv "$RES/FLAME_masks_tmp/FLAME_masks.pkl" "$RES/FLAME_masks.pkl"
178
- rm -rf "$RES/FLAME_masks.zip" "$RES/FLAME_masks_tmp"
179
- else
180
- log "downloading FLAME_masks.pkl (HF mirror)"
181
- hf_mirror "data/FLAME_masks.pkl" "$RES/FLAME_masks.pkl"
182
- fi
183
- fi
184
-
185
- if [ ! -s "$RES/FLAME_texture.npz" ]; then
186
- if [ "$HAVE_CREDS" = 1 ]; then
187
- log "downloading FLAME texture space (official, ~1.3 GB)"
188
- flame_official "TextureSpace.zip" "$RES/TextureSpace.zip"
189
- unzip -o -q "$RES/TextureSpace.zip" -d "$RES/TextureSpace_tmp"
190
- mv "$RES/TextureSpace_tmp/FLAME_texture.npz" "$RES/FLAME_texture.npz"
191
- rm -rf "$RES/TextureSpace.zip" "$RES/TextureSpace_tmp"
192
- else
193
- log "downloading FLAME_texture.npz (HF mirror, ~1.3 GB)"
194
- hf_mirror "data/flame/FLAME_texture.npz" "$RES/FLAME_texture.npz"
195
- fi
196
- fi
197
-
198
- if [ ! -s "$RES/albedoModel2020_FLAME_albedoPart.npz" ]; then
199
- log "downloading AlbedoMM albedo model (official GitHub release, ~1.8 GB)"
200
- wget -q --show-progress --continue \
201
- "https://github.com/waps101/AlbedoMM/releases/download/v1.0/albedoModel2020_FLAME_albedoPart.npz" \
202
- -O "$RES/albedoModel2020_FLAME_albedoPart.npz"
203
- fi
204
-
205
- # ------------------------------------------------------------------ verify
206
- log "verifying imports + files"
207
- REPO_DIR="$REPO" python3 - <<'EOF'
208
- import os, sys, warnings
209
- warnings.filterwarnings("ignore")
210
- import chumpy, gdown, kornia, cv2, nvdiffrast
211
- repo = os.environ["REPO_DIR"]
212
- sys.path.insert(0, repo)
213
- import networks # pulls torchvision resnet50 + icosahedron
214
- for f in ["checkpoints/coarse_reconstruction.pkl", "checkpoints/uv_completion.pkl",
215
- "checkpoints/makeup_extraction.pkl",
216
- "resources/generic_model.pkl", "resources/FLAME_masks.pkl",
217
- "resources/FLAME_texture.npz",
218
- "resources/albedoModel2020_FLAME_albedoPart.npz",
219
- "resources/landmark_embedding.npy", "resources/skin_mask.png",
220
- "libs/face_parsing/module_weight/79999_iter.pth",
221
- "libs/face_landmark/checkpoint/mobilenet_224_model_best_gdconv_external.pth.tar"]:
222
- p = os.path.join(repo, f)
223
- assert os.path.exists(p) and os.path.getsize(p) > 0, p
224
- print(" all good")
225
- EOF
226
-
227
- log "install complete. run: ./makeup_extract_run.sh <photo.jpg>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
makeup-extract/makeup_extract_run.sh DELETED
@@ -1,127 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # makeup_extract_run.sh — single photo -> 3D makeup decomposition
4
- #
5
- # Runs the full 6-step makeup-extract pipeline (Yang et al., EG 2023) on one
6
- # face image:
7
- # step 0 landmark crop + face parsing -> aligned_img / mask
8
- # step 1 coarse FLAME material reconstruction -> UV albedo/normal/shading
9
- # step 2 UV texture completion (DSD-GAN)
10
- # step 3 material refinement (500-iter optimization, VGG/TV losses)
11
- # step 4 makeup extraction (bare skin / base / alpha / blend)
12
- # step 5 re-render composite with extracted textures
13
- #
14
- # Usage:
15
- # ./makeup_extract_run.sh <photo.jpg> [out_dir]
16
- #
17
- # out_dir default: /workspace/outputs/makeup-extract/<image-stem>/
18
- #
19
- # Notes:
20
- # - non-square inputs are padded to square first (upstream assumes square)
21
- # - raw per-step outputs are cached in $MAKEUP_EXTRACT_HOME/results/<stem>/;
22
- # delete that folder to force a re-run
23
- # =============================================================================
24
- set -euo pipefail
25
-
26
- if [ $# -lt 1 ] || [ "${1:-}" = "--help" ] || [ "${1:-}" = "-h" ]; then
27
- grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0
28
- fi
29
-
30
- M_HOME="${MAKEUP_EXTRACT_HOME:-/workspace/makeup-extract}"
31
- REPO="$M_HOME/makeup-extract"
32
-
33
- log() { echo -e "\n\033[1;36m[makeup-extract]\033[0m $*"; }
34
-
35
- IMG="$(realpath "$1")"
36
- [ -f "$IMG" ] || { echo "input image not found: $IMG"; exit 1; }
37
-
38
- for f in checkpoints/coarse_reconstruction.pkl resources/generic_model.pkl \
39
- resources/FLAME_texture.npz resources/albedoModel2020_FLAME_albedoPart.npz; do
40
- [ -s "$REPO/$f" ] || { echo "missing $REPO/$f - run ./makeup_extract_install.sh first"; exit 1; }
41
- done
42
-
43
- STEM=$(basename "$IMG" | sed 's/\.[^.]*$//' | tr -c 'a-zA-Z0-9' '_' | sed 's/_*$//;s/^_*//' | cut -c1-60)
44
- STAGE="$M_HOME/staging/$STEM"
45
- RESULTS="$M_HOME/results/$STEM"
46
- OUT_DIR="${2:-/workspace/outputs/makeup-extract/$STEM}"
47
- mkdir -p "$STAGE" "$RESULTS" "$OUT_DIR"
48
-
49
- export TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1 # repo predates torch 2.6 torch.load change
50
- export TORCH_HOME="$M_HOME/weights_cache/torch" # VGG19/resnet18 aux weights (keep off ephemeral /root)
51
-
52
- START=$(date +%s)
53
-
54
- # ---------------------------------------------------------------- stage input
55
- # Upstream step_0 assumes a square image; pad to square (centered, black).
56
- log "staging input (pad to square if needed)"
57
- IMG_PATH="$IMG" STAGED="$STAGE/input.png" python3 - <<'EOF'
58
- import os, cv2
59
- img = cv2.imread(os.environ["IMG_PATH"])
60
- h, w = img.shape[:2]
61
- if h != w:
62
- s = max(h, w)
63
- top = (s - h) // 2; left = (s - w) // 2
64
- img = cv2.copyMakeBorder(img, top, s - h - top, left, s - w - left,
65
- cv2.BORDER_CONSTANT, value=(0, 0, 0))
66
- print(f" padded {w}x{h} -> {s}x{s}")
67
- cv2.imwrite(os.environ["STAGED"], img)
68
- EOF
69
-
70
- cd "$REPO"
71
-
72
- # ---------------------------------------------------------------- pipeline
73
- if [ -s "$RESULTS/render_texture/img_compose_with_bg.png" ]; then
74
- log "cached results found in $RESULTS (delete to re-run)"
75
- else
76
- log "step 0/5: landmark crop + face parsing"
77
- python3 step_0_preprocess.py -i "$STAGE/input.png" -o "$RESULTS"
78
-
79
- log "step 1/5: coarse facial material reconstruction"
80
- python3 step_1_coarse_reconstruction.py \
81
- --aligned_img_path "$RESULTS/aligned_img.png" \
82
- --segmented_img_path "$RESULTS/segmented_img.png" \
83
- -o "$RESULTS/coarse_reconstruction"
84
-
85
- log "step 2/5: UV completion"
86
- python3 step_2_uv_completion.py \
87
- --uv_mm_path "$RESULTS/coarse_reconstruction/uv_rncr.png" \
88
- --uv_tex_path "$RESULTS/coarse_reconstruction/uv_tex.png" \
89
- -o "$RESULTS/material_refinement"
90
-
91
- log "step 3/5: material refinement (500-iter optimization)"
92
- python3 step_3_material_refinement.py \
93
- --mm_param_path "$RESULTS/coarse_reconstruction/mm_param.npz" \
94
- --uv_albe_path "$RESULTS/coarse_reconstruction/uv_albe.png" \
95
- --uv_norm_path "$RESULTS/coarse_reconstruction/uv_norm.png" \
96
- --uv_rnsr_path "$RESULTS/coarse_reconstruction/uv_rnsr.png" \
97
- --completion_tex_path "$RESULTS/material_refinement/completion.png" \
98
- -o "$RESULTS/material_refinement"
99
-
100
- log "step 4/5: makeup extraction"
101
- python3 step_4_makeup_extraction.py \
102
- --uv_albe_path "$RESULTS/material_refinement/albe_refine.png" \
103
- -o "$RESULTS/makeup_extraction"
104
-
105
- log "step 5/5: render composite"
106
- python3 step_5_render_texture.py \
107
- --mm_param_path "$RESULTS/coarse_reconstruction/mm_param.npz" \
108
- --uv_bare_skin_path "$RESULTS/makeup_extraction/bare_skin.png" \
109
- --uv_make_alpha_path "$RESULTS/makeup_extraction/make_alpha.png" \
110
- --uv_make_blend_path "$RESULTS/makeup_extraction/make_blend.png" \
111
- --uv_diffuse_shading_path "$RESULTS/material_refinement/diff_refine.png" \
112
- --uv_specular_shading_path "$RESULTS/material_refinement/rnsr_refine.png" \
113
- --input_aligned_image "$RESULTS/aligned_img.png" \
114
- --aligned_image_mask_path "$RESULTS/mask_img.png" \
115
- -o "$RESULTS/render_texture"
116
- fi
117
-
118
- # ---------------------------------------------------------------- outputs
119
- log "collecting outputs"
120
- cp "$RESULTS/aligned_img.png" "$OUT_DIR/"
121
- cp "$RESULTS/makeup_extraction/"{bare_skin,make_base,make_alpha,make_blend}.png "$OUT_DIR/"
122
- cp "$RESULTS/material_refinement/"{albe_refine,norm_refine,diff_refine,rnsr_refine,tex_refine,completion}.png "$OUT_DIR/"
123
- cp "$RESULTS/render_texture/"{img_compose,img_compose_with_bg}.png "$OUT_DIR/"
124
-
125
- ELAPSED=$(( $(date +%s) - START ))
126
- log "done in ${ELAPSED}s. outputs in $OUT_DIR"
127
- ls -la "$OUT_DIR"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
p3dm/README.md DELETED
@@ -1,63 +0,0 @@
1
- # p3dm — Pixel3DMM single-photo → 3D head mesh
2
-
3
- One photo in, fitted 3D FLAME head mesh out, using
4
- [Pixel3DMM](https://github.com/SimonGiebenhain/pixel3dmm) (Giebenhain et al. 2025).
5
- Runs on the native python of the `plx1029/comfyui-qwen` RunPod image
6
- (python 3.12, torch 2.8+cu128) — **no venv, no conda, torch is not reinstalled**.
7
-
8
- ## Install (once per pod, ~5 min + ~5 GB downloads)
9
-
10
- ```bash
11
- export FLAME_USERNAME="you@mail.com" # free account: https://flame.is.tue.mpg.de
12
- export FLAME_PASSWORD="..."
13
- ./p3dm_install.sh
14
- ```
15
-
16
- Everything lands in `$P3DM_HOME` (default `/workspace/p3dm`). Re-running is safe;
17
- completed steps are skipped. FLAME creds are only needed the first time.
18
-
19
- ## Run (~2 min per photo on H100, first run +1 min for CUDA JIT)
20
-
21
- ```bash
22
- ./p3dm_run.sh photo.jpg # -> /workspace/outputs/photo/
23
- ./p3dm_run.sh photo.jpg -o /some/dir --iters 800 --force
24
- ```
25
-
26
- Outputs: `<name>.ply` (posed mesh, FLAME topology 5023 verts),
27
- `<name>_canonical.ply` (neutral pose), `<name>_overlay.mp4` (input vs. fit),
28
- `<name>_normals.png` / `<name>_uv.png` (network predictions), `<name>_crop.jpg`.
29
-
30
- Plus a `raw/` subfolder with the machine-readable fit data:
31
- `<name>.frame` (torch pickle: FLAME shape/exp/jaw/neck/eyes params + fitted
32
- camera `fl`/`pp`/`R_base_0`/`t_base_0` + `img_size`), float32 un-quantized
33
- `<name>_normals.npy` (512×512×3) and `<name>_uv.npy` (512×512×2),
34
- `<name>_cropbox.npy` (ymin/ymax/xmin/xmax of the face crop in the staged
35
- image), `<name>_seg.png` (facer segmentation), `<name>_landmarks.npy`
36
- (PIPNet), `<name>_mica_identity.npy`. The FLAME UV layout lives in the repo's
37
- `assets/` (`flame_uv_coords.npy`, `head_template.obj`).
38
-
39
- Pipeline: crop+landmarks (PIPNet) → identity (MICA) → segmentation (facer)
40
- → ViT normals + uv prediction → FLAME fitting (nvdiffrast).
41
-
42
- ## What the installer patches (vs. upstream repo)
43
-
44
- Upstream targets python 3.9 / torch 2.x / numpy 1.23. To run on the native
45
- py3.12 / torch 2.8 / numpy 2.1 env, the installer applies:
46
-
47
- - **pytorch3d** from MiroPsota's prebuilt wheel index (no source build)
48
- - **chumpy** patched (`getargspec` → `getfullargspec`, numpy alias imports)
49
- - numpy 2 aliases (`np.Inf`, `np.float`, `np.bool`, …) rewritten in MICA/pixel3dmm
50
- - `scipy.integrate.simps` → `simpson` (PIPNet)
51
- - PIPNet's Cython NMS replaced with pure-numpy NMS (Cython 3 can't build it)
52
- - FaceBoxes detector auto-downscales large images (fixes missed detection on
53
- high-res headshots where the face fills the frame)
54
- - `TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1` at runtime (repo predates torch 2.6
55
- `weights_only` default)
56
- - pip runs against a constraints file pinning the already-installed
57
- numpy/torch/pillow/… so the ComfyUI env is never disturbed
58
-
59
- ## Licenses
60
-
61
- FLAME (registration required) and MICA weights are downloaded from their
62
- official sources at install time and are **not** redistributed in this repo.
63
- Pixel3DMM is CC BY-NC 4.0 — non-commercial use.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
p3dm/p3dm_install.sh DELETED
@@ -1,248 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # p3dm_install.sh — one-shot installer for Pixel3DMM (single-photo -> 3D head)
4
- #
5
- # Installs https://github.com/SimonGiebenhain/pixel3dmm plus its preprocessing
6
- # stack (facer, MICA, PIPNet) into the NATIVE python environment (no venv, no
7
- # conda, no torch reinstall). Tested on: Ubuntu 24.04, python 3.12,
8
- # torch 2.8.0+cu128, H100 (RunPod image plx1029/comfyui-qwen).
9
- #
10
- # Idempotent: safe to re-run; finished steps are skipped.
11
- #
12
- # Requirements:
13
- # - torch with CUDA already installed (the script will not install torch)
14
- # - nvcc in PATH (needed once, at first run, for nvdiffrast JIT compile)
15
- # - FLAME account creds (https://flame.is.tue.mpg.de) via env vars
16
- # FLAME_USERNAME / FLAME_PASSWORD, otherwise the script prompts for them.
17
- #
18
- # Usage:
19
- # FLAME_USERNAME=you@mail.com FLAME_PASSWORD=... ./p3dm_install.sh
20
- #
21
- # Layout (override with P3DM_HOME):
22
- # $P3DM_HOME/pixel3dmm code + weights
23
- # $P3DM_HOME/preprocessed per-image preprocessing cache
24
- # $P3DM_HOME/tracking_output per-image fitting results
25
- # =============================================================================
26
- set -euo pipefail
27
-
28
- P3DM_HOME="${P3DM_HOME:-/workspace/p3dm}"
29
- REPO="$P3DM_HOME/pixel3dmm"
30
- PP="$REPO/src/pixel3dmm/preprocessing"
31
-
32
- log() { echo -e "\n\033[1;36m[p3dm-install]\033[0m $*"; }
33
-
34
- mkdir -p "$P3DM_HOME" "$P3DM_HOME/preprocessed" "$P3DM_HOME/tracking_output"
35
- cd "$P3DM_HOME"
36
-
37
- # ---------------------------------------------------------------- sanity checks
38
- log "checking python/torch/nvcc"
39
- python3 - <<'EOF'
40
- import torch, sys
41
- assert torch.cuda.is_available(), "CUDA torch required"
42
- print(f" torch {torch.__version__} cuda {torch.version.cuda} ok")
43
- EOF
44
- command -v nvcc >/dev/null || echo " WARNING: nvcc not found - nvdiffrast JIT compile will fail at runtime"
45
-
46
- # ---------------------------------------------------- constraints (protect env)
47
- # Pin the currently-installed versions of shared heavyweight packages so pip
48
- # cannot up/downgrade them while installing pixel3dmm deps (keeps ComfyUI safe).
49
- log "generating pip constraints from current env"
50
- python3 - > "$P3DM_HOME/constraints.txt" <<'EOF'
51
- from importlib.metadata import version, PackageNotFoundError
52
- for p in ["numpy","torch","torchvision","pillow","scipy","einops","transformers","huggingface_hub","safetensors"]:
53
- try: print(f"{p}=={version(p)}")
54
- except PackageNotFoundError: pass
55
- EOF
56
- CONS="-c $P3DM_HOME/constraints.txt"
57
-
58
- # ------------------------------------------------------------------ pip deps
59
- log "installing python dependencies (native env, constrained)"
60
- pip install -q $CONS omegaconf opencv-python tensorboard wandb scikit-image pyvista \
61
- dreifus h5py pytorch_lightning mediapy ninja insightface onnxruntime loguru yacs \
62
- distinctipy validators timm tyro environs gdown face-alignment trimesh
63
-
64
- # pytorch3d: prebuilt wheel matching the local torch (MiroPsota builds), else source build
65
- if ! python3 -c "import pytorch3d" 2>/dev/null; then
66
- log "installing pytorch3d (prebuilt wheel)"
67
- PT=$(python3 -c "import torch; print(torch.__version__.split('+')[0])")
68
- CU=$(python3 -c "import torch; print('cu'+torch.version.cuda.replace('.',''))")
69
- pip install -q $CONS "pytorch3d==0.7.9+pt${PT}${CU}" \
70
- -f https://miropsota.github.io/torch_packages_builder/pytorch3d/ \
71
- || { log "no prebuilt wheel for pt${PT}${CU}, building from source (slow)"; \
72
- pip install -q $CONS --no-build-isolation "git+https://github.com/facebookresearch/pytorch3d.git@stable"; }
73
- fi
74
-
75
- # nvdiffrast (compiles CUDA kernels lazily at first use)
76
- python3 -c "import nvdiffrast" 2>/dev/null || {
77
- log "installing nvdiffrast"
78
- pip install -q $CONS --no-build-isolation "git+https://github.com/NVlabs/nvdiffrast.git"
79
- }
80
-
81
- # chumpy: only needed to unpickle FLAME. Needs patching for py>=3.11 / numpy>=2.
82
- if ! python3 -c "import chumpy" 2>/dev/null; then
83
- log "installing + patching chumpy for py3.12/numpy2"
84
- pip install -q --no-deps --no-build-isolation chumpy || pip install -q --no-deps --no-build-isolation chumpy
85
- CHDIR=$(python3 -c "import importlib.util,os; print(os.path.dirname(importlib.util.find_spec('chumpy').origin))")
86
- grep -rl "inspect.getargspec" "$CHDIR"/*.py | xargs -r sed -i "s/inspect\.getargspec/inspect.getfullargspec/g"
87
- sed -i "s/^from numpy import bool, int, float, complex, object, unicode, str, nan, inf$/from numpy import nan, inf/" "$CHDIR/__init__.py"
88
- python3 -c "import chumpy" || { echo "chumpy patch failed"; exit 1; }
89
- fi
90
-
91
- # ------------------------------------------------------------------ pixel3dmm
92
- if [ ! -d "$REPO" ]; then
93
- log "cloning pixel3dmm"
94
- git clone --depth 1 https://github.com/SimonGiebenhain/pixel3dmm.git "$REPO"
95
- fi
96
- pip show pixel3dmm >/dev/null 2>&1 || (cd "$REPO" && pip install -q -e . --no-deps)
97
-
98
- # env paths config
99
- mkdir -p ~/.config/pixel3dmm
100
- cat > ~/.config/pixel3dmm/.env <<EOF
101
- PIXEL3DMM_CODE_BASE="$REPO"
102
- PIXEL3DMM_PREPROCESSED_DATA="$P3DM_HOME/preprocessed"
103
- PIXEL3DMM_TRACKING_OUTPUT="$P3DM_HOME/tracking_output"
104
- EOF
105
-
106
- # ------------------------------------------------- preprocessing repos + patches
107
- cd "$PP"
108
- if [ ! -d facer ]; then
109
- log "cloning facer"
110
- git clone --depth 1 https://github.com/FacePerceiver/facer.git
111
- cp replacement_code/farl.py facer/facer/face_parsing/farl.py
112
- cp replacement_code/facer_transform.py facer/facer/transform.py
113
- fi
114
- pip show pyfacer >/dev/null 2>&1 || (cd facer && pip install -q -e . --no-deps)
115
-
116
- if [ ! -d MICA ]; then
117
- log "cloning MICA"
118
- git clone --depth 1 https://github.com/Zielon/MICA.git
119
- cp replacement_code/mica_demo.py MICA/demo.py
120
- cp replacement_code/mica.py MICA/micalib/models/mica.py
121
- fi
122
-
123
- if [ ! -d PIPNet ]; then
124
- log "cloning PIPNet"
125
- git clone --depth 1 https://github.com/jhb86253817/PIPNet.git
126
- fi
127
-
128
- log "applying py3.12 / numpy2 / scipy compat patches"
129
- # scipy renamed simps -> simpson
130
- grep -rl "from scipy.integrate import simps$" PIPNet/lib 2>/dev/null | \
131
- xargs -r sed -i "s/from scipy.integrate import simps$/from scipy.integrate import simpson as simps/"
132
- # numpy 2 removed aliases (MICA + pixel3dmm)
133
- grep -rlE "np\.(Inf|float\b|int\b|bool\b|object\b|NaN)" --include=*.py MICA "$REPO/src/pixel3dmm/utils" | \
134
- xargs -r sed -i -E "s/np\.Inf/np.inf/g; s/np\.NaN/np.nan/g; s/np\.float\b/np.float64/g; s/np\.int\b/np.int64/g; s/np\.bool\b/bool/g; s/np\.object\b/object/g"
135
- # let FaceBoxes auto-downscale large images (fixes missed detections on
136
- # high-res headshots where the face fills the frame) and map coords back
137
- sed -i "s|detections, _ = detector.detect(image, my_thresh, 1)|detections, _p3dm_s = detector.detect(image, my_thresh, None); detections = [[d[0], d[1], int(d[2]/_p3dm_s), int(d[3]/_p3dm_s), int(d[4]/_p3dm_s), int(d[5]/_p3dm_s)] for d in detections]|" \
138
- "$REPO/src/pixel3dmm/preprocessing/pipnet_utils.py"
139
- # save un-quantized float32 .npy next to the 8-bit PNG network predictions
140
- REPO_PATH="$REPO" python3 - <<'PYEOF'
141
- import os
142
- path = os.environ["REPO_PATH"] + "/scripts/network_inference.py"
143
- src = open(path).read()
144
- anchor = " f'{out_folders[prediction_type]}/{image_names[i][:-4]}.png')\n"
145
- patch = anchor + """
146
- # also dump the un-quantized prediction (same [0,1] convention as the PNG)
147
- np.save(f'{out_folders[prediction_type]}/{image_names[i][:-4]}.npy',
148
- tmp_output.permute(1, 2, 0).detach().cpu().float().numpy())
149
- """
150
- if "un-quantized prediction" not in src:
151
- assert anchor in src, "network_inference.py anchor not found"
152
- src = src.replace(anchor, patch)
153
- # skip-if-done check must account for the extra .npy per image
154
- old_skip = """ if os.path.exists(out_folders[prediction_type]):
155
- if len(os.listdir(out_folders[prediction_type])) == len(image_names):
156
- return"""
157
- new_skip = """ if os.path.exists(out_folders[prediction_type]):
158
- _done = os.listdir(out_folders[prediction_type])
159
- if all(f[:-4] + '.png' in _done and f[:-4] + '.npy' in _done for f in image_names):
160
- return"""
161
- assert old_skip in src, "network_inference.py skip anchor not found"
162
- src = src.replace(old_skip, new_skip)
163
- open(path, "w").write(src)
164
- PYEOF
165
- # replace PIPNet's Cython nms (does not build with Cython>=3) with pure numpy
166
- cat > PIPNet/FaceBoxesV2/utils/nms_wrapper.py <<'PYEOF'
167
- # Pure-numpy NMS replacement for the original Cython extension.
168
- import numpy as np
169
-
170
-
171
- def nms(dets, thresh):
172
- if dets.shape[0] == 0:
173
- return []
174
- x1, y1, x2, y2, scores = dets[:, 0], dets[:, 1], dets[:, 2], dets[:, 3], dets[:, 4]
175
- areas = (x2 - x1 + 1) * (y2 - y1 + 1)
176
- order = scores.argsort()[::-1]
177
- keep = []
178
- while order.size > 0:
179
- i = order[0]
180
- keep.append(int(i))
181
- xx1 = np.maximum(x1[i], x1[order[1:]])
182
- yy1 = np.maximum(y1[i], y1[order[1:]])
183
- xx2 = np.minimum(x2[i], x2[order[1:]])
184
- yy2 = np.minimum(y2[i], y2[order[1:]])
185
- w = np.maximum(0.0, xx2 - xx1 + 1)
186
- h = np.maximum(0.0, yy2 - yy1 + 1)
187
- inter = w * h
188
- ovr = inter / (areas[i] + areas[order[1:]] - inter)
189
- inds = np.where(ovr <= thresh)[0]
190
- order = order[inds + 1]
191
- return keep
192
- PYEOF
193
-
194
- # ------------------------------------------------------------------ FLAME assets
195
- FLAME_DIR="$PP/MICA/data"
196
- if [ ! -f "$FLAME_DIR/FLAME2020/generic_model.pkl" ]; then
197
- log "downloading FLAME 2020+2023 (requires FLAME account)"
198
- if [ -z "${FLAME_USERNAME:-}" ]; then read -rp "FLAME username: " FLAME_USERNAME; fi
199
- if [ -z "${FLAME_PASSWORD:-}" ]; then read -rsp "FLAME password: " FLAME_PASSWORD; echo; fi
200
- urle () { local LANG=C i x; for (( i = 0; i < ${#1}; i++ )); do x="${1:i:1}"; [[ "${x}" == [a-zA-Z0-9.~-] ]] && echo -n "${x}" || printf '%%%02X' "'${x}"; done; }
201
- U=$(urle "$FLAME_USERNAME"); P=$(urle "$FLAME_PASSWORD")
202
- mkdir -p "$FLAME_DIR/FLAME2020" "$FLAME_DIR/FLAME2023"
203
- wget -q --post-data "username=$U&password=$P" 'https://download.is.tue.mpg.de/download.php?domain=flame&sfile=FLAME2020.zip&resume=1' -O "$FLAME_DIR/FLAME2020.zip" --no-check-certificate --continue
204
- wget -q --post-data "username=$U&password=$P" 'https://download.is.tue.mpg.de/download.php?domain=flame&sfile=FLAME2023.zip&resume=1' -O "$FLAME_DIR/FLAME2023.zip" --no-check-certificate --continue
205
- unzip -o -q "$FLAME_DIR/FLAME2020.zip" -d "$FLAME_DIR/FLAME2020/"
206
- unzip -o -q "$FLAME_DIR/FLAME2023.zip" -d "$FLAME_DIR/FLAME2023/"
207
- rm -f "$FLAME_DIR/FLAME2020.zip" "$FLAME_DIR/FLAME2023.zip"
208
- # the zips nest one level deeper than the code expects -> flatten
209
- [ -d "$FLAME_DIR/FLAME2020/FLAME2020" ] && mv "$FLAME_DIR/FLAME2020/FLAME2020/"* "$FLAME_DIR/FLAME2020/" && rmdir "$FLAME_DIR/FLAME2020/FLAME2020"
210
- [ -d "$FLAME_DIR/FLAME2023/FLAME2023" ] && mv "$FLAME_DIR/FLAME2023/FLAME2023/"* "$FLAME_DIR/FLAME2023/" && rmdir "$FLAME_DIR/FLAME2023/FLAME2023"
211
- [ -f "$FLAME_DIR/FLAME2020/generic_model.pkl" ] || { echo "FLAME download failed (bad credentials?)"; exit 1; }
212
- fi
213
-
214
- # ------------------------------------------------------------- model weights
215
- dl () { # dl <gdrive-id> <dest>
216
- if [ ! -s "$2" ]; then log "downloading $(basename "$2")"; mkdir -p "$(dirname "$2")"; gdown "$1" -O "$2" -q; fi
217
- [ -s "$2" ] || { echo "download failed: $2"; exit 1; }
218
- }
219
- dl 1SDV_8_qWTe__rX_8e4Fi-BE3aES0YzJY "$REPO/pretrained_weights/uv.ckpt"
220
- dl 1KYYlpN-KGrYMVcAOT22NkVQC0UAfycMD "$REPO/pretrained_weights/normals.ckpt"
221
- dl 1bYsI_spptzyuFmfLYqYkcJA6GZWZViNt "$PP/MICA/data/pretrained/mica.tar"
222
- dl 1nVkaSbxy3NeqblwMTGvLg4nF49cI_99C "$PP/PIPNet/snapshots/WFLW/pip_32_16_60_r18_l2_l1_10_1_nb10/epoch59.pth"
223
-
224
- # insightface detection/recognition models (hosted on gdrive per MICA install)
225
- mkdir -p ~/.insightface/models
226
- if [ ! -d ~/.insightface/models/antelopev2/antelopev2 ] && [ ! -f ~/.insightface/models/antelopev2/glintr100.onnx ]; then
227
- dl 16PWKI_RjjbE4_kqpElG-YFqe8FpXjads ~/.insightface/models/antelopev2.zip
228
- unzip -o -q ~/.insightface/models/antelopev2.zip -d ~/.insightface/models/antelopev2 && rm ~/.insightface/models/antelopev2.zip
229
- fi
230
- if [ ! -d ~/.insightface/models/buffalo_l/buffalo_l ] && [ ! -f ~/.insightface/models/buffalo_l/det_10g.onnx ]; then
231
- dl 1navJMy0DTr1_DHjLWu1i48owCPvXWfYc ~/.insightface/models/buffalo_l.zip
232
- unzip -o -q ~/.insightface/models/buffalo_l.zip -d ~/.insightface/models/buffalo_l && rm ~/.insightface/models/buffalo_l.zip
233
- fi
234
-
235
- # ------------------------------------------------------------------ verify
236
- log "verifying imports"
237
- python3 - <<'EOF'
238
- import pytorch3d, nvdiffrast, chumpy, insightface, facer, pixel3dmm
239
- from pixel3dmm import env_paths
240
- import os
241
- for f in [env_paths.CKPT_UV_PRED, env_paths.CKPT_N_PRED,
242
- env_paths.FLAME_ASSETS + '/FLAME2020/generic_model.pkl',
243
- env_paths.FLAME_ASSETS + '/pretrained/mica.tar']:
244
- assert os.path.getsize(f) > 0, f
245
- print(" all good")
246
- EOF
247
-
248
- log "install complete. run: ./p3dm_run.sh <photo.jpg>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
p3dm/p3dm_run.sh DELETED
@@ -1,100 +0,0 @@
1
- #!/usr/bin/env bash
2
- # =============================================================================
3
- # p3dm_run.sh — single photo in -> 3D FLAME head mesh out (Pixel3DMM)
4
- #
5
- # Usage:
6
- # ./p3dm_run.sh <photo.(jpg|png)> [-o <output_dir>] [--iters N] [--force]
7
- #
8
- # -o DIR output directory (default: /workspace/outputs/<name>)
9
- # --iters N fitting iterations (default: 800, per-paper single-image)
10
- # --force redo preprocessing/fit even if cached results exist
11
- #
12
- # Outputs (in <output_dir>):
13
- # <name>.ply posed 3D head mesh (FLAME topology)
14
- # <name>_canonical.ply canonical (neutral-pose) mesh
15
- # <name>_overlay.mp4 input vs. fitted-mesh render comparison
16
- # <name>_normals.png predicted screen-space normals
17
- # <name>_uv.png predicted uv correspondence map
18
- # <name>_crop.jpg the face crop that was fitted
19
- #
20
- # Requires p3dm_install.sh to have been run first.
21
- # =============================================================================
22
- set -euo pipefail
23
-
24
- P3DM_HOME="${P3DM_HOME:-/workspace/p3dm}"
25
- REPO="$P3DM_HOME/pixel3dmm"
26
-
27
- IMG="" ; OUT="" ; ITERS=800 ; FORCE=0
28
- while [ $# -gt 0 ]; do
29
- case "$1" in
30
- -o) OUT="$2"; shift 2;;
31
- --iters) ITERS="$2"; shift 2;;
32
- --force) FORCE=1; shift;;
33
- -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0;;
34
- *) IMG="$1"; shift;;
35
- esac
36
- done
37
- [ -n "$IMG" ] && [ -f "$IMG" ] || { echo "usage: $0 <photo.jpg> [-o outdir] [--iters N] [--force]"; exit 1; }
38
- [ -d "$REPO" ] || { echo "pixel3dmm not installed - run p3dm_install.sh first"; exit 1; }
39
-
40
- # sanitized job name from the file stem (pixel3dmm uses it as an identifier)
41
- STEM="$(basename "$IMG")"; STEM="${STEM%.*}"
42
- NAME="$(echo "$STEM" | tr -c 'a-zA-Z0-9' '_' | sed 's/_*$//;s/^_*//' | cut -c1-60)"
43
- OUT="${OUT:-/workspace/outputs/$NAME}"
44
-
45
- export TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD=1 # repo predates torch>=2.6 weights_only default
46
- if [ -z "${TORCH_CUDA_ARCH_LIST:-}" ]; then
47
- export TORCH_CUDA_ARCH_LIST="$(python3 -c "import torch; print('%d.%d+PTX' % torch.cuda.get_device_capability())")"
48
- fi
49
-
50
- log() { echo -e "\033[1;35m[p3dm]\033[0m $*"; }
51
- T0=$(date +%s)
52
-
53
- # stage the photo in a folder named after the job (pipeline expects a folder or video)
54
- STAGE="$P3DM_HOME/staging/$NAME"
55
- PRE="$P3DM_HOME/preprocessed/$NAME"
56
- if [ "$FORCE" = 1 ]; then rm -rf "$STAGE" "$PRE" "$P3DM_HOME/tracking_output/${NAME}_nV"*; fi
57
- mkdir -p "$STAGE"
58
- cp -f "$IMG" "$STAGE/$NAME.${IMG##*.}"
59
-
60
- cd "$REPO"
61
- log "1/4 preprocessing (crop, landmarks, segmentation, MICA)"
62
- python3 scripts/run_preprocessing.py --video_or_images_path "$STAGE" > /dev/null 2>&1 \
63
- || { echo "preprocessing failed, rerunning verbosely:"; python3 scripts/run_preprocessing.py --video_or_images_path "$STAGE"; exit 1; }
64
- [ -d "$PRE/cropped" ] || { echo "ERROR: no face detected / cropping failed for $IMG"; exit 1; }
65
-
66
- log "2/4 normals prediction"
67
- python3 scripts/network_inference.py model.prediction_type=normals video_name="$NAME" > /dev/null 2>&1 \
68
- || { python3 scripts/network_inference.py model.prediction_type=normals video_name="$NAME"; exit 1; }
69
-
70
- log "3/4 uv-map prediction"
71
- python3 scripts/network_inference.py model.prediction_type=uv_map video_name="$NAME" > /dev/null 2>&1 \
72
- || { python3 scripts/network_inference.py model.prediction_type=uv_map video_name="$NAME"; exit 1; }
73
-
74
- log "4/4 FLAME fitting ($ITERS iters)"
75
- python3 scripts/track.py video_name="$NAME" iters="$ITERS" 2>&1 | grep -E "DONE|Error|Traceback" || true
76
-
77
- TRACK_DIR="$(ls -td "$P3DM_HOME/tracking_output/${NAME}_nV"*/ 2>/dev/null | head -1)"
78
- [ -n "$TRACK_DIR" ] && [ -f "$TRACK_DIR/mesh/00000.ply" ] || { echo "ERROR: tracking produced no mesh"; exit 1; }
79
-
80
- mkdir -p "$OUT"
81
- cp "$TRACK_DIR/mesh/00000.ply" "$OUT/$NAME.ply"
82
- cp "$TRACK_DIR/mesh/canonical.ply" "$OUT/${NAME}_canonical.ply"
83
- [ -f "$TRACK_DIR/result.mp4" ] && cp "$TRACK_DIR/result.mp4" "$OUT/${NAME}_overlay.mp4"
84
- cp "$PRE/p3dmm/normals/00000.png" "$OUT/${NAME}_normals.png" 2>/dev/null || true
85
- cp "$PRE/p3dmm/uv_map/00000.png" "$OUT/${NAME}_uv.png" 2>/dev/null || true
86
- CROP=$(ls "$PRE/cropped/" | head -1); cp "$PRE/cropped/$CROP" "$OUT/${NAME}_crop.${CROP##*.}" 2>/dev/null || true
87
-
88
- # raw fitting + preprocessing data (FLAME params, fitted camera, float preds,
89
- # crop transform, segmentation, landmarks) for downstream use
90
- mkdir -p "$OUT/raw"
91
- cp "$TRACK_DIR/checkpoint/00000.frame" "$OUT/raw/${NAME}.frame"
92
- cp "$PRE/p3dmm/normals/00000.npy" "$OUT/raw/${NAME}_normals.npy" 2>/dev/null || true
93
- cp "$PRE/p3dmm/uv_map/00000.npy" "$OUT/raw/${NAME}_uv.npy" 2>/dev/null || true
94
- cp "$PRE/crop_ymin_ymax_xmin_xmax.npy" "$OUT/raw/${NAME}_cropbox.npy" 2>/dev/null || true
95
- cp "$PRE/seg_og/00000.png" "$OUT/raw/${NAME}_seg.png" 2>/dev/null || true
96
- cp "$PRE/PIPnet_landmarks/00000.npy" "$OUT/raw/${NAME}_landmarks.npy" 2>/dev/null || true
97
- cp "$PRE/mica/00000/identity.npy" "$OUT/raw/${NAME}_mica_identity.npy" 2>/dev/null || true
98
-
99
- log "done in $(( $(date +%s) - T0 ))s -> $OUT"
100
- ls -la "$OUT"