pp_addpm({At=>'Top'},<<'======EOD======'); =head1 NAME PDL::Transform - Coordinate transforms, image warping, and N-D functions =head1 SYNOPSIS use PDL::Transform; my $t = new PDL::Transform::() $out = $t->apply($in) # Apply transform to some N-vectors (Transform method) $out = $in->apply($t) # Apply transform to some N-vectors (PDL method) $im1 = $t->map($im); # Transform image coordinates (Transform method) $im1 = $im->map($t); # Transform image coordinates (PDL method) $t2 = $t->compose($t1); # compose two transforms $t2 = $t x $t1; # compose two transforms (by analogy to matrix mult.) $t3 = $t2->inverse(); # invert a transform $t3 = !$t2; # invert a transform (by analogy to logical "not") =head1 DESCRIPTION PDL::Transform is a convenient way to represent coordinate transformations and resample images. It embodies functions mapping R^N -> R^M, both with and without inverses. Provision exists for parametrizing functions, and for composing them. You can use this part of the Transform object to keep track of arbitrary functions mapping R^N -> R^M with or without inverses. The simplest way to use a Transform object is to transform vector data between coordinate systems. The L method accepts a PDL whose 0th dimension is coordinate index (all other dimensions are threaded over) and transforms the vectors into the new coordinate system. Transform also includes image resampling, via the L method. You define a coordinate transform using a Transform object, then use it to remap an image PDL. The output is a remapped, resampled image. You can define and compose several transformations, then apply them all at once to an image. The image is interpolated only once, when all the composed transformations are applied. In keeping with standard practice, but somewhat counterintuitively, the L engine uses the inverse transform to map coordinates FROM the destination dataspace (or image plane) TO the source dataspace; hence PDL::Transform keeps track of both the forward and inverse transform. For terseness and convenience, most of the constructors are exported into the current package with the name C>, so the following (for example) are synonyms: $t = new PDL::Transform::Radial(); # Long way $t = t_radial(); # Short way Several math operators are overloaded, so that you can compose and invert functions with expression syntax instead of method syntax (see below). =head1 EXAMPLE Coordinate transformations and mappings are a little counterintuitive at first. Here are some examples of transforms in action: use PDL::Transform; $a = rfits('m51.fits'); # Substitute path if necessary! $ts = t_linear(Scale=>3); # Scaling transform $w = pgwin(xs); $w->imag($a); ## Grow m51 by a factor of 3; origin is at lower left. $b = $ts->map($a,{pix=>1}); # pix option uses direct pixel coord system $w->imag($b); ## Shrink m51 by a factor of 3; origin still at lower left. $c = $ts->unmap($a, {pix=>1}); $w->imag($c); ## Grow m51 by a factor of 3; origin is at scientific origin. $d = $ts->map($a,$a->hdr); # FITS hdr template prevents autoscaling $w->imag($d); ## Shrink m51 by a factor of 3; origin is still at sci. origin. $e = $ts->unmap($a,$a->hdr); $w->imag($e); ## A no-op: shrink m51 by a factor of 3, then autoscale back to size $f = $ts->map($a); # No template causes autoscaling of output =head1 OPERATOR OVERLOADS =over 3 =item '!' The bang is a unary inversion operator. It binds exactly as tightly as the normal bang operator. =item 'x' By analogy to matrix multiplication, 'x' is the compose operator, so these two expressions are equivalent: $f->inverse()->compose($g)->compose($f) # long way !$f x $g x $f # short way Both of those expressions are equivalent to the mathematical expression f^-1 o g o f, or f^-1(g(f(x))). =item '**' By analogy to numeric powers, you can apply an operator a positive integer number of times with the ** operator: $f->compose($f)->compose($f) # long way $f**3 # short way =back =head1 INTERNALS Transforms are perl hashes. Here's a list of the meaning of each key: =over 3 =item func Ref to a subroutine that evaluates the transformed coordinates. It's called with the input coordinate, and the "params" hash. This springboarding is done via explicit ref rather than by subclassing, for convenience both in coding new transforms (just add the appropriate sub to the module) and in adding custom transforms at run-time. Note that, if possible, new Cs should support L operation to save memory when the data are flagged inplace. But C should always return its result even when flagged to compute in-place. C should treat the 0th dimension of its input as a dimensional index (running 0..N-1 for R^N operation) and thread over all other input dimensions. =item inv Ref to an inverse method that reverses the transformation. It must accept the same "params" hash that the forward method accepts. This key can be left undefined in cases where there is no inverse. =item idim, odim Number of useful dimensions for indexing on the input and output sides (ie the order of the 0th dimension of the coordinates to be fed in or that come out). If this is set to 0, then as many are allocated as needed. =item name A shorthand name for the transformation (convenient for debugging). You should plan on using UNIVERAL::isa to identify classes of transformation, e.g. all linear transformations should be subclasses of PDL::Transform::Linear. That makes it easier to add smarts to, e.g., the compose() method. =item itype An array containing the name of the quantity that is expected from the input piddle for the transform, for each dimension. This field is advisory, and can be left blank if there's no obvious quantity associated with the transform. This is analogous to the CTYPEn field used in FITS headers. =item oname Same as itype, but reporting what quantity is delivered for each dimension. =item iunit The units expected on input, if a specific unit (e.g. degrees) is expected. This field is advisory, and can be left blank if there's no obvious unit associated with the transform. =item ounit Same as iunit, but reporting what quantity is delivered for each dimension. =item params Hash ref containing relevant parameters or anything else the func needs to work right. =item is_inverse Bit indicating whether the transform has been inverted. That is useful for some stringifications (see the PDL::Transform::Linear stringifier), and may be useful for other things. =back Transforms should be inplace-aware where possible, to prevent excessive memory usage. If you define a new type of transform, consider generating a new stringify method for it. Just define the sub "stringify" in the subclass package. It should call SUPER::stringify to generate the first line (though the PDL::Transform::Composition bends this rule by tweaking the top-level line), then output (indented) additional lines as necessary to fully describe the transformation. =head1 NOTES Transforms have a mechanism for labeling the units and type of each coordinate, but it is just advisory. A routine to identify and, if necessary, modify units by scaling would be a good idea. Currently, it just assumes that the coordinates are correct for (e.g.) FITS scientific-to-pixel transformations. Composition works OK but should probably be done in a more sophisticated way so that, for example, linear transformations are combined at the matrix level instead of just strung together pixel-to-pixel. =head1 FUNCTIONS There are both operators and constructors. The constructors are all exported, all begin with "t_", and all return objects that are subclasses of PDL::Transform. The L, L, L, and L methods are also exported to the C package: they are both Transform methods and PDL methods. =cut ======EOD====== pp_addpm({At=>'Bot'},<<'======EOD======'); =head1 AUTHOR Copyright 2002, 2003 Craig DeForest. There is no warranty. You are allowed to redistribute this software under certain conditions. For details, see the file COPYING in the PDL distribution. If this file is separated from the PDL distribution, the copyright notice should be included in the file. =cut package PDL::Transform; use Carp; use overload '""' => \&_strval; use overload 'x' => \&_compose_op; use overload '**' => \&_pow_op; use overload '!' => \&t_inverse; use PDL; use PDL::MatrixOps; use PDL::NiceSlice; our $PI = 3.1415926535897932384626; our $DEG2RAD = $PI / 180; our $RAD2DEG = 180/$PI; our $E = exp(1); #### little helper kludge parses a list of synonyms sub _opt { my($hash) = shift; my($synonyms) = shift; my($alt) = shift; # default is undef -- ok. local($_); foreach $_(@$synonyms){ return (UNIVERSAL::isa($alt,'PDL')) ? PDL->pdl($hash->{$_}) : $hash->{$_} if defined($hash->{$_}) ; } return $alt; } ###################################################################### # # Stringification hack. _strval just does a method search on stringify # for the object itself. This gets around the fact that stringification # overload is a subroutine call, not a method search. # sub _strval { my($me) = shift; $me->stringify(); } ###################################################################### # # PDL::Transform overall stringifier. Subclassed stringifiers should # call this routine first then append auxiliary information. # sub stringify { my($me) = shift; my($mestr) = (ref $me); $mestr =~ s/PDL::Transform:://; my $out = $mestr . " (" . $me->{name} . "): "; $out .= "fwd ". ((defined ($me->{func})) ? "ok" : "missing")."; "; $out .= "inv ". ((defined ($me->{inv})) ? "ok" : "missing").".\n"; } ======EOD====== pp_add_exported('apply'); pp_addpm(<<'======EOD_apply======'); =head2 apply =for sig Signature: (data(); PDL::Transform t) =for usage $out = $data->apply($t); $out = $t->apply($data); =for ref Apply a transformation to some input coordinates. In the example, C<$t> is a PDL::Transform and C<$data> is a PDL to be interpreted as a collection of N-vectors (with index in the 0th dimension). The output is a similar but transformed PDL. For convenience, this is both a PDL method and a Transform method. =cut use Carp; *PDL::apply = \&apply; sub apply { my($me) = shift; my($from) = shift; if(UNIVERSAL::isa($me,'PDL')){ my($a) = $from; $from = $me; $me = $a; } if(UNIVERSAL::isa($me,'PDL::Transform') && UNIVERSAL::isa($from,'PDL')){ return &{$me->{func}}($from,$me->{params}) if defined($me->{func}); croak "PDL::Transform with no defined func -- this is a bug.\n"; } else { croak "apply requires both a PDL and a PDL::Transform.\n"; } } ======EOD_apply====== pp_add_exported('invert'); pp_addpm(<<'======EOD_invert======'); =head2 invert =for sig Signature: (data(); PDL::Transform t) =for usage $out = $t->invert($data); $out = $data->invert($t); =for ref Apply an inverse transformation to some input coordinates. In the example, C<$t> is a PDL::Transform and C<$data> is a piddle to be interpreted as a collection of N-vectors (with index in the 0th dimension). The output is a similar piddle. For convenience this is both a PDL method and a PDL::Transform method. =cut *PDL::invert = \&invert; sub invert { my($me) = shift; my($data) = shift; if(UNIVERSAL::isa($me,'PDL')){ my($a) = $data; $data = $me; $me = $a; } if(UNIVERSAL::isa($me,'PDL::Transform') && UNIVERSAL::isa($data,'PDL')){ return undef unless defined($me->{inv}); return &{$me->{inv}}($data, $me->{params}); } else { croak("invert requires a PDL and a PDL::Transform (did you want 'inverse' instead?)\n"); } } ======EOD_invert====== pp_addhdr(<<'==EOD_map_auxiliary=='); /* * Singular-value decomposition code is borrowed from * MatrixOps -- cut-and-pasted here because of linker trouble. * It's used by the auxiliary matrix manipulation code, below. * */ void pdl_xform_svd(PDL_Double *W, PDL_Double *Z, int nRow, int nCol) { int i, j, k, EstColRank, RotCount, SweepCount, slimit; PDL_Double eps, e2, tol, vt, p, h2, x0, y0, q, r, c0, s0, c2, d1, d2; eps = 1e-6; slimit = nCol/4; if (slimit < 6.0) slimit = 6; SweepCount = 0; e2 = 10.0*nRow*eps*eps; tol = eps*.1; EstColRank = nCol; for (i=0; i= r) { if (q<=e2*Z[0] || fabs(p)<=tol*q) RotCount--; else { p /= q; r = 1 - r/q; vt = sqrt(4*p*p+r*r); c0 = sqrt(fabs(.5*(1+r/vt))); s0 = p/(vt*c0); for (i=0; i=3 && Z[(EstColRank-1)]<=Z[0]*tol+tol*tol) EstColRank--; } } /* * PDL_xform_aux: * This handles the matrix manipulation part of the Jacobian filtered * mapping code. It's separate from the main code because it's * independent of the data type of the original arrays. * *Given a pre-allocated workspace and * an integer set of coordinates, generate the discrete Jacobian * from the map, pad the singular values, and return the inverse * Jacobian, the largest singular value of the Jacobian itself, and * the determinant of the original Jacobian. Boundary values use the * asymmetric discrete Jacobian; others use the symmetric discrete Jacobian. * * The map and workspace must be of type PDL_D. If the dimensionality is * d, then the workspace must have at least 3*n^2+n elements. The * inverse of the padded Jacobian is returned in the first n^2 elements. * The determinant of the original Jacobian gets stuffed into the n^2 * element of the workspace. The largest padded singular value is returned. * */ PDL_Double PDL_xform_aux ( pdl *map, PDL_Long *coords, PDL_Double *tmp, PDL_Double sv_min) { short ndims; PDL_Long i, j, k; PDL_Long offset; PDL_Double det; PDL_Double *jptr; PDL_Double *svptr; PDL_Double *aptr,*bptr; PDL_Double max_sv = 0.0; ndims = map->ndims-1; /****** Accumulate the Jacobian */ /* Accumulate the offset into the map array */ for( i=offset=0; idimincs[i+1]; jptr = tmp + ndims*ndims; for( i=0; i= map->dims[i+1]-1); char symmetric = !(bot || top); PDL_Double *ohi,*olo; PDL_Long diff = map->dimincs[i+1]; ohi = ((PDL_Double *)map->data) + ( offset + ( top ? 0 : diff )); olo = ((PDL_Double *)map->data) + ( offset - ( bot ? 0 : diff )); for( j=0; jdimincs[0]; olo += map->dimincs[0]; if(symmetric) jel /= 2; *(jptr++) = jel; } } /****** Singular-value decompose the Jacobian * The svd routine produces the squares of the singular values, * and requires normalization for one of the rotation matrices. */ jptr = tmp + ndims*ndims; svptr = tmp + 3*ndims*ndims; pdl_xform_svd(jptr,svptr,ndims,ndims); aptr = svptr; for (i=0;i max_sv ) max_sv = *aptr; aptr++; } /****** Generate the inverse matrix */ /* Multiply B-transpose times 1/S times A-transpose. */ /* since S is diagonal we just divide by the appropriate element. */ /* */ aptr = tmp + ndims*ndims; bptr = aptr + ndims*ndims; jptr= tmp; for(i=0;i'k0()', # Dummy to set type (should match the type of "in"). OtherPars=>'SV *in; SV *out; SV *map; SV *boundary; SV *method; SV *big; SV *blur; SV *sv_min; SV *flux', Code=><<'==EOD_map_c_code==', /* * Pixel interpolation & averaging code * * Calls a common coordinate-transformation block (see following hdr) * that isn't dependent on the type of the input variable. * * The inputs are SVs to avoid hassling with threadloops; threading * is handled internally. To simplify the threading business, any * thread dimensions should all be collapsed to a single one by the * perl front-end. * */ short ndims; /* Number of dimensions we're working in */ PDL_Double *tmp; /* Workspace for prefrobnication */ PDL_Long *ovec; /* output pixel loop vector */ PDL_Long *ivec; /* input pixel loop vector */ PDL_Long *ibvec; /* input pixel base offset vector */ PDL_Double *dvec; /* Residual vector for linearization */ PDL_Double *tvec; /* Temporary floating-point vector */ PDL_Double *acc; /* Threaded accumulator */ char *bounds; /* Boundary condition packed string */ char method; /* Method identifier (gets one of 'h','g') */ PDL_Long big; /* Max size of input footprint for each pix */ PDL_Double blur; /* Scaling of filter */ PDL_Double sv_min; /* minimum singular value */ char flux; /* Flag to indicate flux conservation */ PDL_Double *map_ptr; PDL_Long i, j; $GENERIC() badval = sqrt(-1); pdl *in = PDL->SvPDLV($COMP(in)); pdl *out = PDL->SvPDLV($COMP(out)); pdl *map = PDL->SvPDLV($COMP(map)); PDL->make_physical(in); PDL->make_physical(out); PDL->make_physical(map); ndims = map->ndims -1; /***************************************************************************/ /** Check that the dimensionalities are consistent. This is **/ /** allegedly done already in the perl part, but shouldn't cost more **/ /** than a few hundred clock cycles -- cheap at twice the price. **/ /** **/ /**/ if(map->ndims != out->ndims) /**/ /**/ barf("Bug in map: index dims aren't consistent\n"); /**/ /**/ for(i=0;indims-1;i++) { /**/ /**/ static char buf[100]; /**/ /**/ if(map->dims[i+1] != out->dims[i]) { /**/ /**/ sprintf(buf,"Bug in map: dim %d is %d; expected %d\n", /**/ /**/ i,map->dims[i+1],out->dims[i]); /**/ /**/ barf(buf); /**/ /**/ } /**/ /**/ } /**/ /**/ if(map->dims[0] != in->ndims - 1) /**/ /**/ barf("Bug in map - index vec - input mismatch\n"); /**/ /**/ if(out->dims[out->ndims-1] != in->dims[in->ndims-1]) /**/ /**/ barf("Bug in map: thread dimension error\n"); /**/ /***************************************************************************/ /* * Allocate all our dynamic workspaces at once... /* */ ovec = (PDL_Long *)(PDL->smalloc( sizeof(PDL_Long) * 3 * ndims + sizeof(PDL_Double) * (3*ndims*ndims + 3*ndims) + sizeof(PDL_Double) * in->dims[ndims] + sizeof(char) * ndims ) ); ivec = &(ovec[ndims]); ibvec = &(ivec[ndims]); dvec = (PDL_Double *)(&(ibvec[ndims])); tvec = &(dvec[ndims]); acc = &(tvec[ndims]); tmp = &(acc[in->dims[ndims]]); bounds = (char *)(&(tmp [3*ndims*ndims+ndims])); /*** * Fill in the boundary condition array * (cut-and-pasted from the range() code...) */ { char *bstr; STRLEN blen; bstr = SvPV($COMP(boundary),blen); if(blen == 0) { /* If no boundary is specified then every dim gets truncated */ int i; for (i=0;idata); /* Main pixel loop */ do { PDL_Long psize; PDL_Double wgt; /* Prefrobnicate the transformation matrix */ psize = (PDL_Long)(blur * PDL_xform_aux(map, ovec, tmp, sv_min) + 0.5 )+1; if(psize <= big) { /* * Use the prefrobnicated matrix to generate a local linearization * dvec gets the delta; ibvec gets the base. */ { PDL_Double *mp = map_ptr; for (i=0;idimincs[0]; } } /* * Initialize input delta vector */ for(i=0;idims[ndims]; i++) *(ac++) = 0.0; } wgt = 1.0e-9; /* negligible but nonzero */ do { /* Input accumulation loop */ int bound_ok = 1; PDL_Long i_off = 0; /* Find the offset for the current input point, applying boundaries */ for(i=0; i= in->dims[i]) { switch(bounds[i]) { case 0: /* no breakage allowed */ barf("index out-of-bounds in map"); break; case 1: /* truncation */ bound_ok = 0; break; case 2: /* extension -- crop */ if(j<0) j = 0; else if(j>= in->dims[i]) j = in->dims[i] - 1; break; case 3: /* periodic -- mod it */ j %= in->dims[i]; if(j<0) j += in->dims[i]; break; case 4: /* mirror -- reflect off the edges */ j += in->dims[i]; j %= (in->dims[i] * 2); if(j < 0) j += in->dims[i] * 2; j -= in->dims[i]; if(j < 0) { j *= -1; j -= 1; } break; default: barf("Unknown boundary condition in map -- bug alert!"); break; } } i_off += in->dimincs[i] * j; } if(bound_ok) { PDL_Double alpha = 1.0; PDL_Double *ap = tvec; PDL_Double *bp = dvec; PDL_Double *cp; PDL_Long *ip = ivec; for(i=0; i 1 ) { alpha = 0; i = ndims; } else alpha *= (0.5 + 0.5 * cos( dd * 3.1415926536 )); } break; case 'g': {PDL_Double sum = 0; for(i=0; i 4) /* 2 pixels -- four half-widths */ i = ndims; } if(sum > 4) alpha = 0; else alpha = exp(-sum * 1.386294); /* Gaussian, rt(2)-pix HWHM */ } break; default: { char buf[80]; sprintf(buf,"This can't happen: method='%c'",method); barf(buf); } } /* We've got the weighting, now accumulate the values as needed... */ wgt += alpha; { PDL_Double *ac = acc; $GENERIC() *dat = (($GENERIC() *)(in->data)) + i_off; for( i=0; i < out->dims[ndims]; i++ ) { *(ac++) += *dat * alpha; dat += in->dimincs[ndims]; } } } /* end of bound_ok check */ /* Advance input accumulation loop */ for(i=0; (i < ndims) && (++(ivec[i]) > psize); i++) { ivec[i] = -psize; } } while(idata; for(i=0;idimincs[i] * ovec[i]; if(flux) { PDL_Double det = tmp[ndims*ndims]; for(i=0; i < out->dims[ndims]; i++) { *dat = *(ac++) / wgt * det; dat += out->dimincs[ndims]; } } else { for(i=0; i < out->dims[ndims]; i++) { *dat = *(ac++) / wgt; dat += out->dimincs[ndims]; } } } /* End of code for normal pixels */ } else { /* The pixel was ludicrously huge -- just set this pixel to nan */ $GENERIC() *dat = out->data; for(i=0;idimincs[i] * ovec[i]; for(i=0;idims[ndims];i++) { *dat = badval; /* Should handle bad values too -- not yet */ dat += out->dimincs[ndims]; } } /* Increment the pixel counter */ { PDL_Long *aptr = ovec; for(i=0; (idimincs[i+1]) && /* Funky pre-test increment */ (++(ovec[i]) >= out->dims[i]); /* Actual carry test */ i++) { ovec[i] = 0; map_ptr -= out->dims[i] * map->dimincs[i+1]; } } } while(i<<'==EOD_map_doc==', =head2 PDL::match =for usage $b = $a->match($c); =for ref Resample a scientific image to the same coordinate system as another. The example above is syntactic sugar for $b = $a->map(t_identity, $c, ...); it resamples the input PDL with the identity transformation in scientific coordinates, and matches the pixel coordinate system to $c's FITS header. =head2 map =for usage $b = $a->map($xform,[