CharacterKit/PhotoSurfaceFit.cs

PhotoSurfaceFit is a static class that fits a PhotoFace model to detected facial landmarks across multiple photos. It builds 3D geometry from face parameters, projects to 2D with camera/perspective, computes residuals against observed landmarks, and runs iterative search and least-squares refinement to optimize per-face weights, regional weights, offsets, corrections and camera adjustments.

Native Interop
using System;

namespace CharacterEditor;
/// <summary>Fits a shared identity to observed surface landmarks using calibrated native controls.</summary>
public static class PhotoSurfaceFit
{
	public sealed record Diagnostics(float[][] Poses, float[] Perspective, float Error);
	// The deployed analyzer returns 25 landmarks. Keep that established path
	// unchanged while the denser analyzer and surface fitter are being evaluated.
	public static bool CanFit(IReadOnlyList<FaceMeasurement> views) => views.Any(v => v.FrontUsable && PhotoSurfaceBasis.Ids.Count(v.Landmarks.ContainsKey) >= 96);
	public static Vector3[] Geometry(PhotoFace face)
	{
		var points = new Vector3[PhotoSurfaceBasis.Ids.Length];
		for (int p = 0; p < points.Length; p++)
		{
			var overall = Vector3.Zero;
			for (int h = 0; h < 7; h++)
				overall += PhotoSurfaceBasis.Heads[h][p] * face.HeadWeights[h];
			points[p] = overall;
			for (int r = 0; r < FaceRegions.Ids.Length; r++)
			{
				if (!face.RegionWeights.TryGetValue(FaceRegions.Ids[r], out var weights))
					continue;
				var regional = Vector3.Zero;
				for (int h = 0; h < 7; h++)
					regional += PhotoSurfaceBasis.Heads[h][p] * weights[h];
				points[p] += (regional - overall) * PhotoSurfaceBasis.Masks[p][r];
			}
		}

		for (int c = 0; c < PhotoSurfaceBasis.Controls.Length; c++)
		{
			var control = PhotoSurfaceBasis.Controls[c];
			float offset = face.Offsets.GetValueOrDefault(control);
			if (MathF.Abs(offset) < .000001f)
				continue;
			var weights = face.WeightsFor(FaceRegions.ForSlider(control));
			for (int h = 0; h < 7; h++)
			{
				var delta = PhotoSurfaceBasis.Deltas[h * PhotoSurfaceBasis.Controls.Length * 2 + c * 2 + (offset >= 0 ? 1 : 0)];
				float amount = weights[h] * MathF.Abs(offset) * 2;
				for (int p = 0; p < points.Length; p++)
					points[p] += delta[p] * amount;
			}
		}

		for (int c = 0; c < PhotoFaceCorrections.Controls.Length; c++)
		{
			var control = PhotoFaceCorrections.Controls[c];
			float amount = face.Corrections.GetValueOrDefault(control.Id);
			if (control.Fine)
				continue;
			if (MathF.Abs(amount) < .000001f)
				continue;
			var weights = face.WeightsFor(control.Region);
			for (int h = 0; h < 7; h++)
			{
				var delta = PhotoCorrectionBasis.Deltas[h * PhotoFaceCorrections.Controls.Length * 2 + c * 2 + (amount >= 0 ? 1 : 0)];
				for (int p = 0; p < points.Length; p++)
					points[p] += delta[p] * weights[h] * MathF.Abs(amount);
			}
		}

		PhotoContourWarp.Apply(points, face);
		return points;
	}

	public static Vector2[] Project(Vector3[] points, float[] pose, float perspective = 0)
	{
		var m = pose ?? new float[]
		{
			1,
			0,
			0,
			0,
			0,
			1,
			0,
			0,
			0,
			0,
			1,
			0,
			0,
			0,
			0,
			1
		};
		var result = new Vector2[points.Length];
		// A camera in front of the human (+X, looking -X) has image-right +Y.
		// This convention must also be used when binding detector points to triangles.
		float meanDepth = points.Average(p => p.y * m[2] + (p.z - 63) * m[6] + p.x * m[10]);
		for (int i = 0; i < points.Length; i++)
		{
			var p = points[i];
			float depth = p.y * m[2] + (p.z - 63) * m[6] + p.x * m[10];
			float divisor = Math.Clamp(1 - perspective * (depth - meanDepth), .5f, 1.5f);
			result[i] = new Vector2(p.y * m[0] + (p.z - 63) * m[4] + p.x * m[8], -(p.y * m[1] + (p.z - 63) * m[5] + p.x * m[9])) / divisor;
		}

		return result;
	}

