package
ICC::Support::bern;
use
strict;
use
Carp;
our
$VERSION
= 0.41;
# revised 2018-09-04
#
# Copyright © 2004-2020 by William B. Birkett
# inherit from Shared
# enable static variables
# create new bern object
# hash keys are: 'input', 'output', 'fit', 'fix_hl', 'fix_sh'
# default values for 'input' and 'output' are []
# parameters: ([ref_to_attribute_hash])
# returns: (ref_to_object)
sub
new {
# get object class
my
$class
=
shift
();
# create empty bern object
my
$self
= [
{},
# object header
[],
# parameter array
[],
# min/max t-values
[]
# min/max i/o-values
];
# if one parameter, a hash reference
if
(
@_
== 1 &&
ref
(
$_
[0]) eq
'HASH'
) {
# set object contents from hash
_new_from_hash(
$self
,
$_
[0]);
}
elsif
(
@_
) {
# error
croak(
'invalid parameter(s)'
);
}
# return blessed object
return
(
bless
(
$self
,
$class
));
}
# create inverse object
# swaps the 'input' and 'output' arrays
# returns: (ref_to_object)
sub
inv {
# clone new object
my
$self
= Storable::dclone(
shift
());
# swap 'input' and 'output' arrays
(
$self
->[1][0],
$self
->[1][1]) = (
$self
->[1][1],
$self
->[1][0]);
(
$self
->[2][0],
$self
->[2][1]) = (
$self
->[2][1],
$self
->[2][0]);
(
$self
->[3][0],
$self
->[3][1]) = (
$self
->[3][1],
$self
->[3][0]);
# return inverted object
return
(
$self
);
}
# write bern object to ICC profile
# note: writes an equivalent 'curv' object
# parameters: (ref_to_parent_object, file_handle, ref_to_tag_table_entry)
sub
write_fh {
# verify 4 parameters
(
@_
== 4) or croak(
'wrong number of parameters'
);
# write bern data to profile
goto
&_writeICCbern
;
}
# get tag size (for writing to profile)
# note: writes an equivalent curv object
# returns: (tag_size)
sub
size {
# get parameters
my
(
$self
) =
@_
;
# get count value (defaults to 4370)
my
$n
=
$self
->[0]{
'curv_points'
} // 4370;
# return size
return
(12 +
$n
* 2);
}
# compute curve function
# parameters: (input_value)
# returns: (output_value)
sub
transform {
# get parameters
my
(
$self
,
$in
) =
@_
;
# verify input
(Scalar::Util::looks_like_number(
$in
)) or croak(
'invalid transform input'
);
# compute intermediate value(s)
my
@t
= _rev(
$self
->[1][0],
$self
->[2][0],
$self
->[3][0],
$in
);
# warn if multiple values
(
@t
== 1) or carp(
'multiple transform values'
);
# return first output value
return
(_fwd(
$self
->[1][1],
$t
[0]));
}
# compute inverse curve function
# parameters: (input_value)
# returns: (output_value)
sub
inverse {
# get parameters
my
(
$self
,
$in
) =
@_
;
# verify input
(Scalar::Util::looks_like_number(
$in
)) or croak(
'invalid transform input'
);
# compute intermediate value(s)
my
@t
= _rev(
$self
->[1][1],
$self
->[2][1],
$self
->[3][1],
$in
);
# warn if multiple values
(
@t
== 1) or carp(
'multiple transform values'
);
# return first output value
return
(_fwd(
$self
->[1][0],
$t
[0]));
}
# compute curve derivative
# parameters: (input_value)
# returns: (derivative_value)
sub
derivative {
# get parameters
my
(
$self
,
$in
) =
@_
;
# return forward derivative
return
(_derivative(
$self
, 0,
$in
));
}
# compute parametric partial derivatives
# parameters: (input_value)
# returns: (partial_derivative_array)
sub
parametric {
# get parameters
my
(
$self
,
$in
) =
@_
;
# return forward parametric partial derivatives
return
(_parametric(
$self
, 0,
$in
));
}
# get/set header hash reference
# parameters: ([ref_to_new_hash])
# returns: (ref_to_hash)
sub
header {
# get object reference
my
$self
=
shift
();
# if there are parameters
if
(
@_
) {
# if one parameter, a hash reference
if
(
@_
== 1 &&
ref
(
$_
[0]) eq
'HASH'
) {
# set header to copy of hash
$self
->[0] = Storable::dclone(
shift
());
}
else
{
# error
croak(
'parameter must be a hash reference'
);
}
}
# return reference
return
(
$self
->[0]);
}
# get/set input array reference
# an array of two values is a range
# otherwise, it contains input values
# parameters: ([ref_to_array])
# returns: (ref_to_array)
sub
input {
# get object reference
my
$self
=
shift
();
# if one parameter supplied
if
(
@_
== 1) {
# verify 'input' vector (between 1 and 21 numeric values)
(ICC::Shared::is_num_vector(
$_
[0]) && 1 <= @{
$_
[0]} && 21 >= @{
$_
[0]}) or croak(
'invalid input values'
);
# store output values
$self
->[1][0] = [@{
$_
[0]}];
# check for min/max values
_minmax(
$self
, 0);
}
elsif
(
@_
) {
# error
carp(
'too many parameters'
);
}
# return array reference
return
(
$self
->[1][0]);
}
# get/set output array reference
# parameters: ([ref_to_array])
# returns: (ref_to_array)
sub
output {
# get object reference
my
$self
=
shift
();
# if one parameter supplied
if
(
@_
== 1) {
# verify 'output' vector (between 1 and 21 numeric values)
(ICC::Shared::is_num_vector(
$_
[0]) && 1 <= @{
$_
[0]} && 21 >= @{
$_
[0]}) or croak(
'invalid input values'
);
# store output values
$self
->[1][1] = [@{
$_
[0]}];
# check for min/max values
_minmax(
$self
, 1);
}
elsif
(
@_
) {
# error
carp(
'too many parameters'
);
}
# return array reference
return
(
$self
->[1][1]);
}
# normalize transform
# adjusts Bernstein coefficients linearly
# adjusts 'input' and 'output' by default
# hash keys: 'input', 'output'
# parameter: ([hash_ref])
# returns: (ref_to_object)
sub
normalize {
# get parameters
my
(
$self
,
$hash
) =
@_
;
# local variables
my
(
$m
,
$b
,
$val
,
$src
,
$x0
,
$x1
,
$y0
,
$y1
,
$f0
,
$f1
,
@minmax
);
# set hash key array
my
@key
=
qw(input output)
;
# for ('input', 'output')
for
my
$i
(0, 1) {
# undefine slope
$m
=
undef
;
# if no parameter (default)
if
(!
defined
(
$hash
)) {
# if array has 2 or more coefficients
if
(@{
$self
->[1][
$i
]} > 1 && (
$x0
=
$self
->[1][
$i
][0]) != (
$x1
=
$self
->[1][
$i
][-1])) {
# compute adjustment values
$m
=
abs
(1/(
$x0
-
$x1
));
$b
= -
$m
* (
$x0
<
$x1
?
$x0
:
$x1
);
# set endpoints
(
$y0
,
$y1
) =
$x0
<
$x1
? (0, 1) : (1, 0);
}
}
else
{
# if hash contains key
if
(
defined
(
$val
=
$hash
->{
$key
[
$i
]})) {
# if array has 2 or more coefficients
if
(@{
$self
->[1][
$i
]} > 1 &&
$self
->[1][
$i
][0] !=
$self
->[1][
$i
][-1]) {
# if hash value is an array reference
if
(
ref
(
$val
) eq
'ARRAY'
) {
# if two parameters
if
(@{
$val
} == 2) {
# get parameters
(
$y0
,
$y1
) = @{
$val
};
# get endpoints
$x0
=
$self
->[1][
$i
][0];
$x1
=
$self
->[1][
$i
][-1];
# if three parameters
}
elsif
(@{
$val
} == 3) {
# get parameters
(
$src
,
$y0
,
$y1
) = @{
$val
};
# if source is 'endpoints'
if
(!
ref
(
$src
) &&
$src
eq
'endpoints'
) {
# get endpoints
$x0
=
$self
->[1][
$i
][0];
$x1
=
$self
->[1][
$i
][-1];
# if source is 'minmax'
}
elsif
(!
ref
(
$src
) &&
$src
eq
'minmax'
) {
# get all possible min/max values
@minmax
= (
$self
->[1][
$i
][0],
$self
->[1][
$i
][-1], @{
$self
->[3][
$i
]});
# get min/max values
$x0
= List::Util::min(
@minmax
);
$x1
= List::Util::max(
@minmax
);
}
# if four parameters
}
elsif
(@{
$val
} == 4) {
# get parameters
(
$x0
,
$x1
,
$y0
,
$y1
) = @{
$val
};
}
# if x/y values are numeric
if
((4 ==
grep
{Scalar::Util::looks_like_number(
$_
)} (
$x0
,
$x1
,
$y0
,
$y1
)) && (
$x0
!=
$x1
)) {
# compute slope
$m
= (
$y0
-
$y1
)/(
$x0
-
$x1
);
# compute offset
$b
=
$y0
-
$m
*
$x0
;
}
else
{
# warning
carp(
"invalid '$key[$i]' parameter(s)"
);
}
}
}
else
{
# warning
carp(
"can't normalize '$key[$i]' coefficients"
);
}
}
}
# if slope is defined
if
(
defined
(
$m
)) {
# set endpoint flags
$f0
=
$self
->[1][
$i
][0] ==
$x0
;
$f1
=
$self
->[1][
$i
][-1] ==
$x1
;
# adjust Bernstein coefficients (y = mx + b)
@{
$self
->[1][
$i
]} =
map
{
$m
*
$_
+
$b
} @{
$self
->[1][
$i
]};
# set endpoints (to ensure exact values)
$self
->[1][
$i
][0] =
$y0
if
(
$f0
);
$self
->[1][
$i
][-1] =
$y1
if
(
$f1
);
}
}
# re-calculate min/max values
_minmax(
$self
);
# return
return
(
$self
);
}
# check if monotonic
# returns min/max values
# returns t-values by default
# returns 'input' values if format is 'input'
# returns 'output' values if format is 'output'
# curve is monotonic if no min/max values
# parameters: ([format])
# returns: (array_of_values)
sub
monotonic {
# get object reference
my
(
$self
,
$fmt
) =
@_
;
# local variables
my
(
@s
);
# compute min/max values
_minmax(
$self
);
# if format undefined
if
(!
defined
(
$fmt
)) {
# return sorted t-values ('input' and 'output')
return
(
sort
{
$a
<=>
$b
} (@{
$self
->[2][0]}, @{
$self
->[2][1]}));
# if format 'input'
}
elsif
(
$fmt
eq
'input'
) {
# return input values
return
(
map
{_fwd(
$self
->[1][0],
$_
)}
sort
{
$a
<=>
$b
} (@{
$self
->[2][0]}, @{
$self
->[2][1]}));
# if format 'output'
}
elsif
(
$fmt
eq
'output'
) {
# return output values
return
(
map
{_fwd(
$self
->[1][1],
$_
)}
sort
{
$a
<=>
$b
} (@{
$self
->[2][0]}, @{
$self
->[2][1]}));
}
else
{
# error
croak(
'unsupported format for min/max values'
);
}
}
# update internal object elements
# this method used primarily when optimizing
# call after changing 'input' or 'output' values
sub
update {
# recompute min/max values
goto
&_minmax
;
}
# make table for profile objects
# assumes curve domain/range is (0 - 1)
# parameters: (number_of_table_entries, [direction])
# returns: (ref_to_table_array)
sub
table {
# get parameters
my
(
$self
,
$n
,
$dir
) =
@_
;
# local variables
my
(
$up
,
$table
);
# validate number of table entries
(
$n
==
int
(
$n
) &&
$n
>= 2) or carp(
'invalid number of table entries'
);
# purify direction flag
$dir
= (
$dir
) ? 1 : 0;
# array upper index
$up
=
$n
- 1;
# for each table entry
for
my
$i
(0 ..
$up
) {
# compute table value
$table
->[
$i
] = _transform(
$self
,
$dir
,
$i
/
$up
);
}
# return table reference
return
(
$table
);
}
# make equivalent 'curv' object
# assumes curve domain/range is (0 - 1)
# parameters: (number_of_table_entries, [direction])
# returns: (ref_to_curv_object)
sub
curv {
# return 'curv' object reference
return
(ICC::Profile::curv->new(table(
@_
)));
}
# print object contents to string
# format is an array structure
# parameter: ([format])
# returns: (string)
sub
sdump {
# get parameters
my
(
$self
,
$p
) =
@_
;
# local variables
my
(
$s
,
$fmt
,
$fmt2
);
# resolve parameter to an array reference
$p
=
defined
(
$p
) ?
ref
(
$p
) eq
'ARRAY'
?
$p
: [
$p
] : [];
# get format string
$fmt
=
defined
(
$p
->[0]) && !
ref
(
$p
->[0]) ?
$p
->[0] :
'undef'
;
# set string to object ID
$s
=
sprintf
(
"'%s' object, (0x%x)\n"
,
ref
(
$self
),
$self
);
# if input_parameter_array defined
if
(
defined
(
$self
->[1][0])) {
# set format
$fmt2
=
join
(
', '
, (
'%.3f'
) x @{
$self
->[1][0]});
# append parameter string
$s
.=
sprintf
(
" input parameters [$fmt2]\n"
, @{
$self
->[1][0]});
}
# if output_parameter_array defined
if
(
defined
(
$self
->[1][1])) {
# set format
$fmt2
=
join
(
', '
, (
'%.3f'
) x @{
$self
->[1][1]});
# append parameter string
$s
.=
sprintf
(
" output parameters [$fmt2]\n"
, @{
$self
->[1][1]});
}
# return
return
(
$s
);
}
# find min/max values within the interval (0 - 1)
# used by '_rev' to determine solution regions
# called by the 'update' method
# i/o_index: 0 - input, 1 - output, undef - both
# parameters: (ref_to_object, [i/o_index])
sub
_minmax {
# get parameters
my
(
$self
,
$io
) =
@_
;
# local variables
my
(
@s
,
$t
,
$d
);
my
(
$par
,
$fwd
,
$fwd2
,
$drv
);
# for input and/or output curves
for
my
$i
(
defined
(
$io
) ? (
$io
? 1 : 0) : (0, 1)) {
# initialize s-values array
@s
= ();
# get ref to Bernstein parameters
$par
=
$self
->[1][
$i
];
# if number of parameters > 2
if
(@{
$par
} > 2) {
# compute forward differences
$fwd
= [
map
{
$par
->[
$_
] -
$par
->[
$_
- 1]} (1 .. $
#{$par})];
# if forward differences have different signs (+/-)
if
(
grep
{(
$fwd
->[0] < 0) ^ (
$fwd
->[
$_
] < 0)} (1 .. $
#{$fwd})) {
# compute second forward differences
$fwd2
= [
map
{
$fwd
->[
$_
] -
$fwd
->[
$_
- 1]} (1 .. $
#{$fwd})];
# compute derivative values over range (0 - 1)
$drv
= [
map
{$
#{$par} * _fwd($fwd, $_/(4 * $#{$par}))} (0 .. 4 * $#{$par})];
# for each pair of derivatives
for
my
$j
(1 .. $
#{$drv}) {
# if derivatives have different signs (+/-)
if
((
$drv
->[
$j
- 1] < 0) ^ (
$drv
->[
$j
] < 0)) {
# estimate root from the derivative values
$t
= (
$j
*
$drv
->[
$j
- 1] - (
$j
- 1) *
$drv
->[
$j
])/((
$drv
->[
$j
- 1] -
$drv
->[
$j
]) * 4 * $
#{$par});
# loop until derivative approaches 0
while
(
abs
(
$d
= $
#{$par} * _fwd($fwd, $t)) > 1E-9) {
# adjust root using Newton's method
$t
-=
$d
/($
#{$fwd} * $#{$par} * _fwd($fwd2, $t));
}
# push root on t-value array
push
(
@s
,
$t
)
if
(
$t
> 0 &&
$t
< 1);
}
}
}
}
# warn if root values were found
"curve $i is not monotonic\n"
if
(
@s
&& !
defined
(
$self
->[2][
$i
]));
# save root t-values
$self
->[2][
$i
] = [
@s
];
# save root i/o-values
$self
->[3][
$i
] = [
map
{_fwd(
$par
,
$_
)}
@s
];
}
}
# combined transform
# nominal domain (0 - 1)
# parameters: (ref_to_object, direction, input_value)
# returns: (output_value)
sub
_transform {
# get parameters
my
(
$self
,
$dir
,
$in
) =
@_
;
# local variables
my
(
$p0
,
$p1
,
$p2
,
$p3
,
@t
);
# verify input
(Scalar::Util::looks_like_number(
$in
)) or croak(
'invalid transform input'
);
# get parameter arrays
$p0
=
$self
->[1][
$dir
];
$p1
=
$self
->[2][
$dir
];
$p2
=
$self
->[3][
$dir
];
$p3
=
$self
->[1][1 -
$dir
];
# compute intermediate value(s)
@t
= _rev(
$p0
,
$p1
,
$p2
,
$in
);
# warn if multiple values
(
@t
== 1) or carp(
'multiple transform values'
);
# return first output value
return
(_fwd(
$p3
,
$t
[0]));
}
# combined derivative
# nominal domain (0 - 1)
# parameters: (ref_to_object, direction, input_value)
# returns: (derivative_value)
sub
_derivative {
# get parameters
my
(
$self
,
$dir
,
$in
) =
@_
;
# local variables
my
(
$p0
,
$p1
,
$p2
,
$p3
,
@t
);
my
(
$di
,
$do
);
# verify input
(Scalar::Util::looks_like_number(
$in
)) or croak(
'invalid transform input'
);
# get parameter arrays
$p0
=
$self
->[1][
$dir
];
$p1
=
$self
->[2][
$dir
];
$p2
=
$self
->[3][
$dir
];
$p3
=
$self
->[1][1 -
$dir
];
# compute intermediate value(s)
@t
= _rev(
$p0
,
$p1
,
$p2
,
$in
);
# warn if multiple values
(
@t
== 1) ||
"multiple transform values\n"
;
# compute input derivative (using first value)
(
$di
= _drv(
$p0
,
$t
[0])) or croak(
'infinite derivative value'
);
# compute output derivative (using first value)
$do
= _drv(
$p3
,
$t
[0]);
# return combined derivative
return
(
$do
/
$di
);
}
# compute parametric partial derivatives
# direction: 0 - normal, 1 - inverse
# parameters: (ref_to_object, direction, input_value)
# returns: (partial_derivative_array)
sub
_parametric {
# get parameters
my
(
$self
,
$dir
,
$in
) =
@_
;
# local variables
my
(
$p0
,
$p1
,
$p2
,
$p3
);
my
(
@t
,
$t
,
$di
,
$do
,
$bfi
,
$bfo
,
@pj
);
# verify input
(Scalar::Util::looks_like_number(
$in
)) or croak(
'invalid transform input'
);
# get parameter arrays
$p0
=
$self
->[1][
$dir
];
$p1
=
$self
->[2][
$dir
];
$p2
=
$self
->[3][
$dir
];
$p3
=
$self
->[1][1 -
$dir
];
# compute intermediate value(s)
@t
= _rev(
$p0
,
$p1
,
$p2
,
$in
);
# warn if multiple values
(
@t
== 1) ||
"multiple transform values\n"
;
# use first value
$t
=
$t
[0];
# compute input derivative
$di
= _drv(
$p0
,
$t
);
# compute input Bernstein polynomials
$bfi
= @{
$p0
} ? _bernstein($
#{$p0}, $t) : undef();
# compute output derivative
$do
= _drv(
$p3
,
$t
);
# compute output Bernstein polynomials
$bfo
= @{
$p3
} ? _bernstein($
#{$p3}, $t) : undef();
# initialize array
@pj
= ();
# if there are input parameters
if
(
defined
(
$bfi
)) {
# for each parameter
for
my
$i
(0 .. $
#{$bfi}) {
# verify non-zero input derivative
(
$di
) or croak(
'infinite Jacobian element'
);
# compute partial derivative
$pj
[
$i
] = -
$bfi
->[
$i
] *
$do
/
$di
;
}
}
# add output parameters (if any)
push
(
@pj
, @{
$bfo
})
if
(
defined
(
$bfo
));
# return array of partial derivatives
return
(
@pj
);
}
# direct forward transform
# nominal domain (0 - 1)
# parameters: (parameter_array_reference, input_value)
# returns: (output_value)
sub
_fwd {
# get parameters
my
(
$par
,
$in
) =
@_
;
# if no parameters
if
(! @{
$par
}) {
# return input
return
(
$in
);
# one parameter
}
elsif
(@{
$par
} == 1) {
# return constant
return
(
$par
->[0]);
# two parameters
}
elsif
(@{
$par
} == 2) {
# return linear interpolated value
return
((1 -
$in
) *
$par
->[0] +
$in
*
$par
->[1]);
# three or more parameters
}
else
{
# check if ICC::Support::Lapack module is loaded
state
$lapack
=
defined
(
$INC
{
'ICC/Support/Lapack.pm'
});
# if input < 0
if
(
$in
< 0) {
# return extrapolated value
return
(
$par
->[0] +
$in
* _drv(
$par
, 0));
# if input > 1
}
elsif
(
$in
> 1) {
# return extrapolated value
return
(
$par
->[-1] + (
$in
- 1) * _drv(
$par
, 1));
}
else
{
# if Lapack module loaded
if
(
$lapack
) {
# return Bernstein interpolated value
return
(ICC::Support::Lapack::bernstein(
$in
,
$par
));
}
else
{
# return Bernstein interpolated value
return
(ICC::Shared::dotProduct(_bernstein($
#{$par}, $in), $par));
}
}
}
}
# direct reverse transform
# nominal range (0 - 1)
# parameters: (parameter_array_reference, x-min/max_array_reference, y-min/max_array_reference, input_value)
# returns: (output_value -or- array_of_output_values)
sub
_rev {
# get parameters
my
(
$par
,
$xminmax
,
$yminmax
,
$in
) =
@_
;
# local variables
my
(
@xs
,
@ys
,
$slope
,
$ext
,
@sol
);
my
(
$p
,
$loop
,
$xlo
,
$xhi
,
$xval
,
$yval
);
# if no parameters
if
(! @{
$par
}) {
# return input
return
(
$in
);
# one parameter
}
elsif
(@{
$par
} == 1) {
# warn if input not equal to constant
(
$in
==
$par
->[0]) or carp(
'curve input not equal to constant parameter'
);
# return input
return
(
$in
);
# two parameters
}
elsif
(@{
$par
} == 2) {
# return linear interpolated value
return
((
$in
-
$par
->[0])/(
$par
->[1] -
$par
->[0]));
# three or more parameters
}
else
{
# setup segment arrays
@xs
= (0, @{
$xminmax
}, 1);
@ys
= (
$par
->[0], @{
$yminmax
},
$par
->[-1]);
# if slope(0) not 0
if
(
$slope
= _drv(
$par
, 0)) {
# compute extrapolation from 0
$ext
= (
$in
-
$par
->[0])/
$slope
;
# save if less than 0
push
(
@sol
,
$ext
)
if
(
$ext
< 0);
}
# test first y-value
push
(
@sol
,
$xs
[0])
if
(
$ys
[0] ==
$in
);
# for each array segment
for
my
$i
(1 ..
$#xs
) {
# if input value falls within segment
if
((
$in
>
$ys
[
$i
- 1] &&
$in
<
$ys
[
$i
]) || (
$in
<
$ys
[
$i
- 1] &&
$in
>
$ys
[
$i
])) {
# get segment polarity
$p
=
$ys
[
$i
] >
$ys
[
$i
- 1];
# initialize limits
$xlo
=
$xs
[
$i
- 1];
$xhi
=
$xs
[
$i
];
# initialize loop counter
$loop
= 0;
# bisection method loop
while
(
$xhi
-
$xlo
> 1E-3 &&
$loop
++ < 15) {
# compute midpoint
$xval
= (
$xlo
+
$xhi
)/2;
# compute y-value
$yval
= _fwd(
$par
,
$xval
);
# if y-value > input -or- y-value < input
if
(
$p
? (
$yval
>
$in
) : (
$yval
<
$in
)) {
# set high limit
$xhi
=
$xval
;
}
else
{
# set low limit
$xlo
=
$xval
;
}
}
# initialize loop counter
$loop
= 0;
# Newton's method loop
while
(
abs
(
$in
-
$yval
) > 1E-12 &&
$loop
++ < 5 && (
$slope
= _drv(
$par
,
$xval
))) {
# adjust x-value
$xval
+= (
$in
-
$yval
)/
$slope
;
# compute y-value
$yval
= _fwd(
$par
,
$xval
);
}
# print warning if failed to converge
printf
(
"'_rev' function of 'bern' object failed to converge with input of %.2f\n"
,
$in
)
if
(
abs
(
$in
-
$yval
) > 1E-12);
# save result
push
(
@sol
,
$xval
);
}
# add segment y-value, if a solution
push
(
@sol
,
$xs
[
$i
])
if
(
$ys
[
$i
] ==
$in
);
}
# if slope(1) not 0
if
(
$slope
= _drv(
$par
, 1)) {
# compute extrapolation from 1
$ext
= (
$in
-
$par
->[-1])/
$slope
+ 1;
# save if greater than 1
push
(
@sol
,
$ext
)
if
(
$ext
> 1);
}
}
# return result (array or first solution)
return
(
wantarray
?
@sol
:
$sol
[0]);
}
# forward derivative
# nominal domain is (0 - 1)
# parameters: (parameter_array_reference, input_value)
# returns: (output_value)
sub
_drv {
# get parameters
my
(
$par
,
$in
) =
@_
;
# if no parameters
if
(! @{
$par
}) {
# return identity derivative
return
(1);
# one parameter
}
elsif
(@{
$par
} == 1) {
# return constant derivative
return
(0);
# two parameters
}
elsif
(@{
$par
} == 2) {
# return linear derivative
return
(
$par
->[1] -
$par
->[0]);
# three or more parameters
}
else
{
# check if ICC::Support::Lapack module is loaded
state
$lapack
=
defined
(
$INC
{
'ICC/Support/Lapack.pm'
});
# limit input value (0 - 1) to force linear extrapolation
$in
=
$in
< 0 ? 0 : (
$in
> 1 ? 1 :
$in
);
# compute parameter differences
my
$diff
= [
map
{
$par
->[
$_
+ 1] -
$par
->[
$_
]} (0 .. $
#{$par} - 1)];
# if Lapack module loaded
if
(
$lapack
) {
# return Bernstein interpolated derivative
return
($
#{$par} * ICC::Support::Lapack::bernstein($in, $diff));
}
else
{
# return Bernstein interpolated derivative
return
($
#{$par} * ICC::Shared::dotProduct(_bernstein($#{$diff}, $in), $diff));
}
}
}
# compute Bernstein basis values
# parameters: (bernstein_degree, t)
# returns: (ref_to_bernstein_array)
sub
_bernstein {
# get parameters
my
(
$degree
,
$t0
) =
@_
;
# local variables
my
(
$bern
,
$t1
,
$m0
,
$m1
,
$n
);
# binomial coefficients
state
$pascal
= [
[1],
[1, 1],
[1, 2, 1],
[1, 3, 3, 1],
[1, 4, 6, 4, 1],
[1, 5, 10, 10, 5, 1],
[1, 6, 15, 20, 15, 6, 1],
[1, 7, 21, 35, 35, 21, 7, 1],
[1, 8, 28, 56, 70, 56, 28, 8, 1],
[1, 9, 36, 84, 126, 126, 84, 36, 9, 1],
[1, 10, 45, 120, 210, 252, 210, 120, 45, 10, 1],
[1, 11, 55, 165, 330, 462, 462, 330, 165, 55, 11, 1],
[1, 12, 66, 220, 495, 792, 924, 792, 495, 220, 66, 12, 1],
[1, 13, 78, 286, 715, 1287, 1716, 1716, 1287, 715, 286, 78, 13, 1],
[1, 14, 91, 364, 1001, 2002, 3003, 3432, 3003, 2002, 1001, 364, 91, 14, 1],
[1, 15, 105, 455, 1365, 3003, 5005, 6435, 6435, 5005, 3003, 1365, 455, 105, 15, 1],
[1, 16, 120, 560, 1820, 4368, 8008, 11440, 12870, 11440, 8008, 4368, 1820, 560, 120, 16, 1],
[1, 17, 136, 680, 2380, 6188, 12376, 19448, 24310, 24310, 19448, 12376, 6188, 2380, 680, 136, 17, 1],
[1, 18, 153, 816, 3060, 8568, 18564, 31824, 43758, 48620, 43758, 31824, 18564, 8568, 3060, 816, 153, 18, 1],
[1, 19, 171, 969, 3876, 11628, 27132, 50388, 75582, 92378, 92378, 75582, 50388, 27132, 11628, 3876, 969, 171, 19, 1],
[1, 20, 190, 1140, 4845, 15504, 38760, 77520, 125970, 167960, 184756, 167960, 125970, 77520, 38760, 15504, 4845, 1140, 190, 20, 1]
];
# copy binomial coefficients for this degree
$bern
= [@{
$pascal
->[
$degree
]}];
# compute input complement
$t1
= 1 -
$t0
;
# initialize multipliers
$m0
=
$m1
= 1;
# get polynomial degree
$n
= $
#{$bern};
# for each polynomial
for
(1 ..
$n
) {
# multiply
$bern
->[
$_
] *= (
$m0
*=
$t0
);
$bern
->[
$n
-
$_
] *= (
$m1
*=
$t1
);
}
# return array reference
return
(
$bern
);
}
# fit bern object to data
# uses LAPACK dgels function to perform a least-squares fit
# the parameters to be fitted have the value 'undef' and must all be within the
# output side of the parameter array, e.g. [[0, 100], [undef, undef, undef, undef]]
# fit data is an array containing input and output vectors
# parameters: (ref_to_object, fit_data_reference, ref_to_parameter_hash)
# returns: (dgels_info_value)
sub
_fit {
# get parameters
my
(
$self
,
$fit
,
$hash
) =
@_
;
# local variables
my
(
$m
,
$n
,
$d
,
$t
,
$p
,
$span
,
@sol
);
my
(
@so
,
$outz
,
$bern
,
$nrhs
,
$info
);
my
(
$outz2
,
$bern2
,
$fix_hl
,
$fix_sh
,
@c2
);
# check if ICC::Support::Lapack module is loaded
state
$lapack
=
defined
(
$INC
{
'ICC/Support/Lapack.pm'
});
# verify Lapack module loaded (need to add non-Lapack routine someday)
(
$lapack
) or croak(
"'fit' option requires ICC::Support::Lapack module"
);
# get degree
$d
= $
#{$self->[1][1]};
# for each output parameter
for
my
$i
(0 ..
$d
) {
# if parameter is undefined
if
(!
defined
(
$self
->[1][1][
$i
])) {
# push index
push
(
@so
,
$i
);
# set value to 0
$self
->[1][1][
$i
] = 0;
}
}
# if no 'undef' parameters
if
(
@so
== 0) {
# warning
"no 'undef' parameters to fit\n"
;
# return
return
(0)
}
# check for min/max values
_minmax(
$self
);
# for each sample
for
my
$i
(0 .. $
#{$fit->[0]}) {
# compute output difference
$outz
->[
$i
] = [
$fit
->[1][
$i
] - transform(
$self
,
$fit
->[0][
$i
])];
# get intermediate value
$t
= _rev(
$self
->[1][0],
$self
->[2][0],
$self
->[3][0],
$fit
->[0][
$i
]);
# compute Bernstein values for undefined parameters
$bern
->[
$i
] = [@{_bernstein(
$d
,
$t
)}[
@so
]];
}
# make copies
$outz2
= Storable::dclone(
$outz
);
$bern2
= Storable::dclone(
$bern
);
# get array sizes
$m
= @{
$bern
};
$n
= @{
$bern
->[0]};
$nrhs
= @{
$outz
->[0]};
# fit the data, return if failed
(
$info
= ICC::Support::Lapack::dgels(
'N'
,
$m
,
$n
,
$nrhs
,
$bern
,
$m
,
$outz
,
$m
)) &&
return
(
$info
);
# get curve polarity (0 - increasing, 1 - decreasing)
$p
= (
$span
= (
$so
[-1] ==
$d
?
$outz
->[
$n
- 1][0] :
$self
->[1][1][-1]) - (
$so
[0] == 0 ?
$outz
->[0][0] :
$self
->[1][1][0])) < 0;
# get flags (indicating HL or SH contrast needs to be fixed)
$fix_hl
=
$hash
->{
'fix_hl'
} &&
$so
[0] == 0 &&
$so
[1] == 1 && ((
$outz
->[0][0] >
$outz
->[1][0]) ^
$p
);
$fix_sh
=
$hash
->{
'fix_sh'
} &&
$so
[-1] ==
$d
&&
$so
[-2] ==
$d
- 1 && ((
$outz
->[
$n
- 1][0] <
$outz
->[
$n
- 2][0]) ^
$p
);
# if degree >= 4 and HL or SH needs fixing
if
(
$d
>= 4 && (
$fix_hl
||
$fix_sh
)) {
# for each sample
for
my
$i
(0 .. $
#{$bern2}) {
# if highlight fix
if
(
$fix_hl
) {
# get first 2 Bernstein values
@c2
=
splice
(@{
$bern2
->[
$i
]}, 0, 2);
# combine to single value
unshift
(@{
$bern2
->[
$i
]}, (
$c2
[0] +
$c2
[1]));
}
# if shadow fix
if
(
$fix_sh
) {
# get last 2 Bernstein values
@c2
=
splice
(@{
$bern2
->[
$i
]}, -2);
# combine to single value
push
(@{
$bern2
->[
$i
]}, (
$c2
[0] +
$c2
[1]));
}
}
# get array sizes
$m
= @{
$bern2
};
$n
= @{
$bern2
->[0]};
$nrhs
= @{
$outz2
->[0]};
# fit the data, return if failed
(
$info
= ICC::Support::Lapack::dgels(
'N'
,
$m
,
$n
,
$nrhs
,
$bern2
,
$m
,
$outz2
,
$m
)) &&
return
(
$info
);
# extract solution
@sol
=
map
{
$outz2
->[
$_
][0]} (0 ..
$n
- 1);
# replace combined coefficient(s)
splice
(
@sol
, 1, 0,
$sol
[0] +
$span
* 0.001)
if
(
$fix_hl
);
splice
(
@sol
, -1, 0,
$sol
[-1] -
$span
* 0.001)
if
(
$fix_sh
);
}
else
{
# extract solution
@sol
=
map
{
$outz
->[
$_
][0]} (0 ..
$n
- 1);
}
# copy solution to output array
@{
$self
->[1][1]}[
@so
] =
@sol
;
# return
return
(0);
}
# set object contents from parameter hash
# parameters: (ref_to_object, ref_to_parameter_hash)
sub
_new_from_hash {
# get parameters
my
(
$self
,
$hash
) =
@_
;
# local variables
my
(
$in
,
$out
,
$fit
);
# if 'input' is defined
if
(
defined
(
$in
=
$hash
->{
'input'
})) {
# if 'input' is a vector (21 values or less, some may be 'undef')
if
(ICC::Shared::is_vector(
$in
) && 21 >= @{
$in
}) {
# set object 'input' array
$self
->[1][0] = [@{
$in
}];
# if 'input' is an integer between 1 and 20
}
elsif
(Scalar::Util::looks_like_number(
$in
) &&
$in
==
int
(
$in
) &&
$in
> 0 &&
$in
< 21) {
# make identity curve
$self
->[1][0] = [
map
{
$_
/
$in
} (0 ..
$in
)];
}
else
{
# error
croak(
"invalid 'input' values"
);
}
}
else
{
# disable input
$self
->[1][0] = [];
}
# if 'output' is defined
if
(
defined
(
$out
=
$hash
->{
'output'
})) {
# if 'output' is a vector (21 values or less, some may be 'undef')
if
(ICC::Shared::is_vector(
$out
) && 21 >= @{
$out
}) {
# set object 'output' array
$self
->[1][1] = [@{
$out
}];
# if 'output' is an integer between 1 and 20
}
elsif
(Scalar::Util::looks_like_number(
$out
) &&
$out
==
int
(
$out
) &&
$out
> 0 &&
$out
< 21) {
# make identity curve
$self
->[1][1] = [
map
{
$_
/
$out
} (0 ..
$out
)];
}
else
{
# error
croak(
"invalid 'output' values"
);
}
}
else
{
# disable output
$self
->[1][1] = [];
}
# if 'fit' is defined and not empty
if
(
defined
(
$fit
=
$hash
->{
'fit'
}) && @{
$fit
}) {
# verify 'fit' vectors
(ICC::Shared::is_num_vector(
$fit
->[0]) && 1 <= @{
$fit
->[0]}) or croak(
"invalid 'fit' input values"
);
(ICC::Shared::is_num_vector(
$fit
->[1]) && 1 <= @{
$fit
->[1]}) or croak(
"invalid 'fit' output values"
);
(@{
$fit
->[0]} == @{
$fit
->[1]}) or croak(
"'fit' input and output vectors are different size"
);
# fit object to data
(_fit(
$self
,
$fit
,
$hash
)) && croak(
"'fit' operation failed"
);
}
else
{
# verify object parameters are all defined
(@{
$self
->[1][0]} ==
grep
{
defined
(
$_
)} @{
$self
->[1][0]}) or croak(
"some 'input' values are 'undef'"
);
(@{
$self
->[1][1]} ==
grep
{
defined
(
$_
)} @{
$self
->[1][1]}) or croak(
"some 'output' values are 'undef'"
);
}
# add segment min/max y-values
_minmax(
$self
);
}
# write bern object to ICC profile
# note: writes an equivalent curv object
# parameters: (ref_to_object, ref_to_parent_object, file_handle, ref_to_tag_table_entry)
sub
_writeICCbern {
# get parameters
my
(
$self
,
$parent
,
$fh
,
$tag
) =
@_
;
# local variables
my
(
$n
,
$up
,
@table
);
# get count value (defaults to 4096, the maximum allowed in an 'mft2' tag)
$n
=
$self
->[0]{
'curv_points'
} // 4096;
# validate number of table entries
(
$n
==
int
(
$n
) &&
$n
>= 2) or carp(
'invalid number of table entries'
);
# array upper index
$up
=
$n
- 1;
# for each table entry
for
my
$i
(0 ..
$up
) {
# transform value
$table
[
$i
] = transform(
$self
,
$i
/
$up
);
}
# seek start of tag
seek
(
$fh
,
$tag
->[1], 0);
# write tag type signature and count
$fh
pack
(
'a4 x4 N'
,
'curv'
,
$n
);
# write array
$fh
pack
(
'n*'
,
map
{
$_
< 0 ? 0 : (
$_
> 1 ? 65535 :
$_
* 65535 + 0.5)}
@table
);
}
1;