I needed to resize a folder of images the other day. So I wrote this handy script:
Update (08/10/12) — I had a conversation about shaders and threading with a SIGGRAPH attendee on a flight from LA back home. As a result of that conversation I've decided to multi-thread this script in an effort to make it faster. In the process I beat the crap out of my MacBook Pro but it took the beating like a champ. Thankfully this made me realize that the Ruby garbage collection wasn't working fast enough. So I added some code to free the image resources when they were no longer being used. This had an added benefit of speeding things up even more. I found the Parallel gem which made the multi-threading part of it insanely easy. See for yourself below.
#!/usr/bin/ruby
require "RMagick" # image processing wrapper for ImageMagick
require "Parallel" # https://github.com/grosser/parallel
# ensure dirs are available for writing
["thumbs","full","processed"].each do |dir|
unless File.directory? dir
puts "Creating #{dir}/"
Dir.mkdir dir
end
end
# encapsulates the image resizing logic
def resizeImage(f)
# get the first frame of the image (JPGs usually have only one)
img = Magick::Image.read(f).first
# prep thumb and full sizes
thumb = img.resize_to_fit(100, 100)
full = img.resize_to_fit(580, 580)
# write resized images
thumb.write("thumbs/#{f}") {self.quality = 60}
full.write("full/#{f}") {self.quality = 75}
# free up RAM
img.destroy!
thumb.destroy!
full.destroy!
end
# Loop through items in the dir
files = Dir.entries "."
# clean file list of known non-files
files.delete_if {|f| [".", "..", ".DS_Store"].index(f) != nil or Dir.directory?(f) }
# let the user know we've begun...
puts ("Resizing #{totalFiles} files...")
# process the images in parallel threads (up to 4)
completed = Parallel.each(files){|file|
if File.file? file
resizeImage(file)
# move the file to processed folder
system("mv \"#{file}\" processed/")
# micro-sleep so other processes can get some CPU time
sleep 0.3
end
}
# Show a growl notification if available.
system("growlnotify --appIcon Terminal -m 'Finshed resizing images!' Ruby")
puts "Finished resizing #{completed.length} images"
The script uses the multi-process threading model which allows the Parallel gem to spawn a process for every native thread available on each processing core of the machine it's running on. With my early 2012 MBP with an Intel Core i7 quad-core CPU that means 8 threads at once. That's an increase of 800% from what the original script could do. I highly recommend this gem for your simple parallel processing needs. I suppose you might not need the micro-sleep
in the worker block if you ran the script through nice
but I find that it doesn't affect the speed too much as-is.
I've included the first version for your comparing pleasure. It is quite primitive. I updated it with the bits to free up resources so it should run a bit faster too.
#!/usr/bin/ruby
require "RMagick"
# ensure dirs are available for writing
["thumbs","full","processed"].each do |dir|
unless File.directory? dir
Dir.mkdir dir
end
end
# Loop through items in the dir
files = Dir.entries "."
fileCounter = 1
totalFiles = files.length-2
files.each do |f|
if File.directory? f
puts "Skipping directory: #{f}"
next
end
if [".", "..", ".DS_Store"].index(f) == nil
puts "Resizing file: #{f} - #{fileCounter} of #{totalFiles}"
img = Magick::Image.read(f).first
thumb = img.resize_to_fit(100, 100)
full = img.resize_to_fit(580, 580)
thumb.write("thumbs/#{f}") {self.quality = 60}
full.write("full/#{f}") {self.quality = 75}
# free up RAM
img.destroy!
thumb.destroy!
full.destroy!
fileCounter += 1
# move the file to processed folder
system("mv \"#{f}\" processed/")
end
sleep 0.5
end
system("growlnotify -t 'Ruby' -m 'Finshed resizing images!'")
puts "Done."
I sincerely hope this helps someone. Enjoy!
Comments
Chritof 2013-02-12 06:43:04 -0500
if !File.directory? dir => unless File.directory? dir
Stephen 2013-02-17 18:02:42 -0500
Thanks for the tip. I still have much to learn about ruby.