Attachment @ Test image for color, grayscale, or single hue gradient file_download
2021-06-21
2021
06-21
«img-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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/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 HSB -verbose info: | grep -A 20 Red
#
# Get the single value of a statistic (saturation mean):
#   convert file.jpg -colorspace HSB -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:-

$VERBOSE = nil

require 'shellwords'

EXIT_CODES = { colored:              0,
               colored_mid_sat:      1,
               grayscale:            2,
               monochrome:           3,
               monochrome_mid_sat:   4,
               error:              100, }

if ARGV.size == 0
  puts "USAGE: #{File.basename __FILE__} img.ext [-h] [-s] [-q]"
  puts "  FLAGS:"
  puts "    -h    show histograms"
  puts "    -s    show statistics"
  puts "    -q    quiet mode"
  puts "  EXIT CODES:"
  EXIT_CODES.each{|k, n| puts "    #{n.to_s.ljust 3}   #{k}"}
  exit EXIT_CODES[:error]
end

def test_image(fname, opts = {})
  opts = { hist: false, stats: false, quiet: true }.merge opts

  unless File.exists?(fname)
    STDERR.puts "ERROR: file not found" unless opts[:quiet]
    return :error
  end

  # computing histogram
  histlines = `convert #{fname.shellescape} -dither FloydSteinberg -colors 64 -colorspace HSB -format %c histogram:info:- 2> /dev/null`.split("\n")
  puts "----- full histogram -----\n#{histlines.join "\n"}" if opts[:hist]

  # extract values from histogram (num.pixels, hue, saturation, brightness)
  hist = histlines.grep(/hs[ib]/i).map{|line|
    # http://www.imagemagick.org/Usage/files/#histogram
    # pixel count:    R   G   B  #HEX    hsb(hue, saturation, brightness)
    #        9274: (  1, 89,135) #015987 hsb(0.582895%,35.0103%,53.0617%)
    next unless m = line.match(Regexp.new ' +([0-9]+):.+#([0-9A-F]+) hs[ib]a*\(([0-9\.]+)(%*),([0-9\.]+)%*,([0-9\.]+)%')
    hue = m.captures[2].to_f  # percentage values: 0-100 (old imagemagick version)
    hue = hue/360*100 if m.captures[3].to_s.size == 0 # degrees 0-360 => convert to 0-100
    hue = hue.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[4].to_f,
      int:  m.captures[5].to_f,
    }
  }.compact

  if hist.size < 2
    STDERR.puts "ERROR: histogram size (#{hist.size}) < 2" unless opts[:quiet]
    return :error
  end

  # compute saturation mean and unique hue std.deviation
  removed_rows = []
  if opts[:hist]
    puts "----- new histogram -----"
    puts hist[0].keys.map{|i| "#{i}  ".upcase.rjust(10)}.join(" | ")
  end
  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 opts[:hist]
    end

    stats
  }#hist.each_with_index.inject
  if opts[:hist]
    puts '   --- deleted ---'
    puts hist[0].keys.map{|i| "#{i}  ".upcase.rjust(10)}.join(" | ")
    puts removed_rows.map{|l| l.values.map{|i| i.to_s.rjust(10)}.join(" | ")}
  end

  hist_size = hist.size - removed_rows.size
  sat_mean  = stats[:sat_sum] / stats[:tot_pixels]
  hue_sd    = (stats[:hue_m2 ] / (hist_size - 1)) ** 0.5  if hist_size > 3
  hue2_sd   = (stats[:hue2_m2] / (hist_size - 1)) ** 0.5  if hist_size > 3
  min_hue   = [hue_sd, hue2_sd].min                       if hist_size > 3
  hue_outl  = stats[:hue_outlayered_pixels].to_f / stats[:tot_pixels] * 100

  img_type = \
    if    hist_size == 0 ; :grayscale
    elsif hist_size <= 3 ; :monochrome
    elsif sat_mean  < 10 ; :grayscale   # low saturation
    # middle saturation: hue must be big for a colored image
    elsif sat_mean  < 30 ; min_hue > 25 ? :colored_mid_sat : :monochrome_mid_sat
    else                 ; :colored     # high saturation
    end

  puts([
    fname.ljust(30),
    'S:%5.2f'  % sat_mean ,
    'H:%5.2f'  % hue_sd .to_f,
    'H2:%5.2f' % hue2_sd.to_f,
    'N:%2d'    % hist.size,
    'R:%2d'    % removed_rows.size,
    'O:%3d
'
% hue_outl , '=> ', img_type, ].join(' ')) if opts[:stats] puts img_type unless opts[:quiet] || opts[:stats] img_type end # test_image --------------------------------------------------------------- img_type = test_image ARGV[0], \ hist: ARGV.include?('-h'), quiet: ARGV.include?('-q'), stats: ARGV.include?('-s') exit EXIT_CODES[img_type] # ls *.jpg | sort | while read fname; do ../img-test.rb "$fname"; done