CharacterKit/PhotoFace.cs

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.

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