Code/CharacterKit/PhotoFace.cs

Defines PhotoFace data and fitting logic for converting face measurements into a portable, constrained face representation and applying those offsets to character appearance. PhotoFace holds metadata, weights and slider offsets with validation, deep copy and an ApplyOffsets helper. PhotoFaceFit contains algorithms to pick starting head blends and to generate a PhotoFace from 1–5 FaceMeasurement views using heuristics and a simple optimization loop.

File Access
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;
	}
}