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: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
© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.