You may well ask what have turtles to do with photo mosaics. As you might suspect, I'm not alluding to amphibean reptiles, but rather the control paradigm used by the Logo programming language and its turtle graphics. As with Logo, my turtles also have orientation and position. However, instead of using a pen to draw vector graphics, mine overlay photo mosaic tiles.
The motivation behind this unusual approach takes a bit of explaining. It crossed my mind that it might be interesting to make a video clip of the photo mosaic process as generated by the "img2photomosaic" script. At first blush, I would simply need to save the mosaic after each tile has been overlaid. Think of it as taking snapshots of the intermediate stages. But that means that there would be as many snapshot files and, hence, video frames as there are tiles. For the images I've been working with, this implies 30 to 40,000 frames, each between 2 and 3 Mbytes. Even considering any possible frame optimization, the resulting videos would be huge. To surmount the problem, one obvious solution is to scale down each frame. Experience shows that scaling indeed reduces the video size but this causes two new problems. First, the tiles themselves are so small that it makes it hard to appreciate what's going on. Also, the video is a tad boring as it takes a while before the tiny tiling process starts to make the process interesting. I could instead take snapshots after a certain number of tiles have been overlaid. Though it would drastically cut down the number of frames, the resulting video would certainly appear very jerky. So it occurred to me that a way of salvaging the project would be to have multiple, concurrent tile-overlaying processes. Instead of the single, bottom to top, left to right process, start many similar processes at various locations. Of course, the crux of the problem now becomes how to orchestrate all these processes. Upon considering some sort of recursive version of "img2photomosaic", difficult control issues quickly emerged. It just wasn't clear to me how they could be resolved in a simple manner. So I opted instead to unleash a bale of turtles on the task!
Let's begin by exploring how to make a virtual turtle move around a checkerboard so as to visit every square. Each square has a row and column coordinate, in the range 0 to 7. The board's origin is located at the top left corner square, with the x-coordinate running horizontally and the y one vertically. A turtle is a hash with heading and coordinate values. So, with north pointing towards decreasing y-values, we then have the following Perl statement for a turtle placed at the origin with a southern heading:
my %Turtle = ( HEADING => 'S',
X => 0,
Y => 0
);
Next we need to establish rules governing the turle's movements. If no board edge (boundary) ahead or the adjacent square ahead has not been visited (i.e., no tracks present), move into the square. Otherwise, turn left in the current square and try again. However, if the turtle spins around completely, halt. This last condition is readily checked by counting the number of consecutive turns and stopping after four. This pseudocode can be implemented as follows:
my @Tracks; #array for recording turtle's locations
my $No_Consecutive_Turns; #number of consecutive left turns by turtle
$Tracks[$Turtle{X}][$Turtle{Y}] = 1; #record turtle's initial location
until( $No_Consecutive_Turns == 4 ) { #repeat
if( &noTracksAhead() and &noBoundaryAhead() ) { # if next step forward is permitted
&stepForward(); # move forward one step
} else { # else
&turnLeft(); # turn left
} # end if-else
} #until complete spin around
exit;
Finally, we just need the four subroutines that carry out the checks and movements:
my $No_Rows = 8;
my $No_Cols = 8;
sub noTracksAhead {
if ( $Turtle{HEADING} eq 'S' ) { return !$Tracks[$Turtle{X}][$Turtle{Y}+1]; }
elsif( $Turtle{HEADING} eq 'N' ) { return !$Tracks[$Turtle{X}][$Turtle{Y}-1]; }
elsif( $Turtle{HEADING} eq 'E' ) { return !$Tracks[$Turtle{X}+1][$Turtle{Y}]; }
elsif( $Turtle{HEADING} eq 'W' ) { return !$Tracks[$Turtle{X}-1][$Turtle{Y}]; }
}
sub noBoundaryAhead {
if ( $Turtle{HEADING} eq 'S' ) { return $Turtle{Y} + 1 < $No_Rows; }
elsif( $Turtle{HEADING} eq 'N' ) { return $Turtle{Y} - 1 >= 0; }
elsif( $Turtle{HEADING} eq 'E' ) { return $Turtle{X} + 1 < $No_Cols; }
elsif( $Turtle{HEADING} eq 'W' ) { return $Turtle{X} - 1 >= 0; }
}
sub stepForward {
if ( $Turtle{HEADING} eq 'S' ) { ++$Turtle{Y}; }
elsif( $Turtle{HEADING} eq 'N' ) { --$Turtle{Y}; }
elsif( $Turtle{HEADING} eq 'E' ) { ++$Turtle{X}; }
elsif( $Turtle{HEADING} eq 'W' ) { --$Turtle{X}; }
$Tracks[$Turtle{X}][$Turtle{Y}] = 1; # update turtle's accessed locations
$No_Consecutive_Turns = 0; # reset the turn count
}
sub turnLeft {
$Turtle{HEADING} =~ tr/NSEW/WENS/; # set new heading
++$No_Consecutive_Turns; # increment the turn count
}
Of course, other rules can also achieve the desired result. However, the above approach is straightfoward in that it doesn't require the turtle to turn right at any stage and, thus, knowing how and when to. The simple animated GIF to the left represents the actual trek generated by the above code. You can download the complete routine "__turtle.pl" here and its MD5 checksum here.
Having a single turtle trekking around the checkerboard is just the first step. Our ultimate goal is to have a group of turtles, collectively known apparently as a bale. Accordingly, the above code can be readily extended by converting the variable "$No_Consecutive_Turns" to an array with one element for each turtle. The hash "%Turtle" now becomes "@Bale", an array of hashes. For example, for four turtles placed at each corner, we have
my @Bale = ( { HEADING => 'S', X => 0, Y => 0 }, #define turtle at top left corner
{ HEADING => 'E', X => 0, Y => $No_Rows - 1 }, #define turtle at bottom left corner
{ HEADING => 'N', X => $No_Cols - 1, Y => $No_Rows - 1 }, #define turtle at bottom right corner
{ HEADING => 'W', X => $No_Cols - 1, Y => 0 } #define turtle at top right corner
);
The control loop becomes
my @Tracks; #array for recording bale's locations
my @No_Consecutive_Turns; #number of consecutive left turns by turtles
$Tracks[$Bale[$_]{X}][$Bale[$_]{Y}] = 1 for (0..$#Bale); #record turtles initial locations
until( &sumArray(@No_Consecutive_Turns) == 4 * @Bale ) { #repeat
for my $turtle ( 0..$#Bale ) { # repeat for each turtle
next if $No_Consecutive_Turns[$turtle] == 4; # skip if turtle has spun around
if( &noTracksAhead($turtle) and &noBoundaryAhead($turtle) ) { # if next step forward is permitted
&stepForward($turtle); # move forward one step
} else { # else
&turnLeft($turtle); # turn left
} # end if-else
} # until all turtles processed
} #until all turtles have spun around
exit;
Note the presence of the additional subroutine "sumArray". As it name suggests, it simply totals the values of all the elements
of an array. Thus, the bale's trek comes to an end when the sum of consecutive turns by all the turtles is equal to four
times the bale's size.
Similar modifications are also carried out to the various subroutines. For example, "noTracksAhead" now requires a turtle's bale-index value in order to check for its tracks ahead:
sub noTracksAhead {
my $turtle = shift;
if ( $Bale[$turtle]{HEADING} eq 'S' ) { return !$Tracks[$Bale[$turtle]{X}][$Bale[$turtle]{Y}+1]; }
elsif( $Bale[$turtle]{HEADING} eq 'N' ) { return !$Tracks[$Bale[$turtle]{X}][$Bale[$turtle]{Y}-1]; }
elsif( $Bale[$turtle]{HEADING} eq 'E' ) { return !$Tracks[$Bale[$turtle]{X}+1][$Bale[$turtle]{Y}]; }
elsif( $Bale[$turtle]{HEADING} eq 'W' ) { return !$Tracks[$Bale[$turtle]{X}-1][$Bale[$turtle]{Y}]; }
}
The animated GIF on the right represents the actual trek generated by the above code. You can download the complete routine "__bale.pl" here and its MD5 checksum here.
There's an important caveat here: the starting locations and headings are critical. Success is guaranteed if the turtles start at opposite corners with the appropriate unique headings. Otherwise, a turtle can become boxed in prematurely or an area of the board unreachable after being fenced in by the turtles' tracks.
In accordance then with this warning, the following recursive approach was adopted for initilizating the locations. Divide a region into quadrants. Place either
![]() One turtle per quadrant |
![]() Two turtles per quadrant |
![]() Four turtles per quadrant |
The number of turtles one gets therefore for the first five recursion levels are as follows:
| Recursion Depth | ||||||
|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | ||
| Number of turtles per quadrant |
1 | 1 | 4 | 16 | 64 | 256 |
| 2 | 2 | 8 | 32 | 128 | 512 | |
| 4 | 4 | 16 | 64 | 256 | 1024 | |
Note that the quadrants will be of equal size if the grid has an even number of rows and columns. Otherwise, they will be assymetric. However these distortions will be inconsequential if the quadrant boundaries are close enough to the true center lines. Hence there's no need for any additional scaling of the original image beyond that of insuring an integral number of tiles. Also note that the above initial positions guarantee that the turtles will not wander off into adjacent quadrants. Finally, even though certain combinations yield the same bale size, the resulting visual effects will be different.
Fusing the above turtle controls then to our "img2photomosaic" filter yields the Perl script img2photomosaic_slides.pl, which is displayed below. It is released for personal, non-commercial and non-profit use only. The program generates a sequence of slide images which can then be converted into a video clip by third party applications. Our featured AVI video clip, ugolino_640x480_256.avi, was produced with the free, now a decade old Fast Movie Processor 1.41.
The listing includes the line numbers in order to reference them in the following general remarks:
To create the slides for our example video clip, the following command was executed:
perl img2photomosaic_slides.pl ugolino.jpg ALL 15 15 640 480 4 3
If you have any questions regarding the code or my explanations, please do not hesitate in contacting me.
001 use strict;
002 use File::Basename;
003 use Image::Magick;
004 #===== Copyright 2008, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
005 # img2photomosaic_slides.pl: Photo mosaic slide generator.
006 #===============================================================================================================================
007 # Usage : perl img2photomosaic_slides.pl ImageFile TileCollections TileWidth TileHeight
008 # SlideWidth SlideHeight InitType InitRepeat Verbose
009 # Arguments : ImageFile = path of image file to be filtered.
010 # TileCollections = CSV string specifying which tiles to use or exclude, via references to their source
011 # collections:
012 # "ALL" to use all the available tiles, or
013 # "+,ImageCollectionName1,ImageCollectionName2,..." to use just the tiles in the subset, or
014 # "-,ImageCollectionName1,ImageCollectionName2,..." to exclude the subset from use.
015 # TileWidth = width of a tile in pixels
016 # TileHeight = height of a tile in pixels
017 # SlideWidth = width of each slide in pixels
018 # SlideHeight = height of each slide in pixels
019 # InitType = number of turtles per quadrant: 1,2 or 4
020 # InitRepeat = recursion depth for partioning the image into quadrants
021 # Verbose = boolean flag for verbose output
022 # Input Files : See arguments. Also "photomosaic_tiles.dbm", the DBM file of tile information.
023 # Output Directory : Slide subdirectory "frames". Directory will be created if it does not exist.
024 # Output Files : (a) Mosaic GIF file in the form "basename_wxh_px_tiles.gif" where "basename" denotes the basename of the
025 # source image file, "w" the tile width and "h" the tile height,
026 # (b) the slide JPEG files, in the form "nnn.jpg" where "nnn" denotes a positive zero-padded integer.
027 # Temporary Files : None.
028 # Remarks : (a) Requires PerlMagick. Used module from ImageMagick 6.3.7,
029 # (b) Setting both 'SlideWidth' and 'SlideHeight' to zero will suppress any scaling of the slides,
030 # (c) The number of turtles, corresponding to the values of 'InitType' and 'InitRepeat', will be as follows:
031 # InitRepeat
032 # 0 1 2 3 4 ....
033 # +---------------------------------------------
034 # 1 | 1 4 16 64 256
035 # InitType 2 | 2 8 32 128 512
036 # 4 | 4 16 64 256 1024
037 # History : v1.0.0 - March 9, 2008 - Original release.
038 #===============================================================================================================================
039 # 0) INITIALIZE:
040 system( 'cls' ) if $^O =~ /^MSWin/; #clear screen if running Windows
041 print "$0\n", '=' x length($0), "\n\n"; #display program name
042
043 my $SourceFile = shift || die 'ERROR: No image file specified'; #get path of source image file
044 my $Collections = shift || die 'ERROR: No tile collection references specified'; #get tile collection references
045 my $TileWidth = shift || die 'ERROR: No tile width specified'; #get pixel width of a tile
046 my $TileHeight = shift || die 'ERROR: No tile height specified'; #get pixel height of a tile
047 my $SlideWidth = shift;
048 my $SlideHeight = shift;
049 my $InitType = shift;
050 my $InitRepeat = shift;
051 my $Verbose = shift; #get boolean flag for verbose mode
052 die "ERROR: Cannot locate image file '$SourceFile'" unless -e $SourceFile;
053 die "ERROR: Also set 'SlideWidth' to zero to suppress scaling"
054 if $SlideHeight == 0 and $SlideWidth;
055 die "ERROR: Also set 'SlideHeight' to zero to suppress scaling"
056 if $SlideWidth == 0 and $SlideHeight;
057 die "ERROR: Invalid value for 'InitType'" unless
058 ( $InitType == 1 ) or ( $InitType == 2 ) or ( $InitType == 4 );
059 die "ERROR: Invalid value for 'InitRepeat'" if $InitRepeat < 0;
060
061 fileparse_set_fstype(''); #force Unix syntax
062 my ( $basename, #extract basename &
063 $path #extract path
064 ) = fileparse( $SourceFile, '\..*' ); # of source image file
065 my $MosaicFile = "$path${basename}_${TileWidth}x${TileHeight}_px_tiles.jpg"; #compose filename of mosaic image
066
067 my $NoSlides; #slide count
068 my $FrameDir = 'frames'; #frame subdirectory
069 die "ERROR: Cannot create directory '$FrameDir': $!"
070 unless ( -d $FrameDir ) or ( mkdir $FrameDir, 0600 );
071
072 my $Tile = Image::Magick->new( magick=>"GIF" ); #instantiate an image object for a tile
073 my $TileDir = "tiles_${TileWidth}x${TileHeight}_px"; #tile subdirectory
074 my $TileDBM = "$TileDir/photomosaic_tiles.dbm"; #DBM filepath for tile info
075
076 my %TileRGB; #hash of tile rgb values
077 { #start naked block
078 my %tileInfo; # DBM hash
079 my ($collOption) = $Collections =~ /^(ALL|\+|\-)/; # collections processing option
080 die "ERROR: Cannot recognize collection option '$collOption' in '$Collections'"
081 unless $collOption;
082 my $collName; # name of a tile's associated image collection
083 my $key; # DBM hash key
084 my $value; # DBM hash value
085 my @rgb; # array of rgb values for a tile
086 dbmopen( %tileInfo, $TileDBM, undef ) # open the DBM file of tile info
087 or die "ERROR: No tiles available of the specified width and height.";
088 while( ( $key, $value ) = each %tileInfo ) { # repeat for each tile record
089 ( $collName, @rgb ) = split /,/, $value; # parse collection name & rgb values
090 push @{$TileRGB{$key}}, @rgb # retain this tile
091 if ( $collOption eq 'ALL' ) or # if inclusion of all tiles requested, or
092 ( $collOption eq '+' and $Collections =~ /\Q$collName\E,*/ ) or # if use only some collections, or
093 ( $collOption eq '-' and $Collections !~ /\Q$collName\E,*/ ); # if not excluded
094 } # until all tile records processed
095 dbmclose %tileInfo; # close the DBM file
096 undef %tileInfo; # discard DBM hash
097 } #end naked block
098 #-------------------------------------------------------------------------------------------------------------------------------
099 # 1) SETUP MOSAIC IMAGE:
100 my $source = Image::Magick->new; #instantiate an image object for the source
101 my ( $SourceWidth, #get width &
102 $SourceHeight #get height
103 ) = ( $source->Ping( $SourceFile ) )[0,1]; #of source
104 undef $source; #destroy the image object for the source
105
106 my $No_Tile_Cols = $SourceWidth / $TileWidth; #compute raw number of tile columns
107 $No_Tile_Cols = int( ++$No_Tile_Cols ) #adjust to an integral number
108 unless $No_Tile_Cols == int( $No_Tile_Cols ); # if necessary
109 my $No_Tile_Rows = $SourceHeight / $TileHeight; #compute raw number of tile rows
110 $No_Tile_Rows = int( ++$No_Tile_Rows ) #adjust to an integral number
111 unless $No_Tile_Rows == int( $No_Tile_Rows ); # if necessary
112
113 my $MosaicWidth = $No_Tile_Cols * $TileWidth; #compute pixel width of mosaic image
114 my $MosaicHeight = $No_Tile_Rows * $TileHeight; #compute pixel height of mosaic image
115 my $Mosaic = Image::Magick->new( magick=>"JPG" ); #instantiate an image object for the mosaic
116 $Mosaic->Read( $SourceFile ); #init mosaic with source image file
117 $Mosaic->Quantize( colorspace=>'RGB' ); #insure correct colorspace
118 $Mosaic->Scale( geometry=>"${MosaicWidth}x${MosaicHeight}!" ); #scale mosaic image
119
120 print "Source Image: $SourceFile\n", #echo initialization results
121 " Width: $SourceWidth px\n",
122 " Height: $SourceHeight px\n",
123 "Mosaic Image: $MosaicFile\n",
124 " Width: $MosaicWidth px\n",
125 " Height: $MosaicHeight px\n",
126 "Mosaic Details: ", $No_Tile_Cols * $No_Tile_Rows, " tiles\n",
127 " No. of rows: $No_Tile_Rows\n",
128 " No. of cols: $No_Tile_Cols\n",
129 " Tile width: $TileWidth px\n",
130 " Tile height: $TileHeight px\n";
131 #-------------------------------------------------------------------------------------------------------------------------------
132 # 2) INITIALIZE BALE:
133 my @Bale; #array of hashes for the turtle headings & coordinates
134 my @Tracks; #array for recording bale's prior locations
135 { #start naked block
136 my $no_turtles; # number of turtles
137 $no_turtles = 4**$InitRepeat if $InitType == 1; # compute number of turtles according to initialization type
138 $no_turtles = 2*4**$InitRepeat if $InitType == 2;
139 $no_turtles = 4**($InitRepeat+1) if $InitType == 4;
140
141 print "Bale Details: $no_turtles turtles\n",
142 " Init Type: $InitType turtle(s) per quadrant\n",
143 " Init Repeat: $InitRepeat\n\n";
144 die "ERROR: Bale size exceeds number of tiles"
145 if $no_turtles > $No_Tile_Cols * $No_Tile_Rows;
146 }
147 &initBale( 0, 0, $No_Tile_Cols - 1, $No_Tile_Rows - 1, $InitRepeat ); # init all turtles
148 print( "Pause. Press the ENTER key to continue..." ), <STDIN> if $Verbose;
149 #-------------------------------------------------------------------------------------------------------------------------------
150 # 3) PUT TURTLES IN MOTION:
151 my $Snapshot = Image::Magick->new( magick=>"JPG" ); #instantiate an image object for the mosaic
152 my $SnapshotIdx = 0; #snapshot index
153 my $SnapshotIdxWidth = length( $No_Tile_Cols * $No_Tile_Rows ); #set width of slide basenames
154 my @No_Consecutive_Turns; #array for number of consecutive left turns by each turtle
155
156 &outputSnapshot(); #output source image as first slide
157 &overlayTile($_) for ( 0..$#Bale ); #overlay first tiles before putting bale in motion
158 &outputSnapshot(); #output resulting slide
159
160 until( &sumArray(@No_Consecutive_Turns) == 4 * @Bale ) { #repeat
161 for my $turtle ( 0..$#Bale ) { # repeat for each turtle
162 next if $No_Consecutive_Turns[$turtle] == 4; # skip if turtle has spun around
163 if( &noTracksAhead($turtle) and &noBoundaryAhead($turtle) ) { # if next step forward is permitted
164 &stepForward($turtle); # move forward one step
165 &overlayTile($turtle); # overlay tile at location
166 } else { # else
167 &turnLeft($turtle); # turn left
168 } # end if-else
169 } # until all turtles processed
170 &outputSnapshot(); # output resulting slide
171 } #until all turtles have spun around
172 $Mosaic->Write( filename => $MosaicFile ); #save the mosaic to file
173 print "\a\nNumber of slides: $NoSlides\n\nDone!\n"; #report end of processing
174 exit; #end processing
175 #===== SUBROUTINES =============================================================================================================
176 # Usage : &bestTileMatch( $RED, $GREEN, $BLUE );
177 # Purpose : Matches a tile to the mean RGB channel values of an image area.
178 # Arguments : $RED = mean 8-bit red color
179 # $GREEN = mean 8-bit green color
180 # $BLUE = mean 8-bit blue color
181 # Externals : $TileRGB, $Verbose
182 # Subs : None.
183 # Remarks : None.
184 # History : v1.0.0 - January 23, 2008 - Original release.
185
186 sub bestTileMatch { #begin sub
187 my ( $red, $green, $blue ) = @_; # parametrize the arguments
188 my $bestTile; # name of tile that best matches mean color
189 my $deltaRed; # difference in red color values
190 my $deltaGreen; # difference in green color values
191 my $deltaBlue; # difference in blue color values
192 my $key; # hash key = tile name
193 my $value; # hash value = array of channel color intensities
194 my $metric; # squared Euclidean metric of intensities
195 my $minMetric = 65536 * 65536; # minimum value of the metric
196
197 while( ( $key, $value ) = each %TileRGB ) { # repeat for each possible tile
198 $deltaRed = $red - @{$value}[0]; # compute difference in red colors
199 $deltaGreen = $green - @{$value}[1]; # compute difference in green colors
200 $deltaBlue = $blue - @{$value}[2]; # compute difference in blue colors
201 $metric = $deltaRed * $deltaRed + # compute metric
202 $deltaGreen * $deltaGreen +
203 $deltaBlue * $deltaBlue;
204 $minMetric = $metric, $bestTile = $key if $metric < $minMetric; # update minimum metric & possible best tile
205 } # until all tiles processed
206 print "=> $bestTile\n" if $Verbose; # report if requested
207 return $bestTile; # return best matching tile name
208 } #end sub bestTileMatch
209 #-------------------------------------------------------------------------------------------------------------------------------
210 # Usage : &meanColor( @COLORS );
211 # Purpose : Computes the mean 8-bit channel color.
212 # Arguments : @COLORS = list of normalized channel intensities to be averaged.
213 # Externals : $Verbose
214 # Subs : None.
215 # Remarks : None.
216 # History : v1.0.0 - January 23, 2008 - Original release.
217
218 sub meanColor { #begin sub
219 my $mean; # mean 8-bit color value
220
221 $mean += $_ for @_; # sum all the normalized color values
222 $mean = 256 * $mean / scalar @_; # compute the mean 8-bit value
223 printf "%5.1f ", $mean if $Verbose; # report if requested
224 return $mean; # return the mean
225 } #end sub meanColor
226 #-------------------------------------------------------------------------------------------------------------------------------
227 # Usage : &overlayTile( TURTLE );
228 # Purpose : Overlays the region corresponding to a turtle's location with the best matching photomosaic tile.
229 # Arguments : TURTLE = turtle's array index for @Bale.
230 # Externals : $Tile, $TileDir, $TileHeight, $TileWidth, $Verbose, @Bale
231 # Subs : &bestTileMatch, &meanColor.
232 # Remarks : None.
233 # History : v1.0.0 - March 9, 2008 - Original release.
234
235 sub overlayTile { #begin sub
236 my $turtle = shift; # turtle index
237 my $x_top_left = $Bale[$turtle]{X} * $TileWidth; # image x-coordinate of a tile's top-left corner
238 my $y_top_left = $Bale[$turtle]{Y} * $TileHeight; # image y-coordinate of a tile's top-left corner
239 my $x_bottom_right = $x_top_left + $TileWidth - 1 ; # image x-coordinate of a tile's bottom-right corner
240 my $y_bottom_right = $y_top_left + $TileHeight - 1 ; # image y-coordinate of a tile's bottom-right corner
241 my $best_tile; # name of best matching tile
242 my $geometry; # geometry of a tile: width, height & x,y offsets
243
244 print " overlayTile: $x_top_left,$y_top_left-$x_bottom_right,$y_bottom_right: " # report progress if requested
245 if $Verbose;
246
247 $geometry = "${TileWidth}x${TileHeight}+$x_top_left+$y_top_left"; # define tile geometry
248 $best_tile = &bestTileMatch ( # match mean image-area colors to a tile
249 &meanColor ( # compute mean red color
250 $Mosaic->GetPixels ( # get normalized red intensities of all pixels
251 map => 'r',
252 geometry => $geometry,
253 normalize => 'true'
254 )
255 ),
256 &meanColor ( # compute mean green color
257 $Mosaic->GetPixels ( # get normalized green intensities of all pixels
258 map => 'g',
259 geometry => $geometry,
260 normalize => 'true'
261 )
262 ),
263 &meanColor ( # compute mean blue color
264 $Mosaic->GetPixels ( # get normalized blue intensities of all pixels
265 map => 'b',
266 geometry => $geometry,
267 normalize => 'true'
268 )
269 )
270 );
271 @$Tile = (); # reset tile object
272 $Tile->Read( "$TileDir/$best_tile" ); # init tile with its image file
273 $Mosaic->Composite( # overlay image subregion with tile
274 image => $Tile,
275 compose => 'Over',
276 geometry => $geometry
277 );
278 } #end sub overlayTile
279 #-------------------------------------------------------------------------------------------------------------------------------
280 # Usage : &noTracksAhead( TURTLE );
281 # Purpose : Returns true if the next grid location ahead has not been visited. Otherwise, returns false.
282 # Arguments : TURTLE = turtle's array index for @Bale.
283 # Externals : @Bale, @Tracks
284 # Subs : None.
285 # Remarks : None.
286 # History : v1.0.0 - March 3, 2008 - Original release.
287
288 sub noTracksAhead { #begin sub
289 my $turtle = shift;
290
291 if ( $Bale[$turtle]{HEADING} eq 'S' ) { return !$Tracks[$Bale[$turtle]{X}][$Bale[$turtle]{Y}+1]; }
292 elsif( $Bale[$turtle]{HEADING} eq 'N' ) { return !$Tracks[$Bale[$turtle]{X}][$Bale[$turtle]{Y}-1]; }
293 elsif( $Bale[$turtle]{HEADING} eq 'E' ) { return !$Tracks[$Bale[$turtle]{X}+1][$Bale[$turtle]{Y}]; }
294 elsif( $Bale[$turtle]{HEADING} eq 'W' ) { return !$Tracks[$Bale[$turtle]{X}-1][$Bale[$turtle]{Y}]; }
295 } #end sub noTracksAhead
296 #-------------------------------------------------------------------------------------------------------------------------------
297 # Usage : &noBoundaryAhead( TURTLE );
298 # Purpose : Returns true if the next grid location ahead is beyond a boundary. Otherwise, returns false.
299 # Arguments : TURTLE = turtle's array index for @Bale.
300 # Externals : @Bale, @Tracks
301 # Subs : None.
302 # Remarks : None.
303 # History : v1.0.0 - March 3, 2008 - Original release.
304
305 sub noBoundaryAhead { #begin sub
306 my $turtle = shift;
307
308 if ( $Bale[$turtle]{HEADING} eq 'S' ) { return $Bale[$turtle]{Y} + 1 < $No_Tile_Rows; }
309 elsif( $Bale[$turtle]{HEADING} eq 'N' ) { return $Bale[$turtle]{Y} - 1 >= 0; }
310 elsif( $Bale[$turtle]{HEADING} eq 'E' ) { return $Bale[$turtle]{X} + 1 < $No_Tile_Cols; }
311 elsif( $Bale[$turtle]{HEADING} eq 'W' ) { return $Bale[$turtle]{X} - 1 >= 0; }
312 } #end sub noBoundaryAhead
313 #-------------------------------------------------------------------------------------------------------------------------------
314 # Usage : &stepForward( TURTLE );
315 # Purpose : Repositions a turtle to an adjacent grid location in accordance with its heading.
316 # Arguments : TURTLE = turtle's array index for @Bale.
317 # Externals : $Verbose, @Bale, @No_Consecutive_Turns
318 # Subs : None.
319 # Remarks : None.
320 # History : v1.0.0 - March 3, 2008 - Original release.
321
322 sub stepForward { #begin sub
323 my $turtle = shift;
324
325 if ( $Bale[$turtle]{HEADING} eq 'S' ) { ++$Bale[$turtle]{Y}; }
326 elsif( $Bale[$turtle]{HEADING} eq 'N' ) { --$Bale[$turtle]{Y}; }
327 elsif( $Bale[$turtle]{HEADING} eq 'E' ) { ++$Bale[$turtle]{X}; }
328 elsif( $Bale[$turtle]{HEADING} eq 'W' ) { --$Bale[$turtle]{X}; }
329 $Tracks[$Bale[$turtle]{X}][$Bale[$turtle]{Y}] = 1; # update bale's accessed locations
330 $No_Consecutive_Turns[$turtle] = 0; # reset turtle's turn count
331 print "stepForward: Turtle #$turtle - $Bale[$turtle]{X}, $Bale[$turtle]{Y}\n" # report new location if requested
332 if $Verbose;
333 } #end sub stepForward
334 #-------------------------------------------------------------------------------------------------------------------------------
335 # Usage : &turnLeft( TURTLE );
336 # Purpose : Changes a turtle's heading in accordance with a left turn.
337 # Arguments : TURTLE = turtle's array index for @Bale.
338 # Externals : $Verbose, @Bale, @No_Consecutive_Turns
339 # Subs : None.
340 # Remarks : None.
341 # History : v1.0.0 - March 3, 2008 - Original release.
342
343 sub turnLeft { #begin sub
344 my $turtle = shift;
345
346 $Bale[$turtle]{HEADING} =~ tr/NSEW/WENS/; # change heading to the left
347 ++$No_Consecutive_Turns[$turtle]; # update turtle's turn count
348 print "turnLeft: Turtle #$turtle - $Bale[$turtle]{HEADING}\n" # report new heading if requested
349 if $Verbose;
350 } #end sub turnLeft
351 #-------------------------------------------------------------------------------------------------------------------------------
352 # Usage : &sumArray( ARRAY );
353 # Purpose : Returns the sum of the elements of the specified array .
354 # Arguments : ARRAY = array to be summed
355 # Externals : None.
356 # Subs : None.
357 # Remarks : None.
358 # History : v1.0.0 - March 3, 2008 - Original release.
359
360 sub sumArray { #begin sub
361 my $sum;
362
363 $sum += $_ for @_;
364 return $sum;
365 } #end sub sumArray
366 #-------------------------------------------------------------------------------------------------------------------------------
367 # Usage : &outputSnapshot();
368 # Purpose : Outputs a scaled snapshot of the photomosaic to file.
369 # Arguments : None.
370 # Externals : $FrameDir, $NoSlides, $SlideHeight, $SlideWidth, $Snapshot, $SnapshotIdx, $SnapshotIdxWidth
371 # Subs : None.
372 # Remarks : None.
373 # History : v1.0.0 - March 9, 2008 - Original release.
374
375 sub outputSnapshot { #begin sub
376 my $snapshotFile = "$FrameDir/" . # compose file name
377 sprintf( "%${SnapshotIdxWidth}.${SnapshotIdxWidth}d", ++$SnapshotIdx ) .
378 '.jpg';
379 @$Snapshot = (); # reset snapshot object
380 $Snapshot = $Mosaic->Clone(); # init snapshot to full mosaic image
381 $Snapshot->Scale( geometry => "${SlideWidth}x${SlideHeight}!" ); # scale snapshot image
382 $Snapshot->Write( filename => $snapshotFile ); # save snapshot to file
383 ++$NoSlides; # increment slide count
384 } #end sub outputSnapshot
385 #-------------------------------------------------------------------------------------------------------------------------------
386 # Usage : &initBale( X_TOP_LEFT, Y_TOP_LEFT, X_BOTTOM_RIGHT, Y_BOTTOM_RIGHT, RECURSION_COUNT );
387 # Purpose : Recursively assigns initial grid locations and headings to all the turtles on a per quadrant basis.
388 # Arguments : X_TOP_LEFT = grid x-coordinate of top-left corner
389 # X_TOP_LEFT = grid y-coordinate of top-left corner
390 # X_BOTTOM_RIGHT = grid x-coordinate of bottom-right corner
391 # Y_BOTTOM_RIGHT = grid y-coordinate of bottom-right corner
392 # RECURSION_COUNT = counter for the recursion depth
393 # Externals : $InitType, @Bale, @Tracks
394 # Subs : None.
395 # Remarks : None.
396 # History : v1.0.0 - March 4, 2008 - Original release.
397
398 sub initBale { #begin sub
399 my $x_top_left = shift; # grid x-coordinate of top-left corner
400 my $y_top_left = shift; # grid y-coordinate of top-left corner
401 my $x_bottom_right = shift; # grid x-coordinate of bottom-right corner
402 my $y_bottom_right = shift; # grid y-coordinate of bottom-right corner
403 my $recursionCount = shift; # recursion counter
404
405 { # start naked block
406 unless( $Tracks[$x_top_left][$y_top_left] ) { # if top-left corner vacant
407 push @Bale, { HEADING => 'S', X => $x_top_left, Y => $y_top_left }; # define turtle at corner
408 $Tracks[$x_top_left][$y_top_left] = 1; # record turtle location
409 } # end if
410 last if $InitType == 1; # check if only one turtle to be defined
411 unless( $Tracks[$x_bottom_right][$y_bottom_right] ) { # if bottom-right corner vacant
412 push @Bale, { HEADING => 'N', X => $x_bottom_right, Y => $y_bottom_right }; # define turtle at corner
413 $Tracks[$x_bottom_right][$y_bottom_right] = 1; # record turtle location
414 } # end if
415 last if $InitType == 2; # check if only two turtles to be defined
416 unless( $Tracks[$x_top_left][$y_bottom_right] ) { # if bottom-left corner vacant
417 push @Bale, { HEADING => 'E', X => $x_top_left, Y => $y_bottom_right }; # define turtle at corner
418 $Tracks[$x_top_left][$y_bottom_right] = 1; # record turtle location
419 } # end if
420 unless( $Tracks[$x_bottom_right][$y_top_left] ) { # if top-right corner vacant
421 push @Bale, { HEADING => 'W', X => $x_bottom_right, Y => $y_top_left }; # define turtle at corner
422 $Tracks[$x_bottom_right][$y_top_left] = 1; # record turtle location
423 } # end if
424 } # end naked block
425 if( $recursionCount ) { # if recursion in effect
426 my $x_center = $x_top_left + int( 0.5 * ( $x_bottom_right - $x_top_left ) ); # x-coordinate at 'center'
427 my $y_center = $y_top_left + int( 0.5 * ( $y_bottom_right - $y_top_left ) ); # y-coordinate at 'center'
428 &initBale( $x_top_left, $y_top_left, $x_center, $y_center, $recursionCount-1 ); # init top-left quadrant
429 &initBale( $x_top_left, $y_center+1, $x_center, $y_bottom_right, $recursionCount-1 ); # init bottom-left quadrant
430 &initBale( $x_center+1, $y_top_left, $x_bottom_right, $y_center, $recursionCount-1 ); # init top-right quadrant
431 &initBale( $x_center+1, $y_center+1, $x_bottom_right, $y_bottom_right, $recursionCount-1 ); # init bottom-right quadrant
432 } # end if
433 } #end sub initBale
434 #===== Copyright 2008, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
435 # end of img2photomosaic_slides.pl
© 2012 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.