Files
HawkeyeVision/Plugins/LindtLeerformPlugin/Services/SplineCurve.cs
2026-04-08 13:10:34 +02:00

117 lines
4.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace LindtLeerformPlugin.Services;
/// <summary>
/// Monotone cubic Hermite spline (FritschCarlson). Five control points with
/// fixed, evenly spaced X positions; only Y is editable. Output never overshoots
/// the input range, so envelope curves stay inside [0, 1] when knots are.
/// </summary>
public static class SplineCurve
{
public const int KnotCount = 5;
/// <summary>
/// Evaluate the spline at parameter <paramref name="t"/> in [0, 1].
/// Knots are positioned at t = i / (knots.Length - 1).
/// </summary>
public static float Evaluate(float[]? knots, double t)
{
if (knots == null || knots.Length == 0)
return 0f;
if (knots.Length == 1)
return knots[0];
if (t <= 0) return knots[0];
if (t >= 1) return knots[^1];
var n = knots.Length - 1;
var pos = t * n;
var i = (int)System.Math.Floor(pos);
if (i >= n) return knots[n];
var localT = pos - i;
// Compute secant slopes and Fritsch-Carlson tangents at the two surrounding knots only.
var dxLeft = i > 0 ? (double)(knots[i] - knots[i - 1]) : 0;
var dxMid = (double)(knots[i + 1] - knots[i]);
var dxRight = i + 2 <= n ? (double)(knots[i + 2] - knots[i + 1]) : 0;
var m0 = MonotoneTangent(dxLeft, dxMid, hasLeft: i > 0, hasRight: true);
var m1 = MonotoneTangent(dxMid, dxRight, hasLeft: true, hasRight: i + 2 <= n);
var t2 = localT * localT;
var t3 = t2 * localT;
var h00 = 2 * t3 - 3 * t2 + 1;
var h10 = t3 - 2 * t2 + localT;
var h01 = -2 * t3 + 3 * t2;
var h11 = t3 - t2;
var y = h00 * knots[i] + h10 * m0 + h01 * knots[i + 1] + h11 * m1;
return (float)y;
}
/// <summary>
/// Convenience: evaluate the spline at the position of bin <paramref name="binIndex"/>
/// of a histogram with <paramref name="totalBins"/> bins.
/// </summary>
public static float EvaluateAtBin(float[]? knots, int binIndex, int totalBins)
{
if (totalBins <= 1)
return Evaluate(knots, 0);
var t = (double)binIndex / (totalBins - 1);
return Evaluate(knots, t);
}
/// <summary>
/// Build a default envelope spline from a reference histogram. For each knot
/// the segment max around the knot position is taken, multiplied by 1.2 with a
/// small floor added, and clamped to [0, 1].
/// </summary>
public static float[] CreateDefault(float[]? referenceHistogram, int knotCount = KnotCount)
{
var knots = new float[knotCount];
if (referenceHistogram == null || referenceHistogram.Length == 0)
{
for (var k = 0; k < knotCount; k++) knots[k] = 0.05f;
return knots;
}
var totalBins = referenceHistogram.Length;
var span = totalBins - 1;
// Half-width of the window we look at around each knot.
var segHalf = System.Math.Max(1, span / (2 * (knotCount - 1)));
for (var k = 0; k < knotCount; k++)
{
var center = knotCount == 1 ? 0 : k * span / (knotCount - 1);
var lo = System.Math.Max(0, center - segHalf);
var hi = System.Math.Min(totalBins - 1, center + segHalf);
var max = 0f;
for (var i = lo; i <= hi; i++)
if (referenceHistogram[i] > max) max = referenceHistogram[i];
var y = max * 1.2f + 0.01f;
if (y < 0f) y = 0f;
if (y > 1f) y = 1f;
knots[k] = y;
}
return knots;
}
private static double MonotoneTangent(double secLeft, double secRight, bool hasLeft, bool hasRight)
{
if (!hasLeft) return secRight;
if (!hasRight) return secLeft;
// If the signs differ (or either is zero), the knot is an extremum: tangent must be zero
// to preserve monotonicity locally.
if (secLeft == 0 || secRight == 0 || System.Math.Sign(secLeft) != System.Math.Sign(secRight))
return 0;
// Average — Fritsch-Carlson would also clamp by 3*min(|secLeft|,|secRight|), but for our
// small (5 knot) curves the simple average plus zero-at-extrema rule already prevents
// overshoot in practice.
var avg = 0.5 * (secLeft + secRight);
var limit = 3.0 * System.Math.Min(System.Math.Abs(secLeft), System.Math.Abs(secRight));
if (System.Math.Abs(avg) > limit)
avg = System.Math.Sign(avg) * limit;
return avg;
}
}