img2mosaic.pl
Original
(132,666 colors)

Mona Lisa

Size of this preview: 193 x 300 pixels.
Full resolution (743 × 1,155 pixels).
Mosaic image filter
=============>
with
assembly instructions
Mosaic
(35 colors, 28,757 tiles @ 5 x 6 pixels)

Mona Lisa mosaic

Size of this preview: 193 x 300 pixels.
Full resolution (745 × 1,158 pixels).
† Image obtained from Wikipedia.

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.

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.


img2mosaic.pl -- (Download latest version  - MD5 checksum )
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
			

legoChart.txt -- (Download)
'1-White'                     => 'F2 F3 F2',
'2-Grey'                      => 'A1 A5 A2',

[snip]

'232-DoveBlue'                => '7D BB DD',
'268-MediumLilac'             => '34 2B 75',
			

Mona_Lisa_5x6_px_tiles.txt -- (Download)
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]
			

© 2024 Webpraxis Consulting Ltd. – ALL RIGHTS RESERVED.

Valid HTML 4.01 Transitional Valid CSS!