Created
January 16, 2012 22:34
-
-
Save run4flat/1623396 to your computer and use it in GitHub Desktop.
PDL::Transform::Color - Convert between color systems
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
=head1 NAME | |
PDL::Transform::Color - Convert between color systems | |
=head1 SYNOPSIS | |
# load an image (in this case the cartographic demo) | |
use PDL::Transform::Cartography; $rgb = earth_image(); | |
# convert the image to CMYK | |
$cmyk = $rgb->apply(t_cmyk()); | |
=head1 DESCRIPTION | |
PDL::Transform::Color bundles several important color system | |
transformations. Unlike most transforms, PDL::Transform::Color | |
transforms are optimized to work with color images. Color images | |
typically have many pixels in the spatial direction and 3 or 4 pixels | |
in the color-index direction, but are sometimes stored with the | |
color index in either the 0 or 2 dim. The PDL::Transform::Color | |
transformations should examine the first few dims of their input | |
and, if only one suitably sized dim is found, they should transform | |
that dim rather than the 0 dim. | |
Conversions are relative to the RGB system, so (e.g.) C<t_cmyk> | |
converts RGB to CMYK and C<!t_cmyk> converts CMYK to RGB. | |
All values are promoted to floating point before conversion, to | |
avoid quantization problems associated with fixed-point/integer | |
arithmetic in the general case. | |
At present, the color representations all utilize simple linear | |
theory -- no provision is made (other than gamma-encoding) for | |
nonlinearities in visual perception, nor for the various issues that | |
arise with the particular spectral response from individual pigments. | |
=head1 STANDARD OPTIONS | |
Several options are standard. They are: | |
=over 3 | |
=item * 'gamma' (or 'g') - gamma of the RGB space (default 1.0) | |
This is the gamma correction factor used to get physical values from | |
the RGB values in the RGB space. Conversion is performed in a gamma=1 | |
space -- i.e. if you specify a gamma to the forward transform, the | |
input RGB values are assumed to be gamma encoded, and are decoded to | |
linear physical values before processing. | |
=item * 'max' - max in-gamut value of the RGB space (default 1.0 or 255) | |
Some of the conversions (notably CMYK) require a range of the RGB | |
space to define their gamut. The minimum is always taken to be 0. | |
=back | |
=head1 AUTHOR | |
Copyright 2012, Craig DeForest ([email protected]). | |
This module may be modified and distributed under the same | |
terms as PDL itself. The module comes with NO WARRANTY. | |
=head1 FUNCTIONS | |
This module defines and exports transform constructors ('t_<foo>') only. | |
=cut | |
use PDL::Transform; | |
package PDL::Transform::Color; | |
use PDL::Core ':Internal'; # Load 'topdl' | |
@ISA = ('Exporter','PDL::Transform'); | |
$VERSION = "0.2"; | |
BEGIN { | |
use Exporter(); | |
@EXPORT_OK = qw(t_cmyk t_hsv); | |
@EXPORT = @EXPORT_OK; | |
%EXPORT_TAGS = (Func=>[@EXPORT_OK]); | |
} | |
use PDL; | |
use PDL::Transform; | |
use PDL::NiceSlice; | |
use Carp; | |
############################## | |
# Steal _opt from PDL::Transform. | |
*PDL::Transform::Color::_opt = \&PDL::Transform::_opt; | |
############################## | |
# Enable our own stringifier | |
use overload '""' => \&_strval; | |
sub _strval { | |
my($me) = shift; | |
$me->stringify(); | |
} | |
use strict; | |
use PDL::Constants; | |
sub _new { new('PDL::Transform::Color',@_); } # not exported | |
sub new { | |
my $class = shift; | |
my $opt = shift; | |
my $me = PDL::Transform::new($class); | |
$me->{name} = "generic color transform"; | |
$me->{idim} = 0; | |
$me->{odim} = 0; | |
$me->{itype} = ['red','green','blue']; | |
$me->{iunit} = ['brightness','brightness','brightness']; | |
$me->{func} = \*PDL::Transform::_identity; | |
$me->{inv} = \*PDL::Transform::_identity; | |
$me->{params} = {}; | |
# Parse standard options | |
$me->{options}->{gamma} = _opt($opt,["gamma","g"]); | |
$me->{options}->{max} = _opt($opt,["max","m"]); | |
bless $me,$class; | |
} | |
# Find the correct active and put it in front; return the dim number where it goes | |
# Also, promote integer types to float for internal work... | |
sub _rectify { | |
my $input = shift; | |
if($input->type !~ m/(double|float)/) { | |
$input = float $input; | |
} | |
my $pos = shift; | |
if(defined($pos)) { | |
if($pos) { | |
return ($input->mv($pos,0),$pos); | |
} else { | |
return ($input,undef); | |
} | |
} | |
my $dims = pdl($input->dims); | |
my $okdims = which($dims <= 5); | |
if($okdims->nelem == 0) { | |
die "PDL::Transform::Color: couldn't find an appropriate dim (size <= 5) for color\n vectors. Specify position explicitly in the transform constructor."; | |
} | |
$pos = $okdims->at(0); | |
return ($input->mv($pos,0),$pos); | |
} | |
=head2 t_gamma - expand/decode encoded data to physical (gamma=1, max=1). | |
=for usage | |
$im_phys = $im->apply(t_gamma(gamma=>2.2)); | |
=for ref | |
t_gamma is mostly used internally to handle gamma conversion of RGB | |
values before other transforms are applied. It is automatically used | |
by the other transforms if you include a C<gamma> option to the | |
constructor. | |
=cut | |
sub t_gamma { | |
my $opt = shift; | |
my $me = _new($opt); | |
$me->{name} = "Gamma decoding and scaling"; | |
$me->{func} = sub { | |
my($d,$o) = @_; | |
my $out; | |
if($d->type !~ m/(double|float)/) { | |
$out = float $d; | |
} else { | |
$out = $d->copy; | |
} | |
my $max = $o->{max}; | |
if(!defined($max)) { | |
# guess max based on type of input | |
if($d->type =~ m/(byte|short|ushort|long)/) { | |
$max = 255; | |
} else { | |
$max = 1.0; | |
} | |
} | |
$out /= $max; | |
$out **= $o->{gamma} // 1; | |
return $out; | |
}; | |
$me->{inv} = sub { | |
my($d,$o) = @_; | |
my $out = double $d; | |
die "t_gamma inverse: gamma encoding - can't encode with gamma=0!" unless($o->{gamma}//1); | |
$out **= (1.0/( $o->{gamma} // 1) ); | |
$out *= $o->{max} // 1; | |
return $out; | |
}; | |
return $me; | |
} | |
=head2 t_cmyk - convert RGB to CMYK (or vice versa) | |
=for usage | |
$cmyk = $im->apply(t_cmyk); | |
=for ref | |
C<t_cmyk> converts to four-color separation subtractive process | |
values, maximizing black ink at the expense of the cyan, magenta, and | |
yellow channels. Standard options (notably C<max> and C<gamma>) are | |
accepted, but the CMYK representation is always scaled 0-1, with a | |
gamma of unity. | |
Linear subtractive conversion is used -- thus the CMYK values | |
represent corrected halftone fraction with idealized subtractive | |
pigments that are exactly conjugate to the RGB colors. | |
Like most of the color conversions, C<t_cmyk> doesn't necessarily work | |
in the 0 dim of the input -- it attempts to find the color dim in one | |
of the first three dims of the input PDL. That is because some image | |
manipulation code puts the colors in the 0 dim and some in the 2 dim. | |
=cut | |
sub t_cmyk { | |
my $opt = shift; | |
my $me = _new($opt); | |
$me->{name} = "CMYK conversion"; | |
# Function and inverse work in 0-1 linear physical space (t_gamma composition fixes scaling) | |
$me->{func} = sub { | |
my($d,$o) = @_; | |
my ($d2,$where) = _rectify($d,$o->{pos}); | |
# Expand the color dim by one in the output (to make room for K in CMYK) | |
my @dims = $d->dims; | |
$dims[$where]++; | |
# Generate the output to match the expanded input dims, and make a working | |
# link into it with the active dim at 0 | |
my $out = PDL->new_from_specification($d2->type, @dims); | |
my $oo = ($where) ? $out->mv($where,0) : $out; | |
# Convert RGB->CMY and copy any extra information over | |
$oo->(0:2) .= 1 - $d2->(0:2); | |
if($oo->dim(0) > 4) { | |
$oo->(4:-1) .= $d2->(2:-1); | |
} | |
# Find the K channel | |
$oo->((3)) .= $oo->(0:2)->minimum; | |
$oo->(0:2) -= $oo->(3); | |
# The $oo stuff flowed to $out; return that to preserve shape | |
return $out; | |
}; | |
$me->{inv} = sub { | |
my($d,$o) = @_; | |
my($d2, $where) = _rectify($d,$o->{pos}); | |
my @dims = $d->dims; | |
if($dims[$where] < 4) { | |
die "t_cmyk inverse: color dim has size ".$dims[0].", too small for cmyk (4 needed)\n"; | |
} | |
$dims[$where]--; | |
my $out = PDL->new_from_specification($d2->type, @dims); | |
my $oo = ($where) ? $out->mv($where,0) : $out; | |
# Convert CMY to RGB | |
$oo->(0:2) .= 1 - $d2->(0:2); | |
# Correct RGB downward for the K portion. | |
$oo->(0:2) -= $d2->(3); | |
return $out; | |
}; | |
$me->{otype} = ['cyan','magenta','yellow','black']; | |
$me->{ounit} = ['ink fraction','ink fraction','ink fraction','ink fraction']; | |
return t_compose(t_gamma($me->{options}), $me); | |
} | |
############################## | |
# _t_hs_ivl - handle hsi, hsv, or hsl -- which differ only in their treatment | |
# of brightness. Hue is normalized 0..1 | |
# | |
# Definitions from en.wikipedia.org... | |
sub _hue_from_rgb { | |
my $rgb = shift; | |
my $m = shift; | |
my $C = shift() - $m; | |
# Find index of max... | |
my $maxdex = $rgb->qsorti->(-1); # index (R,G,B) of maximum component | |
my $dexes = ($maxdex + pdl(0,1,2)) % 3; | |
my $wonky = $rgb->(:,*3)->index($dexes); # R,G,B permuted | |
my $offset = $maxdex->((0)) * 2; | |
my $H = ( ($wonky->(1:2) * pdl(1,-1))->sumover ) / ($C + ($C==0)) + $offset ; | |
$H += 6 * ($H<0); | |
$H /= 6; | |
return $H; | |
} | |
=head2 t_hsv - convert RGB to HSV (or vice versa) | |
=for usage | |
$hsv = $im->apply(t_hsv); | |
=for ref | |
HSV is Hue/Saturation/Value. | |
=cut | |
sub t_hsv { | |
my $opt = shift; | |
my $me = _new($opt); | |
$me->{name} = "HSV conversion"; | |
$me->{func} = sub { | |
my ($d,$o) = @_; | |
my ($d2, $where) = _rectify($d, $o->{pos}); | |
# Generate output to match the input dims, and make a working link | |
# to it with the active dim at 0 | |
my $out = PDL->new_from_specification( $d2->type, $d->dims ); | |
my $oo = ($where) ? $out->mv($where,0) : $out; | |
my $m = $d2->minimum; | |
my $M = $d2->maximum; | |
$oo->((0)) .= _hue_from_rgb($d2, $m, $M); # H | |
# Find min/max and chroma | |
$oo->((2)) .= $M; # V | |
$oo->((1)) .= ($M-$m) / ($M + ($M==0)); # S | |
# Copy ancillary info... | |
if($d2->dim(0) > 3) { | |
$oo->(3:-1) .= $d2->(3:-1); | |
} | |
return $out; | |
}; | |
$me->{inv} = sub { | |
my ($d,$o) = @_; | |
my ($d2, $where) = _rectify($d, $o->{pos}); | |
# Generate output to match the input dims, and make a working link | |
# to it with the active dim at 0 | |
my $out = PDL->new_from_specification( $d2->type, $d->dims ); | |
my $oo = ($where) ? $out->mv($where,0) : $out; | |
my $Hp = $d2->((0)) * 6; | |
$Hp -= 6 * ($Hp/6)->floor; | |
my $wonky = PDL->new_from_specification($d2->type, $d2->dims ); | |
$wonky->((2)) .= 0; | |
$wonky->((0)) .= $d2->((1)) * $d2->((2)); # C = S * V | |
$wonky->((1)) .= $wonky->((0)) * (1 - ($Hp - 2 * (($Hp/2)->floor) - 1)->abs ); # X | |
my $lookup = pdl([0,1,2],[1,0,2],[2,0,1],[2,1,0],[1,2,0],[0,2,1]); | |
my $dex = ($Hp->floor); | |
my $l2 = $lookup->mv(0,-1)->slice(":". (",*1"x$dex->ndims))->index($dex)->mv(-1,0); | |
$oo->(0:2) .= $wonky->(:,*1)->index( $l2 ); # R'G'B' | |
$oo += $d2->(2) - $wonky->(0); # Add in unsaturated part | |
# Copy ancillary info... | |
if($d2->dim(0) > 3) { | |
$oo->(3:-1) .= $d2->(3:-1); | |
} | |
return $out; | |
}; | |
$me->{otype} = ['Hue','Saturation','Value']; | |
$me->{ounit} = ['degrees','scaled','brightness']; | |
return t_compose(t_gamma($me->{options}),$me); | |
} | |
=head2 t_hsv - convert RGB to HSV (or vice versa) | |
=head2 t_hsl - convert RGB to HSL (or vice versa) | |
=head2 t_yuv - convert RGB to YUV (or vice versa) | |
=head2 t_cielab - convert RGB to CIELAB 1976 (or vice versa) | |
=head2 t_ciexyz - convert RGB to CIE 1931 XYZ (or vice versa) | |
1; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment