Here are some demos of things that I've been doing lately with PDL.
(22-Apr-2005) A couple of weeks ago I got interested in demoing the image warping package, and quickly ginned up a nice movie showing a family of coordinate transformations. The resampling code is now much faster than it was back when the galaxy demo (bottom of this page) was written. Here is an image of the shuttle Atlantis docked with Mir (before Mir re-entered), together with a family of distorted images. The distorted images are being rotated in conformal azimuthal coordinates (CAC) in CA coordinates, the horizontal direction corresponds to azimuthal angle, and the vertical direction corresponds to the logarithm of distance. Hence the rotation couples azimuth to scale, and distance from the origin couples back to azimuthal angle. Features inside of a critical radius rotate counterclockwise; features outside of that radius rotate clockwise. Likewise, stuff that is left of center is constantly shrinking, while stuff that is right of center is constantly growing. There is a scale singularity along a vertical line from the center of the images. Because the transformation is conformal, all of the image features are recognizable.
I believe that this family of distortions is what M.C. Escher used in his famous "Gallery" print of a gallery that contains a picture that, in turn, contains the town that contains the gallery. Putting a scale ambiguity in the image, across the singular line, would give you print with the same basic theme.
(click the images to see a large version of each)
![]() |
![]() | ![]() (maximal scale weirdness) | ![]() (inside-out) |
I also generated a movie in 1 degree
increments of rotation; it's sort of trippy to watch. The movie was
created with the set of PDL commands over there --->. Very
straightforward to do with the The transforms themselves are a little complicated as they are working
directly in the pixel coordinate system (without autoscaling). The |
use PDL::Transform;
|
This isn't PDL-specific, just a nice technique that I've recently implemented in PDL. A lot of solar physics data is in the form of image sequences, some of which contain glitches from cosmic rays or radiation that impacted the detector during the observation. Usually spatial filters are used to remove the glitches before analysis. That technique fails when the image itself is too spiky: the filter has difficulty distinguishing solar data from cosmic ray noise.
Zspike is an algorithm that uses the slower temporal evolution of most features to eliminate spikes. It ignores completely the spatial information in the image sequence, treating each pixel as an independent time series of values. Spikes are identified by a strong second derivative component.
Here's a demonstration of a single frame (of a longer image sequence) being despiked by zspike. Note that the spikes blend in with the image data. Left: original data; center: bad-pixel mask from ZSPIKE; right: corrected image. To appreciate the algorithm better, you should view this MPEG movie (32 MB, sorry).

