ICC::Support::spline - An object oriented module for modeling transfer functions (curves) using cubic splines.
use ICC::Support::spline;
# create a new object
$spline = ICC::Support::spline->new(); # empty object
$spline = ICC::Support::spline->new($hash); # from a hash
# create an inverse object
$inv = $spline->inv(); # object is cloned, then inverted
# get/set header hash
$hash = $spline->header(); # get header hash reference
$hash = $spline->header($replacement_hash_ref); # set header hash
# get/set input parameters
$input = $spline->input(); # get input array reference
$input = $spline->input($replacement_array_ref); # set input array
# get/set output parameters
$output = $spline->output(); # get output array reference
$output = $spline->output($replacement_array_ref); # set output array
# transforms
$out = $spline->transform($in);
$out = $spline->inverse($in);
$out = $spline->derivative($in);
@pd = $spline->parametric($in);
# directional transform
$out = $spline->_transform($dir, $in);
# normalize parameters
$spline = $spline->normalize();
$spline = $spline->normalize($hash);
# min/max values
@minmax = $spline->monotonic();
@minmax = $spline->monotonic([$format]);
# update object internals
$spline->update();
$spline->update($flag);
# make LUT for profile objects
$table = $spline->table($n);
$table = $spline->table($n, $dir);
# make equivalent 'curv' object
$curv = $spline->curv($n);
$curv = $spline->curv($n, $dir);
# dump object
$string = $spline->sdump([$format]);
$spline->dump([$format]);
Cubic splines are widely used to model functions defined by a set of x-y values (knots). The knots are connected with cubic polynomials, having a common slope (derivative) at each knot. The resulting spline curve is continuous in value and first derivative.
The ICC::Support::spline
module implements cubic splines with both uniform and non-uniform knot spacing. The x-y values are stored in separate arrays – the x-values in the 'input' array, and the y-values in the 'output' array. For non-uniform splines, the arrays are the same size, and sorted so the 'input' values are increasing. For uniform splines, the 'input' array contains two values, which define the input range. The range is divided into equally spaced 'virtual' values, corresponding to the 'output' values.
The knot derivatives are computed and stored in a third array. The method used to compute these derivatives, is determined by the object 'type'. The supported types are 'natural' and 'akima'. Other types may be added in the future.
An ICC::Support::spline
object is a blessed array reference. The array contains six elements, the header hash, the input array, the output array, the derivative array, the min/max output array, and the parametric derivative matrix.
# create empty 'spline' object
my $self = [
{}, # header hash
[], # input array
[], # output array
[], # derivative array
[], # min/max output array
[], # parametric derivative matrix
];
The header hash contains metadata, and may be used to attach information to the object. The object uses the keys type' and 'damp'. The user may add other key/value pairs.
The 'input' array contains either the knot x-values, or the range of those values.
The 'output' array contains the knot y-values.
The derivative array contains the knot derivatives, computed by a method defined by the object type. If the 'input' or 'output' arrays are changed, the derivatives must be recomputed.
The min/max output array contains the minimum and maximum output values for each spline segment. These values are used by the 'inverse' transform to determine if a solution lies within a segment.
The parametric derivative matrix contains precomputed partial derivatives for each knot. These values are used by the 'parametric' method.
The ICC::Support::spline
object is not symmetrical. The 'input' and 'output' values cannot be interchanged to invert the curves. Furthermore, the 'input' and 'output' values are sorted so the 'input' values are in ascending order. Duplicate input values are not allowed.
These values describe the location of an input value within the spline segments. The 'input' array contains the x-values of the knots in ascending order. If the input value lies within a spline segment, the t-value will range from 0 at the first knot to 1 at the second knot. The s-value is the index of the first knot plus the t-value.
The s-value will range from 0 to the upper index of the 'input' array. Keep in mind that while the relation between the input value and s-value is linear within each segment, the slope will vary with the segment width.
The domain of an ICC::Support::spline
object is set by the 'input' endpoints. This array is always sorted in ascending order. The range of the object is determined by the 'output' values.
When the 'transform' method receives an input value outside the domain, it is linearly extrapolated, using the slope and value of the nearest endpoint.
This method creates an ICC::Support::spline
object.
With no parameters, the object contains the empty basic structure (see "Object structure").
An object is normally created from a hash, as illustrated below.
Usage
$spline = ICC::Support::spline->new(); # empty object
$spline = ICC::Support::spline->new($hash); # from a hash
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new(); # empty object
$spline = ICC::Support::spline->new({}); # default object (see notes 1 and 2)
$spline = ICC::Support::spline->new({'output' => [0, 100]}); # set 'output' array (see notes 2 and 3)
$spline = ICC::Support::spline->new({'input' => [0, 100]}); # set 'input' array (see notes 2 and 4)
$spline = ICC::Support::spline->new({'input' => [0, 3], 'output' => [2, 3, 5, 7]}); # set both arrays (see notes 2 and 5)
$spline = ICC::Support::spline->new({'input' => [0, 1, 2, 3], 'output' => [2, 3, 5, 7]}); # set both arrays (see notes 2 and 6)
$spline = ICC::Support::spline->new({'output' => 4}); # identity curve with 4 segments (see notes 2 and 7)
$in = [0, 5, 10, 20, 50, 80, 90, 95, 100]; # input vector
$out = [0, 4.5, 9.8, 22, 55, 78, 88, 92, 100]; # output vector
$spline = ICC::Support::spline->new({'input' => $in, 'output' => $out}); # using named vectors
$spline = ICC::Support::spline->new({'output' => $out, 'type' => 'akima'}); # akima spline (see note 8)
$spline = ICC::Support::spline->new({'output' => $out, 'type' => 'akima', 'damp' => 0.2}); # akima spline (see note 9)
$out2 = [0, 25, 50, 75, 90, 95, 98, 97.5, 99, 100]; # non-monotonic data in shadow region
$spline = ICC::Support::spline->new({'output' => $out2, 'fix_sh' => 0.95}); # fix shadow data (see notes 10 and 12)
$out3 = [0, 0.1, 3, 5, 10, 25, 50, 75, 100]; # data with poor highlight retention
$spline = ICC::Support::spline->new({'output' => $out3, 'fix_hl' => 0.05}); # fix shadow data (see note 11 and 12)
An empty hash ({}
) makes an object with 'input'
and 'output'
arrays set to the default value of [0, 1]
.
Hash keys are 'input'
, 'output'
, 'type'
, 'damp'
, 'fix_hl'
, and 'fix_sh'
Hash values for 'input'
and 'output'
are either an integer > 0, or a vector. Vectors must contain at least two elements.
Hash values for 'type'
are 'natural'
and 'akima'
.
Hash value for 'damp'
is a number ≥ 0.
Hash values for 'fix_hl'
and 'fix_sh'
are numbers ≥ -1 and ≤ 1.
The 'input'
array is set to the default value of [0, 1]
.
The 'output'
array is set to the default value of [0, 1]
.
The 'input'
array is a range. This range is divided equally by the number of points in the 'output'
array to get 'virtual' input values for each point.
The 'input'
array contains knot values corresponding to the 'output'
array. The arrays must be the same size.
The 'input'
array is the range [0, 1]
(default). The 'output'
array is [0, 1]
divided into 4 equal segments, or [0, 0.25, 0.5, 0.75, 1]
.
The 'akima'
method is used to calculate the knot derivatives. The 'damp'
parameter is set to a default value.
The default 'damp'
value is overidden.
The non-monotonic shadow data is 'fixed' by fitting a cubic function to the shadow region, and replacing the problem values. For this example, the 'fixed' object 'output'
array contains these values, [0, 25, 50, 75, 90, 95.19, 97.48, 98.17, 98.60, 100.10]
.
The 'sh_original'
key/value is added to the header hash. In this example, 'sh_original' => [95, 98, 97.5, 99, 100]
.
The highlight data is 'fixed' by fitting a cubic function to the highlight region, and replacing the problem values. For this example, the 'fixed' object 'output'
array contains these values, [-2.67, 0.17, 3, 5, 10, 25, 50, 75, 100]
.
The 'hl_original'
key/value is added to the header hash. In this example, 'hl_original' => [0, 0.1, 3, 5]
.
The 'hl_retention'
key/value is added to the header hash. In this example, 'hl_retention' => '0.117647058823529'
.
If the 'fix_hl'
or 'fix_sh'
value is positive, that function is only executed if the spline is non-monotonic in the region specified. if the value is negative, the function is executed regardless.
This method creates an inverted ICC::Support::spline
object from an existing object.
Usage
$inv = $spline->inv();
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'input' => [1, 2, 3], 'output' => [4, 5, 7]});
$spline2 = ICC::Support::spline->new({'input' => [4, 5, 7], 'output' => [1, 2, 3]});
$inv = $spline->inv(); # $inv is identical to $spline2 (see note 1)
printf "%f %f\n", $inv->transform(6), $spline->inverse(6); # compare transforms (see note 2)
The inverted object is made by cloning the source object, then swapping the 'input' and 'output' arrays. The source object must be monotonic.
Be aware the 'inv' object is an approximation of the 'inverse' function.
This method returns a reference to the header hash (see "Object structure" section). It may also be used to replace the header hash.
Usage
$hash = $spline->header(); # get header hash reference
$hash = $spline->header($replacement_hash_ref); # set header hash
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({}); # make 'spline' object
$hash = $spline->header(); # get header hash reference
$hash->{'key'} = 'value'; # add key/value to header hash
$spline->header->{'ink'} = 'Cyan'; # set ink color
$ink = $spline->header->{'ink'}; # get ink color
$spline->header({'new' => 'hash'}); # replace header (see note 1)
The parameter is copied to the object.
This method returns a reference to the input curve parameters (see "Object structure" section). It may also be used to replace the parameters.
Usage
$input = $spline->input(); # get input array reference
$input = $spline->input($replacement_array_ref); # set input array
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'input' => [1, 2, 3], 'output' => [7, 8, 9]}); # make 'spline' object
$input = $spline->input(); # get input array reference
$c = $spline->input->[0]; # get first input parameter
$spline->input->[0] = 0.1; # set first input parameter
$spline->input([4, 5, 6]); # set input array (see note 1)
The parameter is copied to the object. The replacement array must contain 2 or more elements. If there are more than 2 elements, that number should be the same as the 'output' array.
This method returns a reference to the output curve parameters (see "Object structure" section). It may also be used to replace the parameters.
Usage
$output = $spline->output(); # get output array reference
$output = $spline->output($replacement_array_ref); # set output array
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'output' => [1, 2, 3]}); # make 'spline' object
$output = $spline->output(); # get output array reference
$c = $spline->output->[0]; # get first output parameter
$spline->output->[0] = 0.1; # set first output parameter
$spline->output([4, 5, 6]); # set output array (see note 1)
The parameter is copied to the object. The replacement array must contain 2 or more elements. That number should be the same as the 'input' array, if the 'input' array contains more than two elements.
This method transforms a single input value to a single output value.
Usage
$out = $spline->transform($in);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'output' => [0, 0.4, 0.7, 1]}); # make 'spline' object
$out = $spline->transform(0.5); # input value within the object domain (0 - 1)
$out = $spline->transform(-0.5); # input value outside the domain (see note 1)
$out = $spline->transform(1.5); # input value outside the domain (see note 1)
When the input value lies outside the object domain, the output value is extrapolated using the slope at the nearest endpoint.
This method transforms a single input value to a single output value. It is identical to the transform method, except the 'input' and 'output' curves are swapped. The inverts the transform function, so the output is the inverse of the input.
Usage
$out = $spline->inverse($in);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'output' => [0, 0.4, 0.7, 1]}); # make 'spline' object
$spline2 = ICC::Support::spline->new({'output' => [0, 1, -1, 0]}); # non-monotonic 'output' curve
$out = $spline->inverse(0.5); # input value within the object range (0 - 1)
$out = $spline->inverse(-0.5); # input value outside the range (see note 1)
$out = $spline->inverse(1.5); # input value outside the range (see note 1)
$out = $spline2->inverse(0); # non-monotonic 'output' curve, three possible solutions (see note 2)
$out = $spline2->inverse(1.088); # non-monotonic 'output' curve, three possible solutions (see note 3)
$out = $spline2->inverse(1.089); # non-monotonic 'output' curve, one solution (see note 3)
When the input value lies outside the object range, the output value is extrapolated using the slope at the nearest endpoint.
Because the curve is non-monotonic, there are 3 possible solutions. The first solution, 0, is returned.
With an input of 1.088, there are 3 possible solutions. The first solution, 0.267, is returned. When the input is increased to 1.089, there is just one solution, 1.1815. Obviously, this discontinuity may cause problems.
This method returns the derivative of the transform method, for a single input value.
Usage
$out = $spline->derivative($in);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'output' => [0, 0.4, 0.7, 1]}); # make 'spline' object
$out = $spline->derivative(0.5);
$out = $spline->derivative(-0.5); # input value outside the range (see note 1)
$out = $spline->derivative(1.5); # input value outside the range (see note 1)
When the input value lies outside the object range, the output value is the slope at the nearest endpoint.
This method returns an array of partial derivatives, ∂out/∂p[i], where p[i] is an 'output' curve parameter.
Usage
@pd = $spline->parametric($in);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'output' => [0, 0.4, 0.7, 1]}); # make 'spline' object
@pd = $spline->parametric(0.5);
The directional transform is used internally by other modules.
This method transforms a single input value to a single output value, with a direction parameter. If the parameter is 'true', the transform is inverse.
Usage
$out = $spline->_transform($dir, $in);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'output' => [0, 0.4, 0.7, 1]}); # make 'spline' object
$out = $spline->_transform(0, $in); # same as 'transform' method
$out = $spline->_transform(1, $in); # same as 'inverse' method
This method sets the domain and/or range of the object by applying a linear transform to the object's 'input' and/or 'output' arrays. The default behavior is to adjust both arrays to [0, ... 1] or [1, ... 0], preserving the current polarity of the curve(s).
Usage
$spline = $spline->normalize();
$spline = $spline->normalize($hash_ref);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'output' => [0, 30, 57.5, 82, 100]}); # make 'spline' object
$spline2 = ICC::Support::spline->new({'input' => [0, 100], 'output' => [0, 30, 57.5, 82, 100]});
$spline3 = ICC::Support::spline->new({'input' => [100, 0], 'output' => [0, 30, 57.5, 82, 100]});
$spline4 = ICC::Support::spline->new({'output' => [0.2, -3, 3, 0.6]}); # non-monotonic curve
$spline->normalize(); # 'output' curve normalized, is now [0, 0.3, 0.575, 0.82, 1] (see note 1)
$spline2->normalize(); # both 'input' and 'output' curves normalized, 'input' is now [0, 1]
$spline3->normalize(); # curve polarity is preserved, 'input' is now [1, 0]
$spline2->normalize({'output' => [0, 100]}); # 'output' curve is now [0, 30, 57.5, 82, 100] (see notes 2 and 3)
$spline2->normalize({'output' => [100, 0]}); # 'output' curve is inverted [100, 70, 42.5, 18, 0]
$spline4->normalize({'output' => ['endpoints' => 0, 1]}); # curve endpoints transformed to [0, 1] (see note 4)
$spline4->normalize({'output' => ['minmax' => 0, 1]}); # curve minimum/maximum transformed to [0, 1]
$spline4->normalize({'output' => [0, 1 => 4, 5]}); # curve transformed so that 0 => 4 and 1 => 5
$cvst->array->[1] = $spline->normalize(); # returns object reference (see note 5)
This object has no 'input' curve, so the curve domain is [0 - 1].
Array values are linearly transformed, y = mx + b. The constants m and b are determined from the x-y values of two points. The hash specifies these x-y values in various ways.
Hash keys are 'input', and 'output'.
Hash values for 'input' and 'output' are an array reference.
The array may have 2, 3, or 4 elements. The last 2 elements are the y-values of two points for the linear transform. If there are 3 array elements, the first element is either 'endpoints' or 'minmax'. If there are 4 array elements, the first two elements are the transform x-values.
If there are 2 array elements, or 'endpoints' is specified, the x-values are the first and last array values.
If 'minmax' is specfied, the x-values are the minimum and maximum curve values. These values might not be the endpoints.
In Perl, '=>
' is the same as a comma, so ['endpoints' => 0, 1]
is the same as ['endpoints', 0, 1]
.
The object reference is returned to support "pass-through" method calls.
This method returns a combined list of min/max values for the 'input' and 'output' curves. By default, s-values are returned. These values are converted to 'input' or 'output' values by adding the optional format parameter. If the list is empty, there are no min/max values, and the curve is monotonic.
Usage
@minmax = $spline->monotonic();
@minmax = $spline->monotonic($format);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'output' => [0.5, -5, 2, 0.9]}); # non-monotonic curve
@minmax = $spline->monotonic(); # returns s-values
@minmax = $spline->monotonic('input'); # returns 'input' values
@minmax = $spline->monotonic('output'); # returns 'output' values
print "curve is non-monotonic\n" if (@minmax);
This method updates the internal elements of the object. It should be called when the contents of the curve arrays are changed externally, e.g. curve optimization.
Usage
$spline->update();
$spline->update($flag);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'input' => [0, 1, 2, 3], 'output' => [4, 5, 6, 7]}); # make 'spline' object
$spline->output->[2] = 6.17; # change an output value
$spline->update(); # update (see note 1)
$spline->input->[2] = 2.05; # change an input value
$spline->update(1); # update (see note 1)
If 'output' values are changed externally, call 'update()'. If 'input' values are changed externally, call 'update(1)', which invokes additional functions. It is not necessary to call 'update' when replacing the arrays with the 'input' and 'output' methods.
This method makes a lookup table (LUT) for use in certain profile objects ('curv', 'mft1', 'mft2'). It is assumed that the domain and range of the 'spline' object is [0, 1].
Usage
$table = $spline->table($n);
$table = $spline->table($n, $dir);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'output' => [0, 30, 57.5, 82, 100]}); # make 'spline' object
$spline->normalize(); # normalize the object
$table = $spline->table(1285); # make a table with 1285 steps
$table = $spline->table(1285, 1); # make an inverse table with 1285 steps
This method makes an equivalent 'curv' object. It is assumed that the domain and range of the 'spline' object is [0, 1].
Usage
$curv = $spline->curv($n);
$curv = $spline->curv($n, $dir);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'output' => [0, 30, 57.5, 82, 100]}); # make 'spline' object
$spline->normalize(); # normalize the object
$curv = $spline->curv(1285); # make a 'curv' object with 1285 steps
$curv = $spline->curv(1285, 1); # make an inverse 'curv' object with 1285 steps
This method returns a string showing the structure of the 'spline' object.
Usage
$string = $spline->sdump([$format]);
$spline->dump([$format]);
Examples
use ICC::Support::spline;
$spline = ICC::Support::spline->new({'input' => [0, 100], 'output' => 4});
$string = $spline->sdump(); # dump to string
$spline->dump(); # dump to STDOUT
Akima, Hiroshi "A New Method of Interpolation and Smooth Curve Fitting Based on Local Procedures" ACM Journal, Vol. 17, No. 4, October 1970.
Weisstein, Eric W. "Cubic Spline." MathWorld. http://mathworld.wolfram.com/CubicSpline.html
Programs in this distribution, authored by William B. Birkett, are licensed under the GNU General Public License, v3.
See https://www.gnu.org/licenses/gpl-3.0.html for license details.
William B. Birkett, <wbirkett@doplganger.com>
Copyright © 2004-2024 by William B. Birkett