	sealed class View
	{
		public FaceMeasurement Measurement;
		public int[] Indices;
		public Vector2[] Observed;
		public float[] Weights;
		public float Scale;
		public float Perspective;
		public float YawCorrection;
		public float PitchCorrection;
		public int[][] Features;
	}

	static float CorrectionPenalty(PhotoFaceCorrections.Control control) => control.Fine ? .0000003f : .00002f;
	static View Prepare(FaceMeasurement measurement)
	{
		// At a strong turn the detector invents the hidden eye and cheek. Only the
		// visible central profile constrains depth; those invented points cannot
		// become a second set of widths or force the far eyelids to move.
		int[] profileIds =
		{
			10,
			152,
			168,
			6,
			197,
			195,
			5,
			4,
			1,
			19,
			94,
			2,
			0,
			13,
			14,
			17, 151, 9, 8,
			// Visible eyebrow contours constrain ridge depth at strong turns.
			// Hidden-side points are still downweighted below.
			70, 63, 105, 66, 107, 55, 65, 52, 53, 46,
			300, 293, 334, 296, 336, 285, 295, 282, 283, 276
		};
		var indices = Enumerable.Range(0, PhotoSurfaceBasis.Ids.Length).Where(i => measurement.Landmarks.ContainsKey(PhotoSurfaceBasis.Ids[i]) && (measurement.Yaw < 60 || profileIds.Contains(PhotoSurfaceBasis.Ids[i]))).ToArray();
		var observed = indices.Select(i => measurement.Landmarks[PhotoSurfaceBasis.Ids[i]]).ToArray();
		var groups = indices.GroupBy(i => PhotoSurfaceBasis.Regions[i]).ToDictionary(x => x.Key, x => x.Count());
		var weights = indices.Select(i =>
		{
			string group = PhotoSurfaceBasis.Regions[i];
			float weight = group == "outline" ? (measurement.Yaw < 20 ? .75f : .25f) : group == "cheeks" ? .75f : 1;
			float side = -PhotoSurfaceBasis.Heads[2][i].y * (measurement.Pose?[8] ?? 0);
			if (measurement.Yaw > 25 && side < -.4f)
				weight *= measurement.Yaw > 40 ? .05f : .15f;
			return weight / groups[group];
		}).ToArray();
		float scale = measurement.Landmarks.TryGetValue(234, out var l) && measurement.Landmarks.TryGetValue(454, out var r) ? (l - r).Length : 0;
		scale = Math.Max(scale, (observed.Max(x => x.y) - observed.Min(x => x.y)) * .55f);
		return new()
		{
			Measurement = measurement,
			Indices = indices,
			Observed = observed,
			Weights = weights,
			Features = measurement.Yaw > 30 ? Array.Empty<int[]>() : new[]
			{
				Enumerable.Range(0, indices.Length).Where(k => indices[k] < 16).ToArray(),
				Enumerable.Range(0, indices.Length).Where(k => indices[k] >= 16 && indices[k] < 32).ToArray(),
				Enumerable.Range(0, indices.Length).Where(k => PhotoSurfaceBasis.Regions[indices[k]] == "mouth").ToArray(),
				Enumerable.Range(0, indices.Length).Where(k => PhotoSurfaceBasis.Regions[indices[k]] == "nose").ToArray(),
				Enumerable.Range(0, indices.Length).Where(k => PhotoSurfaceBasis.Regions[indices[k]] == "jaw").ToArray(),
				Enumerable.Range(0, indices.Length).Where(k => PhotoSurfaceBasis.Regions[indices[k]] == "forehead").ToArray(),
				Enumerable.Range(0, indices.Length).Where(k => PhotoSurfaceBasis.Regions[indices[k]] == "cheeks").ToArray()
			}.Where(f => f.Length >= 6).ToArray(),
			Scale = Math.Max(scale, .0001f)
		};
	}

