Following the completion of anim_grid_transits.pl, my imagination went into overdrive, envisioning all sorts of possible variants. In this article, I'll present one in which an image is sliced and diced into tiles. These are then placed at random locations on the canvas and subsequently translated back to their original locations, thus reconstituting the source image.
The required algorithm is an amalgam of the slice-and-dice portion of the script img2puzzle.pl and the translation algorithm found in anim_grid_transits.pl. In case you're not familiar with the latter script, I'll repeat two of its salient points.
To make the tiles transit from one point to another is not technically demanding. All the required intermediate coordinates can be linearly interpolated using the parametric representation for a line segment. The parametric form has the advantage of not necessitating special attention to the case of a vertical line segment as would be the case with the more familiar Cartesian representation y = mx + b. Thus, for any point P(x,y) on the line segment between P(x0,y0) and P(x1,y1), we have
Two caveats to keep in mind here. Points on a line are real numbers and thus dense, whereas screen pixels are integers and thus discreet. So using the parametric representation for interpolating pixel coordinates will always require taking the integer part of the computational results. Consequently, slight discrepancies might occur between the actual and theoretical positioning of the image. Furthermore, roundoff often comes into play when incrementing λ by a given step size. For example, a step size of 0.1 does not have a finite binary representation. So, after 10 steps, λ will be slightly less than 1 and the tile being transited might be off by one or two pixels from its theoretical final location. Hence, should these types of errors have a detrimental effect on the quality of the animation, certain counter-measures will be required.
As is the case with
anim_grid_transits.pl,
an XML file conveys the animation specifications.
To explain the required XML tags, let's examine the data file leonardo_topandbottom.xml
( [Download leonardo_topandbottom.xml]
[MD5 checksum] ).
It governs the creation of the following animated GIF image:
| XML file "leonardo_topandbottom.xml" | Remarks | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| <animation> | Animation starting at the top and bottom of the canvas | ||||||||||||||||||||
| <output>leonardo_topandbottom.gif</output> | name of the output file | ||||||||||||||||||||
| <frames>40</frames> | the number of intermediate, interpolated frames for each transit | ||||||||||||||||||||
| <delay>15</delay> | the number of milliseconds in delaying the image views | ||||||||||||||||||||
| <loops>0</loops> | the number of times to cycle the animated GIF: 0 results in infinite looping | ||||||||||||||||||||
| <image> | start of source image specifications | ||||||||||||||||||||
| <file>Leonardo.jpg</file> | name of the source file | ||||||||||||||||||||
| <scale>0.35</scale> | scaling factor to be applied to the source. A value of "1" implies no scaling. It is applied to the source image prior to the final dimension adjustments for an integral number of tile rows and columns. | ||||||||||||||||||||
| </image> | end of source image specifications | ||||||||||||||||||||
| <tile> | start of tile specifications | ||||||||||||||||||||
| <width>10</width> | in pixels | ||||||||||||||||||||
| <height>10</height> | in pixels | ||||||||||||||||||||
| </tile> | end of tile specifications | ||||||||||||||||||||
| <startLocation>topAndBottom</startLocation> | starting locations of the tiles. Examples for all the recognized options are:
|
||||||||||||||||||||
| <inverse>0</inverse> | boolean option (0 or 1) to inverse the animation, that is, disassemble the source image
|
||||||||||||||||||||
| </animation> | end of XML declaration |
For the processing phase, Perl is used along with ImageMagick's drawing primitives accessed through its PerlMagick interface. The Perl script anim_reassemble.pl, displayed below, is the resulting code. It is released for personal, non-commercial and non-profit use only.
The listing includes the line numbers in order to reference them in the following general remarks.
perl anim_reassemble.pl Leonardo_topAndBottom.xml
Here we are requesting that the XML file "Leonardo_topAndBottom.xml" be processed. A screen shot at the end of processing is:
compared to
If you have any questions regarding the code or my explanations, please do not hesitate in contacting me.
001 use strict;
002 use warnings;
003 use POSIX qw(ceil);
004 use Switch;
005 use Image::Magick;
006 use Term::ANSIScreen qw/:color :cursor :screen/;
007 use Win32::Console::ANSI qw/ Cursor /;
008 use XML::Simple;
009 $XML::Simple::PREFERRED_PARSER = 'XML::Parser';
010 use constant FATAL => colored ['white on red'], "\aFATAL ERROR: ";
011 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
012 # anim_reassemble.pl: Creates an animated GIF that re-assembles an image.
013 #===============================================================================================================================
014 # Usage : perl anim_reassemble.pl XmlFile
015 # Arguments : XmlFile = path of XML file for the animation specifications.
016 # Input Files : See arguments.
017 # Output Files : The animated GIF specified in the XML data file.
018 # Temporary Files : None.
019 # Remarks : See http://www.webpraxis.ab.ca/transits/anim_reassemble.shtml for details.
020 # History : v1.0.0 - September 17, 2009 - Original release.
021 #===============================================================================================================================
022 # 0) INITIALIZE:
023 $| = 1; #set STDOUT buffer to auto-flush
024 cls(); #clear screen
025 print colored ['black on white'], "$0\n\n\n", #display program name
026 colored ['reset'], 'Initializing... '; #report start of initialization
027 srand( time() ^ ($$ + ($$ << 15)) ); #seed the random number generator
028
029 my $XmlFile = shift || die FATAL, 'No XML file specified'; #get path of XML data file
030 my $Anim = XMLin( $XmlFile ); #read the XML data file
031
032 my $ImageFile = $$Anim{image}{file}; #parameterize path of source image file
033 my $AnimFile = $$Anim{output}; #parameterize path of target image file
034 my $TileWidth = $$Anim{tile}{width}; #parameterize pixel width of a tile
035 my $TileHeight = $$Anim{tile}{height}; #parameterize pixel height of a tile
036 my @Spinners = ( '-', '\\', '|', '/' ); #define symbols for spinner
037 die FATAL, "Cannot locate image file\n" unless -e $ImageFile;
038 print colored ['bold green'], "XML data read\n\n"; #report end of initialization
039 #-------------------------------------------------------------------------------------------------------------------------------
040 # 1) ADJUST IMAGE DIMENSIONS FOR AN INTEGRAL NUMBER OF TILES:
041 my $Image = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the image
042 $Image->Read( "$ImageFile" ); #init with source image file
043 my ( $ImageWidth, #get width & height of source image
044 $ImageHeight ) = $Image->Get( 'width', 'height' );
045
046 my $NoTileCols = POSIX::ceil( $$Anim{image}{scale} * $ImageWidth / $TileWidth ); #compute integral number of tile columns
047 my $NoTileRows = POSIX::ceil( $$Anim{image}{scale} * $ImageHeight / $TileHeight ); #compute integral number of tile rows
048 my $TileCount = $NoTileCols * $NoTileRows; #compute total number of tiles
049
050 my $CanvasWidth = $NoTileCols * $TileWidth; #set canvas width
051 my $CanvasHeight = $NoTileRows * $TileHeight; #set canvas height
052
053 $Image->Scale( geometry=>"${CanvasWidth}x${CanvasHeight}!" ); #scale image
054
055 print "\r", clline, #echo initialization results
056 colored ['reset'], 'Source Image: ', colored ['bold white'], $ImageFile, "\n",
057 colored ['reset'], ' Width: ', colored ['bold white'], $ImageWidth, " px\n",
058 colored ['reset'], ' Height: ', colored ['bold white'], $ImageHeight, " px\n",
059 colored ['reset'], 'Target Image: ', colored ['bold white'], $AnimFile, "\n",
060 colored ['reset'], ' Width: ', colored ['bold white'], $CanvasWidth, " px\n",
061 colored ['reset'], ' Height: ', colored ['bold white'], $CanvasHeight, " px\n",
062 colored ['reset'], 'Tile Details: ', colored ['bold white'], $TileCount, " tiles\n",
063 colored ['reset'], ' No. of rows: ', colored ['bold white'], $NoTileRows, "\n",
064 colored ['reset'], ' No. of cols: ', colored ['bold white'], $NoTileCols, "\n",
065 colored ['reset'], ' Tile width: ', colored ['bold white'], $TileWidth, " px\n",
066 colored ['reset'], ' Tile height: ', colored ['bold white'], $TileHeight, " px\n\n",
067 colored ['reset'], '-' x 80, "\n\n";
068 #-------------------------------------------------------------------------------------------------------------------------------
069 # 2) CREATE TILE IMAGES AND RECORD THEIR PROVENANCE:
070 print 'Creating tile images... '; #report start of tile processing
071 my ( $CursorX, $CursorY ) = Cursor(); #record cursor position
072
073 my $Tiles = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the tiles
074 my @Tile_X_end; #array for tile top-left end x-coordinates
075 my @Tile_Y_end; #array for tile top-left end y-coordinates
076 { #start naked block as firewall
077 my $tile = Image::Magick->new( magick => 'GIF' ); # instantiate an image object for a tile
078
079 for ( my $y_topLeft = 0; # for each tile row: start at top and work down
080 $y_topLeft <= $CanvasHeight - $TileHeight;
081 $y_topLeft += $TileHeight
082 ) {
083 for ( my $x_topLeft = 0; # for each tile column: work left to right
084 $x_topLeft <= $CanvasWidth - $TileWidth;
085 $x_topLeft += $TileWidth
086 ) {
087 my $geometry = "${TileWidth}x${TileHeight}+$x_topLeft+$y_topLeft"; # define tile geometry
088 $tile = $Image->Clone(); # init tile with image
089 $tile->Crop( geometry => $geometry ); # crop tile area
090 $tile->Set( page => '0x0+0+0' ); # shrink canvas
091 push @$Tiles, @$tile; # store the tile image
092 push @Tile_X_end, $x_topLeft; # store the tile end coords
093 push @Tile_Y_end, $y_topLeft;
094 print locate( $CursorY, $CursorX ), clline, # report tile processing
095 colored ['bold yellow'], '(', $Spinners[$#$Tiles % 4], ')';
096 } # until all tile columns processed
097 } # until all tile rows processed
098 undef $tile; # destroy the tile image object
099 } #end naked block
100 print locate( $CursorY, $CursorX ), clline, #report end of tile processing
101 colored ['bold green'], scalar @Tile_X_end, " tiles\n\n";
102 #-------------------------------------------------------------------------------------------------------------------------------
103 # 3) ASSIGN STARTING COORDINATES FOR THE TILES:
104 my @Tile_X_start; #array for top-left start x-coordinates
105 my @Tile_Y_start; #array for top-left start y-coordinates
106
107 print 'Setting start coordinates... '; #report start of coordinate processing
108 { #start naked block
109 my $maxX = $CanvasWidth - $TileWidth; # largest top-left x-coordinate
110 my $maxY = $CanvasHeight - $TileHeight; # largest top-left y-coordinate
111 my $supX = $maxX + 1; # supremum of the random x-coordinates
112 my $supY = $maxY + 1; # supremum of the random y-coordinates
113
114 switch ( $$Anim{startLocation} ) {
115 case 'top' { push @Tile_X_start, int( rand $supX ) for ( 1..$TileCount );
116 @Tile_Y_start = ( 0 ) x $TileCount;
117 }
118 case 'bottom' { push @Tile_X_start, int( rand $supX ) for ( 1..$TileCount );
119 @Tile_Y_start = ( $maxY ) x $TileCount;
120 }
121 case 'left' { @Tile_X_start = ( 0 ) x scalar @Tile_X_end;
122 push @Tile_Y_start, int( rand $supY ) for ( 1..$TileCount );
123 }
124 case 'right' { @Tile_X_start = ( $maxX ) x $TileCount;
125 push @Tile_Y_start, int( rand $supY ) for ( 1..$TileCount );
126 }
127 case 'topAndBottom' { for ( 1..$TileCount ) {
128 push @Tile_X_start, int rand $supX;
129 push @Tile_Y_start, ( int rand 2 ) ? 0 : $maxY;
130 }
131 }
132 case 'leftAndRight' { for ( 1..$TileCount ) {
133 push @Tile_X_start, ( int rand 2 ) ? 0 : $maxX;
134 push @Tile_Y_start, int rand $supY;
135 }
136 }
137 case 'allSides' { for ( 1..$TileCount ) {
138 switch ( int rand 4 ) {
139 case 0 { push @Tile_X_start, 0;
140 push @Tile_Y_start, int rand $supY;
141 }
142 case 1 { push @Tile_X_start, $maxX;
143 push @Tile_Y_start, int rand $supY;
144 }
145 case 2 { push @Tile_X_start, int rand $supX;
146 push @Tile_Y_start, 0;
147 }
148 case 3 { push @Tile_X_start, int rand $supX;
149 push @Tile_Y_start, $maxY;
150 }
151 }
152 }
153 }
154 case 'topLeft' { @Tile_X_start = ( 0 ) x $TileCount;
155 @Tile_Y_start = ( 0 ) x $TileCount;
156 }
157 case 'bottomLeft' { @Tile_X_start = ( 0 ) x $TileCount;
158 @Tile_Y_start = ( $maxY ) x $TileCount;
159 }
160 case 'topRight' { @Tile_X_start = ( $maxX ) x $TileCount;
161 @Tile_Y_start = ( 0 ) x $TileCount;
162 }
163 case 'bottomRight' { @Tile_X_start = ( $maxX ) x $TileCount;
164 @Tile_Y_start = ( $maxY ) x $TileCount;
165 }
166 case 'center' { @Tile_X_start = ( int $maxX / 2 ) x $TileCount;
167 @Tile_Y_start = ( int $maxY / 2 ) x $TileCount;
168 }
169 case 'random' { for( 1..$TileCount ) {
170 push @Tile_X_start, int rand $supX;
171 push @Tile_Y_start, int rand $supY;
172 }
173 }
174 else { die FATAL, "Unrecognised/unspecified start location\n";
175 }
176 }
177 } #end naked block
178 print colored ['bold green'], $$Anim{startLocation}, "\n\n"; #report end of coordinate processing
179 #-------------------------------------------------------------------------------------------------------------------------------
180 # 4) CREATE ANIMATION FRAMES:
181 print 'Creating animation frames... '; #report start of frame processing
182 ( $CursorX, $CursorY ) = Cursor(); #record cursor position
183
184 my $Canvas = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for the canvas
185 my $Frames = Image::Magick->new( magick => 'GIF' ); #instantiate an image object for animation frames
186 my $FrameNo = 0; #init no of animation frames
187 my $Interpolate = sub { my ( $lambda, $start, $end ) = @_; #anonymous sub for interpolating x,y-coordinates
188 int( $lambda * $end + ( 1. - $lambda ) * $start );
189 };
190 my $DeltaLambda = 1. / ( $$Anim{frames} + 1. ); #set transit step-size
191
192 for( my $lambda = 0.; $lambda < 1. - $DeltaLambda/2.; $lambda += $DeltaLambda ) { #repeat for each transit step, except last
193 @$Canvas = (); # clear canvas
194 $Canvas->Set( size => "${CanvasWidth}x${CanvasHeight}" ); # set canvas size
195 $Canvas->ReadImage( 'xc:transparent' ); # set canvas background to transparent
196 for( 0..$TileCount-1 ) { # repeat for each tile
197 my $x_topLeft = &$Interpolate( $lambda, $Tile_X_start[$_], $Tile_X_end[$_] ); # compute top-left x-coordinate
198 my $y_topLeft = &$Interpolate( $lambda, $Tile_Y_start[$_], $Tile_Y_end[$_] ); # compute top-left y-coordinate
199 $Canvas->Composite # add tile to canvas
200 ( image => $Tiles->[$_],
201 compose => 'Over',
202 geometry => "${TileWidth}x${TileHeight}+$x_topLeft+$y_topLeft"
203 );
204 } # until all tiles processed
205 push @$Frames, @$Canvas; # add canvas to image sequence
206 print locate( $CursorY, $CursorX ), clline, # report frame processing
207 colored ['bold yellow'], '(', $Spinners[++$FrameNo % 4], ')';
208 } #until all but last transit steps done
209 push @$Frames, @$Image; #add full image as last frame
210 print locate( $CursorY, $CursorX ), clline, #report end of frame processing
211 colored ['bold green'], ++$FrameNo, " frames\n\n";
212 undef $Canvas; #destroy the canvas object
213 undef $Image; #destroy the source image object
214 undef $Tiles; #destroy the image object for the tiles
215 #-------------------------------------------------------------------------------------------------------------------------------
216 # 5) CREATE ANIMATED GIF IMAGE:
217 print 'Creating animated GIF image... '; #report start of animation processing
218 @$Frames = reverse @$Frames if $$Anim{inverse}; #process any inverse request
219 $Frames->Write #output the animation
220 ( delay => $$Anim{delay},
221 loop => $$Anim{loops},
222 dispose => 'background',
223 filename => $AnimFile
224 );
225 print colored ['bold green'], $AnimFile, "\n"; #report end of animation processing
226 exit;
227 #===== Copyright 2009, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
228 # end of anim_reassemble.pl
© 2012 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.