0

In this code

im = Vips::Image.new_from_file "some.jpg"
r = (im * [1,0,0]).avg
g = (im * [0,1,0]).avg
b = (im * [0,0,1]).avg

p [r,g,b]                      # => [57.1024, 53.818933333333334, 51.9258]

p Vips::Image.sRGB2HSV [r,g,b]

the last line throws

/ruby-vips-1.0.3/lib/vips/argument.rb:154:in `set_property': invalid argument Array (expect #<Class:0x007fbd7c923600>) (ArgumentError)`

P.S.: temporary took and refactored the ChunkyPNG implementation:

def to_hsv r, g, b
  r, g, b  = [r, g, b].map{ |component| component.fdiv 255 }
  min, max = [r, g, b].minmax
  chroma   = max - min
  [
    60.0 * ( chroma.zero? ? 0 : case max
      when r ; (g - b) / chroma
      when g ; (b - r) / chroma + 2
      when b ; (r - g) / chroma + 4
      else 0
    end % 6 ),
    chroma / max,
    max,
  ]
end
Nakilon
  • 34,866
  • 14
  • 107
  • 142

2 Answers2

2

Pixel averaging should really be in a linear colorspace. XYZ is an easy one, but scRGB would work well too. Once you have a 1x1 pixel image, convert to HSV and read out the value.

#!/usr/bin/ruby

require 'vips'

im = Vips::Image.new_from_file ARGV[0]

# xyz colourspace is linear, ie. the value is each channel is proportional to
# the number of photons of that frequency 
im = im.colourspace "xyz"

# 'shrink' is a fast box filter, so each output pixel is the simple average of
# the corresponding input pixels ... this will shrink the whole image to a
# single pixel
im = im.shrink im.width, im.height

# now convert the one pixel image to hsv and read out the values
im = im.colourspace "hsv"
h, s, v = im.getpoint 0, 0

puts "h = #{h}"
puts "s = #{s}"
puts "v = #{v}"

I wouldn't use HSV myself, LCh is generally much better.

https://en.wikipedia.org/wiki/Lab_color_space#Cylindrical_representation:_CIELCh_or_CIEHLC

For LCh, just change the end to:

im = im.colourspace "lch"
l, c, h = im.getpoint 0, 0
jcupitt
  • 10,213
  • 2
  • 23
  • 39
  • Cool. You probably can write it as `im.bandsplit.map(&:avg)` – Nakilon Jan 30 '17 at 14:20
  • I'll compare results of this and mine today or tomorrow. – Nakilon Jan 30 '17 at 14:21
  • Oh, good point on the `map(&:avg)`. It's unlikely to give exactly the same result. – jcupitt Jan 30 '17 at 14:47
  • Also, I wouldn't use HSV, it's poorly defined and very non-linear wrt. to human vision. If you're trying to pick objects by hue (for example), LCh is much better. vips only has HSV for compatibility with very old software. – jcupitt Jan 30 '17 at 14:51
0

I realised, that it is obviously wrong to calculate average Hue as arithmetic average, so I solved it by adding vectors of length equal to Saturation. But I didn't find how to iterate over pixels in vips so I used a crutch of chunky_png:

require "vips"
require "chunky_png"

def get_average_hsv_by_filename filename
  im = Vips::Image.new filename
  im.write_to_file "temp.png"
  y, x = 0, 0
  ChunkyPNG::Canvas.from_file("temp.png").to_rgba_stream.unpack("N*").each do |rgba|
    h, s, v = ChunkyPNG::Color.to_hsv(rgba)
    a = h * Math::PI / 180
    y += Math::sin(a) * s
    x += Math::cos(a) * s
  end
  h = Math::atan2(y, x) / Math::PI * 180
  _, s, v = im.colourspace("hsv").bandsplit.map(&:avg)
  [h, s, v]
end

For large images I used .resize that seems to inflict only up to ~2% error when resizing down to 10000 square pixels area with default kernel.

Nakilon
  • 34,866
  • 14
  • 107
  • 142
  • @user894763, please, take a look at this. – Nakilon Apr 14 '17 at 11:52
  • It was not tested on images with alpha channel though. – Nakilon Apr 14 '17 at 11:53
  • You're right, averaging hue won't work for values either side of the 0/360 boundary. If the image is a natural scene, you really want to average photons (or something proportional to number of photons), so XYZ, then convert to HSV at the end. I'll update my answer. – jcupitt Apr 17 '17 at 16:36