photo_mosaic_from_images.pl 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. #!/usr/bin/perl
  2. # Daniel "Trizen" Șuteu
  3. # License: GPLv3
  4. # Date: 06 January 2017
  5. # https://github.com/trizen
  6. # A simple RGB mosaic generator from a collection of images, using the pattern from a given image.
  7. use 5.010;
  8. use strict;
  9. use autodie;
  10. use warnings;
  11. use GD qw();
  12. use POSIX qw(ceil);
  13. use List::Util qw(min);
  14. use File::Find qw(find);
  15. use Getopt::Long qw(GetOptions);
  16. GD::Image->trueColor(1);
  17. my $size = 15;
  18. my $wcrop = 1 / 2; # width crop ratio
  19. my $hcrop = 1 / 6; # height crop ratio
  20. my $output_file = 'mosaic.png';
  21. sub usage {
  22. my ($code) = @_;
  23. print <<"EOT";
  24. usage: $0 [options] [main_image] [photos_dir]
  25. options:
  26. --size=i : the size of a mosaic square (default: $size)
  27. --wcrop=f : width cropping ratio (default: $wcrop)
  28. --hcrop=f : height cropping ratio (default: $hcrop)
  29. --output=s : output filename (default: $output_file)
  30. example:
  31. perl $0 --size=20 main.jpg images
  32. EOT
  33. exit($code);
  34. }
  35. GetOptions(
  36. 'size=i' => \$size,
  37. 'wcrop=f' => \$wcrop,
  38. 'hcrop=f' => \$hcrop,
  39. 'output=s' => \$output_file,
  40. 'h|help' => sub { usage(0) },
  41. )
  42. or die("$0: error in command line arguments\n");
  43. sub analyze_image {
  44. my ($file, $images) = @_;
  45. my $img = eval { GD::Image->new($file) } || return;
  46. say "Analyzing: $file";
  47. $img = resize_image($img);
  48. my ($width, $height) = $img->getBounds;
  49. my $red_avg = 0;
  50. my $green_avg = 0;
  51. my $blue_avg = 0;
  52. my $avg = 0;
  53. my $pixels = $width * $height;
  54. foreach my $y (0 .. $height - 1) {
  55. foreach my $x (0 .. $width - 1) {
  56. my $pixel = $img->getPixel($x, $y);
  57. my ($red, $green, $blue) = $img->rgb($pixel);
  58. $avg += ($red + $green + $blue) / 3 / $pixels;
  59. $red_avg += $red / $pixels;
  60. $green_avg += $green / $pixels;
  61. $blue_avg += $blue / $pixels;
  62. }
  63. }
  64. my ($x, $y, $z) = map { ($_ + $avg) / 2 } ($red_avg, $green_avg, $blue_avg);
  65. push @{$images->[$x][$y][$z]}, $img;
  66. }
  67. sub resize_image {
  68. my ($image) = @_;
  69. # Get image dimensions
  70. my ($width, $height) = $image->getBounds();
  71. # File is already at the wanted resolution
  72. if ($width == $size and $height == $size) {
  73. return $image;
  74. }
  75. # Get the minimum ratio
  76. my $min_r = min($width / $size, $height / $size);
  77. my $n_width = sprintf('%.0f', $width / $min_r);
  78. my $n_height = sprintf('%.0f', $height / $min_r);
  79. # Create a new GD image with the new dimensions
  80. my $gd = GD::Image->new($n_width, $n_height);
  81. $gd->copyResampled($image, 0, 0, 0, 0, $n_width, $n_height, $width, $height);
  82. # Create a new GD image with the wanted dimensions
  83. my $cropped = GD::Image->new($size, $size);
  84. # Crop from left and right
  85. if ($n_width > $size) {
  86. my $diff = $n_width - $size;
  87. my $left = ceil($diff * $wcrop);
  88. $cropped->copy($gd, 0, 0, $left, 0, $size, $size);
  89. }
  90. # Crop from top and bottom
  91. elsif ($n_height > $size) {
  92. my $diff = $n_height - $size;
  93. my $top = int($diff * $hcrop);
  94. $cropped->copy($gd, 0, 0, 0, $top, $size, $size);
  95. }
  96. # No crop needed
  97. else {
  98. $cropped = $gd;
  99. }
  100. return $cropped;
  101. }
  102. sub find_closest {
  103. my ($red, $green, $blue, $images) = @_;
  104. my ($R, $G, $B);
  105. # Finds the closest red value
  106. for (my $j = 0 ; ; ++$j) {
  107. if (exists($images->[$red + $j]) and defined($images->[$red + $j])) {
  108. $R = $images->[$red + $j];
  109. last;
  110. }
  111. if ($red - $j >= 0 and defined($images->[$red - $j])) {
  112. $R = $images->[$red - $j];
  113. last;
  114. }
  115. }
  116. # Finds the closest green value
  117. for (my $j = 0 ; ; ++$j) {
  118. if (exists($R->[$green + $j]) and defined($R->[$green + $j])) {
  119. $G = $R->[$green + $j];
  120. last;
  121. }
  122. if ($green - $j >= 0 and defined($R->[$green - $j])) {
  123. $G = $R->[$green - $j];
  124. last;
  125. }
  126. }
  127. # Finds the closest blue value
  128. for (my $j = 0 ; ; ++$j) {
  129. if (exists($G->[$blue + $j]) and defined($G->[$blue + $j])) {
  130. $B = $G->[$blue + $j];
  131. last;
  132. }
  133. if ($blue - $j >= 0 and defined($G->[$blue - $j])) {
  134. $B = $G->[$blue - $j];
  135. last;
  136. }
  137. }
  138. $B->[rand @$B]; # returns a random image (when there are more candidates)
  139. }
  140. my $main_file = shift(@ARGV) // usage(2);
  141. my @photo_dirs = (@ARGV ? @ARGV : usage(2));
  142. my $img = GD::Image->new($main_file) || die "Can't load image `$main_file`: $!";
  143. if ($size <= 0) {
  144. die "$0: size must be greater than zero (got: $size)\n";
  145. }
  146. my @images; # stores all the image objects
  147. find {
  148. no_chdir => 1,
  149. wanted => sub {
  150. if (/\.(?:jpe?g|png)\z/i) {
  151. analyze_image($_, \@images);
  152. }
  153. },
  154. } => @photo_dirs;
  155. my ($width, $height) = $img->getBounds;
  156. my $mosaic = GD::Image->new($width, $height);
  157. foreach my $y (0 .. $height / $size) {
  158. foreach my $x (0 .. $width / $size) {
  159. $mosaic->copy(find_closest($img->rgb($img->getPixel($x * $size, $y * $size)), \@images),
  160. $x * $size, $y * $size, 0, 0, $size, $size);
  161. }
  162. }
  163. open my $fh, '>:raw', $output_file;
  164. print $fh (
  165. $output_file =~ /\.png\z/i
  166. ? $mosaic->png
  167. : $mosaic->jpeg
  168. );
  169. close $fh;