histogram implemented
This commit is contained in:
116
Plugins/LindtLeerformPlugin/Services/SplineCurve.cs
Normal file
116
Plugins/LindtLeerformPlugin/Services/SplineCurve.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
namespace LindtLeerformPlugin.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Monotone cubic Hermite spline (Fritsch–Carlson). 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user