|
Original† (132,666 colors)
Full resolution (743 × 1,155 pixels). |
Mosaic image filter =============> with assembly instructions |
Mosaic (35 colors, 28,757 tiles @ 5 x 6 pixels)
Full resolution (745 × 1,158 pixels). |
My attention was grabbed recently by various artists creating mosaics with such items as LEGO™ bricks, photo collections or plain wooden tiles. These works got me thinking about the complexities behind their creations. In turn, this led me to ponder whether I could accomplish something similar, given my artistic deficiencies. Accordingly, I decided to tackle the "simplest" task, namely creating a filter that would convert an image into a mosaic of rectangular or square "tiles", each of a single color. Moreover, I would want it to generate a blueprint for assembling the resulting mosaic.
To accomplish my goal, I decided to use what I know best, namely Perl with ImageMagick's drawing primitives accessed through its PerlMagick interface. Another important consideration is selecting a color chart for the tiles. Too few colors would yield in an unrecognizable result or, at best, something Andy Warhol might have created. With the images of LEGO™ art still fresh in my mind and for the purpose of this presentation, I decided to use a color palette based on the LEGO™ Color Chart, wherein only the opaque colors are retained.
The basic task is readily identifiable. Given an area of the original image, compute the mean red, green and blue values of all the pixels. Compare these averages to the rgb values of each tile in the color chart. Select the tile that comes "closest", that is the one that minimizes a metric of rgb values. Finally, fill in the area with the tile's color. Repeat the process for all possible areas.
The Perl script img2mosaic.pl, displayed below, implements this algorithm. 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 img2mosaic.pl Mona_Lisa.jpg legoChart.txt 5 6
Note that the "Verbose" parameter can be omitted or set to "0" to suppress the computational results for each tile. If set, say "1", then the details will be displayed. The following line is a typical example:Tile# 193,1 @ 0,0-4,5: 100.6 116.8 81.2 => 168-GunMetallic
The tile number follows a matrix notation with the row number (193) followed by the column number(1). The corresponding image coordinates are stated next with the top right corner (0,0) followed by the bottom left corner (5,4). The next three numbers are the average values of the rgb intensities in that order. Finally, the name of the best matching tile is printed.BTW, the tile dimension of 5 x 6 pixels was chosen for no other reason than the width to height ratio of an interlocked unit LEGO™ brick is 5 to 6 (see http://www.owlnet.rice.edu/~elec201/Book/legos.html).
Mona_Lisa_5x6_px_tiles.gif
Mona_Lisa_5x6_px_tiles.txt
'1-White' => 'F2 F3 F2',
Note that the key is composite. The left hand side of the hyphen is an id code that is used to reference the tile in the assembly instructions (see below). In this particular case, it's the official LEGO™ brick code which insures uniqueness. The right hand side of the hyphen is just a name for the color.@{ $TileRGB{ '1-White' } } = (242,243,242) (used for finding the best matching tile),
$TileColor{ '1-White' } = '#F2F3F2' (used for drawing the tile), and
$TileId{ '1-White' } = 1 (used for referencing the tile in the assembly instructions).
Row# 17: 149[41] 141 26 149[106]
It means that the 17th row consists of 41 tiles with id "149", corresponding to the tile named "149-BlackMetallic" as declared in the color chart. Single instances of Tile "141" and "26" follow next. Finally there's another run of "149" tiles, this time 106 of them.If this mosaic were to be built, it would be roughly 1.2 x 1.9 meters. Anyway, if you have any questions regarding the code or my explanations, please do not hesitate in contacting me.
BTW, for photo-mosaics instead, one needs to replace the handling of a color chart with that of a database of mean rgb values for a collection of thumbnail images.
001 use strict;
002 use File::Basename;
003 use FileHandle;
004 use Image::Magick;
005 #===== Copyright 2008, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
006 # img2mosaic.pl: Mosaic image filter with assembly instructions.
007 #===============================================================================================================================
008 # Usage : perl img2mosaic.pl ImageFile ColorChartFile TileWidth TileHeight Verbose
009 # Arguments : ImageFile = path of image file to be filtered.
010 # ColorChartFile = path of text file containing the hash declarations for the color chart.
011 # TileWidth = width of a tile in pixels
012 # TileHeight = height of a tile in pixels
013 # Verbose = boolean flag for verbose output
014 # Input Files : See arguments.
015 # Output Files : Mosaic GIF file in the form "basename_wxh_px_tiles.gif" where "basename" denotes the basename of the
016 # source image file, "w" the tile width and "h" the tile height.
017 # Mosaic text assembly instructions in the form "basename_wxh_px_tiles.txt".
018 # Temporary Files : None.
019 # Remarks : Requires PerlMagick. Used module from ImageMagick 6.3.7.
020 # History : v1.0.0 - January 24, 2008 - Original release.
021 #===============================================================================================================================
022 # 0) INITIALIZE:
023 system( 'cls' ) if $^O =~ /^MSWin/; #clear screen if running Windows
024 print "$0\n", '=' x length($0), "\n\n"; #display program name
025
026 my $SourceFile = shift || die 'ERROR: No image file specified'; #get path of source image file
027 my $ChartFile = shift || die 'ERROR: No color-chart file specified'; #get path of color-chart file
028 my $TileWidth = shift || die 'ERROR: No tile width specified'; #get pixel width of a tile
029 my $TileHeight = shift || die 'ERROR: No tile height specified'; #get pixel height of a tile
030 my $Verbose = shift; #get boolean flag for verbose mode
031 die 'ERROR: Cannot locate image file' unless -e $SourceFile;
032 die 'ERROR: Cannot locate color-chart file' unless -e $ChartFile;
033
034 my ( $basename, #extract basename &
035 $path #extract path
036 ) = fileparse( $SourceFile, '\..*' ); # of source image file
037 my $MosaicFile = "$path${basename}_${TileWidth}x${TileHeight}_px_tiles.gif"; #compose filename of mosaic image
038 my $ReportFile = "$path${basename}_${TileWidth}x${TileHeight}_px_tiles.txt"; #compose filename of report
039 local *REPORT; #define filehandle for the report
040
041 my %ColorChart; #hash of tile id codes => rgb color values
042 push @ARGV, $ChartFile; #set up reading of color chart
043 eval '%ColorChart = (' . join( '', <> ) . ')'; #load chart into hash
044
045 my %TileRGB; #hash of arrays for each color channel value
046 my %TileColor; #hash of rgb fill attributes
047 my %TileId; #hash of tile id codes
048 { #start naked block
049 my $key; # chart hash key
050 my $value; # chart hash value
051 my @rgb; # array of rgb channel colors
052
053 while( ( $key, $value ) = each %ColorChart ) { # repeat for each chart entry
054 @rgb = split / /, $value; # extract hex channel colors
055 push @{ $TileRGB{ $key } }, hex for @rgb; # create RGB array of decimal values
056 $TileColor{ $key } = '#' . join '', @rgb; # create rgb fill attribute
057 $TileId{ $key } = ( split /-/, $key )[0]; # extract tile id code
058 } # until all chart entries processed
059 } #end naked block
060 undef %ColorChart; #discard the color chart
061
062 my %TileCounts; #hash for tile totals per type
063 my $Blueprint; #mosaic assembly instructions
064 #-------------------------------------------------------------------------------------------------------------------------------
065 # 1) SETUP MOSAIC IMAGE:
066 my $source = Image::Magick->new; #instantiate an image object for the source
067 my ( $SourceWidth, #get width &
068 $SourceHeight #get height
069 ) = ( $source->Ping( $SourceFile ) )[0,1]; #of source
070 undef $source; #destroy the image object for the source
071
072 my $No_Tile_Cols = $SourceWidth / $TileWidth; #compute raw number of tile columns
073 $No_Tile_Cols = int( ++$No_Tile_Cols ) #adjust to an integral number
074 unless $No_Tile_Cols == int( $No_Tile_Cols ); # if necessary
075 my $No_Tile_Rows = $SourceHeight / $TileHeight; #compute raw number of tile rows
076 $No_Tile_Rows = int( ++$No_Tile_Rows ) #adjust to an integral number
077 unless $No_Tile_Rows == int( $No_Tile_Rows ); # if necessary
078
079 my $MosaicWidth = $No_Tile_Cols * $TileWidth; #compute pixel width of mosaic image
080 my $MosaicHeight = $No_Tile_Rows * $TileHeight; #compute pixel height of mosaic image
081 my $Mosaic = Image::Magick->new( magick=>"GIF" ); #instantiate an image object for the mosaic
082 $Mosaic->Read( $SourceFile ); #init mosaic with source image file
083 $Mosaic->Quantize( colors=>scalar keys %TileColor, colorspace=>'RGB' ); #insure correct colorspace
084 $Mosaic->Scale( width=>$MosaicWidth, height=>$MosaicHeight ); #scale mosaic image
085
086 print "Source Image: $SourceFile\n", #echo initialization results
087 " Width: $SourceWidth px\n",
088 " Height: $SourceHeight px\n",
089 "Mosaic Image: $MosaicFile\n",
090 " Width: $MosaicWidth px\n",
091 " Height: $MosaicHeight px\n",
092 "Report File: $ReportFile\n\n",
093 "Mosaic Details: ", $No_Tile_Cols * $No_Tile_Rows, " tiles\n",
094 " No. of rows: $No_Tile_Rows\n",
095 " No. of cols: $No_Tile_Cols\n",
096 " Tile width: $TileWidth px\n",
097 " Tile height: $TileHeight px\n\n";
098 print( "Pause. Press the ENTER key to continue..." ), <STDIN> if $Verbose;
099 #-------------------------------------------------------------------------------------------------------------------------------
100 # 2) CREATE MOSAIC IMAGE:
101 { #start naked block
102 my $best_tile; # name of best matching tile
103 my $colNo; # tile column number
104 my $rowNo; # tile row number
105 my $geometry; # geometry of a tile: width, height & x,y offsets
106 my $x_top_left; # x-coordinate of a tile's top-left corner
107 my $y_top_left; # y-coordinate of a tile's top-left corner
108 my $x_bottom_right; # x-coordinate of a tile's bottom-right corner
109 my $y_bottom_right; # y-coordinate of a tile's bottom-right corner
110
111 print "Creating mosaic image...\n";
112 for ( $y_top_left = $MosaicHeight - $TileHeight; # for each tile row: start at bottom and work up
113 $y_top_left >= 0;
114 $y_top_left -= $TileHeight
115 ) {
116 ++$rowNo; # update tile row number
117 $colNo = 0; # reset tile column number
118 $y_bottom_right = $y_top_left + $TileHeight - 1; # compute y-coord. of the tile's bottom-right corner
119 for ( $x_top_left = 0; # for each tile column: work left to right
120 $x_top_left < $MosaicWidth - 1;
121 $x_top_left += $TileWidth
122 ) {
123 ++$colNo; # update tile column number
124 $x_bottom_right = $x_top_left + $TileWidth - 1; # compute x-coord. of the tile's bottom-right corner
125 print "\tTile# $rowNo,$colNo \@ $x_top_left,$y_top_left-$x_bottom_right,$y_bottom_right: "
126 if $Verbose; # report progress if requested
127
128 $geometry = "${TileWidth}x${TileHeight}+$x_top_left+$y_top_left"; # define tile geometry
129 $best_tile = &bestTileMatch # match mean image-area colors to a tile
130 ( &meanColor # compute mean red color
131 ( $Mosaic->GetPixels # get normalized red intensities of all pixels
132 ( map => 'r',
133 geometry => $geometry,
134 normalize => 'true'
135 )
136 ),
137 &meanColor # compute mean green color
138 ( $Mosaic->GetPixels # get normalized green intensities of all pixels
139 ( map => 'g',
140 geometry => $geometry,
141 normalize => 'true'
142 )
143 ),
144 &meanColor # compute mean blue color
145 ( $Mosaic->GetPixels # get normalized blue intensities of all pixels
146 ( map => 'b',
147 geometry => $geometry,
148 normalize => 'true'
149 )
150 )
151 );
152 $Blueprint .= "$TileId{ $best_tile } "; # add tile id to assembly instructions
153 ++$TileCounts{ $best_tile }; # update corresponding tile total
154
155 $Mosaic->Draw( primitive => 'rectangle', # replace image subregion with tile
156 fill => $TileColor{ $best_tile },
157 points => "$x_top_left,$y_top_left $x_bottom_right,$y_bottom_right"
158 );
159 } # until all tile columns processed
160 $Blueprint .= "\n"; # mark end of row in assembly blueprint
161 } # until all tile rows processed
162 $Mosaic->Write( filename => $MosaicFile ); # save the mosaic to file
163 undef $Mosaic; # destroy the mosaic image object
164 } #end naked block
165 #-------------------------------------------------------------------------------------------------------------------------------
166 # 3) REPORT TILE BREAKDOWN:
167 open REPORT, ">$ReportFile";
168 print "\nWriting the report...\n\n";
169 print REPORT "Tile Totals: ", $No_Tile_Rows * $No_Tile_Cols, " ($No_Tile_Rows rows x $No_Tile_Cols columns)\n\n";
170 printf REPORT "\t%-25s: %d\n", $_, $TileCounts{$_} for sort keys %TileCounts;
171 #-------------------------------------------------------------------------------------------------------------------------------
172 # 4) REPORT AGGREGATED BLUEPRINT:
173 { #start naked block
174 my $repetitions; # number of tile-id repetitions
175
176 print REPORT "\nBlueprint (bottom to top, left to right):\n\n"; # report header
177 study $Blueprint; # prep for substitutions
178 for ( map{ $TileId{ $_ } } keys %TileCounts ) { # for each id of tile present
179 for ( $repetitions = $No_Tile_Cols; $repetitions > 1; --$repetitions ) { # for each possible repetition
180 $Blueprint =~ s/($_ ){$repetitions}/"$_\[$repetitions\] "/esg; # aggregate
181 } # until all repetitions examined
182 } # until all tile ids processed
183
184 REPORT->format_name( "BLUEPRINT" ); # declare report format for blueprint
185 $: = " "; # set the line-break character
186 write REPORT for split /\n/, $Blueprint; # output assembly instructions
187 } #end naked block
188 close REPORT;
189 print "\a\nDone!\n"; #report end of processing
190 exit; #end processing
191 #===== FORMATS =================================================================================================================
192 { #start naked block
193 my $rowNo; # blueprint row number
194
195 format BLUEPRINT = # format for reporting the assembly instructions
196 Row# @>>: ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
197 ++$rowNo, $_
198 ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<~~
199 $_
200 .
201 } #end naked block
202
203 #===== SUBROUTINES =============================================================================================================
204 # Usage : &bestTileMatch( $RED, $GREEN, $BLUE );
205 # Purpose : Matches a tile to the mean RGB channel values of an image area.
206 # Arguments : $RED = mean 8-bit red color
207 # $GREEN = mean 8-bit green color
208 # $BLUE = mean 8-bit blue color
209 # Externals : $TileRGB, $Verbose
210 # Subs : None.
211 # Remarks : None.
212 # History : v1.0.0 - January 23, 2008 - Original release.
213
214 sub bestTileMatch { #begin sub
215 my ( $red, $green, $blue ) = @_; # parametrize the arguments
216 my $bestTile; # name of tile that best matches mean color
217 my $deltaRed; # difference in red color values
218 my $deltaGreen; # difference in green color values
219 my $deltaBlue; # difference in blue color values
220 my $key; # hash key = tile name
221 my $value; # hash value = array of channel color intensities
222 my $metric; # squared Euclidean metric of intensities
223 my $minMetric = 65536 * 65536; # minimum value of the metric
224
225 while( ( $key, $value ) = each %TileRGB ) { # repeat for each possible tile
226 $deltaRed = $red - @{$value}[0]; # compute difference in red colors
227 $deltaGreen = $green - @{$value}[1]; # compute difference in green colors
228 $deltaBlue = $blue - @{$value}[2]; # compute difference in blue colors
229 $metric = $deltaRed * $deltaRed + # compute metric
230 $deltaGreen * $deltaGreen +
231 $deltaBlue * $deltaBlue;
232 $minMetric = $metric, $bestTile = $key if $metric < $minMetric; # update minimum metric & possible best tile
233 } # until all tiles processed
234 print "=> $bestTile\n" if $Verbose; # report if requested
235 return $bestTile; # return best matching tile name
236 } #end sub bestTileMatch
237 #-------------------------------------------------------------------------------------------------------------------------------
238 # Usage : &meanColor( @COLORS );
239 # Purpose : Computes the mean 8-bit channel color.
240 # Arguments : @COLORS = list of normalized channel intensities to be averaged.
241 # Externals : $Verbose
242 # Subs : None.
243 # Remarks : None.
244 # History : v1.0.0 - January 23, 2008 - Original release.
245
246 sub meanColor { #begin sub
247 my $mean; # mean 8-bit color value
248
249 $mean += $_ for @_; # sum all the normalized color values
250 $mean = 256 * $mean / scalar @_; # compute the mean 8-bit value
251 printf "%5.1f ", $mean if $Verbose; # report if requested
252 return $mean; # return the mean
253 } #end sub meanColor
254 #===== Copyright 2008, Webpraxis Consulting Ltd. - ALL RIGHTS RESERVED - Email: webpraxis@gmail.com ============================
255 # end of img2mosaic.pl
'1-White' => 'F2 F3 F2',
'2-Grey' => 'A1 A5 A2',
[snip]
'232-DoveBlue' => '7D BB DD',
'268-MediumLilac' => '34 2B 75',
Tile Totals: 28757 (193 rows x 149 columns)
105-BrightYellowishOrange: 37
108-EarthYellow : 1038
119-BrightYellowishGreen : 18
12-LightOrangeBrown : 307
120-LightYellowishGreen : 106
121-MediumYellowishOrange: 444
127-Gold : 930
128-DarkNougat : 5
137-MediumOrange : 8
138-SandYellow : 370
141-EarthGreen : 1595
147-SandYellowMetallic : 497
148-DarkGreyMetallic : 13
149-BlackMetallic : 14890
150-LightGreyMetallic : 2
168-GunMetallic : 131
18-Nougat : 400
180-Curry : 284
192-ReddishBrown : 933
193-FlameReddishOrange : 5
200-LemonMetalic : 886
209-DarkCurry : 676
210-FadedGreen : 14
216-Rust : 287
217-Brown : 330
224-LightBrickYellow : 165
225-WarmYellowishOrange : 550
226-CoolYellow : 78
25-EarthOrange : 1030
26-Black : 976
29-MediumGreen : 385
3-LightYellow : 70
36-LightYellowichOrange : 73
38-DarkOrange : 565
5-BrickYellow : 659
Blueprint (bottom to top, left to right):
Row# 1: 149[149]
Row# 2: 149[149]
Row# 3: 149[149]
Row# 4: 149[149]
Row# 5: 149[149]
Row# 6: 149[149]
Row# 7: 149[149]
Row# 8: 149[149]
Row# 9: 149[149]
Row# 10: 149[149]
Row# 11: 149[149]
Row# 12: 149[149]
Row# 13: 149[149]
Row# 14: 149[149]
Row# 15: 149[149]
Row# 16: 149[149]
Row# 17: 149[41] 141 26 149[106]
Row# 18: 149[41] 192[2] 141 149[105]
Row# 19: 149[40] 26 141 192[2] 141 149[104]
Row# 20: 149[40] 141[2] 192 216 192[2] 26 149[102]
Row# 21: 149[40] 26 192[3] 216 192[2] 141 149[101]
Row# 22: 149[41] 141 192[3] 216[2] 192 141 26 149[34] 26 149[64]
Row# 23: 149[36] 26[4] 149[2] 192[4] 216 192[3] 141 26 149[29] 26 141[3]
149[64]
Row# 24: 149[33] 26[2] 149 141 192 141 26 149[2] 26 192[3] 216[3] 192[3] 141 26
149[9] 26 149[5] 141 192 141[2] 26 149[6] 141 192[3] 26 149[64]
Row# 25: 149[33] 26[2] 149 26 192[2] 141[2] 26[2] 192[3] 216[4] 192[4] 26
149[5] 26[2] 141 26 149[3] 26 25 192[4] 26 149[4] 141[2] 192[3] 26
149[65]
[snip]
Row# 192: 108[4] 25[3] 108 25 108[16] 168[5] 200 168 108 217 108 168 217 108[2]
217[6] 200 217[3] 200[6] 147 200[2] 210[2] 200[8] 147 200[3] 147[6]
138[2] 147[2] 138 147 138[6] 147 18 147 138[5] 18[4] 138[4] 18 138[2]
18[5] 127 18[2] 127 18[6] 29[7] 5 29[2] 127 5 127[3] 29 5 29[8]
Row# 193: 168 148 108[4] 25 108[4] 168[2] 108[10] 168 217 108 168 200[4] 168
108[3] 217[2] 200[2] 217[6] 200[13] 210[2] 200[2] 147 200[5] 138
200[2] 147 200 147[3] 138[13] 147 138 147 18 209 138[4] 18 209 18[2]
138[10] 18[2] 29 18 29 127 29 18[3] 29 18 29[8] 5 29 127 29 5[3]
29[10]
© 2012 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.