File Coverage

lib/ICC/Support/bern.pm
Criterion Covered Total %
statement 19 458 4.1
branch 2 230 0.8
condition 1 115 0.8
subroutine 6 33 18.1
pod 15 17 88.2
total 43 853 5.0


line stmt bran cond sub pod time code
1             package ICC::Support::bern;
2              
3 2     2   97229 use strict;
  2         12  
  2         54  
4 2     2   10 use Carp;
  2         4  
  2         190  
5              
6             our $VERSION = 0.41;
7              
8             # revised 2018-09-04
9             #
10             # Copyright © 2004-2019 by William B. Birkett
11              
12             # add development directory
13 2     2   461 use lib 'lib';
  2         662  
  2         13  
14              
15             # inherit from Shared
16 2     2   649 use parent qw(ICC::Shared);
  2         308  
  2         10  
17              
18             # enable static variables
19 2     2   121 use feature 'state';
  2         19  
  2         10969  
20              
21             # create new bern object
22             # hash keys are: 'input', 'output', 'fit', 'fix_hl', 'fix_sh'
23             # default values for 'input' and 'output' are []
24             # parameters: ([ref_to_attribute_hash])
25             # returns: (ref_to_object)
26             sub new {
27              
28             # get object class
29 1     1 1 978 my $class = shift();
30              
31             # create empty bern object
32 1         4 my $self = [
33             {}, # object header
34             [], # parameter array
35             [], # min/max t-values
36             [] # min/max i/o-values
37             ];
38              
39             # if one parameter, a hash reference
40 1 50 33     8 if (@_ == 1 && ref($_[0]) eq 'HASH') {
    50          
41            
42             # set object contents from hash
43 0         0 _new_from_hash($self, $_[0]);
44            
45             } elsif (@_) {
46            
47             # error
48 0         0 croak('invalid parameter(s)');
49            
50             }
51              
52             # return blessed object
53 1         3 return(bless($self, $class));
54              
55             }
56              
57             # create inverse object
58             # swaps the 'input' and 'output' arrays
59             # returns: (ref_to_object)
60             sub inv {
61              
62             # clone new object
63 0     0 1   my $self = Storable::dclone(shift());
64              
65             # swap 'input' and 'output' arrays
66 0           ($self->[1][0], $self->[1][1]) = ($self->[1][1], $self->[1][0]);
67 0           ($self->[2][0], $self->[2][1]) = ($self->[2][1], $self->[2][0]);
68 0           ($self->[3][0], $self->[3][1]) = ($self->[3][1], $self->[3][0]);
69              
70             # return inverted object
71 0           return($self);
72              
73             }
74              
75             # write bern object to ICC profile
76             # note: writes an equivalent 'curv' object
77             # parameters: (ref_to_parent_object, file_handle, ref_to_tag_table_entry)
78             sub write_fh {
79              
80             # verify 4 parameters
81 0 0   0 0   (@_ == 4) or croak('wrong number of parameters');
82              
83             # write bern data to profile
84 0           goto &_writeICCbern;
85              
86             }
87              
88             # get tag size (for writing to profile)
89             # note: writes an equivalent curv object
90             # returns: (tag_size)
91             sub size {
92              
93             # get parameters
94 0     0 0   my ($self) = @_;
95              
96             # get count value (defaults to 4370)
97 0   0       my $n = $self->[0]{'curv_points'} // 4370;
98              
99             # return size
100 0           return(12 + $n * 2);
101              
102             }
103              
104             # compute curve function
105             # parameters: (input_value)
106             # returns: (output_value)
107             sub transform {
108              
109             # get parameters
110 0     0 1   my ($self, $in) = @_;
111              
112             # verify input
113 0 0         (Scalar::Util::looks_like_number($in)) or croak('invalid transform input');
114              
115             # compute intermediate value(s)
116 0           my @t = _rev($self->[1][0], $self->[2][0], $self->[3][0], $in);
117              
118             # warn if multiple values
119 0 0         (@t == 1) or carp('multiple transform values');
120              
121             # return first output value
122 0           return(_fwd($self->[1][1], $t[0]));
123              
124             }
125              
126             # compute inverse curve function
127             # parameters: (input_value)
128             # returns: (output_value)
129             sub inverse {
130              
131             # get parameters
132 0     0 1   my ($self, $in) = @_;
133              
134             # verify input
135 0 0         (Scalar::Util::looks_like_number($in)) or croak('invalid transform input');
136              
137             # compute intermediate value(s)
138 0           my @t = _rev($self->[1][1], $self->[2][1], $self->[3][1], $in);
139              
140             # warn if multiple values
141 0 0         (@t == 1) or carp('multiple transform values');
142              
143             # return first output value
144 0           return(_fwd($self->[1][0], $t[0]));
145              
146             }
147              
148             # compute curve derivative
149             # parameters: (input_value)
150             # returns: (derivative_value)
151             sub derivative {
152              
153             # get parameters
154 0     0 1   my ($self, $in) = @_;
155              
156             # return forward derivative
157 0           return(_derivative($self, 0, $in));
158              
159             }
160              
161             # compute parametric partial derivatives
162             # parameters: (input_value)
163             # returns: (partial_derivative_array)
164             sub parametric {
165              
166             # get parameters
167 0     0 1   my ($self, $in) = @_;
168              
169             # return forward parametric partial derivatives
170 0           return(_parametric($self, 0, $in));
171              
172             }
173              
174             # get/set header hash reference
175             # parameters: ([ref_to_new_hash])
176             # returns: (ref_to_hash)
177             sub header {
178              
179             # get object reference
180 0     0 1   my $self = shift();
181              
182             # if there are parameters
183 0 0         if (@_) {
184            
185             # if one parameter, a hash reference
186 0 0 0       if (@_ == 1 && ref($_[0]) eq 'HASH') {
187            
188             # set header to copy of hash
189 0           $self->[0] = Storable::dclone(shift());
190            
191             } else {
192            
193             # error
194 0           croak('parameter must be a hash reference');
195            
196             }
197            
198             }
199              
200             # return reference
201 0           return($self->[0]);
202              
203             }
204              
205             # get/set input array reference
206             # an array of two values is a range
207             # otherwise, it contains input values
208             # parameters: ([ref_to_array])
209             # returns: (ref_to_array)
210             sub input {
211              
212             # get object reference
213 0     0 1   my $self = shift();
214              
215             # if one parameter supplied
216 0 0         if (@_ == 1) {
    0          
217            
218             # verify 'input' vector (between 1 and 21 numeric values)
219 0 0 0       (ICC::Shared::is_num_vector($_[0]) && 1 <= @{$_[0]} && 21 >= @{$_[0]}) or croak('invalid input values');
  0   0        
  0            
220            
221             # store output values
222 0           $self->[1][0] = [@{$_[0]}];
  0            
223            
224             # check for min/max values
225 0           _minmax($self, 0);
226            
227             } elsif (@_) {
228            
229             # error
230 0           carp('too many parameters');
231            
232             }
233              
234             # return array reference
235 0           return($self->[1][0]);
236              
237             }
238              
239             # get/set output array reference
240             # parameters: ([ref_to_array])
241             # returns: (ref_to_array)
242             sub output {
243              
244             # get object reference
245 0     0 1   my $self = shift();
246              
247             # if one parameter supplied
248 0 0         if (@_ == 1) {
    0          
249            
250             # verify 'output' vector (between 1 and 21 numeric values)
251 0 0 0       (ICC::Shared::is_num_vector($_[0]) && 1 <= @{$_[0]} && 21 >= @{$_[0]}) or croak('invalid input values');
  0   0        
  0            
252            
253             # store output values
254 0           $self->[1][1] = [@{$_[0]}];
  0            
255            
256             # check for min/max values
257 0           _minmax($self, 1);
258            
259             } elsif (@_) {
260            
261             # error
262 0           carp('too many parameters');
263            
264             }
265              
266             # return array reference
267 0           return($self->[1][1]);
268              
269             }
270              
271             # normalize transform
272             # adjusts Bernstein coefficients linearly
273             # adjusts 'input' and 'output' by default
274             # hash keys: 'input', 'output'
275             # parameter: ([hash_ref])
276             # returns: (ref_to_object)
277             sub normalize {
278              
279             # get parameters
280 0     0 1   my ($self, $hash) = @_;
281              
282             # local variables
283 0           my ($m, $b, $val, $src, $x0, $x1, $y0, $y1, $f0, $f1, @minmax);
284              
285             # set hash key array
286 0           my @key = qw(input output);
287              
288             # for ('input', 'output')
289 0           for my $i (0, 1) {
290            
291             # undefine slope
292 0           $m = undef;
293            
294             # if no parameter (default)
295 0 0         if (! defined($hash)) {
296            
297             # if array has 2 or more coefficients
298 0 0 0       if (@{$self->[1][$i]} > 1 && ($x0 = $self->[1][$i][0]) != ($x1 = $self->[1][$i][-1])) {
  0            
299            
300             # compute adjustment values
301 0           $m = abs(1/($x0 - $x1));
302 0 0         $b = -$m * ($x0 < $x1 ? $x0 : $x1);
303            
304             # set endpoints
305 0 0         ($y0, $y1) = $x0 < $x1 ? (0, 1) : (1, 0);
306            
307             }
308            
309             } else {
310            
311             # if hash contains key
312 0 0         if (defined($val = $hash->{$key[$i]})) {
313            
314             # if array has 2 or more coefficients
315 0 0 0       if (@{$self->[1][$i]} > 1 && $self->[1][$i][0] != $self->[1][$i][-1]) {
  0            
316            
317             # if hash value is an array reference
318 0 0         if (ref($val) eq 'ARRAY') {
319            
320             # if two parameters
321 0 0         if (@{$val} == 2) {
  0 0          
    0          
322            
323             # get parameters
324 0           ($y0, $y1) = @{$val};
  0            
325            
326             # get endpoints
327 0           $x0 = $self->[1][$i][0];
328 0           $x1 = $self->[1][$i][-1];
329            
330             # if three parameters
331 0           } elsif (@{$val} == 3) {
332            
333             # get parameters
334 0           ($src, $y0, $y1) = @{$val};
  0            
335            
336             # if source is 'endpoints'
337 0 0 0       if (! ref($src) && $src eq 'endpoints') {
    0 0        
338            
339             # get endpoints
340 0           $x0 = $self->[1][$i][0];
341 0           $x1 = $self->[1][$i][-1];
342            
343             # if source is 'minmax'
344             } elsif (! ref($src) && $src eq 'minmax') {
345            
346             # get all possible min/max values
347 0           @minmax = ($self->[1][$i][0], $self->[1][$i][-1], @{$self->[3][$i]});
  0            
348            
349             # get min/max values
350 0           $x0 = List::Util::min(@minmax);
351 0           $x1 = List::Util::max(@minmax);
352            
353             }
354            
355             # if four parameters
356 0           } elsif (@{$val} == 4) {
357            
358             # get parameters
359 0           ($x0, $x1, $y0, $y1) = @{$val};
  0            
360            
361             }
362            
363             # if x/y values are numeric
364 0 0 0       if ((4 == grep {Scalar::Util::looks_like_number($_)} ($x0, $x1, $y0, $y1)) && ($x0 != $x1)) {
  0            
365            
366             # compute slope
367 0           $m = ($y0 - $y1)/($x0 - $x1);
368            
369             # compute offset
370 0           $b = $y0 - $m * $x0;
371            
372             } else {
373            
374             # warning
375 0           carp("invalid '$key[$i]' parameter(s)");
376            
377             }
378            
379             }
380            
381             } else {
382            
383             # warning
384 0           carp("can't normalize '$key[$i]' coefficients");
385            
386             }
387            
388             }
389            
390             }
391            
392             # if slope is defined
393 0 0         if (defined($m)) {
394            
395             # set endpoint flags
396 0           $f0 = $self->[1][$i][0] == $x0;
397 0           $f1 = $self->[1][$i][-1] == $x1;
398            
399             # adjust Bernstein coefficients (y = mx + b)
400 0           @{$self->[1][$i]} = map {$m * $_ + $b} @{$self->[1][$i]};
  0            
  0            
  0            
401            
402             # set endpoints (to ensure exact values)
403 0 0         $self->[1][$i][0] = $y0 if ($f0);
404 0 0         $self->[1][$i][-1] = $y1 if ($f1);
405            
406             }
407            
408             }
409              
410             # re-calculate min/max values
411 0           _minmax($self);
412              
413             # return
414 0           return($self);
415              
416             }
417              
418             # check if monotonic
419             # returns min/max values
420             # returns t-values by default
421             # returns 'input' values if format is 'input'
422             # returns 'output' values if format is 'output'
423             # curve is monotonic if no min/max values
424             # parameters: ([format])
425             # returns: (array_of_values)
426             sub monotonic {
427              
428             # get object reference
429 0     0 1   my ($self, $fmt) = @_;
430              
431             # local variables
432 0           my (@s);
433              
434             # compute min/max values
435 0           _minmax($self);
436              
437             # if format undefined
438 0 0         if (! defined($fmt)) {
    0          
    0          
439            
440             # return sorted t-values ('input' and 'output')
441 0           return(sort {$a <=> $b} (@{$self->[2][0]}, @{$self->[2][1]}));
  0            
  0            
  0            
442            
443             # if format 'input'
444             } elsif ($fmt eq 'input') {
445            
446             # return input values
447 0           return(map {_fwd($self->[1][0], $_)} sort {$a <=> $b} (@{$self->[2][0]}, @{$self->[2][1]}));
  0            
  0            
  0            
  0            
448            
449             # if format 'output'
450             } elsif ($fmt eq 'output') {
451            
452             # return output values
453 0           return(map {_fwd($self->[1][1], $_)} sort {$a <=> $b} (@{$self->[2][0]}, @{$self->[2][1]}));
  0            
  0            
  0            
  0            
454            
455             } else {
456            
457             # error
458 0           croak('unsupported format for min/max values');
459            
460             }
461            
462             }
463              
464             # update internal object elements
465             # this method used primarily when optimizing
466             # call after changing 'input' or 'output' values
467             sub update {
468              
469             # recompute min/max values
470 0     0 1   goto &_minmax;
471              
472             }
473              
474             # make table for profile objects
475             # assumes curve domain/range is (0 - 1)
476             # parameters: (number_of_table_entries, [direction])
477             # returns: (ref_to_table_array)
478             sub table {
479              
480             # get parameters
481 0     0 1   my ($self, $n, $dir) = @_;
482              
483             # local variables
484 0           my ($up, $table);
485              
486             # validate number of table entries
487 0 0 0       ($n == int($n) && $n >= 2) or carp('invalid number of table entries');
488              
489             # purify direction flag
490 0 0         $dir = ($dir) ? 1 : 0;
491              
492             # array upper index
493 0           $up = $n - 1;
494              
495             # for each table entry
496 0           for my $i (0 .. $up) {
497            
498             # compute table value
499 0           $table->[$i] = _transform($self, $dir, $i/$up);
500            
501             }
502              
503             # return table reference
504 0           return($table);
505              
506             }
507              
508             # make equivalent 'curv' object
509             # assumes curve domain/range is (0 - 1)
510             # parameters: (number_of_table_entries, [direction])
511             # returns: (ref_to_curv_object)
512             sub curv {
513              
514             # return 'curv' object reference
515 0     0 1   return(ICC::Profile::curv->new(table(@_)));
516              
517             }
518              
519             # print object contents to string
520             # format is an array structure
521             # parameter: ([format])
522             # returns: (string)
523             sub sdump {
524              
525             # get parameters
526 0     0 1   my ($self, $p) = @_;
527              
528             # local variables
529 0           my ($s, $fmt, $fmt2);
530              
531             # resolve parameter to an array reference
532 0 0         $p = defined($p) ? ref($p) eq 'ARRAY' ? $p : [$p] : [];
    0          
533              
534             # get format string
535 0 0 0       $fmt = defined($p->[0]) && ! ref($p->[0]) ? $p->[0] : 'undef';
536              
537             # set string to object ID
538 0           $s = sprintf("'%s' object, (0x%x)\n", ref($self), $self);
539              
540             # if input_parameter_array defined
541 0 0         if (defined($self->[1][0])) {
542            
543             # set format
544 0           $fmt2 = join(', ', ('%.3f') x @{$self->[1][0]});
  0            
545            
546             # append parameter string
547 0           $s .= sprintf(" input parameters [$fmt2]\n", @{$self->[1][0]});
  0            
548            
549             }
550              
551             # if output_parameter_array defined
552 0 0         if (defined($self->[1][1])) {
553            
554             # set format
555 0           $fmt2 = join(', ', ('%.3f') x @{$self->[1][1]});
  0            
556            
557             # append parameter string
558 0           $s .= sprintf(" output parameters [$fmt2]\n", @{$self->[1][1]});
  0            
559            
560             }
561              
562             # return
563 0           return($s);
564              
565             }
566              
567             # find min/max values within the interval (0 - 1)
568             # used by '_rev' to determine solution regions
569             # called by the 'update' method
570             # i/o_index: 0 - input, 1 - output, undef - both
571             # parameters: (ref_to_object, [i/o_index])
572             sub _minmax {
573              
574             # get parameters
575 0     0     my ($self, $io) = @_;
576              
577             # local variables
578 0           my (@s, $t, $d);
579 0           my ($par, $fwd, $fwd2, $drv);
580              
581             # for input and/or output curves
582 0 0         for my $i (defined($io) ? ($io ? 1 : 0) : (0, 1)) {
    0          
583            
584             # initialize s-values array
585 0           @s = ();
586            
587             # get ref to Bernstein parameters
588 0           $par = $self->[1][$i];
589            
590             # if number of parameters > 2
591 0 0         if (@{$par} > 2) {
  0            
592            
593             # compute forward differences
594 0           $fwd = [map {$par->[$_] - $par->[$_ - 1]} (1 .. $#{$par})];
  0            
  0            
595            
596             # if forward differences have different signs (+/-)
597 0 0         if (grep {($fwd->[0] < 0) ^ ($fwd->[$_] < 0)} (1 .. $#{$fwd})) {
  0            
  0            
598            
599             # compute second forward differences
600 0           $fwd2 = [map {$fwd->[$_] - $fwd->[$_ - 1]} (1 .. $#{$fwd})];
  0            
  0            
601            
602             # compute derivative values over range (0 - 1)
603 0           $drv = [map {$#{$par} * _fwd($fwd, $_/(4 * $#{$par}))} (0 .. 4 * $#{$par})];
  0            
  0            
  0            
  0            
604            
605             # for each pair of derivatives
606 0           for my $j (1 .. $#{$drv}) {
  0            
607            
608             # if derivatives have different signs (+/-)
609 0 0         if (($drv->[$j - 1] < 0) ^ ($drv->[$j] < 0)) {
610            
611             # estimate root from the derivative values
612 0           $t = ($j * $drv->[$j - 1] - ($j - 1) * $drv->[$j])/(($drv->[$j - 1] - $drv->[$j]) * 4 * $#{$par});
  0            
613            
614             # loop until derivative approaches 0
615 0           while (abs($d = $#{$par} * _fwd($fwd, $t)) > 1E-9) {
  0            
616            
617             # adjust root using Newton's method
618 0           $t -= $d/($#{$fwd} * $#{$par} * _fwd($fwd2, $t));
  0            
  0            
619            
620             }
621            
622             # push root on t-value array
623 0 0 0       push(@s, $t) if ($t > 0 && $t < 1);
624            
625             }
626            
627             }
628            
629             }
630            
631             }
632            
633             # warn if root values were found
634 0 0 0       print "curve $i is not monotonic\n" if (@s && ! defined($self->[2][$i]));
635            
636             # save root t-values
637 0           $self->[2][$i] = [@s];
638            
639             # save root i/o-values
640 0           $self->[3][$i] = [map {_fwd($par, $_)} @s];
  0            
641            
642             }
643            
644             }
645              
646             # combined transform
647             # nominal domain (0 - 1)
648             # parameters: (ref_to_object, direction, input_value)
649             # returns: (output_value)
650             sub _transform {
651              
652             # get parameters
653 0     0     my ($self, $dir, $in) = @_;
654              
655             # local variables
656 0           my ($p0, $p1, $p2, $p3, @t);
657              
658             # verify input
659 0 0         (Scalar::Util::looks_like_number($in)) or croak('invalid transform input');
660              
661             # get parameter arrays
662 0           $p0 = $self->[1][$dir];
663 0           $p1 = $self->[2][$dir];
664 0           $p2 = $self->[3][$dir];
665 0           $p3 = $self->[1][1 - $dir];
666              
667             # compute intermediate value(s)
668 0           @t = _rev($p0, $p1, $p2, $in);
669              
670             # warn if multiple values
671 0 0         (@t == 1) or carp('multiple transform values');
672              
673             # return first output value
674 0           return(_fwd($p3, $t[0]));
675              
676             }
677              
678             # combined derivative
679             # nominal domain (0 - 1)
680             # parameters: (ref_to_object, direction, input_value)
681             # returns: (derivative_value)
682             sub _derivative {
683              
684             # get parameters
685 0     0     my ($self, $dir, $in) = @_;
686              
687             # local variables
688 0           my ($p0, $p1, $p2, $p3, @t);
689 0           my ($di, $do);
690              
691             # verify input
692 0 0         (Scalar::Util::looks_like_number($in)) or croak('invalid transform input');
693              
694             # get parameter arrays
695 0           $p0 = $self->[1][$dir];
696 0           $p1 = $self->[2][$dir];
697 0           $p2 = $self->[3][$dir];
698 0           $p3 = $self->[1][1 - $dir];
699              
700             # compute intermediate value(s)
701 0           @t = _rev($p0, $p1, $p2, $in);
702              
703             # warn if multiple values
704 0 0         (@t == 1) || print "multiple transform values\n";
705              
706             # compute input derivative (using first value)
707 0 0         ($di = _drv($p0, $t[0])) or croak('infinite derivative value');
708              
709             # compute output derivative (using first value)
710 0           $do = _drv($p3, $t[0]);
711              
712             # return combined derivative
713 0           return($do/$di);
714              
715             }
716              
717             # compute parametric partial derivatives
718             # direction: 0 - normal, 1 - inverse
719             # parameters: (ref_to_object, direction, input_value)
720             # returns: (partial_derivative_array)
721             sub _parametric {
722              
723             # get parameters
724 0     0     my ($self, $dir, $in) = @_;
725              
726             # local variables
727 0           my ($p0, $p1, $p2, $p3);
728 0           my (@t, $t, $di, $do, $bfi, $bfo, @pj);
729              
730             # verify input
731 0 0         (Scalar::Util::looks_like_number($in)) or croak('invalid transform input');
732              
733             # get parameter arrays
734 0           $p0 = $self->[1][$dir];
735 0           $p1 = $self->[2][$dir];
736 0           $p2 = $self->[3][$dir];
737 0           $p3 = $self->[1][1 - $dir];
738              
739             # compute intermediate value(s)
740 0           @t = _rev($p0, $p1, $p2, $in);
741              
742             # warn if multiple values
743 0 0         (@t == 1) || print "multiple transform values\n";
744              
745             # use first value
746 0           $t = $t[0];
747              
748             # compute input derivative
749 0           $di = _drv($p0, $t);
750              
751             # compute input Bernstein polynomials
752 0 0         $bfi = @{$p0} ? _bernstein($#{$p0}, $t) : undef();
  0            
  0            
753              
754             # compute output derivative
755 0           $do = _drv($p3, $t);
756              
757             # compute output Bernstein polynomials
758 0 0         $bfo = @{$p3} ? _bernstein($#{$p3}, $t) : undef();
  0            
  0            
759              
760             # initialize array
761 0           @pj = ();
762              
763             # if there are input parameters
764 0 0         if (defined($bfi)) {
765            
766             # for each parameter
767 0           for my $i (0 .. $#{$bfi}) {
  0            
768            
769             # verify non-zero input derivative
770 0 0         ($di) or croak('infinite Jacobian element');
771            
772             # compute partial derivative
773 0           $pj[$i] = -$bfi->[$i] * $do/$di;
774            
775             }
776            
777             }
778              
779             # add output parameters (if any)
780 0 0         push(@pj, @{$bfo}) if (defined($bfo));
  0            
781              
782             # return array of partial derivatives
783 0           return(@pj);
784              
785             }
786              
787             # direct forward transform
788             # nominal domain (0 - 1)
789             # parameters: (parameter_array_reference, input_value)
790             # returns: (output_value)
791             sub _fwd {
792              
793             # get parameters
794 0     0     my ($par, $in) = @_;
795              
796             # if no parameters
797 0 0         if (! @{$par}) {
  0 0          
    0          
798            
799             # return input
800 0           return($in);
801            
802             # one parameter
803 0           } elsif (@{$par} == 1) {
804            
805             # return constant
806 0           return($par->[0]);
807            
808             # two parameters
809 0           } elsif (@{$par} == 2) {
810            
811             # return linear interpolated value
812 0           return((1 - $in) * $par->[0] + $in * $par->[1]);
813            
814             # three or more parameters
815             } else {
816            
817             # check if ICC::Support::Lapack module is loaded
818 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
819            
820             # if input < 0
821 0 0         if ($in < 0) {
    0          
822            
823             # return extrapolated value
824 0           return($par->[0] + $in * _drv($par, 0));
825            
826             # if input > 1
827             } elsif ($in > 1) {
828            
829             # return extrapolated value
830 0           return($par->[-1] + ($in - 1) * _drv($par, 1));
831            
832             } else {
833            
834             # if Lapack module loaded
835 0 0         if ($lapack) {
836            
837             # return Bernstein interpolated value
838 0           return(ICC::Support::Lapack::bernstein($in, $par));
839            
840             } else {
841            
842             # return Bernstein interpolated value
843 0           return(ICC::Shared::dotProduct(_bernstein($#{$par}, $in), $par));
  0            
844            
845             }
846            
847             }
848            
849             }
850            
851             }
852              
853             # direct reverse transform
854             # nominal range (0 - 1)
855             # parameters: (parameter_array_reference, x-min/max_array_reference, y-min/max_array_reference, input_value)
856             # returns: (output_value -or- array_of_output_values)
857             sub _rev {
858              
859             # get parameters
860 0     0     my ($par, $xminmax, $yminmax, $in) = @_;
861              
862             # local variables
863 0           my (@xs, @ys, $slope, $ext, @sol);
864 0           my ($p, $loop, $xlo, $xhi, $xval, $yval);
865              
866             # if no parameters
867 0 0         if (! @{$par}) {
  0 0          
    0          
868            
869             # return input
870 0           return($in);
871            
872             # one parameter
873 0           } elsif (@{$par} == 1) {
874            
875             # warn if input not equal to constant
876 0 0         ($in == $par->[0]) or carp('curve input not equal to constant parameter');
877            
878             # return input
879 0           return($in);
880            
881             # two parameters
882 0           } elsif (@{$par} == 2) {
883            
884             # return linear interpolated value
885 0           return(($in - $par->[0])/($par->[1] - $par->[0]));
886            
887             # three or more parameters
888             } else {
889            
890             # setup segment arrays
891 0           @xs = (0, @{$xminmax}, 1);
  0            
892 0           @ys = ($par->[0], @{$yminmax}, $par->[-1]);
  0            
893            
894             # if slope(0) not 0
895 0 0         if ($slope = _drv($par, 0)) {
896            
897             # compute extrapolation from 0
898 0           $ext = ($in - $par->[0])/$slope;
899            
900             # save if less than 0
901 0 0         push(@sol, $ext) if ($ext < 0);
902            
903             }
904            
905             # test first y-value
906 0 0         push(@sol, $xs[0]) if ($ys[0] == $in);
907            
908             # for each array segment
909 0           for my $i (1 .. $#xs) {
910            
911             # if input value falls within segment
912 0 0 0       if (($in > $ys[$i - 1] && $in < $ys[$i]) || ($in < $ys[$i - 1] && $in > $ys[$i])) {
      0        
      0        
913            
914             # get segment polarity
915 0           $p = $ys[$i] > $ys[$i - 1];
916            
917             # initialize limits
918 0           $xlo = $xs[$i - 1];
919 0           $xhi = $xs[$i];
920            
921             # initialize loop counter
922 0           $loop = 0;
923            
924             # bisection method loop
925 0   0       while ($xhi - $xlo > 1E-3 && $loop++ < 15) {
926            
927             # compute midpoint
928 0           $xval = ($xlo + $xhi)/2;
929            
930             # compute y-value
931 0           $yval = _fwd($par, $xval);
932            
933             # if y-value > input -or- y-value < input
934 0 0         if ($p ? ($yval > $in) : ($yval < $in)) {
    0          
935            
936             # set high limit
937 0           $xhi = $xval;
938            
939             } else {
940            
941             # set low limit
942 0           $xlo = $xval;
943            
944             }
945            
946             }
947            
948             # initialize loop counter
949 0           $loop = 0;
950            
951             # Newton's method loop
952 0   0       while (abs($in - $yval) > 1E-12 && $loop++ < 5 && ($slope = _drv($par, $xval))) {
      0        
953            
954             # adjust x-value
955 0           $xval += ($in - $yval)/$slope;
956            
957             # compute y-value
958 0           $yval = _fwd($par, $xval);
959            
960             }
961            
962             # print warning if failed to converge
963 0 0         printf("'_rev' function of 'bern' object failed to converge with input of %.2f\n", $in) if (abs($in - $yval) > 1E-12);
964            
965             # save result
966 0           push(@sol, $xval);
967            
968             }
969            
970             # add segment y-value, if a solution
971 0 0         push(@sol, $xs[$i]) if ($ys[$i] == $in);
972            
973             }
974            
975             # if slope(1) not 0
976 0 0         if ($slope = _drv($par, 1)) {
977            
978             # compute extrapolation from 1
979 0           $ext = ($in - $par->[-1])/$slope + 1;
980            
981             # save if greater than 1
982 0 0         push(@sol, $ext) if ($ext > 1);
983            
984             }
985            
986             }
987              
988             # return result (array or first solution)
989 0 0         return(wantarray ? @sol : $sol[0]);
990              
991             }
992              
993             # forward derivative
994             # nominal domain is (0 - 1)
995             # parameters: (parameter_array_reference, input_value)
996             # returns: (output_value)
997             sub _drv {
998              
999             # get parameters
1000 0     0     my ($par, $in) = @_;
1001              
1002             # if no parameters
1003 0 0         if (! @{$par}) {
  0 0          
    0          
1004            
1005             # return identity derivative
1006 0           return(1);
1007            
1008             # one parameter
1009 0           } elsif (@{$par} == 1) {
1010            
1011             # return constant derivative
1012 0           return(0);
1013            
1014             # two parameters
1015 0           } elsif (@{$par} == 2) {
1016            
1017             # return linear derivative
1018 0           return($par->[1] - $par->[0]);
1019            
1020             # three or more parameters
1021             } else {
1022            
1023             # check if ICC::Support::Lapack module is loaded
1024 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
1025            
1026             # limit input value (0 - 1) to force linear extrapolation
1027 0 0         $in = $in < 0 ? 0 : ($in > 1 ? 1 : $in);
    0          
1028            
1029             # compute parameter differences
1030 0           my $diff = [map {$par->[$_ + 1] - $par->[$_]} (0 .. $#{$par} - 1)];
  0            
  0            
1031            
1032             # if Lapack module loaded
1033 0 0         if ($lapack) {
1034            
1035             # return Bernstein interpolated derivative
1036 0           return($#{$par} * ICC::Support::Lapack::bernstein($in, $diff));
  0            
1037            
1038             } else {
1039            
1040             # return Bernstein interpolated derivative
1041 0           return($#{$par} * ICC::Shared::dotProduct(_bernstein($#{$diff}, $in), $diff));
  0            
  0            
1042            
1043             }
1044            
1045             }
1046            
1047             }
1048              
1049             # compute Bernstein basis values
1050             # parameters: (bernstein_degree, t)
1051             # returns: (ref_to_bernstein_array)
1052             sub _bernstein {
1053              
1054             # get parameters
1055 0     0     my ($degree, $t0) = @_;
1056              
1057             # local variables
1058 0           my ($bern, $t1, $m0, $m1, $n);
1059              
1060             # binomial coefficients
1061 0           state $pascal = [
1062             [1],
1063             [1, 1],
1064             [1, 2, 1],
1065             [1, 3, 3, 1],
1066             [1, 4, 6, 4, 1],
1067             [1, 5, 10, 10, 5, 1],
1068             [1, 6, 15, 20, 15, 6, 1],
1069             [1, 7, 21, 35, 35, 21, 7, 1],
1070             [1, 8, 28, 56, 70, 56, 28, 8, 1],
1071             [1, 9, 36, 84, 126, 126, 84, 36, 9, 1],
1072             [1, 10, 45, 120, 210, 252, 210, 120, 45, 10, 1],
1073             [1, 11, 55, 165, 330, 462, 462, 330, 165, 55, 11, 1],
1074             [1, 12, 66, 220, 495, 792, 924, 792, 495, 220, 66, 12, 1],
1075             [1, 13, 78, 286, 715, 1287, 1716, 1716, 1287, 715, 286, 78, 13, 1],
1076             [1, 14, 91, 364, 1001, 2002, 3003, 3432, 3003, 2002, 1001, 364, 91, 14, 1],
1077             [1, 15, 105, 455, 1365, 3003, 5005, 6435, 6435, 5005, 3003, 1365, 455, 105, 15, 1],
1078             [1, 16, 120, 560, 1820, 4368, 8008, 11440, 12870, 11440, 8008, 4368, 1820, 560, 120, 16, 1],
1079             [1, 17, 136, 680, 2380, 6188, 12376, 19448, 24310, 24310, 19448, 12376, 6188, 2380, 680, 136, 17, 1],
1080             [1, 18, 153, 816, 3060, 8568, 18564, 31824, 43758, 48620, 43758, 31824, 18564, 8568, 3060, 816, 153, 18, 1],
1081             [1, 19, 171, 969, 3876, 11628, 27132, 50388, 75582, 92378, 92378, 75582, 50388, 27132, 11628, 3876, 969, 171, 19, 1],
1082             [1, 20, 190, 1140, 4845, 15504, 38760, 77520, 125970, 167960, 184756, 167960, 125970, 77520, 38760, 15504, 4845, 1140, 190, 20, 1]
1083             ];
1084              
1085             # copy binomial coefficients for this degree
1086 0           $bern = [@{$pascal->[$degree]}];
  0            
1087              
1088             # compute input complement
1089 0           $t1 = 1 - $t0;
1090              
1091             # initialize multipliers
1092 0           $m0 = $m1 = 1;
1093              
1094             # get polynomial degree
1095 0           $n = $#{$bern};
  0            
1096              
1097             # for each polynomial
1098 0           for (1 .. $n) {
1099            
1100             # multiply
1101 0           $bern->[$_] *= ($m0 *= $t0);
1102 0           $bern->[$n - $_] *= ($m1 *= $t1);
1103            
1104             }
1105              
1106             # return array reference
1107 0           return($bern);
1108              
1109             }
1110              
1111             # fit bern object to data
1112             # uses LAPACK dgels function to perform a least-squares fit
1113             # the parameters to be fitted have the value 'undef' and must all be within the
1114             # output side of the parameter array, e.g. [[0, 100], [undef, undef, undef, undef]]
1115             # fit data is an array containing input and output vectors
1116             # parameters: (ref_to_object, fit_data_reference, ref_to_parameter_hash)
1117             # returns: (dgels_info_value)
1118             sub _fit {
1119              
1120             # get parameters
1121 0     0     my ($self, $fit, $hash) = @_;
1122              
1123             # local variables
1124 0           my ($m, $n, $d, $t, $p, $span, @sol);
1125 0           my (@so, $outz, $bern, $nrhs, $info);
1126 0           my ($outz2, $bern2, $fix_hl, $fix_sh, @c2);
1127              
1128             # check if ICC::Support::Lapack module is loaded
1129 0           state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
1130              
1131             # verify Lapack module loaded (need to add non-Lapack routine someday)
1132 0 0         ($lapack) or croak("'fit' option requires ICC::Support::Lapack module");
1133              
1134             # get degree
1135 0           $d = $#{$self->[1][1]};
  0            
1136              
1137             # for each output parameter
1138 0           for my $i (0 .. $d) {
1139            
1140             # if parameter is undefined
1141 0 0         if (! defined($self->[1][1][$i])) {
1142            
1143             # push index
1144 0           push(@so, $i);
1145            
1146             # set value to 0
1147 0           $self->[1][1][$i] = 0;
1148            
1149             }
1150            
1151             }
1152              
1153             # if no 'undef' parameters
1154 0 0         if (@so == 0) {
1155            
1156             # warning
1157 0           print "no 'undef' parameters to fit\n";
1158            
1159             # return
1160 0           return(0)
1161            
1162             }
1163              
1164             # check for min/max values
1165 0           _minmax($self);
1166              
1167             # for each sample
1168 0           for my $i (0 .. $#{$fit->[0]}) {
  0            
1169            
1170             # compute output difference
1171 0           $outz->[$i] = [$fit->[1][$i] - transform($self, $fit->[0][$i])];
1172            
1173             # get intermediate value
1174 0           $t = _rev($self->[1][0], $self->[2][0], $self->[3][0], $fit->[0][$i]);
1175            
1176             # compute Bernstein values for undefined parameters
1177 0           $bern->[$i] = [@{_bernstein($d, $t)}[@so]];
  0            
1178            
1179             }
1180              
1181             # make copies
1182 0           $outz2 = Storable::dclone($outz);
1183 0           $bern2 = Storable::dclone($bern);
1184              
1185             # get array sizes
1186 0           $m = @{$bern};
  0            
1187 0           $n = @{$bern->[0]};
  0            
1188 0           $nrhs = @{$outz->[0]};
  0            
1189              
1190             # fit the data, return if failed
1191 0 0         ($info = ICC::Support::Lapack::dgels('N', $m, $n, $nrhs, $bern, $m, $outz, $m)) && return($info);
1192              
1193             # get curve polarity (0 - increasing, 1 - decreasing)
1194 0 0         $p = ($span = ($so[-1] == $d ? $outz->[$n - 1][0] : $self->[1][1][-1]) - ($so[0] == 0 ? $outz->[0][0] : $self->[1][1][0])) < 0;
    0          
1195              
1196             # get flags (indicating HL or SH contrast needs to be fixed)
1197 0   0       $fix_hl = $hash->{'fix_hl'} && $so[0] == 0 && $so[1] == 1 && (($outz->[0][0] > $outz->[1][0]) ^ $p);
1198 0   0       $fix_sh = $hash->{'fix_sh'} && $so[-1] == $d && $so[-2] == $d - 1 && (($outz->[$n - 1][0] < $outz->[$n - 2][0]) ^ $p);
1199              
1200             # if degree >= 4 and HL or SH needs fixing
1201 0 0 0       if ($d >= 4 && ($fix_hl || $fix_sh)) {
      0        
1202            
1203             # for each sample
1204 0           for my $i (0 .. $#{$bern2}) {
  0            
1205            
1206             # if highlight fix
1207 0 0         if ($fix_hl) {
1208            
1209             # get first 2 Bernstein values
1210 0           @c2 = splice(@{$bern2->[$i]}, 0, 2);
  0            
1211            
1212             # combine to single value
1213 0           unshift(@{$bern2->[$i]}, ($c2[0] + $c2[1]));
  0            
1214            
1215             }
1216            
1217             # if shadow fix
1218 0 0         if ($fix_sh) {
1219            
1220             # get last 2 Bernstein values
1221 0           @c2 = splice(@{$bern2->[$i]}, -2);
  0            
1222            
1223             # combine to single value
1224 0           push(@{$bern2->[$i]}, ($c2[0] + $c2[1]));
  0            
1225            
1226             }
1227            
1228             }
1229            
1230             # get array sizes
1231 0           $m = @{$bern2};
  0            
1232 0           $n = @{$bern2->[0]};
  0            
1233 0           $nrhs = @{$outz2->[0]};
  0            
1234            
1235             # fit the data, return if failed
1236 0 0         ($info = ICC::Support::Lapack::dgels('N', $m, $n, $nrhs, $bern2, $m, $outz2, $m)) && return($info);
1237            
1238             # extract solution
1239 0           @sol = map {$outz2->[$_][0]} (0 .. $n - 1);
  0            
1240            
1241             # replace combined coefficient(s)
1242 0 0         splice(@sol, 1, 0, $sol[0] + $span * 0.001) if ($fix_hl);
1243 0 0         splice(@sol, -1, 0, $sol[-1] - $span * 0.001) if ($fix_sh);
1244            
1245             } else {
1246            
1247             # extract solution
1248 0           @sol = map {$outz->[$_][0]} (0 .. $n - 1);
  0            
1249            
1250             }
1251              
1252             # copy solution to output array
1253 0           @{$self->[1][1]}[@so] = @sol;
  0            
1254              
1255             # return
1256 0           return(0);
1257              
1258             }
1259              
1260             # set object contents from parameter hash
1261             # parameters: (ref_to_object, ref_to_parameter_hash)
1262             sub _new_from_hash {
1263              
1264             # get parameters
1265 0     0     my ($self, $hash) = @_;
1266              
1267             # local variables
1268 0           my ($in, $out, $fit);
1269              
1270             # if 'input' is defined
1271 0 0         if (defined($in = $hash->{'input'})) {
1272            
1273             # if 'input' is a vector (21 values or less, some may be 'undef')
1274 0 0 0       if (ICC::Shared::is_vector($in) && 21 >= @{$in}) {
  0 0 0        
      0        
      0        
1275            
1276             # set object 'input' array
1277 0           $self->[1][0] = [@{$in}];
  0            
1278            
1279             # if 'input' is an integer between 1 and 20
1280             } elsif (Scalar::Util::looks_like_number($in) && $in == int($in) && $in > 0 && $in < 21) {
1281            
1282             # make identity curve
1283 0           $self->[1][0] = [map {$_/$in} (0 .. $in)];
  0            
1284            
1285             } else {
1286            
1287             # error
1288 0           croak("invalid 'input' values");
1289            
1290             }
1291            
1292             } else {
1293            
1294             # disable input
1295 0           $self->[1][0] = [];
1296            
1297             }
1298              
1299             # if 'output' is defined
1300 0 0         if (defined($out = $hash->{'output'})) {
1301            
1302             # if 'output' is a vector (21 values or less, some may be 'undef')
1303 0 0 0       if (ICC::Shared::is_vector($out) && 21 >= @{$out}) {
  0 0 0        
      0        
      0        
1304            
1305             # set object 'output' array
1306 0           $self->[1][1] = [@{$out}];
  0            
1307            
1308             # if 'output' is an integer between 1 and 20
1309             } elsif (Scalar::Util::looks_like_number($out) && $out == int($out) && $out > 0 && $out < 21) {
1310            
1311             # make identity curve
1312 0           $self->[1][1] = [map {$_/$out} (0 .. $out)];
  0            
1313            
1314             } else {
1315            
1316             # error
1317 0           croak("invalid 'output' values");
1318            
1319             }
1320            
1321             } else {
1322            
1323             # disable output
1324 0           $self->[1][1] = [];
1325            
1326             }
1327              
1328             # if 'fit' is defined and not empty
1329 0 0 0       if (defined($fit = $hash->{'fit'}) && @{$fit}) {
  0            
1330            
1331             # verify 'fit' vectors
1332 0 0 0       (ICC::Shared::is_num_vector($fit->[0]) && 1 <= @{$fit->[0]}) or croak("invalid 'fit' input values");
  0            
1333 0 0 0       (ICC::Shared::is_num_vector($fit->[1]) && 1 <= @{$fit->[1]}) or croak("invalid 'fit' output values");
  0            
1334 0 0         (@{$fit->[0]} == @{$fit->[1]}) or croak("'fit' input and output vectors are different size");
  0            
  0            
1335            
1336             # fit object to data
1337 0 0         (_fit($self, $fit, $hash)) && croak("'fit' operation failed");
1338            
1339             } else {
1340            
1341             # verify object parameters are all defined
1342 0 0         (@{$self->[1][0]} == grep {defined($_)} @{$self->[1][0]}) or croak("some 'input' values are 'undef'");
  0            
  0            
  0            
1343 0 0         (@{$self->[1][1]} == grep {defined($_)} @{$self->[1][1]}) or croak("some 'output' values are 'undef'");
  0            
  0            
  0            
1344            
1345             }
1346              
1347             # add segment min/max y-values
1348 0           _minmax($self);
1349              
1350             }
1351              
1352             # write bern object to ICC profile
1353             # note: writes an equivalent curv object
1354             # parameters: (ref_to_object, ref_to_parent_object, file_handle, ref_to_tag_table_entry)
1355             sub _writeICCbern {
1356              
1357             # get parameters
1358 0     0     my ($self, $parent, $fh, $tag) = @_;
1359              
1360             # local variables
1361 0           my ($n, $up, @table);
1362              
1363             # get count value (defaults to 4096, the maximum allowed in an 'mft2' tag)
1364 0   0       $n = $self->[0]{'curv_points'} // 4096;
1365              
1366             # validate number of table entries
1367 0 0 0       ($n == int($n) && $n >= 2) or carp('invalid number of table entries');
1368              
1369             # array upper index
1370 0           $up = $n - 1;
1371              
1372             # for each table entry
1373 0           for my $i (0 .. $up) {
1374            
1375             # transform value
1376 0           $table[$i] = transform($self, $i/$up);
1377            
1378             }
1379              
1380             # seek start of tag
1381 0           seek($fh, $tag->[1], 0);
1382              
1383             # write tag type signature and count
1384 0           print $fh pack('a4 x4 N', 'curv', $n);
1385              
1386             # write array
1387 0 0         print $fh pack('n*', map {$_ < 0 ? 0 : ($_ > 1 ? 65535 : $_ * 65535 + 0.5)} @table);
  0 0          
1388              
1389             }
1390              
1391             1;