PDL::Transform, I really wanted just three
things:
ZMATCH function I wrote in IDL so
long ago);
The first two things are accomplished by PDL::Transform::t_fits
and PDL::Transform::t_lookup, but mapping requires some care.
Of course, image-to-map conversion is reminiscent of cartography in general,
so over Christmas break 2002 I wrote PDL::Transform::Cartography
as a sort of overkill for the third problem. The Cartography projections
all convert (lon,lat) coordinates on a spherical surface to some sort of
projected coordinate, and most of the standard map projections are there.
There's a vector coastline map of the world included, too. The
method that returns it is
PDL::Transform::Cartography::earth_coast.
Here are some sample command sequences and what they produce.
Many other transformations are present -- say "??Cartography"
in perldl to find out which ones exist. They all can handle both vector
data (as shown here) and image data (via the ->map method;
see below).
The PDL::Transform module (added to CVS in Nov 2002) implements
arbitrary coordinate transforms. A large number of simple transformations,
such as linear scaling, offsets, and FITS pixel-to-scientific conversions,
are predefined. You can string together a bunch of transformations using a
compose() operator.
The main purpose of PDL::Transform is to remap images. The
impetus to develop it was for some image regularization and distortion work
that was needed for my stereoscopic magnetograph, but (in keeping with the perl ideal of
ambitious laziness), I implemented a fully general module that can do
general coordinate transformations in N dimensions. It was at least partially
inspired by Dominic Zarro's plot_map image-morphing package for IDL.
The centerpiece of PDL::Transform is the
arbitrary-grid resampling code in the map method. The
resampling code can do rapid sampling, interpolation, or fully
anti-aliased variable-grid resampling of input data. The
anti-aliasing is especially important for transformations that shrink
part of the image.
For example, some years ago I used a custom piece of code to do conformal 2-D radial expansion on SOHO/LASCO C-3 images, and image very faint structures some 29 solar radii away from the Sun (DeForest, Plunkett, & Andrews 2001). That work was possible because we were able to average together huge numbers of pixels where the radial transformation shrunk the image (far from the Sun).
PDL::Transform::map uses tracking of the Jacobian of the transformation to average over as much of the input data as is appropriate, at each part of the output image: it implements true variable-sized spatial filtering,
based only on the measured Jacobian of the arbitrary transformation you feed in.
Here is a demonstration using the M51 galaxy and a radial coordinate transform similar to the one we used in 2001.
| Orginal image This is a 256x256 FITS image of the M51 galaxy that is distributed with PDL itself. The images below were produced with the following snippet of PDL code, using periodic boundary conditions on the original image (to demonstrate the effects of reduction as well as enlargement).
use PDL::Transform;
$a = rfits('m51.fits');
$t = t_compose(t_scale(256/3.14159/2),t_radial(o=>[128,128],r0=>3));
$w = pgwin(device=>'m51.gif/gif',size=>[4,4],HardLW=>1);
@opt = (XTitle=>'angle',YTitle=>'log(radius)');
$w->imag( $t->map($a,{m=>'sample',b=>'p'}) , {title=>'Sampled',@opt});
$w->imag( $t->map($a,{m=>'linear',b=>'p'}) , {title=>'Interpolated',@opt});
$w->imag( $t->map($a,{m=>'jacob', b=>'p'}) , {title=>'Anti-aliased',@opt});
$w->close;
|
|
![]() | ![]() |
|
| Sampling is the fastest method -- this distortion was accomplished in less than 300ms by a 1GHz Athlon workstation. The original pixels are visible in the lower part of the image, and aliasing is present in the upper part. | Interpolation gets rid of the original pixel images in the lower part of the image, but retains the aliasing in the upper part. Most of the original pixels are ignored because they fall between the centers of the inverse-transformed pixels of the output plane. This distortion required ~600ms. | Anti-aliasing uses variable-sized spatial filtering to eliminate artifacts from the transformed image. It is slower than the other two methods, because each input pixel is transformed several times and the Jacobian is inverted at each output pixel. This distortion required about 10 seconds. |
Movie: this MPEG (5.4 MB) shows the effects of aliasing a little more dramatically. It results from looping the above code over position: the origin moves vertically across the image and through the center of the galaxy, changing the pixel map enough to generate twinkling as the aliasing function changes.
range is a rank-reducing index operator, a generalization
of index and indexND. Like those operators, it has
two-way dataflow: you can get values out of an array by using
the result of range in an expression, or you can put them
back in to the original array via elementwise assignment to the result.
You start with a source array S (with N dimensions of order S0...SN-1) and an index array I with M dimensions of order I0...IM-1. If I0 == N, then you can make the expression:
$S->range($I)and it will have dimensionality (I1..IM-1).
range treats $I as a collection of column-vectors that index elements of $S.
So far, range sounds just like the indexND
operator. But unlike indexND, range is
fully threadable. If I0 < N, then
whole columns of $S are extracted, so
$S->range($I) has dimensionality (I1..IM-1,SM..SN-1).
Furthermore, range's name comes from the fact that it can
extract N-D rectangles out of $S. This aspect is handy for
(e.g.) vectorizing operations on disparate regions of a single data set.
You feed in an additional size vector, like so:
$S->range($I,$D);
$D must be a scalar or an M-vector; scalar values are
treated as if they are repeated over all (M-1) non-collapsed
dimensions of the index. The output is then a list of M-cubes
extracted from $S. In that case the output has dimensionalty (
I0...IM-1,D0...DM-1,SM...SN-1).
Those middle dimensions range over elements within the little pieces that
you extracted. The beginning dimensions range over the different pieces
specified by $I, and the ending dimensions range over the
dimensions of $S that you didn't index.
Notice that you're free to group the dimensions however you want --
you can treat the output as a doubly-threaded collection of
M-1-dimensional cubes, or as a single-threaded collection of
N-dimensional cubes, because the extra $S dimensions are
tacked on the end of the indexed dims.
Sometimes you want to index single hyperplanes out of a single
dimension of a piddle, but get ranges in others. You can do that with
range by setting elements of $D to 0. That
normally wouldn't mean anything useful, because the number of elements
in the output is the product of all the orders of all of its
dimensions. So if you set a particular order to 0, it's instead
treated as a "silent" dimension of order 1 -- you get a single sample
at the corresponding index coordinate, but the dimension is omitted
from the final dimension list.
Silent dimensions may seem strange -- but the limiting case where all the range dimensions are silent is just the indexing case at the top of this little screed. So all that they mean is that you can mix-and-match which dimensions you use to index and which you use to get ranges.
Indexing is a pain to get right in the general case because you
have to keep track of the boundaries. Affine transformations like
slice are very fast but don't let you run off the end of
the source array. range takes a performance hit anyway
by not being affine, so it allows you to specify what to do if you index
off the end of a boundary. You access boundary conditions like so:
$S->range($I,$D,$B)where $B is a numeric or string code for whatever boundary condition you want to use. (See the documentation for all the abbreviations). Allowed conditions are:
$S, so
that $S is effectively duplicated to fill all of ZN
range is very powerful for general N-D operations, and is
extensively used in PDL::Transform::map (see above).
Several trivial examples of its use exist in the PDL documentation;
I'll write some here soon.