anim_reassemble.pl
Creates an animated GIF that re-assembles an image.

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

x = λx1 + (1 - λ)x0, and y = λy1 + (1 - λ)y0,
where λ ϵ [0,1]. Letting λ evolve as λ|0→1 results in the directed line segment from P(x0,y0) to P(x1,y1).

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:

Leonardo da Vinci
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:
top
Mona Lisa
bottom
Mona Lisa
left
Mona Lisa
right
Mona Lisa
topAndBottom
Mona Lisa
leftAndRight
Mona Lisa
allSides
Mona Lisa
topLeft
Mona Lisa
bottomLeft
Mona Lisa
topRight
Mona Lisa
bottomRight
Mona Lisa
center
Mona Lisa
random
Mona Lisa
<inverse>0</inverse> boolean option (0 or 1) to inverse the animation, that is, disassemble the source image
Mona Lisa
</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.

If you have any questions regarding the code or my explanations, please do not hesitate in contacting me.


anim_reassemble.pl -- [Download latest version: v1.0.0 - September 17, 2009]   [MD5 checksum]
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.