Defines PhotoFace, a serializable data class representing a portable, non-photographic fitted face capture and utilities to fit/generate such faces from measured photo-based inputs. PhotoFace stores metadata, head blend weights and per-slider offsets, validates and can apply offsets to a CharacterAppearanceData. PhotoFaceFit contains heuristic algorithms to compute starting head blends and to generate a PhotoFace from 1–5 FaceMeasurement views using median/weighted statistics and a bounded simplex-like optimizer.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CharacterEditor;
/// <summary>Portable fitted basis. No URLs, photographs or analysis landmarks are saved.</summary>
public sealed class PhotoFace
{
public int Version { get; set; } = 1;
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public float Strength { get; set; } = 1;
public string Name { get; set; } = "Photo face";
public float Influence { get; set; } = 1;
public float[] HeadWeights { get; set; } = new[]
{
0f,
0f,
1f,
0f,
0f,
0f,
0f
};
public Dictionary<string, float> Offsets { get; set; } = new();
[JsonIgnore]
public bool Valid => Version == 1 && Guid.TryParseExact(Id, "N", out _) && float.IsFinite(Strength) && Strength >= 0 && Strength <= 2 && Name is not null && Name.Length is> 0 and <= 40 && float.IsFinite(Influence) && Influence >= 0 && Influence <= 1 && HeadWeights?.Length == 7 && HeadWeights.All(x => float.IsFinite(x) && x >= 0 && x <= 1) && MathF.Abs(HeadWeights.Sum() - 1) < .001f && Offsets is not null && Offsets.Count <= 24 && Offsets.All(x => AppearanceCatalog.Sliders.Any(s => s.Group != "Body" && s.Id == x.Key) && float.IsFinite(x.Value) && MathF.Abs(x.Value) <= .5f);
public PhotoFace DeepCopy() => new()
{
Version = Version,
Id = Id,
Strength = Strength,
Name = Name,
Influence = Influence,
HeadWeights = HeadWeights?.ToArray(),
Offsets = Offsets is null ? null : new(Offsets)
};
public static CharacterAppearanceData ApplyOffsets(CharacterAppearanceData source)
{
var shape = source.BaseShape;
if (shape?.PhotoFaces?.Any(x => x.Influence > 0 || shape.Regions.Values.Any(r => r.PhotoWeights.GetValueOrDefault(x.Id) > 0)) != true)
return source;
var copy = source.DeepCopy();
foreach (var slider in AppearanceCatalog.Sliders.Where(s => s.Group != "Body"))
{
float offset = shape.PhotoFaces.Sum(f => FaceRegions.PhotoShare(shape, f, FaceRegions.ForSlider(slider.Id)) * f.Strength * f.Offsets.GetValueOrDefault(slider.Id));
slider.Set(copy, slider.Get(source) + offset);
}
return copy;
}
}
/// <summary>Conservative proportion guide, not a scan or a calibrated identity reconstruction.</summary>
public static class PhotoFaceFit
{
public static bool SameFamily(int a, int b) => (a is 1 or 3) == (b is 1 or 3);
public static float[] StartingHeads(CharacterAppearanceData appearance, string basis = "skin")
{
if (basis == "current")
return appearance.BaseShape?.Normalized() ?? new[]
{
0f,
0f,
1f,
0f,
0f,
0f,
0f
};
if (basis == "female")
return new[]
{
0f,
.5f,
0f,
.5f,
0f,
0f,
0f
};
if (basis == "male")
return new[]
{
.2f,
0f,
.2f,
0f,
.2f,
.2f,
.2f
};
var index = AppearanceCatalog.Skins.ToList().FindIndex(x => x.ResourcePath == appearance.SkinPath);
var result = new float[7];
result[index is >= 0 and < 7 ? index : 2] = 1;
return result;
}
public static PhotoFace Generate(string name, IReadOnlyList<FaceMeasurement> views, float[] prior)
{
if (views is null || views.Count is < 1 or > 5)
throw new Exception("Analyze one to five clear photos first.");
name = name?.Trim();
if (string.IsNullOrEmpty(name) || name.Length > 40)
throw new Exception("Give your face a name (1–40 characters).");
if (prior?.Length != 7 || prior.Any(x => !float.IsFinite(x) || x < 0) || prior.Sum() <= 0)
throw new Exception("Choose a valid starting head blend.");
float priorSum = prior.Sum();
prior = prior.Select(x => x / priorSum).ToArray();
var frontViews = views.Where(x => x.FrontUsable).ToArray();
if (frontViews.Length == 0)
{
var profileOnly = new PhotoFace
{
Name = name,
HeadWeights = prior.ToArray()
};
PhotoProfileFit.Refine(profileOnly, views, null);
return profileOnly;
}
// A turned cheek/eye is not a second independent width measurement. Keep the
// clearest front views for proportions; angled views still constrain depth.
float clearest = frontViews.Min(x => x.Yaw);
frontViews = frontViews.Where(x => x.Yaw <= clearest + 8).ToArray();
// Normalize the real-photo proportions into the authored head style. These
// neutral reference proportions are heuristic, not population/identity claims.
float[] neutral =
{
1.3f,
.8f,
.22f,
.21f,
.21f,
.25f,
.29f,
.39f
};
var target = new float[8];
for (int k = 0; k < 8; k++)
{
var key = PhotoFitAnchors.Keys[k];
float Value(FaceMeasurement v) => k is 3 or 4 ? (v.Ratios["leftEyeWidth"] + v.Ratios["rightEyeWidth"]) * .5f : v.Ratios[key];
var ordered = frontViews.OrderBy(Value).ToArray();
float half = ordered.Sum(x => x.Quality) * .5f, acc = 0, median = Value(ordered[0]);
foreach (var v in ordered)
{
acc += v.Quality;
if (acc >= half)
{
median = Value(v);
break;
}
}
var center = PhotoFitAnchors.Ratios.Average(x => x[k]);
// Smooth compression instead of flattening all larger deviations to an
// identical target. This remains a style mapping, not scan calibration.
target[k] = center * MathF.Exp(.4f * MathF.Tanh(MathF.Log(Math.Max(median, .001f) / neutral[k]) / .4f));
}
var weights = prior.ToArray();
float sum = weights.Sum();
for (int i = 0; i < 7; i++)
weights[i] /= sum;
float Pred(int k, float[] blend) => Enumerable.Range(0, 7).Sum(i => blend[i] * PhotoFitAnchors.Ratios[i][k]);
float Error(float[] blend) => Enumerable.Range(0, 8).Sum(k => MathF.Pow((Pred(k, blend) - target[k]) / PhotoFitAnchors.Ratios.Average(x => x[k]), 2)) + .015f * Enumerable.Range(0, 7).Sum(i => MathF.Pow(blend[i] - prior[i], 2));
// Simple bounded simplex search; no unbounded facial extrapolation.
foreach (float step in new[]
{
.1f,
.025f,
.005f
}
)
for (int pass = 0; pass < 40; pass++)
{
bool changed = false;
for (int from = 0; from < 7; from++)
for (int to = 0; to < 7; to++)
{
if (from == to || !SameFamily(from, to) || weights[from] < step)
continue;
float before = Error(weights);
weights[from] -= step;
weights[to] += step;
if (Error(weights) < before)
changed = true;
else
{
weights[from] += step;
weights[to] -= step;
}
}
if (!changed)
break;
}
sum = weights.Sum();
for (int i = 0; i < 7; i++)
weights[i] = Math.Max(0, weights[i]) / sum;
var face = new PhotoFace
{
Name = name,
HeadWeights = weights
};
void Guide(string slider, int k, float range, float direction = 1) => face.Offsets[slider] = .5f * MathF.Tanh((target[k] / Pred(k, weights) - 1) / range * direction / .5f);
Guide("head", 0, .40f, -1);
Guide("jaw", 1, .30f);
Guide("eyes", 3, .45f);
Guide("nosewidth", 5, .40f);
Guide("nostrils", 5, .40f);
Guide("mouthwidth", 7, .40f);
// Landmarks 98/327 measure the lower nostril wings, not the central tip.
// Preserve the tip's volume instead of pinching it to match that width.
face.Offsets["nosewidth"] *= .35f;
// A front-view bridge-to-tip distance mixes height, depth and camera pitch.
// Tip height is estimated only by the pose-aware profile fit below.
PhotoProfileFit.Refine(face, views, Error);
return face;
}
}