	static float[] CorrectedPose(View view)
	{
		var original = view.Measurement.Pose ?? new float[]
		{
			1,
			0,
			0,
			0,
			0,
			1,
			0,
			0,
			0,
			0,
			1,
			0,
			0,
			0,
			0,
			1
		};
		var result = original.ToArray();
		float y = view.YawCorrection * MathF.PI / 180, p = view.PitchCorrection * MathF.PI / 180, cy = MathF.Cos(y), sy = MathF.Sin(y), cp = MathF.Cos(p), sp = MathF.Sin(p);
		for (int row = 0; row < 3; row++)
		{
			float x = original[row] * cy - original[8 + row] * sy, z = original[row] * sy + original[8 + row] * cy;
			result[row] = x;
			result[4 + row] = original[4 + row] * cp + z * sp;
			result[8 + row] = -original[4 + row] * sp + z * cp;
		}

		return result;
	}

	static void DetectorBias(Vector2[] projection, PhotoFace face, float[] pose)
	{
		float yaw = MathF.Atan2(pose[8], pose[10]) * 180 / MathF.PI;
		int lo = yaw < 0 ? 0 : 1, hi = lo + 1;
		float t = Math.Clamp((yaw - PhotoDetectorBasis.Yaws[lo]) / (PhotoDetectorBasis.Yaws[hi] - PhotoDetectorBasis.Yaws[lo]), 0, 1);
		for (int p = 0; p < projection.Length; p++)
			for (int h = 0; h < 7; h++)
			{
				float weight = face.HeadWeights[h];
				for (int r = 0; r < FaceRegions.Ids.Length; r++)
					if (face.RegionWeights.TryGetValue(FaceRegions.Ids[r], out var w))
						weight += (w[h] - face.HeadWeights[h]) * PhotoSurfaceBasis.Masks[p][r];
				projection[p] += (PhotoDetectorBasis.Bias[h][lo][p] * (1 - t) + PhotoDetectorBasis.Bias[h][hi][p] * t) * weight;
			}

		PhotoContourWarp.DetectorBias(projection, face, yaw);
	}

