require "fileutils"
require "json"
require "open3"
require "tmpdir"
MAX = 750
QUALITY = 80
CORPUS = File.expand_path("../quality/corpus", __dir__)
GROUPS = {
"large 4000x2667" => "large/*.jpg",
"medium 2000x1334" => "medium/*.jpg",
"kodak 768x512" => "src/*.jpg"
}.freeze
GROUP_LIMIT = {"kodak 768x512" => 8}.freeze
CONTENDERS = %w[oximg ruby-vips image_processing/vips image_processing/magick mini_magick].freeze
def sources(group)
files = Dir[File.join(CORPUS, GROUPS.fetch(group))].sort
(limit = GROUP_LIMIT[group]) ? files.first(limit) : files
end
def sh(*cmd)
out, err, status = Open3.capture3(*cmd.map(&:to_s))
raise "#{cmd.first} failed: #{err}" unless status.success?
out
end
def contender(name)
case name
when "oximg"
require "oximg"
->(src, out) { Oximg.resize(src, out, width: MAX, height: MAX, quality: QUALITY) }
when "ruby-vips"
require "vips"
Vips.cache_set_max(0)
->(src, out) { Vips::Image.thumbnail(src, MAX, height: MAX, size: :down).jpegsave(out, Q: QUALITY) }
when "image_processing/vips"
require "image_processing/vips"
Vips.cache_set_max(0)
lambda { |src, out|
ImageProcessing::Vips.source(src).convert("jpeg")
.resize_to_limit(MAX, MAX).saver(quality: QUALITY).call(destination: out)
}
when "image_processing/magick"
require "image_processing/mini_magick"
lambda { |src, out|
ImageProcessing::MiniMagick.source(src).convert("jpeg")
.resize_to_limit(MAX, MAX).saver(quality: QUALITY).call(destination: out)
}
when "mini_magick"
require "mini_magick"
lambda { |src, out|
MiniMagick.convert do |c|
c << src
c.resize "#{MAX}x#{MAX}>"
c.quality QUALITY
c << out
end
}
else
raise ArgumentError, "unknown contender #{name}"
end
end
def run_child(name, group, reps, dir)
run = contender(name)
return puts(JSON.generate(baseline: true)) if group == "--load-only"
times = []
bytes = []
dims = []
sources(group).each do |src|
out = File.join(dir, "#{name.tr("/", "-")}-#{File.basename(src)}")
per_image = reps.times.map do
FileUtils.rm_f(out)
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
run.call(src, out)
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000
end
times << per_image.min
bytes << File.size(out)
dims << out
end
t = Process.times
puts JSON.generate(ms: times.sum / times.size, bytes: bytes.sum / bytes.size,
cpu: t.utime + t.stime + t.cutime + t.cstime, outputs: dims)
end
DARWIN = RbConfig::CONFIG["host_os"].include?("darwin")
TIME = File.executable?("/usr/bin/time")
def peak_rss_mb(stderr)
if DARWIN
stderr[/(\d+)\s+maximum resident set size/, 1].to_i / 1048576.0
else
stderr[/Maximum resident set size \(kbytes\):\s*(\d+)/, 1].to_i / 1024.0
end
end
def measure(name, group, reps, dir)
child = [RbConfig.ruby, __FILE__, "--child", name, group, reps.to_s, dir]
cmd = TIME ? ["/usr/bin/time", DARWIN ? "-l" : "-v", *child] : child
out, err, status = Open3.capture3(*cmd)
raise "#{name}/#{group} failed:\n#{err}" unless status.success?
json = JSON.parse(out.lines.last.to_s, symbolize_names: true)
json.merge(rss_mb: TIME ? peak_rss_mb(err) : nil)
end
def score(src, out, refs, dir)
w, h = sh("magick", "identify", "-format", "%w %h", out).split.map(&:to_i)
return [nil, w, h] unless SCORING
ref = refs[[src, w, h]] ||= begin
path = File.join(dir, "ref-#{File.basename(src, ".*")}-#{w}x#{h}.png")
sh("magick", src, "-colorspace", "RGB", "-filter", "Lanczos",
"-resize", "#{w}x#{h}!", "-colorspace", "sRGB", path)
path
end
[sh("ssimulacra2", ref, out).strip.to_f, w, h]
end
if ARGV.first == "--child"
_, name, group, reps, dir = ARGV
run_child(name, group, reps.to_i, dir)
exit
end
SCORING = ENV["PATH"].to_s.split(File::PATH_SEPARATOR)
.any? { |dir| File.executable?(File.join(dir, "ssimulacra2")) }
reps = (ARGV[0] || 5).to_i
results = {}
baselines = {}
refs = {}
Dir.mktmpdir("oximg-bench") do |dir|
CONTENDERS.each { |name| baselines[name] = measure(name, "--load-only", 0, dir) }
GROUPS.each_key do |group|
next if sources(group).empty?
CONTENDERS.each do |name|
r = measure(name, group, reps, dir)
if SCORING
scores = sources(group).zip(r[:outputs]).map { |src, out| score(src, out, refs, dir).first }
r[:ssim2] = scores.sum / scores.size
end
r[:dims] = sh("magick", "identify", "-format", "%wx%h", r[:outputs].first).strip
results[[group, name]] = r
end
end
end
host = DARWIN ? sh("sysctl", "-n", "machdep.cpu.brand_string").strip : `lscpu`[/Model name:\s*(.+)/, 1].to_s.strip
puts "\n#{`uname -srm`.strip} | #{host}"
puts "ruby #{RUBY_VERSION} | #{sh("magick", "-version").lines.first.strip}"
puts "fit within #{MAX}x#{MAX}, JPEG q#{QUALITY}, best of #{reps} per image\n"
if TIME
puts "baseline RSS, gem loaded, nothing processed:"
baselines.each { |name, b| puts format(" %-24s %6.1f MB", name, b[:rss_mb]) }
else
puts "peak RSS unavailable: /usr/bin/time is not installed"
end
GROUPS.each_key do |group|
rows = CONTENDERS.filter_map { |name| [name, results[[group, name]]] if results[[group, name]] }
next if rows.empty?
n = sources(group).size
puts "\n## #{group} (n=#{n}, out #{rows.first[1][:dims]})"
puts format("%-24s %9s %9s %10s %9s %12s", "gem", "ms", "cpu s", "peak RSS", "KB", "SSIMULACRA2")
rows.each do |name, r|
puts format("%-24s %9.1f %9.2f %10s %9.1f %12s",
name, r[:ms], r[:cpu], r[:rss_mb] ? format("%.1f MB", r[:rss_mb]) : "-",
r[:bytes] / 1024.0, r[:ssim2] ? format("%.2f", r[:ssim2]) : "-")
end
end
File.write(File.join(__dir__, "results-#{DARWIN ? "darwin" : "linux"}.json"), JSON.pretty_generate(
results.map { |(group, name), r| {group: group, gem: name}.merge(r.reject { |k, _| k == :outputs }) }
))
puts