Image manipulation with Prima

Introduction

There are many imaging libraries, and many of them can be accessed from perl. This article is a small excursion in programming in one of those, Prima. Prima is, strictly speaking, not an imaging library, but a GUI library, however over time it has developed so many imaging functions, so it had to be forked in two. A new library IPA (stands for image processing algorithms) is based on basic Prima imaging facilities, and provides more complex and exotic image processing functions. Prima imaging is generally responsible for image loading, saving, simple conversion, and display.

Task

In this article it is shown how to create the gif animation below, using Prima and IPA libraries. Here, I took a picture of myself and converted in into a "hypnotic" animation:

Loading the image

First, the loading. This is the easy part:

use strict;
use Prima;
die "need image\n" unless @ARGV;

my $source = Prima::Image-> load($ARGV[0]);
die "Can't load $ARGV[0]:$@\n" unless $source;
As you can see, $source will hold a Prima::Image object if the image can be loaded. Prima::Image provides many functions: in addition to the above demonstrated loading, it can save images, convert the bit depth, resize, and even draw on images using whatever graphic subsystem the platform provides. Namely, on Unix it will use X11, on Windows - GDI, etc ( see Prima::Image documentation for more). For this task, we shall use all of these functions, and more.

Concentric circles

First, let's create a black-and-white bit mask that will be used further to prepare the final animation. The bit mask should look like concentric circles; it is prepared with the code below. First, the image. For the simplicity sake, the mask will be twice as wide and twice as high as the source image:

my $mask = Prima::Image-> new(
	width  => $source-> width  * 2,
	height => $source-> height * 2,
	type   => im::BW,
);
It's type will be im::BW, which is a shortcut for im::bpp1 | im::GrayScale, a 1-bit grayscale image. Prima supports 1, 4, 8, and 24 bit color images, and 1, 8, 16, and 32 bit grayscale images; the latter are needed mostly for image processing. Prima can also deal with float and double images, in single-band and dual-band forms (the latter can be needed, for example, for images composed from real and imaginary parts. Such images can be created by Fourier transform). Well anyway, for this task we only need a simple binary mask. Let's draw the circles: Calculate the center and number or circles:

my $step   = 2;
my @size   = $mask-> size;
my $max    = $size[$size[0] < $size[1]] / 2;
my @center = map { int($_ + .5) } ($size[0] / 2, $size[1] / 2);
Begin the drawing session, clear the image, and set the line width:

$mask-> begin_paint;
$mask-> clear;
$mask-> lineWidth( $step);
Draw the circles:

for ( my $i = 0; $i < $max; $i += $step * 2) {
   my $d = $i * 2;
   $mask-> ellipse( @center, $d, $d);
}
And finally, close the drawing session:

$mask-> end_paint;
Since drawing calls such as ellipse operate in the GUI system space (in GDI memory, or in X11 bitmaps), Prima must synchronize pixel content between its own and GUI system memory. Session management with begin_paint and end_paint copies image pixels first to GUI memory, and then, after the drawing was done, copies them back.

Here's the result:

Manipulating channels

Now we need to apply the bit mask onto the picture. The fun part begins when the mask will be applied so that its center will match with the location of an eye there. Then the mask shall be applied separately to read, green, and blue channels, with different offsets. But first, we need to split the image into channels. IPA::Misc contains two functions we need: split_channels and combine_channels:

use IPA qw(Misc);

$source-> type(im::bpp24);
my @channels = @{ IPA::Misc::split_channels( $source, 'rgb') };
Apply the mask:

$channels[0]-> put_image(
    243, 159, 
    $mask, rop::AndPut
);

Prima can do direct blitting operation without resorting to GUI functions. It is both faster and lossless, and thus is preferred when possible. Note that the very same call, put_image, if called from within begin_paint / end_paint brackets, gets executed using GUI functions.

Finally, the three channels with the mask applied with different offsets, can be combined back to an RGB image:


my $output = combine_channels( \@channels, 'rgb');
and convert to 256 (or less) colors, because GIF can only contain 8-bit images:

$output-> type(im::bpp8);

Animation

Now, the concentric circles can be combined with different offset. If the line width used to draw a single circle, is 2 pixels wide, then we need 4 different bit masks, drawn with different circle offset. Then all these 4 masks have to be applied to newly created RGB channels, and we would have four $output images:

push @img, $output; 
Now we use Prima's ability to save multiframe GIFs, with specified loop count (0 is indefinite) and frame delay:

my $saved = Prima::Image-> save(
   "anim.gif",
   images    => \@img,
   loopCount => 0,
   delayTime => 5,
);
die "Cannot save anim.gif:$@\n" unless $saved == @img;
save returns number of frames stored successfully.

Here is the final result:

Source code

Download

use strict;
use Prima 1.27;
use IPA qw(Misc);
die "need image\n" unless @ARGV;

my $source = Prima::Image-> load($ARGV[0]);
die "Can't load $ARGV[0]:$@\n" unless $source;
$source-> type(im::bpp24);
	
my $mask = Prima::Image-> new(
	width  => $source-> width  * 2,
	height => $source-> height * 2,
	type   => im::BW,
);
my @size = $mask-> size;
my $max = $size[$size[0] < $size[1]] / 2;

my $step = 2;
my @centers  = (
	[ 243, 159 ],
	[ 208, 150 ],
	[ 233, 111 ],
);

my @img;
for ( my $offset = 0; $offset < $step * 2; $offset++) {
	$mask-> begin_paint;
	$mask-> clear;
	$mask-> lineWidth( $step);
	my @center = map { int($_ + .5) } ($size[0] / 2, $size[1] / 2);
	for ( my $i = 0; $i < $max; $i += $step * 2) {
		my $d = $offset * 2 + $i * 2;
		$mask-> ellipse( @center, $d, $d);
	}
	$mask-> end_paint;
	
	my @channels = @{ IPA::Misc::split_channels( $source, 'rgb') };
	for ( my $i = 0; $i < @channels; $i++) {
		$channels[$i]-> put_image(
			$centers[$i]->[0] - $center[0],
			$centers[$i]->[1] - $center[1],
			$mask, rop::AndPut
		);
	}
	my $output = combine_channels( \@channels, 'rgb');
	$output-> type(im::bpp8);
	push @img, $output; 

}

my $saved = Prima::Image-> save(
	"output.gif",
	images    => \@img,
	loopCount => 0,
	delayTime => 5,
);
die "Cannot save output.gif:$@\n" unless $saved == @img;

Requirements

Dmitry Karasik 2008