From 22a355fbc2e6d3ba65b214749178083235cf099d Mon Sep 17 00:00:00 2001 From: Alexei Matyushkin Date: Fri, 24 Oct 2014 19:07:09 +0200 Subject: [PATCH] Radon transform is added. Hough is still broken. Canny is still producing thick lines. --- bin/magick_canny | 7 - bin/magick_scale | 2 +- lib/rmagick/screwdrivers.rb | 10 + lib/rmagick/screwdrivers/canny.rb | 482 ++++++++++++++-------------- lib/rmagick/screwdrivers/collage.rb | 80 +++-- lib/rmagick/screwdrivers/helpers.rb | 21 +- lib/rmagick/screwdrivers/hough.rb | 114 ++----- lib/rmagick/screwdrivers/poster.rb | 173 +++++----- lib/rmagick/screwdrivers/radon.rb | 327 +++++++++++++++++++ lib/rmagick/screwdrivers/scale.rb | 108 ++++--- lib/rmagick/screwdrivers/sobel.rb | 107 +++--- 11 files changed, 880 insertions(+), 551 deletions(-) create mode 100644 lib/rmagick/screwdrivers/radon.rb diff --git a/bin/magick_canny b/bin/magick_canny index afab18e..bbb7c73 100755 --- a/bin/magick_canny +++ b/bin/magick_canny @@ -100,10 +100,3 @@ outfile = File.basename(file).sub(/(\.\w+)$/, "-canny\\1") Magick::Screwdrivers::canny(file, options).write(File.join File.dirname(file), outfile) { self.quality = options.quality || 90 } - -if options.hough - houghfile = File.basename(file).sub(/(\.\w+)$/, "-hough\\1") - Magick::Screwdrivers::hough(file, {:roughly => options.roughly, :logger => options.logger}).write(File.join File.dirname(file), houghfile) { - self.quality = options.quality || 90 - } -end \ No newline at end of file diff --git a/bin/magick_scale b/bin/magick_scale index 9d0e867..5b7f0f4 100755 --- a/bin/magick_scale +++ b/bin/magick_scale @@ -67,7 +67,7 @@ file = ARGV.first dir = ARGV.size == 2 ? ARGV.last : File.dirname(file) `mkdir -p #{dir}` -Magick::Screwdrivers::scale(file, options).each { |img| +Magick::Screwdrivers::scale_fan(file, options).each { |img| outfile = File.basename(file).sub(/(\.\w+)$/, "-#{img.rows}×#{img.columns}\\1") img.write(File.join dir, outfile) { self.quality = options[:quality] || 90 diff --git a/lib/rmagick/screwdrivers.rb b/lib/rmagick/screwdrivers.rb index 80b1577..c59cf55 100644 --- a/lib/rmagick/screwdrivers.rb +++ b/lib/rmagick/screwdrivers.rb @@ -6,12 +6,22 @@ require "rmagick/screwdrivers/hough" require "rmagick/screwdrivers/sobel" require "rmagick/screwdrivers/canny" +require "rmagick/screwdrivers/radon" module Magick + module Screwdrivers + class << self + attr_reader :options + end + @options = { + :logger => nil + } + end class Image # monkeypatch # (0.299 * px.red + 0.587 * px.green + 0.114 * px.blue).ceil def at x, y self.pixel_color(x, y).intensity end end + end diff --git a/lib/rmagick/screwdrivers/canny.rb b/lib/rmagick/screwdrivers/canny.rb index b43fc6f..f0c9b4d 100644 --- a/lib/rmagick/screwdrivers/canny.rb +++ b/lib/rmagick/screwdrivers/canny.rb @@ -4,256 +4,248 @@ # @author Tom Gibara # ported from java Alexei Matyushkin - require 'RMagick' require 'date' require 'ostruct' module Magick module Screwdrivers - module Canny - GAUSSIAN_CUT_OFF = 0.005 - MAGNITUDE_SCALE = 100.0 - MAGNITUDE_LIMIT = 1000.0 - MAGNITUDE_MAX = (MAGNITUDE_SCALE * MAGNITUDE_LIMIT).ceil - - def self.gaussian x, sigma - Math.exp(-(x * x) / (2.0 * sigma * sigma)) - end - - def self.hypot x, y - Math.hypot x, y # x.abs + y.abs - end - - def self.normalizeContrast data - histogram = (0...256).map { 0 } - for i in 0...data.length - histogram[data[i]] += 1 - end - remap = (0...256).map { 0 } - sum = 0 - j = 0 - for i in 0...histogram.length - sum += histogram[i] - target = sum * (256 - 1) / data.length - for k in (j + 1)..target - remap[k] = i - end - j = target - end - for i in 0...data.length - data[i] = remap[data[i]] - end - data - end - - # NOTE: The elements of the method below (specifically the technique for - # non-maximal suppression and the technique for gradient computation) - # are derived from an implementation posted in the following forum (with the - # clear intent of others using the code): - # http:# forum.java.sun.com/thread.jspa?threadID=546211&start=45&tstart=0 - # My code effectively mimics the algorithm exhibited above. - # Since I don't know the providence of the code that was posted it is a - # possibility (though I think a very remote one) that this code violates - # someone's intellectual property rights. If this concerns you feel free to - # contact me for an alternative, though less efficient, implementation. - - def self.computeGradients kernelRadius, kernelWidth, width, height, data - xConv = (0...width*height).map { 0.0 } - yConv = (0...width*height).map { 0.0 } - - xGradient = (0...width*height).map { 0.0 } - yGradient = (0...width*height).map { 0.0 } - - magnitude = (0...width*height).map { 0 } - - # generate the gaussian convolution masks - kernel = (0...kernelWidth).map { 0.0 } - diffKernel = (0...kernelWidth).map { 0.0 } - for kwidth in 0...kernelWidth - g1 = self.gaussian(kwidth, kernelRadius) - next if (g1 <= Canny::GAUSSIAN_CUT_OFF && kwidth >= 2) - g2 = self.gaussian(kwidth - 0.5, kernelRadius) - g3 = self.gaussian(kwidth + 0.5, kernelRadius) - kernel[kwidth] = (g1 + g2 + g3) / 3.0 / (2.0 * Math::PI * kernelRadius * kernelRadius) - diffKernel[kwidth] = g3 - g2 - end - - initX = kernelWidth - 1 - maxX = width - initX - initY = width * (kernelWidth - 1) - maxY = width * height - initY - - # perform convolution in x and y directions - for x in initX...maxX - (initY...maxY).step(width) { |y| - index = x + y - sumY = sumX = data[index] * kernel[0] - xOffset = 1 - yOffset = width - while xOffset < kernelWidth - sumY += kernel[xOffset] * (data[index - yOffset] + data[index + yOffset]) - sumX += kernel[xOffset] * (data[index - xOffset] + data[index + xOffset]) - yOffset += width - xOffset += 1 - end - yConv[index] = sumY - xConv[index] = sumX - } - end - - for x in initX...maxX - (initY...maxY).step(width) { |y| - sum = 0.0 - index = x + y - for i in 1...kernelWidth - sum += diffKernel[i] * (yConv[index - i] - yConv[index + i]) - end - xGradient[index] = sum - } - end - - for x in kernelWidth...(width - kernelWidth) - (initY...maxY).step(width) { |y| - sum = 0.0 - index = x + y - yOffset = width - for i in 1...kernelWidth - sum += diffKernel[i] * (xConv[index - yOffset] - xConv[index + yOffset]) - yOffset += width - end - yGradient[index] = sum - } - end - - initX = kernelWidth - maxX = width - kernelWidth - initY = width * kernelWidth - maxY = width * (height - kernelWidth) - - for x in initX...maxX - (initY...maxY).step(width) { |y| - index = x + y - indexN = index - width - indexS = index + width - indexW = index - 1 - indexE = index + 1 - indexNW = indexN - 1 - indexNE = indexN + 1 - indexSW = indexS - 1 - indexSE = indexS + 1 - - xGrad = xGradient[index] - yGrad = yGradient[index] - gradMag = self.hypot(xGrad, yGrad) - - # perform non-maximal supression - nMag = self.hypot(xGradient[indexN], yGradient[indexN]) - sMag = self.hypot(xGradient[indexS], yGradient[indexS]) - wMag = self.hypot(xGradient[indexW], yGradient[indexW]) - eMag = self.hypot(xGradient[indexE], yGradient[indexE]) - neMag = self.hypot(xGradient[indexNE], yGradient[indexNE]) - seMag = self.hypot(xGradient[indexSE], yGradient[indexSE]) - swMag = self.hypot(xGradient[indexSW], yGradient[indexSW]) - nwMag = self.hypot(xGradient[indexNW], yGradient[indexNW]) - - # please refer to http://www.tomgibara.com/computer-vision/CannyEdgeDetector.java for algorithm explanation - if (xGrad * yGrad <= 0.0 ? - (xGrad).abs >= (yGrad).abs ? - (tmp = (xGrad * gradMag).abs) >= (yGrad * neMag - (xGrad + yGrad).abs * eMag) && - tmp > (yGrad * swMag - (xGrad + yGrad).abs * wMag) : - (tmp = (yGrad * gradMag).abs) >= (xGrad * neMag - (yGrad + xGrad).abs * nMag) && - tmp > (xGrad * swMag - (yGrad + xGrad).abs * sMag) : - (xGrad).abs >= (yGrad).abs ? - (tmp = (xGrad * gradMag).abs) >= (yGrad * seMag + (xGrad - yGrad).abs * eMag) && - tmp > (yGrad * nwMag + (xGrad - yGrad).abs * wMag) : - (tmp = (yGrad * gradMag).abs) >= (xGrad * seMag + (yGrad - xGrad).abs * sMag) && - tmp > (xGrad * nwMag + (yGrad - xGrad).abs * nMag) - ) - magnitude[index] = gradMag >= Canny::MAGNITUDE_LIMIT ? Canny::MAGNITUDE_MAX : (MAGNITUDE_SCALE * gradMag).ceil - # NOTE: The orientation of the edge is not employed by this - # implementation. It is a simple matter to compute it at - # this poas: Math.atan2(yGrad, xGrad) - else - magnitude[index] = 0 - end - } - end - magnitude - end - - def self.follow x1, y1, i1, threshold, width, height, data, magnitude - x0 = x1 == 0 ? x1 : x1 - 1 - x2 = x1 == width - 1 ? x1 : x1 + 1 - y0 = y1 == 0 ? y1 : y1 - 1 - y2 = y1 == height - 1 ? y1 : y1 + 1 - - data[i1] = magnitude[i1] - for x in x0..x2 - for y in y0..y2 - i2 = x + y * width - if ((y != y1 || x != x1) && data[i2] == 0 && magnitude[i2] >= threshold) - self.follow x, y, i2, threshold, width, height, data, magnitude - return - end - end - end - end - end - - def self.canny file, options = OpenStruct.new - options.lowThreshold = 2.5 unless options.lowThreshold && options.lowThreshold >= 0 - options.highThreshold = 7.5 unless options.highThreshold && options.highThreshold >= 0 - options.kernelRadius = 2.0 unless options.kernelRadius && options.kernelRadius >= 0.1 - options.kernelWidth = 16 unless options.kernelWidth && options.kernelWidth >= 2 - options.color ||= 'black' - options.contrastNormalized = !!options.contrastNormalized - options.roughly = !!options.roughly - - begin - orig = img_from_file(file) - orig = orig.gaussian_blur 0, 3.0 - orig = orig.quantize 256, Magick::GRAYColorspace unless options.roughly - rescue - warn(options.logger, "Skipping invalid file #{file}…") - return nil - end - - width = orig.columns - height = orig.rows - - data = [] - - for r in 0...height - for c in 0...width - data[r*width + c] = (orig.at(c, r) * 256 / Magick::QuantumRange).ceil - end - end - - data = Canny::normalizeContrast data if options.contrastNormalized - - magnitude = Canny::computeGradients options.kernelRadius, options.kernelWidth, width, height, data - - data = (0...width*height).map { 0 } - offset = 0 - for y in 0...height - for x in 0...width - if (data[offset] == 0 && magnitude[offset] >= (options.highThreshold * Canny::MAGNITUDE_SCALE).ceil) - Canny::follow(x, y, offset, (options.lowThreshold * Canny::MAGNITUDE_SCALE).ceil, width, height, data, magnitude) - end - offset += 1 - end - end - - edge = Image.new(orig.columns, orig.rows) { - self.background_color = options.color - } - - for i in 0...data.length - edge.pixel_color i % width, i / width, Pixel.from_color('white') if data[i] > 0 - end - - edge - end + module Canny + attr_reader :options + @options = { + :lowThreshold => 2.5, + :highThreshold => 7.5, + :kernelRadius => 2.0, + :kernelWidth => 16, + :color => 'black', + :contrastNormalized => false, + :roughly => false + } + + GAUSSIAN_CUT_OFF = 0.005 + MAGNITUDE_SCALE = 100.0 + MAGNITUDE_LIMIT = 1000.0 + MAGNITUDE_MAX = (MAGNITUDE_SCALE * MAGNITUDE_LIMIT).ceil + + def self.gaussian x, sigma + Math.exp(-(x * x) / (2.0 * sigma * sigma)) + end + + def self.hypot x, y + Math.hypot x, y # x.abs + y.abs + end + + # NOTE: The elements of the method below (specifically the technique for + # non-maximal suppression and the technique for gradient computation) + # are derived from an implementation posted in the following forum (with the + # clear intent of others using the code): + # http:# forum.java.sun.com/thread.jspa?threadID=546211&start=45&tstart=0 + # My code effectively mimics the algorithm exhibited above. + # Since I don't know the providence of the code that was posted it is a + # possibility (though I think a very remote one) that this code violates + # someone's intellectual property rights. If this concerns you feel free to + # contact me for an alternative, though less efficient, implementation. + + def self.computeGradients kernelRadius, kernelWidth, width, height, data + xConv = (0...width*height).map { 0.0 } + yConv = (0...width*height).map { 0.0 } + + xGradient = (0...width*height).map { 0.0 } + yGradient = (0...width*height).map { 0.0 } + + magnitude = (0...width*height).map { 0 } + + # generate the gaussian convolution masks + kernel = (0...kernelWidth).map { 0.0 } + diffKernel = (0...kernelWidth).map { 0.0 } + for kwidth in 0...kernelWidth + g1 = self.gaussian(kwidth, kernelRadius) + next if (g1 <= Canny::GAUSSIAN_CUT_OFF && kwidth >= 2) + g2 = self.gaussian(kwidth - 0.5, kernelRadius) + g3 = self.gaussian(kwidth + 0.5, kernelRadius) + kernel[kwidth] = (g1 + g2 + g3) / 3.0 / (2.0 * Math::PI * kernelRadius * kernelRadius) + diffKernel[kwidth] = g3 - g2 + end + + initX = kernelWidth - 1 + maxX = width - initX + initY = width * (kernelWidth - 1) + maxY = width * height - initY + + # perform convolution in x and y directions + for x in initX...maxX + (initY...maxY).step(width) { |y| + index = x + y + sumY = sumX = data[index] * kernel[0] + xOffset = 1 + yOffset = width + while xOffset < kernelWidth + sumY += kernel[xOffset] * (data[index - yOffset] + data[index + yOffset]) + sumX += kernel[xOffset] * (data[index - xOffset] + data[index + xOffset]) + yOffset += width + xOffset += 1 + end + yConv[index] = sumY + xConv[index] = sumX + } + end + + for x in initX...maxX + (initY...maxY).step(width) { |y| + sum = 0.0 + index = x + y + for i in 1...kernelWidth + sum += diffKernel[i] * (yConv[index - i] - yConv[index + i]) + end + xGradient[index] = sum + } + end + + for x in kernelWidth...(width - kernelWidth) + (initY...maxY).step(width) { |y| + sum = 0.0 + index = x + y + yOffset = width + for i in 1...kernelWidth + sum += diffKernel[i] * (xConv[index - yOffset] - xConv[index + yOffset]) + yOffset += width + end + yGradient[index] = sum + } + end + + initX = kernelWidth + maxX = width - kernelWidth + initY = width * kernelWidth + maxY = width * (height - kernelWidth) + + for x in initX...maxX + (initY...maxY).step(width) { |y| + index = x + y + indexN = index - width + indexS = index + width + indexW = index - 1 + indexE = index + 1 + indexNW = indexN - 1 + indexNE = indexN + 1 + indexSW = indexS - 1 + indexSE = indexS + 1 + + xGrad = xGradient[index] + yGrad = yGradient[index] + gradMag = self.hypot(xGrad, yGrad) + + # perform non-maximal supression + nMag = self.hypot(xGradient[indexN], yGradient[indexN]) + sMag = self.hypot(xGradient[indexS], yGradient[indexS]) + wMag = self.hypot(xGradient[indexW], yGradient[indexW]) + eMag = self.hypot(xGradient[indexE], yGradient[indexE]) + neMag = self.hypot(xGradient[indexNE], yGradient[indexNE]) + seMag = self.hypot(xGradient[indexSE], yGradient[indexSE]) + swMag = self.hypot(xGradient[indexSW], yGradient[indexSW]) + nwMag = self.hypot(xGradient[indexNW], yGradient[indexNW]) + + # please refer to http://www.tomgibara.com/computer-vision/CannyEdgeDetector.java for algorithm explanation + if (xGrad * yGrad <= 0.0 ? + (xGrad).abs >= (yGrad).abs ? + (tmp = (xGrad * gradMag).abs) >= (yGrad * neMag - (xGrad + yGrad).abs * eMag) && + tmp > (yGrad * swMag - (xGrad + yGrad).abs * wMag) : + (tmp = (yGrad * gradMag).abs) >= (xGrad * neMag - (yGrad + xGrad).abs * nMag) && + tmp > (xGrad * swMag - (yGrad + xGrad).abs * sMag) : + (xGrad).abs >= (yGrad).abs ? + (tmp = (xGrad * gradMag).abs) >= (yGrad * seMag + (xGrad - yGrad).abs * eMag) && + tmp > (yGrad * nwMag + (xGrad - yGrad).abs * wMag) : + (tmp = (yGrad * gradMag).abs) >= (xGrad * seMag + (yGrad - xGrad).abs * sMag) && + tmp > (xGrad * nwMag + (yGrad - xGrad).abs * nMag) + ) + magnitude[index] = gradMag >= Canny::MAGNITUDE_LIMIT ? Canny::MAGNITUDE_MAX : (MAGNITUDE_SCALE * gradMag).ceil + # NOTE: The orientation of the edge is not employed by this + # implementation. It is a simple matter to compute it at + # this poas: Math.atan2(yGrad, xGrad) + else + magnitude[index] = 0 + end + } + end + magnitude + end + + def self.follow x1, y1, i1, threshold, width, height, data, magnitude + x0 = x1 == 0 ? x1 : x1 - 1 + x2 = x1 == width - 1 ? x1 : x1 + 1 + y0 = y1 == 0 ? y1 : y1 - 1 + y2 = y1 == height - 1 ? y1 : y1 + 1 + + data[i1] = magnitude[i1] + for x in x0..x2 + for y in y0..y2 + i2 = x + y * width + if ((y != y1 || x != x1) && data[i2] == 0 && magnitude[i2] >= threshold) + self.follow x, y, i2, threshold, width, height, data, magnitude + return + end + end + end + end + + def self.yo image, options = {} + options = Magick::Screwdrivers.options.merge(@options).merge(OpenStruct === options ? options.to_h : options) + options[:lowThreshold] = 2.5 unless options[:lowThreshold] && options[:lowThreshold] >= 0 + options[:highThreshold] = 7.5 unless options[:highThreshold] && options[:highThreshold] >= 0 + options[:kernelRadius] = 2.0 unless options[:kernelRadius] && options[:kernelRadius] >= 0.1 + options[:kernelWidth] = 16 unless options[:kernelWidth] && options[:kernelWidth] >= 2 + options[:contrastNormalized] = !!options[:contrastNormalized] + options[:roughly] = !!options[:roughly] + + img = Magick::Screwdrivers.imagify image + + # img = img.gaussian_blur 0, 3.0 + img = img.quantize 256, Magick::GRAYColorspace unless options[:roughly] + + width = img.columns + height = img.rows + + data = [] + + for r in 0...height + for c in 0...width + data[r * width + c] = (img.at(c, r) * 256 / Magick::QuantumRange).ceil + end + end + + magnitude = Canny::computeGradients options[:kernelRadius], options[:kernelWidth], width, height, data + + data = (0...width*height).map { 0 } + offset = 0 + for y in 0...height + for x in 0...width + if (data[offset] == 0 && magnitude[offset] >= (options[:highThreshold] * Canny::MAGNITUDE_SCALE).ceil) + Canny::follow(x, y, offset, (options[:lowThreshold] * Canny::MAGNITUDE_SCALE).ceil, width, height, data, magnitude) + end + offset += 1 + end + end + + edge = Image.new(img.columns, img.rows) { + self.background_color = options[:color] + } + + for i in 0...data.length + edge.pixel_color i % width, i / width, Pixel.from_color('white') if data[i] > 0 + end + + edge + end + end + + def self.canny image, options + Canny::yo image, options + end + end + + class Image + def canny options + Screwdrivers::Canny::yo self, options + end end end \ No newline at end of file diff --git a/lib/rmagick/screwdrivers/collage.rb b/lib/rmagick/screwdrivers/collage.rb index 3ecd3c9..1b27068 100644 --- a/lib/rmagick/screwdrivers/collage.rb +++ b/lib/rmagick/screwdrivers/collage.rb @@ -4,43 +4,65 @@ module Magick module Screwdrivers - def self.collage files, options={} - options = { + module Collage + attr_reader :options + @options = { :columns => 5, :scale_range => 0.1, :thumb_width => 120, :rotate_angle => 20, :background => 'white', :border => '#DDDDDD', - :logger => nil - }.merge(options) - files = "#{files}/*" if File.directory?(files) - imgs = ImageList.new - imgnull = Image.new(options[:thumb_width],options[:thumb_width]) { - self.background_color = 'transparent' } - (options[:columns]+2).times { imgs << imgnull.dup } - Dir.glob("#{files}") { |f| - begin - i = img_from_file(f) - rescue - warn(options[:logger], "Skipping invalid file #{f}…") - next + def self.yo image_list, options = {} + options = Magick::Screwdrivers.options.merge(@options).merge(OpenStruct === options ? options.to_h : options) + + # Here we’ll store our collage + memo = ImageList.new + # Blank image of the proper size to montage properly + imgnull = Image.new(options[:thumb_width],options[:thumb_width]) { self.background_color = 'transparent' } + # Handler for one image addition + img_handler = Proc.new do |memo, img| + unless img.nil? + scale = (1.0 + options[:scale_range] * Random::rand(-1.0..1.0)) * options[:thumb_width] / [img.columns, img.rows].max + memo << imgnull.dup if (memo.size % (options[:columns] + 2)).zero? + memo << img.auto_orient.thumbnail(scale).polaroid(Random::rand(-options[:rotate_angle]..options[:rotate_angle])) + memo << imgnull.dup if (memo.size % (options[:columns] + 2)) == options[:columns] + 1 + end + memo end - scale = (1.0 + options[:scale_range]*Random::rand(-1.0..1.0))*options[:thumb_width]/[i.columns, i.rows].max - imgs << imgnull.dup if (imgs.size % (options[:columns]+2)).zero? - imgs << i.auto_orient.thumbnail(scale).polaroid( - Random::rand(-options[:rotate_angle]..options[:rotate_angle]) - ) - imgs << imgnull.dup if (imgs.size % (options[:columns]+2)) == options[:columns]+1 - } - (2*options[:columns]+4-(imgs.size % (options[:columns]+2))).times { imgs << imgnull.dup } - info options[:logger], "Montaging image [#{options[:columns]}×#{imgs.size/(options[:columns]+2)-2}]" - imgs.montage { - self.tile = Magick::Geometry.new(options[:columns]+2) - self.geometry = "-#{options[:thumb_width]/5}-#{options[:thumb_width]/4}" - self.background_color = options[:background] - }.trim(true).border(10,10,options[:background]).border(1,1,options[:border]) + + (options[:columns] + 2).times { memo << imgnull.dup } # fill first row + + case image_list + when ImageList, Array + image_list.each { |img| img_handler.call memo, Magick::Screwdrivers.imagify(img, true) } + when File, String + Dir.glob("#{image_list}" + (File.directory?(image_list) ? "/*" : "")) { |img| # FIXME should find all images within dir + img_handler.call memo, Magick::Screwdrivers.imagify(img, true) + } + else Magick::Screwdrivers.warn "Unknown type of #{image_list} ⇒ #{image_list.class}" + end + + (2 * options[:columns] + 4 - (memo.size % (options[:columns] + 2))).times { memo << imgnull.dup } # fill last row + + Magick::Screwdrivers.info options[:logger], "Montaging image [#{options[:columns]}×#{memo.size / (options[:columns] + 2) - 2}], options: [#{options}]" + memo.montage { + self.tile = Magick::Geometry.new(options[:columns] + 2) + self.geometry = "-#{options[:thumb_width]/5}-#{options[:thumb_width]/4}" + self.background_color = options[:background] + }.trim(true).border(10, 10, options[:background]).border(1, 1, options[:border]) + end + end + + def self.collage files, options + Collage::yo files, options + end + end + + class ImageList + def collage options + Screwdrivers::Collage::yo self, options end end end diff --git a/lib/rmagick/screwdrivers/helpers.rb b/lib/rmagick/screwdrivers/helpers.rb index 980dcf7..48dbdf7 100644 --- a/lib/rmagick/screwdrivers/helpers.rb +++ b/lib/rmagick/screwdrivers/helpers.rb @@ -9,17 +9,16 @@ module Screwdrivers # == Image preparation ================================= # ============================================================== - def self.img_from_file file - img = Magick::Image::read(file).first - - # case img.orientation - # when Magick::RightTopOrientation - # img.rotate!(90) - # when Magick::BottomRightOrientation - # img.rotate!(180) - # when Magick::LeftBottomOrientation - # img.rotate!(-90) - # end + def self.imagify image, silent = false + img = case image + when File, String then Magick::Image::read(image).first + when Image then image + end + + if img.nil? + Magick::Screwdrivers.warn(options[:logger], "Skipping invalid image descriptor #{image}…") + throw ArgumentError.new("Invalid argument in call to imagify descriptor [#{image}]") unless silent + end img end diff --git a/lib/rmagick/screwdrivers/hough.rb b/lib/rmagick/screwdrivers/hough.rb index 2cdb1a1..4c90cf9 100644 --- a/lib/rmagick/screwdrivers/hough.rb +++ b/lib/rmagick/screwdrivers/hough.rb @@ -5,6 +5,11 @@ module Magick module Screwdrivers module Hough + attr_reader :options + @options = { + :roughly => false + } + def self.is_dark? px px.red + px.green + px.blue < 40 end @@ -12,97 +17,42 @@ def self.is_dark? px def self.is_light? px px.red + px.green + px.blue > 600 end - end - # based on http://jonathan-jackson.net/ruby-hough-hack.html - def self.hough file, options={} - options = { - :roughly => false, - :logger => nil - }.merge(options) + # based on http://jonathan-jackson.net/ruby-hough-hack.html + def self.yo image, options = {} + options = Magick::Screwdrivers.options.merge(@options).merge(OpenStruct === options ? options.to_h : options) - begin - orig = img_from_file(file) -# orig = orig.gaussian_blur 0, 3.0 + orig = Magick::Screwdrivers.imagify image + orig = orig.quantize 256, Magick::GRAYColorspace unless options[:roughly] - rescue - warn(options[:logger], "Skipping invalid file #{file}…") - return nil - end - - trigons = { :cos => [], :sin => [] } - hough = Hash.new(0) - (orig.rows - 1).times do |y| - orig.columns.times do |x| - if Hough::is_dark?(orig.pixel_color(x,y)) && Hough::is_light?(orig.pixel_color(x,y + 1)) - (0..Math::PI).step(0.1).each do |theta| - trigons[:cos][theta] ||= Math.cos(theta) - trigons[:sin][theta] ||= Math.sin(theta) - distance = (x * trigons[:cos][theta] + y * trigons[:sin][theta]).to_i - hough[[theta, distance]] += 1 if distance >= 0 + + trigons = { :cos => [], :sin => [] } + hough = Hash.new(0) + (orig.rows - 1).times do |y| + orig.columns.times do |x| + if Hough::is_dark?(orig.pixel_color(x,y)) && Hough::is_light?(orig.pixel_color(x,y + 1)) + (0..Math::PI).step(0.1).each do |theta| + trigons[:cos][theta] ||= Math.cos(theta) + trigons[:sin][theta] ||= Math.sin(theta) + distance = (x * trigons[:cos][theta] + y * trigons[:sin][theta]).to_i + hough[[theta, distance]] += 1 if distance >= 0 + end end end end + + hough.sort_by { |k,v| v } end - - info options[:logger], 'Will print first 200 results:' - hough.sort_by { |k,v| v }.take(200).each { |v| - info options[:logger], v - } - at = hough.sort_by { |k,v| v }.inject(0.0) { |m,v| m + v[0][0] } / 20 - info options[:logger], "Average theta: #{at}" - - orig.rotate at end - # based on http://rosettacode.org/wiki/Hough_transform#Ruby - def self.hough_transform file, options={} - options = { - :roughly => false, - :logger => nil - }.merge(options) - - begin - orig = img_from_file(file) -# orig = orig.gaussian_blur 0, 3.0 - orig = orig.quantize 256, Magick::GRAYColorspace unless options[:roughly] - rescue - warn(options[:logger], "Skipping invalid file #{file}…") - return nil - end - - mx, my = orig.columns * 0.5, orig.rows * 0.5 - max_d = Math.sqrt(mx * mx + my * my) - min_d = -max_d - hough = Hash.new(0) - (0...orig.columns).each do |x| - warn(options[:logger], "#{x} of #{orig.columns}") - (0...orig.rows).each do |y| - if orig.pixel_color(x,y).green > 32 - (0...180).each do |a| - rad = a * (Math::PI / 180.0) - d = (x - mx) * Math.cos(rad) + (y - my) * Math.sin(rad) - hough["#{a.to_i}_#{d.to_i}"] = hough["#{a.to_i}_#{d.to_i}"] + 1 - end - end - end - end - - max = hough.values.max - - houghed = Image.new(orig.columns, orig.rows) { - self.background_color = 'transparent' - } - - hough.each_pair do |k, v| - a, d = k.split('_').map(&:to_i) - c = (v / max) * 255 -# c = heat.get_pixel(c,0) - houghed.pixel_color a, max_d + d, Pixel.new(c, c, c) - end - - houghed + def self.hough image, options + Hough::yo image, options + end + end + + class Image + def hough options + Screwdrivers::Hough::yo self, options end - end end diff --git a/lib/rmagick/screwdrivers/poster.rb b/lib/rmagick/screwdrivers/poster.rb index 91f320f..9a1a3d1 100644 --- a/lib/rmagick/screwdrivers/poster.rb +++ b/lib/rmagick/screwdrivers/poster.rb @@ -4,96 +4,115 @@ module Magick module Screwdrivers - def self.poster file, text1, text2, options={} - options = { - :color => '#FFFFFF', - :stroke => nil, - :width => 600, - :type => :classic, # :classic for black square around - :lineheight => 6, - :background => '#000000', - :font => '/usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-B.ttf', - :max_font_size => 48, - :logger => nil - }.merge(options) + module Poster + attr_reader :options + @options = { + :color => '#FFFFFF', + :stroke => nil, + :width => 600, + :type => :classic, # :classic for black square around + :lineheight => 6, + :background => '#000000', + :font => '/usr/share/fonts/truetype/ubuntu-font-family/Ubuntu-B.ttf', + :max_font_size => 48, + } + def self.yo image, title, text, options = {} + options = Magick::Screwdrivers.options.merge(@options).merge(OpenStruct === options ? options.to_h : options) + options[:lineheight] = 3 if options[:lineheight] < 3 - text1 ||= '' - text2 ||= '' - options[:lineheight] = 3 if options[:lineheight] < 3 + title ||= '' + text ||= '' - img = img_from_file(file) - img.thumbnail!(options[:width].to_f/img.columns.to_f) + img = Magick::Screwdrivers.imagify image + + img.thumbnail!(options[:width].to_f / img.columns.to_f) - mark = Magick::Image.new(img.columns, img.rows) do - self.background_color = options[:type] == :classic ? options[:background] : 'transparent' - end - - gc = Magick::Draw.new - - pointsize = [img.columns, options[:max_font_size]].min - classic_margin = 0 - - loop do - gc.pointsize = pointsize -= 1 + pointsize / 33 + mark = Magick::Image.new(img.columns, img.rows) do + self.background_color = options[:type] == :classic ? options[:background] : 'transparent' + end - m1 = gc.get_type_metrics(text1) - w1 = m1.width - h1 = (m1.bounds.y2 - m1.bounds.y1).round + gc = Magick::Draw.new - m2 = gc.get_type_metrics(text2) - w2 = m2.width - h2 = (m2.bounds.y2 - m2.bounds.y1).round + pointsize = [img.columns, options[:max_font_size]].min + classic_margin = 0 - if w1 < img.columns - 10*options[:lineheight] && w2 < img.columns - 10*options[:lineheight] - if options[:type] == :classic - classic_margin = h2 - mark.resize! img.columns+options[:lineheight]*14/3, img.rows+options[:lineheight]*8+h1+h2 - gc.stroke_width = options[:lineheight]/3 - gc.stroke = '#FFFFFF' - gc.fill = options[:background] - gc.rectangle(options[:lineheight]*7/6, options[:lineheight]*7/6, \ - img.columns+options[:lineheight]*21/6, img.rows+options[:lineheight]*21/6) - gc.composite( - 7*options[:lineheight]/3, 7*options[:lineheight]/3, - 0, 0, - img, Magick::OverCompositeOp - ) - gc.draw mark + loop do + gc.pointsize = pointsize -= 1 + pointsize / 33 + + m1 = gc.get_type_metrics(title) + w1 = m1.width + h1 = (m1.bounds.y2 - m1.bounds.y1).round + + m2 = gc.get_type_metrics(text) + w2 = m2.width + h2 = (m2.bounds.y2 - m2.bounds.y1).round + + if w1 < img.columns - 10 * options[:lineheight] && w2 < img.columns - 10 * options[:lineheight] + if options[:type] == :classic + classic_margin = h2 + mark.resize! img.columns+options[:lineheight]*14/3, img.rows+options[:lineheight]*8+h1+h2 + gc.stroke_width = options[:lineheight]/3 + gc.stroke = '#FFFFFF' + gc.fill = options[:background] + gc.rectangle( + options[:lineheight] * 7.0 / 6.0, + options[:lineheight] * 7 / 6, + img.columns + options[:lineheight] * 21 / 6, + img.rows+options[:lineheight] * 21 / 6 + ) + gc.composite( + 7 * options[:lineheight] / 3, + 7 * options[:lineheight] / 3, + 0, 0, + img, Magick::OverCompositeOp + ) + gc.draw mark + end + break end - break end - end - - gc.fill = options[:color] - gc.stroke = options[:stroke] || 'none' - gc.stroke_width = 1 - gc.font = options[:font] - case options[:type] - when :classic - gc.annotate(mark, 0, 0, 0, classic_margin+2*options[:lineheight], text1) do + gc.fill = options[:color] + gc.stroke = options[:stroke] || 'none' + gc.stroke_width = 1 + gc.font = options[:font] + + case options[:type] + when :classic + gc.annotate(mark, 0, 0, 0, classic_margin+2*options[:lineheight], title) do + self.gravity = Magick::SouthGravity + end + else + gc.annotate(mark, 0, 0, 0, 0, title) do + self.gravity = Magick::NorthGravity + end + end + + gc.annotate(mark, 0, 0, 0, 0, text) do self.gravity = Magick::SouthGravity end - else - gc.annotate(mark, 0, 0, 0, 0, text1) do - self.gravity = Magick::NorthGravity + + case options[:type] + when :classic + mark + when :negative + img.composite(mark, Magick::SouthEastGravity, Magick::SubtractCompositeOp) + when :standard + img.composite(mark, Magick::SouthEastGravity, Magick::OverCompositeOp) + else + warn options[:logger], "Invalid type: #{options[:type]}" end end - - gc.annotate(mark, 0, 0, 0, 0, text2) do - self.gravity = Magick::SouthGravity - end - - case options[:type] - when :classic - mark - when :negative - img.composite(mark, Magick::SouthEastGravity, Magick::SubtractCompositeOp) - when :standard - img.composite(mark, Magick::SouthEastGravity, Magick::OverCompositeOp) - else - warn options[:logger], "Invalid type: #{options[:type]}" - end + end + + def self.poster image, title, text, options + Poster::yo image, title, text, options + end + end + + class Image + def poster title, text, options + Screwdrivers::Poster::yo self, title, text, options end end end diff --git a/lib/rmagick/screwdrivers/radon.rb b/lib/rmagick/screwdrivers/radon.rb new file mode 100644 index 0000000..58323d3 --- /dev/null +++ b/lib/rmagick/screwdrivers/radon.rb @@ -0,0 +1,327 @@ +# encoding: utf-8 + +require 'RMagick' + +module Magick + module Screwdrivers + module Radon + attr_reader :options + @options = { + :roughly => false + } + + # number of beams - odd number to ensure symmetry when centroid is projected on the middle beam + HALFBEAMS = 25 + BEAMS = 2 * HALFBEAMS + 1 + + SQRT2 = Math.sqrt 2.0 + HALFSQRT2 = 0.5 * SQRT2 + HALFSQRT2DIVSIN22 = 0.5 * SQRT2 / Math.sin(Math::PI / 8.0) + DOUBLESQRT2 = 2.0 * SQRT2 + COS45 = SQRT2 / 2.0 + SIN45 = COS45 + COS22 = Math.cos(Math::PI / 8.0) + SIN22 = Math.sin(Math::PI / 8.0) + SEC22 = 1.0 / COS22 + COS67 = Math.cos(3.0 * Math::PI / 8.0) + SIN67 = Math.sin(3.0 * Math::PI / 8.0) + + # based on https:#dl.dropboxusercontent.com/u/18209754/Blog/radonTransformer.h + def self.yo image, options = {} + options = Magick::Screwdrivers.options.merge(@options).merge(OpenStruct === options ? options.to_h : options) + + orig = Magick::Screwdrivers.imagify image + # 500×375 + scale = Math.sqrt(187_500.0 / (orig.columns * orig.rows)) + # Would scale image to process quickier + img = scale < 1 ? orig.scale(scale) : orig + img = img.quantize 256, Magick::GRAYColorspace unless options[:roughly] + + # Pixel Contributions Storage + # sbTotals[8][100000]; # fixed size to avoid allocation of memory during each step (max 1000 sub-beams) + # bTotals[8][beams]; + + numPixels = img.columns * img.rows + object = [] + for r in 0...img.rows + for c in 0...img.columns + object[r * img.columns + c] = { :x => c, :y => r, :c => (img.at(c, r) * 256 / Magick::QuantumRange).ceil } + end + end + + # prepare for projections + centroid, radius = self.centroid_and_radius object + + #calculate sub-beams (estimation where there are 5 sub-beams per pixel) + subBeams = 2 * ((10.0 * radius + HALFBEAMS + 1.0) / BEAMS).ceil + 1 + totalSubBeams = BEAMS * subBeams + halfSubBeams = (totalSubBeams - 1) / 2 + + # for each projection clear result storage + sbTotals = Array.new(8, ((0...totalSubBeams).map { 0 })) + bTotals = Array.new(8, ((0...BEAMS).map { 0 })) + + #calculate beam width + beamWidth = radius / halfSubBeams + + # for each pixel + for i in 0...numPixels + project0degrees object[i], centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + project22degrees object[i], centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + project45degrees object[i], centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + project67degrees object[i], centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + project90degrees object[i], centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + project112degrees object[i], centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + project135degrees object[i], centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + project157degrees object[i], centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + end + + scalingFactor = beamWidth / numPixels + + #for each projection + for h in 0...8 + #beam counter + beam = 0 + + #for each beam grouping + (0...totalSubBeams).step(subBeams) { |i| + sum = 0; + + #add all the beams together in grouping + for j in i...(i + subBeams) + sum += sbTotals[h][j] + end + + #store in final result with scaling + bTotals[h][beam] = sum * scalingFactor; + beam += 1 + } + end + bTotals + end + + def self.centroid_and_radius object + # calculate centroid + sumX = sumY = 0 + numPixels = object.length + + #sum all x and y values for all pixels + for i in 0...numPixels + sumX += object[i][:x] + sumY += object[i][:y] + end + + centroid = { :x => 1.0 * sumX / numPixels, :y => 1.0 * sumY / numPixels } + + radius = 0; + # find the max length from centroid to a pixel + for i in 0...numPixels + # euclidian distance from pixel to centroid (no sqrt for performance reasons) + len = Math.sqrt( + (object[i][:x] - centroid[:x]) * (object[i][:x] - centroid[:x]) + + (object[i][:y] - centroid[:y]) * (object[i][:y] - centroid[:y]) + ) + radius = [radius, len].max + end + + #calculate radius (include missing sqrt from above) to a midpoint of a pixel + #Note: since distance is to midpoint add SQRT2/2 (approximation) + [centroid, radius + HALFSQRT2] + end + + #***************************************************************************************************************************************** + #* PROJECTION FUNCTIONS - 8 projections -> 0 to 157.5 degrees in 22.5 degree increments + #***************************************************************************************************************************************** + + def self.project0degrees point, centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals #horizontal + #projection of center of pixel on new axis + c = (point[:x] - centroid[:x]) / beamWidth + + #rounded projected left and right corners of pixel + l = ((c - 0.5 / beamWidth) + halfSubBeams).ceil + r = (c + 0.5 / beamWidth).floor + halfSubBeams + + #add contributions to sub-beams + for i in l..r + sbTotals[0][i] += 1; + end + end + + def self.project22degrees point, centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + #projected left & right corners of pixel + cl = ( (point[:y] - centroid[:y] - 0.5) * COS22 + (point[:x] - centroid[:x] - 0.5 ) * SIN22 ) / beamWidth + cr = ( (point[:y] - centroid[:y] + 0.5) * COS22 + (point[:x] - centroid[:x] + 0.5 ) * SIN22 ) / beamWidth + + #average of cl and cr (center point of pixel) + c = (cl + cr) / 2.0 + + #rounded values - left and right sub-beams affected + l = cl.ceil + halfSubBeams + r = cr.floor + halfSubBeams + + #length of incline + incl = (0.293 * (cr - cl + 1.0)).floor + + #add contributions to sub-beams + for i in l...(l+incl) + sbTotals[1][i] += HALFSQRT2DIVSIN22 - DOUBLESQRT2 * (i - halfSubBeams - c).abs * beamWidth + end + + for i in (l+incl)..(r-incl) + sbTotals[1][i] += SEC22 + end + + for i in (r-incl+1)..r + sbTotals[1][i] += HALFSQRT2DIVSIN22 - DOUBLESQRT2 * (i - halfSubBeams - c).abs * beamWidth + end + end + + def self.project45degrees point, centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + #projected left & right corners of pixel + cl = ( (point[:y] - centroid[:y] - 0.5) * COS45 + (point[:x] - centroid[:x] - 0.5 ) * SIN45 ) / beamWidth + cr = ( (point[:y] - centroid[:y] + 0.5) * COS45 + (point[:x] - centroid[:x] + 0.5 ) * SIN45 ) / beamWidth + + #average of cl and cr (center point of pixel) + c = (cl + cr) / 2.0 + + #rounded values - left and right sub-beams affected + l = cl.ceil + halfSubBeams + r = cr.floor + halfSubBeams + + #add contributions to sub-beams + for i in l..r + sbTotals[2][i] += SQRT2 - 2.0 * (i - halfSubBeams - c).abs * beamWidth + end + end + + def self.project67degrees point, centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + #projected left & right corners of pixel + cl = ( (point[:y] - centroid[:y] - 0.5) * COS67 + (point[:x] - centroid[:x] - 0.5 ) * SIN67 ) / beamWidth + cr = ( (point[:y] - centroid[:y] + 0.5) * COS67 + (point[:x] - centroid[:x] + 0.5 ) * SIN67 ) / beamWidth + + #average of cl and cr (center point of pixel) + c = (cl + cr) / 2.0 + + #rounded values - left and right sub-beams affected + l = cl.ceil + halfSubBeams + r = cr.floor + halfSubBeams + + #length of incline + incl = (0.293 * (cr - cl + 1.0)).floor + + #add contributions to sub-beams + for i in l...(l+incl) + sbTotals[3][i] += HALFSQRT2DIVSIN22 - DOUBLESQRT2 * (i - halfSubBeams - c).abs * beamWidth + end + + for i in (l+incl)..(r-incl) + sbTotals[3][i] += SEC22; + end + + for i in (r-incl+1)..r + sbTotals[3][i] += HALFSQRT2DIVSIN22 - DOUBLESQRT2 * (i - halfSubBeams - c).abs * beamWidth + end + end + + def self.project90degrees point, centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + #projection of center of pixel on new axis + c = (point[:y] - centroid[:y]) / beamWidth + + #rounded projected left and right corners of pixel + l = (c - 0.5 / beamWidth).ceil + halfSubBeams + r = (c + 0.5 / beamWidth).floor + halfSubBeams + + #add contributions to sub-beams + for i in l..r + sbTotals[4][i] += 1; + end + end + + def self.project112degrees point, centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + #projected left & right corners of pixel + cl = ( -(point[:y] - centroid[:y] + 0.5) * COS22 + (point[:x] - centroid[:x] - 0.5 ) * SIN22 ) / beamWidth + cr = ( -(point[:y] - centroid[:y] - 0.5) * COS22 + (point[:x] - centroid[:x] + 0.5 ) * SIN22 ) / beamWidth + + #average of cl and cr (center point of pixel) + c = (cl + cr) / 2.0 + + #rounded values - left and right sub-beams affected + l = cl.ceil + halfSubBeams + r = cr.floor + halfSubBeams + + #length of incline + incl = (0.293 * (cr - cl + 1.0)).floor + + #add contributions to sub-beams + for i in l...(l+incl) + sbTotals[5][i] += HALFSQRT2DIVSIN22 - DOUBLESQRT2 * (i - halfSubBeams - c).abs * beamWidth + end + + for i in (l+incl)..(r-incl) + sbTotals[5][i] += SEC22 + end + + for i in (r-incl+1)..r + sbTotals[5][i] += HALFSQRT2DIVSIN22 - DOUBLESQRT2 * (i - halfSubBeams - c).abs * beamWidth + end + end + + def self.project135degrees point, centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + #projected left & right corners of pixel + cl = ( -(point[:y] - centroid[:y] + 0.5) * COS45 + (point[:x] - centroid[:x] - 0.5 ) * SIN45 ) / beamWidth + cr = ( -(point[:y] - centroid[:y] - 0.5) * COS45 + (point[:x] - centroid[:x] + 0.5 ) * SIN45 ) / beamWidth + + #average of cl and cr (center point of pixel) + c = (cl + cr) / 2.0 + + #rounded values - left and right sub-beams affected + l = cl.ceil + halfSubBeams + r = cr.floor + halfSubBeams + + #add contributions to sub-beams + for i in l..r + sbTotals[6][i] += SQRT2 - 2 * (i - halfSubBeams - c).abs * beamWidth + end + end + + def self.project157degrees point, centroid, subBeams, totalSubBeams, halfSubBeams, beamWidth, sbTotals + #projected left & right corners of pixel + cl = ( -(point[:y] - centroid[:y] + 0.5) * COS67 + (point[:x] - centroid[:x] - 0.5 ) * SIN67 ) / beamWidth + cr = ( -(point[:y] - centroid[:y] - 0.5) * COS67 + (point[:x] - centroid[:x] + 0.5 ) * SIN67 ) / beamWidth + + #average of cl and cr (center point of pixel) + c = (cl + cr) / 2.0 + + #rounded values - left and right sub-beams affected + l = cl.ceil + halfSubBeams + r = cr.floor + halfSubBeams + + #length of incline + incl = (0.293 * (cr - cl + 1)).floor + + #add contributions to sub-beams + for i in l...(l+incl) + sbTotals[7][i] += HALFSQRT2DIVSIN22 - DOUBLESQRT2 * (i - halfSubBeams - c).abs * beamWidth + end + + for i in (l+incl)..(r-incl) + sbTotals[7][i] += SEC22 + end + + for i in (r-incl+1)..r + sbTotals[7][i] += HALFSQRT2DIVSIN22 - DOUBLESQRT2 * (i - halfSubBeams - c).abs * beamWidth + end + end + end + + def self.radon image, options + Radon::yo image, options + end + end + + class Image + def radon options + Screwdrivers::Radon::yo self, options + end + end +end diff --git a/lib/rmagick/screwdrivers/scale.rb b/lib/rmagick/screwdrivers/scale.rb index 4d5a805..5581332 100644 --- a/lib/rmagick/screwdrivers/scale.rb +++ b/lib/rmagick/screwdrivers/scale.rb @@ -5,67 +5,81 @@ module Magick module Screwdrivers - def self.scale file, options={} - options = { + module Scale + attr_reader :options + @options = { :widths => 600, :date_in_watermark => false, :watermark => nil, :color => 'rgba(66%, 66%, 66%, 0.33)', :overlap => nil, - :logger => nil - }.merge(options) - - options[:overlap] = options[:overlap] && Magick.constants.include?("#{options[:overlap].capitalize}CompositeOp".to_sym) ? - Magick.const_get("#{options[:overlap].capitalize}CompositeOp".to_sym) : Magick::ModulateCompositeOp - - img = img_from_file file - - date = img.get_exif_by_number(36867)[36867] - date = Date.parse(date.gsub(/:/, '/')) if date - date ||= Date.parse(img.properties['exif:DateTime'].gsub(/:/, '/')) if img.properties['exif:DateTime'] - date ||= Date.parse(img.properties['date:modify']) if img.properties['date:modify'] - date ||= Date.parse(img.properties['date:create']) if img.properties['date:create'] - date ||= Date.parse(img.properties['xap:CreateDate']) if img.properties['xap:CreateDate'] - - options[:watermark] = ([ - options[:watermark], date.strftime('%y/%m/%d')] - [nil] - ).join(' :: ').strip if options[:date_in_watermark] + } - result = ImageList.new + def self.yo image, options = {} + options = Magick::Screwdrivers.options.merge(@options).merge(OpenStruct === options ? options.to_h : options) - [*options[:widths]].each { |sz| - unless Integer === sz && sz < img.columns && sz > 0 - warn options[:logger], "Invalid width #{sz} (original is #{img.columns}), skipping…" - next - end + options[:overlap] = options[:overlap] && Magick.constants.include?("#{options[:overlap].capitalize}CompositeOp".to_sym) ? + Magick.const_get("#{options[:overlap].capitalize}CompositeOp".to_sym) : Magick::ModulateCompositeOp - curr = img.resize_to_fit(sz) + img = Magick::Screwdrivers.imagify image - if will_wm = (options[:watermark] && curr.rows >= 400) - mark = Magick::Image.new(curr.rows, curr.columns) do - self.background_color = 'transparent' + date = img.get_exif_by_number(36867)[36867] + date = Date.parse(date.gsub(/:/, '/')) if date + date ||= Date.parse(img.properties['exif:DateTime'].gsub(/:/, '/')) if img.properties['exif:DateTime'] + date ||= Date.parse(img.properties['date:modify']) if img.properties['date:modify'] + date ||= Date.parse(img.properties['date:create']) if img.properties['date:create'] + date ||= Date.parse(img.properties['xap:CreateDate']) if img.properties['xap:CreateDate'] + + options[:watermark] = ([ + options[:watermark], date.strftime('%y/%m/%d')] - [nil] + ).join(' :: ').strip if options[:date_in_watermark] + + result = ImageList.new + + [*options[:widths]].each { |sz| + unless Integer === sz && sz < img.columns && sz > 0 + warn options[:logger], "Invalid width #{sz} (original is #{img.columns}), skipping…" + next end - draw = Magick::Draw.new - draw.encoding = 'Unicode' - draw.annotate(mark, 0, 0, 5, 2, options[:watermark]) do - self.encoding = 'Unicode' - self.gravity = Magick::SouthEastGravity - self.fill = options[:color] - self.stroke = 'transparent' - self.pointsize = 2 + 2 * Math.log(sz, 3).to_i - self.font_family = 'Comfortaa' - self.font_weight = Magick::NormalWeight - self.font_style = Magick::NormalStyle + + curr = img.resize_to_fit(sz) + + if will_wm = (options[:watermark] && curr.rows >= 400) + mark = Magick::Image.new(curr.rows, curr.columns) do + self.background_color = 'transparent' + end + draw = Magick::Draw.new + draw.encoding = 'Unicode' + draw.annotate(mark, 0, 0, 5, 2, options[:watermark]) do + self.encoding = 'Unicode' + self.gravity = Magick::SouthEastGravity + self.fill = options[:color] + self.stroke = 'transparent' + self.pointsize = 2 + 2 * Math.log(sz, 3).to_i + self.font_family = 'Comfortaa' + self.font_weight = Magick::NormalWeight + self.font_style = Magick::NormalStyle + end + curr = curr.composite(mark.rotate(-90), Magick::SouthEastGravity, options[:overlap] ) end - curr = curr.composite(mark.rotate(-90), Magick::SouthEastGravity, options[:overlap] ) - end + + Magick::Screwdrivers.info options[:logger], "Scaling to width #{curr.rows}×#{curr.columns}, method: #{options[:overlap]}, watermark: “#{will_wm ? options[:watermark] : 'NONE'}”" + + result << curr + } - info options[:logger], "Scaling to width #{curr.rows}×#{curr.columns}, method: #{options[:overlap]}, watermark: “#{will_wm ? options[:watermark] : 'NONE'}”" + result + end + end - result << curr - } + def self.scale image, options + Scale::yo image, options + end + end - result + class Image + def fan options + Screwdrivers::Scale::yo self, options end end end diff --git a/lib/rmagick/screwdrivers/sobel.rb b/lib/rmagick/screwdrivers/sobel.rb index 7cc02a6..45453f0 100644 --- a/lib/rmagick/screwdrivers/sobel.rb +++ b/lib/rmagick/screwdrivers/sobel.rb @@ -5,64 +5,67 @@ module Magick module Screwdrivers # based on http://blog.saush.com/2011/04/20/edge-detection-with-the-sobel-operator-in-ruby/ - def self.sobel file, options={} - options = { - :roughly => false, - :logger => nil - }.merge(options) - - sobel_x = [[-1,0,1], [-2,0,2], [-1,0,1]] - sobel_y = [[-1,-2,-1], [0,0,0], [1,2,1]] - - begin - orig = img_from_file(file) -# orig = orig.gaussian_blur 0, 3.0 - orig = orig.quantize 256, Magick::GRAYColorspace unless options[:roughly] - rescue - warn(options[:logger], "Skipping invalid file #{file}…") - return nil - end - - # 500×375 - scale = Math.sqrt(187_500.0 / (orig.columns * orig.rows)) - - info(options[:logger], "Original is [#{orig.columns}×#{orig.rows}] image") - info(options[:logger], "Scale factor: [#{scale}]") - img = scale < 1 ? orig.scale(scale) : orig - - info(options[:logger], "Will process [#{img.columns}×#{img.rows}] image") - - edge = Image.new(img.columns, img.rows) { - self.background_color = 'transparent' + module Sobel + attr_reader :options + @options = { + :roughly => false } + def self.yo image, options = {} + options = Magick::Screwdrivers.options.merge(@options).merge(OpenStruct === options ? options.to_h : options) - for x in 1..img.columns-2 - for y in 1..img.rows-2 - pixel_x = (sobel_x[0][0] * img.at(x-1,y-1)) + (sobel_x[0][1] * img.at(x,y-1)) + (sobel_x[0][2] * img.at(x+1,y-1)) + - (sobel_x[1][0] * img.at(x-1,y)) + (sobel_x[1][1] * img.at(x,y)) + (sobel_x[1][2] * img.at(x+1,y)) + - (sobel_x[2][0] * img.at(x-1,y+1)) + (sobel_x[2][1] * img.at(x,y+1)) + (sobel_x[2][2] * img.at(x+1,y+1)) - - pixel_y = (sobel_y[0][0] * img.at(x-1,y-1)) + (sobel_y[0][1] * img.at(x,y-1)) + (sobel_y[0][2] * img.at(x+1,y-1)) + - (sobel_y[1][0] * img.at(x-1,y)) + (sobel_y[1][1] * img.at(x,y)) + (sobel_y[1][2] * img.at(x+1,y)) + - (sobel_y[2][0] * img.at(x-1,y+1)) + (sobel_y[2][1] * img.at(x,y+1)) + (sobel_y[2][2] * img.at(x+1,y+1)) - - val = Math.sqrt((pixel_x * pixel_x) + (pixel_y * pixel_y)).ceil - edge.pixel_color x, y, Pixel.new(val, val, val) + orig = Magick::Screwdrivers.imagify image + + sobel_x = [[-1,0,1], [-2,0,2], [-1,0,1]] + sobel_y = [[-1,-2,-1], [0,0,0], [1,2,1]] + + # 500×375 + scale = Math.sqrt(187_500.0 / (orig.columns * orig.rows)) + # Would scale image to process quickier + img = scale < 1 ? orig.scale(scale) : orig + img = orig.quantize 256, Magick::GRAYColorspace unless options[:roughly] + + edge = Image.new(img.columns, img.rows) { + self.background_color = 'transparent' + } + + for x in 1..img.columns-2 + for y in 1..img.rows-2 + pixel_x = (sobel_x[0][0] * img.at(x-1,y-1)) + (sobel_x[0][1] * img.at(x,y-1)) + (sobel_x[0][2] * img.at(x+1,y-1)) + + (sobel_x[1][0] * img.at(x-1,y)) + (sobel_x[1][1] * img.at(x,y)) + (sobel_x[1][2] * img.at(x+1,y)) + + (sobel_x[2][0] * img.at(x-1,y+1)) + (sobel_x[2][1] * img.at(x,y+1)) + (sobel_x[2][2] * img.at(x+1,y+1)) + + pixel_y = (sobel_y[0][0] * img.at(x-1,y-1)) + (sobel_y[0][1] * img.at(x,y-1)) + (sobel_y[0][2] * img.at(x+1,y-1)) + + (sobel_y[1][0] * img.at(x-1,y)) + (sobel_y[1][1] * img.at(x,y)) + (sobel_y[1][2] * img.at(x+1,y)) + + (sobel_y[2][0] * img.at(x-1,y+1)) + (sobel_y[2][1] * img.at(x,y+1)) + (sobel_y[2][2] * img.at(x+1,y+1)) + + val = Math.sqrt((pixel_x * pixel_x) + (pixel_y * pixel_y)).ceil + edge.pixel_color x, y, Pixel.new(val, val, val) + end + end + + # edge = edge.scale(orig.columns, orig.rows) if scale < 1 + + case orig.orientation + when Magick::RightTopOrientation + edge.rotate!(90) + when Magick::BottomRightOrientation + edge.rotate!(180) + when Magick::LeftBottomOrientation + edge.rotate!(-90) end + + edge end + end - edge = edge.scale(orig.columns, orig.rows) if scale < 1 - - case orig.orientation - when Magick::RightTopOrientation - edge.rotate!(90) - when Magick::BottomRightOrientation - edge.rotate!(180) - when Magick::LeftBottomOrientation - edge.rotate!(-90) - end + def self.sobel image, options + Sobel::yo image, options + end + end - edge + class Image + def sobel options + Screwdrivers::Sobel::yo self, options end end end