Attachment @ Test image for color, grayscale, or single hue gradient file_download
2016-07-12
2016
07-12
«color_greyscale_image_test.rb»
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env ruby

# How to check image color or back and white -- http://www.imagemagick.org/discourse-server/viewtopic.php?t=19580
#
#   convert file.jpg -colorspace HSL -verbose info:|grep -A 6 Green # values: absolute (percent)
#       Green:
#         min:  0       (0)
#         max:  255     (1)
#         mean: 70.2043 (0.275311)
#         standard deviation: 99.2055 (0.389041)
#         kurtosis: -0.981812
#         skewness:  0.897517
#
# You convert the image to HSL and then output verbose information ("identify"
# statistics).
# In HSL space Saturation define how colorful pixels are, and this is returned
# in the 'Green' channel statistics.
#
# If max is 0 so no color was present at all: it is a perfect grayscale or black/white image.
# If max is 1 so at least one pure color pixel exists: it is not a pure grayscale image.
# 
# The mean indicates how much colorful in general the image is.
# A mean of 40% means the image is not even mostly grayscale (white background).
#
# Other statistics show how the color ratios was distributed about that mean.
#
# NOTE:
#   Due to HSL being a double hexcone model, this colorspace does not work properly
#   for measuring saturation for L>50% as it approaches the top peak. There white
#   is going to be indetermined saturation and turns out is interpreted as high
#   saturation. In its place, use HCL, HCLp, HSI or HSB above. These are all single
#   hexcone type models and should work fine in the above equation.
#
#   convert file.jpg -colorspace HSI -channel g +channel -format "%[fx:mean.g]" info:
#
# ------------------------------------------------------------------------------
#
# How to retrive the single values from the statistics:
#   Retrieving individual Image Channel Statistics -- http://www.imagemagick.org/discourse-server/viewtopic.php?t=21008
#   FX Expressions as Format and Annotate Escapes  -- http://www.imagemagick.org/Usage/transform/#fx_escapes
#   Format and Print Image Properties              -- http://www.imagemagick.org/script/escape.php
#   The Fx Special Effects Image Operator          -- http://www.imagemagick.org/script/fx.php
#
# ------------------------------------------------------------------------------
# 
# Robust command to get H/S/I statistics in the R/G/B sections:
#   convert file.jpg -colorspace HSI -verbose info: | grep -A 20 Red
#
# Get the single value of a statistic (saturation mean):
#   convert file.jpg -colorspace HSI -channel g +channel -format "%[fx:mean.g]" info:
#
# Get image colors histogram for a restricted number of colors (64):
#   convert file.jpg -dither FloydSteinberg -colors 64 -colorspace HSB -format %c histogram:info:-

def die(msg); STDERR.puts "ERROR: #{msg}!"; exit 1; end

fname = ARGV[0].to_s
die 'file not found' if fname.size == 0 || !File.exists?(fname)

# computing histogram
histlines = `convert '#{fname}' -dither FloydSteinberg -colors 64 -colorspace HSB -format %c histogram:info:-`.split("\n")
if ARGV.include?('-h')
  puts "----- full histogram -----"
  puts histlines
end

# extract values from histogram (num.pixels, hue, saturation, intensity)
hist = histlines.grep(/hs[ib]/i).map{|line|
  # http://www.imagemagick.org/Usage/files/#histogram
  # pixel count:    R   G   B  #HEX    hsi(hue, saturation, intensity)
  #        9274: (  1, 89,135) #015987 hsi(0.582895%,35.0103%,53.0617%)
  next unless m = line.match(Regexp.new ' +([0-9]+):.+#([0-9A-F]+) hs[ib]\(([^%]+)%,([^%]+)%,([^%]+)%')
  hue = m.captures[2].to_f.round(4)
  {
    npix: m.captures[0].to_i,
    hex:  m.captures[1],
    hue:  hue,
    hue2: (hue > 50 ? (hue-50) : (hue+50)).round(4), # 180 degrees shifted HUE
    sat:  m.captures[3].to_f,
    int:  m.captures[4].to_f,
  }
}.compact

die "histogram size (#{hist.size}) < 2" if hist.size < 2

# compute saturation mean and unique hue std.deviation
removed_rows = []
puts "----- new histogram -----" if ARGV.include?('-h')
stats = hist.each_with_index.inject({
  tot_pixels: 0,
  sat_sum:    0,
  hue_num:    0, hue_mean:  0, hue_m2:  0,
  hue2_num:   0, hue2_mean: 0, hue2_m2: 0,
  hue_outlayered_pixels: 0,
}){|stats, pair|
  row, i = pair

  stats[:tot_pixels] += row[:npix]

  stats[:sat_sum] += row[:npix] * row[:sat]

  if %w{ 000000 FFFFFF }.include?(row[:hex]) || row[:sat] < 10
    # remove not only the pure black and white colors, but any color that has a
    # very, very low saturation (greys). When the saturation is near zero, the
    # hue has very little meaning, and could point in any direction.
    stats[:hue_outlayered_pixels] += row[:npix]
    removed_rows << row
  else
    # incremental/online formula for std.deviation
    # https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm

    # compute hue std.dev
    delta             = row[:hue] - stats[:hue_mean]
    stats[:hue_num ] += 1
    stats[:hue_mean] += delta / stats[:hue_num]
    stats[:hue_m2  ] += delta * (row[:hue] - stats[:hue_mean])

    # FIX the red hue that wraps around the 360 dregree:
    # Compute again the std.dev with the hues rotated 180 degress.
    # You will either get roughly the same standard deviation, or wildly different.
    # In the second case, one of the std.dev would probably be very small, indicating
    # an image with a red midtone coloring.
    delta             = row[:hue2] - stats[:hue2_mean]
    stats[:hue2_num ] += 1
    stats[:hue2_mean] += delta / stats[:hue2_num]
    stats[:hue2_m2  ] += delta * (row[:hue2] - stats[:hue2_mean])

    puts row.values.map{|i| i.to_s.rjust(10)}.join(" | ") if ARGV.include?('-h')
  end

  stats
}
puts removed_rows.map{|l| l.values.map{|i| i.to_s.rjust(10)}.join(" | ")}.unshift('   --- deleted ---') if ARGV.include?('-h')

sat_mean = stats[:sat_sum] / stats[:tot_pixels]
hue_sd   = (stats[:hue_m2 ] / (hist.size - removed_rows.size - 1)) ** 0.5
hue2_sd  = (stats[:hue2_m2] / (hist.size - removed_rows.size - 1)) ** 0.5
hue_outl = stats[:hue_outlayered_pixels].to_f / stats[:tot_pixels] * 100

def img_type(hist_size, sat_mean, hue_sd, hue2_sd)
  return :grayscale  if hist_size == 0
  return :monochrome if hist_size <= 3

  if sat_mean < 10    # low saturation
    :grayscale
  elsif sat_mean < 30 # middle saturation
    # hue must be big for a colored image
    [hue_sd, hue2_sd].min > 25 ? :colored_mid_sat : :monochrome_mid_sat
  else
    return :colored   # high saturation
  end
end # img_type

puts [
  fname.ljust(30),
  'S:%5.2f'  % sat_mean ,
  'H:%5.2f'  % hue_sd   ,
  'H2:%5.2f' % hue2_sd  ,
  'N:%2d'    % hist.size,
  'R:%2d'    % removed_rows.size,
  'O:%3d
'
% hue_outl , '=> ', img_type(hist.size - removed_rows.size, sat_mean, hue_sd, hue2_sd), ].join(' ') # ls|sort|while read fname; do ../test.rb "$fname"; done