GazeCtrl β€” a gaze-conditioned ControlNet for Qwen-Image-Edit-2509

Turn a person in a photo to face a target gaze direction (dx, dy, dz) β€” head and body, over the full 360Β° including facing away β€” while keeping the same identity.

This is a ControlNet adapter, not a standalone model. It does nothing without the frozen Qwen-Image-Edit-2509 backbone and its VAE. The loading code is in the repository below; these weights are not diffusers-loadable as-is.

How it works

        target gaze (dx,dy,dz)
                 β”‚
                 β–Ό
   flat RGB field   R=(dx+1)/2  G=(dy+1)/2  B=(dz+1)/2      512Γ—512
                 β”‚
              VAE encode
                 β”‚
noisy latent ─► InstantX ControlNet Union (5 blocks) ── 5 residuals ─┐
                                                                     β–Ό
source image ──────────────────────► Qwen-Image-Edit-2509 fp8 (frozen) ──► output
   (in-context reference token β€” this is what carries identity)

The gaze vector is the control signal: no skeleton, no pose-prediction stage, and no text prompt (txt_emb is a zero tensor, so every bit of control arrives through the ControlNet). Identity is carried by the source image entering the backbone as an in-context reference, not by the ControlNet.

Convention β€” dx > 0 looks image-right, dy > 0 looks up, dz > 0 looks toward the camera. yaw = atan2(dx, dz), so Β±180Β° means facing away.

Files

file steps note
cn_step085000.safetensors 85 000 best training-set metrics; the one used for the demo
cn_step090000.safetensors 90 000 final checkpoint of the run

3.54 GB each, bf16, 1768 M parameters.

Usage

import torch
from safetensors.torch import load_file
from qwen_models import (QwenBackbone, QwenControlNet, load_qwen_vae, vae_encode, vae_decode,
                         patchify, unpatchify, compute_rope_freqs_3d, compute_text_rope_freqs)

dev = "cuda"
vae = load_qwen_vae(VAE_PATH, device=dev)
bb  = QwenBackbone(QWEN_IMAGE_EDIT_2509_FP8_PATH, device=dev)   # frozen
cn  = QwenControlNet(); cn.load_state_dict(load_file("cn_step085000.safetensors"))
cn.to(dev, torch.bfloat16).eval()

The full denoising loop β€” flow matching, 16 steps, cn_scale=1.0 to match training β€” is demo/app.py in the code repository. cn_scale is the knob that trades obedience against fidelity; values away from 1.0 change the reported metrics.

Training

data 1864 frames / 183 identities (COCO + WiderFace β†’ Wan2.2 turnarounds), 48 % human-labelled
init InstantX ControlNet Union pretrained weights (181/181 keys)
objective flow-matching velocity MSE, head-weighted w = 1 + 8Β·head_mask + 30Β·eye_mask
optimiser AdamW, lr 1e-4, grad-clip 1.0, batch 1
schedule 90 000 steps, ~2 s/step on one A6000

Evaluation

training identities held-out 70k held-out 90k
gaze_err ↓ 19.11Β° 55.97Β° 57.28Β°
ctrl_slope (1.0 ideal) +0.874 +0.642 +0.667
id_sim (ArcFace) ↑ β€” +0.277 +0.282
face_det β€” 70 % 75 %

Held-out = 8 identities that were never trained on, cn_scale=1.0, 16 steps, seed 0.

Limitations β€” please read these

  • The generalisation gap is large: 19Β° on training identities against ~56Β° on held-out ones. The held-out split is small (8 identities) and carries only automatic labels while 48 % of training labels are human, so some of that gap is label noise β€” but not all of it.
  • Training longer stopped helping. 70k β†’ 90k improved the training-set metric and did nothing for held-out performance.
  • Identity preservation is marginal. id_sim β‰ˆ 0.28 sits on ArcFace's own same-person threshold, and no loss term currently targets identity. Expect drift, worst at large rotations.
  • gaze_err is a proxy. It scores head pose (6DRepNet) against the requested gaze vector. Head pose and gaze differ by construction β€” the two estimators that produced the labels disagree by a median 26.7Β° on the same frame β€” so a floor of that order is expected.
  • Back-facing outputs cannot be scored for identity β€” ArcFace needs a face, hence the separate face_det column.
  • Trained on 512Γ—512 pad-to-square inputs, single person, roughly upright. Crowds, heavy occlusion and unusual framing are out of distribution.
  • Label semantics are not uniform across directions β€” see the dataset card; this is the clearest known defect in the training signal.

Licence

Apache-2.0 for these weights. The base model (Qwen/Qwen-Image-Edit-2509) and the InstantX ControlNet Union it was initialised from keep their own licences; this adapter is useless without the former, so its terms govern any deployment.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Model tree for baki0115/gaze-controlnet-qwen-image-edit

Adapter
(85)
this model