Remove paper folders — centralized in private aleph65/Papers
Browse files- 3dgazenet/README.md +0 -72
- 3dgazenet/gazenet_infer.py +0 -120
- 3dgazenet/gazenet_install.sh +0 -110
- 3dgazenet/gazenet_run.sh +0 -60
- deca/README.md +0 -79
- deca/deca_install.sh +0 -144
- deca/deca_run.sh +0 -96
- facelift/README.md +0 -115
- facelift/facelift_infer.py +0 -312
- facelift/facelift_install.sh +0 -190
- facelift/facelift_run.sh +0 -85
- ffhq-uv/README.md +0 -77
- ffhq-uv/ffhq_uv_install.sh +0 -151
- ffhq-uv/ffhq_uv_run.sh +0 -101
- makeup-extract/README.md +0 -102
- makeup-extract/makeup_extract_install.sh +0 -227
- makeup-extract/makeup_extract_run.sh +0 -127
- p3dm/README.md +0 -63
- p3dm/p3dm_install.sh +0 -248
- p3dm/p3dm_run.sh +0 -100
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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|