	static float[] Residual(PhotoFace face, Vector3[] geometry, View view, bool localContours = false)
	{
		var pose = CorrectedPose(view);
		var projection = Project(geometry, pose, view.Perspective);
		if (view.Measurement.DetectorLandmarks)
			DetectorBias(projection, face, pose);
		float sum = view.Weights.Sum();
		var pc = Vector2.Zero;
		var oc = Vector2.Zero;
		for (int k = 0; k < view.Indices.Length; k++)
		{
			pc += projection[view.Indices[k]] * view.Weights[k];
			oc += view.Observed[k] * view.Weights[k];
		}

		pc /= sum;
		oc /= sum;
		float dot = 0, cross = 0, length = 0;
		for (int k = 0; k < view.Indices.Length; k++)
		{
			var p = projection[view.Indices[k]] - pc;
			var o = view.Observed[k] - oc;
			float w = view.Weights[k];
			dot += Vector2.Dot(p, o) * w;
			cross += (p.x * o.y - p.y * o.x) * w;
			length += p.LengthSquared * w;
		}

		float a = dot / Math.Max(length, .000001f), b = cross / Math.Max(length, .000001f);
		var residual = new float[view.Indices.Length * 2];
		var fitted = localContours ? new Vector2[view.Indices.Length] : null;
		for (int k = 0; k < view.Indices.Length; k++)
		{
			var p = projection[view.Indices[k]] - pc;
			var predicted = new Vector2(a * p.x - b * p.y, b * p.x + a * p.y) + oc;
			if (localContours)
				fitted[k] = predicted;
			var delta = (predicted - view.Observed[k]) / view.Scale;
			float d = delta.Length;
			// A mislabeled/occluded feature must not drag the whole head toward it.
			float robust = d < .035f ? 1 : (.07f * d - .001225f) / (d * d);
			float weight = MathF.Sqrt(view.Weights[k] / sum * robust);
			residual[k * 2] = delta.x * weight;
			residual[k * 2 + 1] = delta.y * weight;
		}

		if (!localContours)
			return residual;
		var values = new List<float>(residual);
		foreach (var feature in view.Features)
		{
			// Local centering separates contour shape from feature placement. Eye
			// aperture is measured relative to the eye, rather than the whole face,
			// so a meaningful eyelid mismatch cannot disappear in a low global RMS.
			var predictedCenter = feature.Aggregate(Vector2.Zero, (s, k) => s + fitted[k]) / feature.Length;
			var observedCenter = feature.Aggregate(Vector2.Zero, (s, k) => s + view.Observed[k]) / feature.Length;
			float extent = Math.Max(feature.Max(k => view.Observed[k].x) - feature.Min(k => view.Observed[k].x), feature.Max(k => view.Observed[k].y) - feature.Min(k => view.Observed[k].y));
			float weight = MathF.Sqrt(.04f / feature.Length);
			foreach (int k in feature)
			{
				var delta = (fitted[k] - predictedCenter - view.Observed[k] + observedCenter) / Math.Max(extent, .0001f);
				float distance = delta.Length;
				float robust = distance <= .12f ? 1 : MathF.Sqrt((.24f * distance - .0144f) / (distance * distance));
				values.Add(delta.x * weight * robust);
				values.Add(delta.y * weight * robust);
			}
		}

		return values.ToArray();
	}

	static float Error(PhotoFace face, Vector3[] geometry, View view, bool localContours = false) => Residual(face, geometry, view, localContours).Sum(r => r * r);
	public static float Error(PhotoFace face, IReadOnlyList<FaceMeasurement> views)
	{
		var usable = views.Where(v => PhotoSurfaceBasis.Ids.Count(v.Landmarks.ContainsKey) >= 12).Select(Prepare).ToArray();
		if (usable.Length == 0)
			return 0;
		var geometry = Geometry(face);
		return usable.Average(v => Error(face, geometry, v));
	}

