File Coverage

blib/lib/ICC/Support/bern.pm
Criterion Covered Total %
statement 16 455 3.5
branch 2 230 0.8
condition 1 115 0.8
subroutine 5 32 15.6
pod 15 17 88.2
total 39 849 4.5


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