	public static PhotoFace Generate(string name, IReadOnlyList<FaceMeasurement> measurements, float[] prior, System.Threading.CancellationToken cancellation = default, Action<Diagnostics> diagnostics = null, bool fitContours = true)
	{
		var views = measurements.Where(v => PhotoSurfaceBasis.Ids.Count(v.Landmarks.ContainsKey) >= 12).Select(Prepare).ToArray();
		var face = new PhotoFace
		{
			Name = name,
			HeadWeights = prior.ToArray()
		};
		bool depth = measurements.Any(v => v.ProfileUsable && v.Yaw >= 25);
		bool localContours = false;
		// The former whole-head volume reaches the lower neck. Identity proportions
		// use face-only width/height corrections so photo fitting cannot move the collar.
		var controls = PhotoSurfaceBasis.Controls.Where(c => c != "head" && (depth || c is not ("nose" or "chin" or "mouthprojection"))).ToArray();
		float Score()
		{
			cancellation.ThrowIfCancellationRequested();
			var geometry = Geometry(face);
			float score = views.Sum(v => Error(face, geometry, v, localContours) * v.Measurement.Quality) / views.Sum(v => v.Measurement.Quality);
			score += views.Sum(v => (v.YawCorrection * v.YawCorrection + v.PitchCorrection * v.PitchCorrection) * .00000001f);
			score += .000015f * face.Offsets.Values.Sum(x => x * x);
			score += PhotoFaceCorrections.Controls.Sum(c => CorrectionPenalty(c) * MathF.Pow(face.Corrections.GetValueOrDefault(c.Id), 2));
			score += .000008f * face.HeadWeights.Zip(prior).Sum(x => MathF.Pow(x.First - x.Second, 2));
			foreach (var w in face.RegionWeights.Values)
				score += .000006f * w.Zip(face.HeadWeights).Sum(x => MathF.Pow(x.First - x.Second, 2));
			return score;
		}

		float best = Score();
		void Blend(float[] weights, float step)
		{
			for (int from = 0; from < 7; from++)
				for (int to = 0; to < 7; to++)
				{
					if (from == to || !PhotoFaceFit.SameFamily(from, to) || weights[from] < step)
						continue;
					weights[from] -= step;
					weights[to] += step;
					float score = Score();
					if (score < best - 1e-10f)
						best = score;
					else
					{
						weights[from] += step;
						weights[to] -= step;
					}
				}
		}

		void Controls(float step)
		{
			foreach (var id in controls)
			{
				float original = face.Offsets.GetValueOrDefault(id), chosen = original;
				foreach (float sign in new[]
				{
					-1f,
					1f
				}

				)
				{
					float value = Math.Clamp(original + step * sign, -.35f, .35f);
					face.Offsets[id] = value;
					float score = Score();
					if (score < best - 1e-10f)
					{
						best = score;
						chosen = value;
					}
				}

				face.Offsets[id] = chosen;
			}
		}

		void Corrections(float step)
		{
			foreach (var control in PhotoFaceCorrections.Controls.Where(c => !c.Fine))
			{
				float original = face.Corrections.GetValueOrDefault(control.Id), chosen = original;
				foreach (float direction in new[]
				{
					-1f,
					1f
				}

				)
				{
					float value = Math.Clamp(original + direction * step, -1, 1);
					face.Corrections[control.Id] = value;
					float score = Score();
					if (score < best - 1e-10f)
					{
						best = score;
						chosen = value;
					}
				}

				face.Corrections[control.Id] = chosen;
			}
		}

		void Cameras(float step)
		{
			foreach (var view in views)
				foreach (bool yaw in new[]
				{
					true,
					false
				}

				)
				{
					float original = yaw ? view.YawCorrection : view.PitchCorrection, chosen = original;
					foreach (float direction in new[]
					{
						-1f,
						1f
					}

					)
					{
						float value = Math.Clamp(original + direction * step, -20, 20);
						if (yaw)
							view.YawCorrection = value;
						else
							view.PitchCorrection = value;
						float score = Score();
						if (score < best - 1e-10f)
						{
							best = score;
							chosen = value;
						}
					}

					if (yaw)
						view.YawCorrection = chosen;
					else
						view.PitchCorrection = chosen;
				}
		}

		foreach (float step in new[]
		{
			8f,
			4f,
			2f,
			.75f
		}

		)
			for (int pass = 0; pass < 2; pass++)
				Cameras(step);
		foreach (float step in new[]
		{
			.2f,
			.08f,
			.025f,
			.008f
		}

		)
			for (int pass = 0; pass < 3; pass++)
			{
				Blend(face.HeadWeights, step);
				Controls(step);
				Corrections(step * 2);
				Cameras(step * 15);
			}

		foreach (var region in new[]
		{
			"nose",
			"jaw",
			"eyes",
			"mouth",
			"forehead",
			"cheeks"
		}

		)
			face.RegionWeights[region] = face.HeadWeights.ToArray();
		foreach (float step in new[]
		{
			.15f,
			.05f,
			.015f,
			.005f
		}

		)
			for (int pass = 0; pass < 2; pass++)
			{
				foreach (var weights in face.RegionWeights.Values)
					Blend(weights, step);
				Controls(step);
				Corrections(step * 2);
				Cameras(step * 15);
			}

		// Camera scale, translation and roll are solved analytically for every view.
		// A small perspective search accounts for differing photo focal distances.
		foreach (var view in views)
		{
			float chosen = view.Perspective;
			foreach (float value in new[]
			{
				0f,
				.015f,
				.03f,
				.05f
			}

			)
			{
				view.Perspective = value;
				float score = Score();
				if (score < best)
				{
					best = score;
					chosen = value;
				}
			}

			view.Perspective = chosen;
		}

		foreach (float step in new[]
		{
			.015f,
			.005f
		}

		)
		{
			Blend(face.HeadWeights, step);
			foreach (var weights in face.RegionWeights.Values)
				Blend(weights, step);
			Controls(step);
			Corrections(step * 2);
			Cameras(step * 15);
		}

		// Single-parameter steps can stall when eye spacing, head proportions and
		// framing need to change together. Refine those variables jointly.
		var parameters = new List<PhotoLeastSquares.Parameter>();
		foreach (var id in controls)
			parameters.Add(new(() => face.Offsets.GetValueOrDefault(id), v => face.Offsets[id] = v, -.35f, .35f, .002f, .15f));
		foreach (var control in PhotoFaceCorrections.Controls.Where(c => !c.Fine))
			parameters.Add(new(() => face.Corrections.GetValueOrDefault(control.Id), v => face.Corrections[control.Id] = v, -1, 1, .004f, .3f));
		foreach (var view in views)
		{
			parameters.Add(new(() => view.YawCorrection, v => view.YawCorrection = v, -20, 20, .1f, 4));
			parameters.Add(new(() => view.PitchCorrection, v => view.PitchCorrection = v, -20, 20, .1f, 4));
			parameters.Add(new(() => view.Perspective, v => view.Perspective = v, 0, .05f, .001f, .01f));
		}

		float[] Residuals()
		{
			cancellation.ThrowIfCancellationRequested();
			var geometry = Geometry(face);
			var values = new List<float>();
			float quality = views.Sum(v => v.Measurement.Quality);
			foreach (var view in views)
			{
				float w = MathF.Sqrt(view.Measurement.Quality / quality);
				values.AddRange(Residual(face, geometry, view, localContours).Select(x => x * w));
			}

			values.AddRange(controls.Select(id => face.Offsets.GetValueOrDefault(id) * MathF.Sqrt(.000015f)));
			values.AddRange(PhotoFaceCorrections.Controls.Select(c => face.Corrections.GetValueOrDefault(c.Id) * MathF.Sqrt(CorrectionPenalty(c))));
			foreach (var view in views)
			{
				values.Add(view.YawCorrection * .0001f);
				values.Add(view.PitchCorrection * .0001f);
			}

			return values.ToArray();
		}

		PhotoLeastSquares.Refine(parameters, Residuals, Score, cancellation);
		// Local contours refine a settled identity and camera. Keeping camera pose
		// fixed here prevents an eyelid or jaw mismatch being hidden by a new angle.
		if (fitContours)
		{
			localContours = true;
			var contours = new List<PhotoLeastSquares.Parameter>();
			foreach (var control in PhotoFaceCorrections.Controls.Where(c => c.Fine && (!c.Depth || depth)))
				contours.Add(new(() => face.Corrections.GetValueOrDefault(control.Id), v => face.Corrections[control.Id] = v, -1, 1, .004f, .35f));
			PhotoLeastSquares.Refine(contours, Residuals, Score, cancellation, 24);
			// Existing sliders can relax as the finer fields take over their contour.
			foreach (var id in controls)
				contours.Add(new(() => face.Offsets.GetValueOrDefault(id), v => face.Offsets[id] = v, -.35f, .35f, .002f, .1f));
			PhotoLeastSquares.Refine(contours, Residuals, Score, cancellation, 12);
		}

		best = Score();
		foreach (var key in face.Offsets.Keys.ToArray())
			if (MathF.Abs(face.Offsets[key]) < .00001f)
				face.Offsets.Remove(key);
		// Remove floating-point drift so endpoints remain valid portable coefficients.
		foreach (var weights in face.RegionWeights.Values.Prepend(face.HeadWeights))
		{
			for (int h = 0; h < 7; h++)
				weights[h] = Math.Clamp(weights[h], 0, 1);
			float total = weights.Sum();
			for (int h = 0; h < 7; h++)
				weights[h] /= total;
		}

		diagnostics?.Invoke(new(views.Select(CorrectedPose).ToArray(), views.Select(v => v.Perspective).ToArray(), Score()));
		return face;
	}
}