Class: Color::CIELAB

Inherits:
Data
  • Object
show all
Includes:
Color
Defined in:
lib/color.rb,
lib/color/cielab.rb

Overview

A Color object for the CIELAB color space (also known as L*a*b*). Color is expressed in a three-dimensional, device-independent “standard observer” model, often in relation to a “reference white” color, usually Color::XYZ::D65 (most purposes) or Color::XYZ::D50 (printing).

‘L*` is the perceptual lightness, bounded to values between 0 (black) and 100 (white). `a*` is the range of green (negative) / red (positive) and `b*` is the range of blue (negative) / yellow (positive).

The ‘a*` and `b*` ranges are technically unbounded but Color clamps them to the values `-128..127`.

For more information, see [CIELAB](en.wikipedia.org/wiki/CIELAB_color_space).

CIELAB colors are immutable Data class instances. Array deconstruction is ‘[l, a, b]` and hash deconstruction is `a:, b:` (see #l, #a, #b).

Constant Summary collapse

DE94_WEIGHTS =

Standard weights applied for perceptual differences using the ΔE*94 algorithm.

{
  graphic_arts: {k_1: 0.045, k_2: 0.015, k_l: 1.0}.freeze,
  textiles: {k_1: 0.048, k_2: 0.014, k_l: 2.0}.freeze
}.freeze

Constants included from Color

EPSILON, TOLERANCE, VERSION

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Color

#==, #components, #css_value, #map, #map_with, normalize, #scale, translate_range, #zip

Constructor Details

#initialize(l:, a:, b:) ⇒ CIELAB

Creates a CIELAB color representation from ‘L*a*b*` native values. The `l` value must be between 0 and 100 and the `a` and `b` values must be between -128 and 127.

“‘ruby Color::CIELAB.new(10, 35, -35) # => CIELAB [10.00 35.00 -35.00] Color::CIELAB.from_values(10, 35, -35) # => CIELAB [10.00 35.00 -35.00] Color::CIELAB[l: 10, a: 35, b: -35] # => CIELAB [10.00 35.00 -35.00] “`



90
91
92
93
94
95
96
# File 'lib/color/cielab.rb', line 90

def initialize(l:, a:, b:)
  super(
    l: normalize(l, RANGES[:L]),
    a: normalize(a, RANGES[:ab]),
    b: normalize(b, RANGES[:ab])
  )
end

Instance Attribute Details

#aObject (readonly)

Returns the value of attribute a

Returns:

  • (Object)

    the current value of a



44
45
46
# File 'lib/color.rb', line 44

def a
  @a
end

#bObject (readonly)

Returns the value of attribute b

Returns:

  • (Object)

    the current value of b



44
45
46
# File 'lib/color.rb', line 44

def b
  @b
end

#lObject (readonly)

Returns the value of attribute l

Returns:

  • (Object)

    the current value of l



44
45
46
# File 'lib/color.rb', line 44

def l
  @l
end

Class Method Details

.from_percentage(*args, **kwargs) ⇒ Object

Creates a CIELAB color representation from percentage values.

‘l` must be between 0% and 100%; `a` and `b` must be between -100% and 100% and will be transposed to the native value -128..127.

“‘ruby Color::CIELAB.from_percentage(10, -30, 30) # => CIELAB [10.0000 -38.7500 37.7500] “`

:call-seq:

from_percentage(l, a, b)
from_percentage(l:, a:, b:)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/color/cielab.rb', line 58

def self.from_percentage(*args, **kwargs)
  l, a, b =
    case [args, kwargs]
    in [[_, _, _], {}]
      args
    in [[], {l:, a:, b:}]
      [l, a, b]
    else
      new(*args, **kwargs)
    end

  new(
    l: l,
    a: Color.translate_range(a, from: -100.0..100.0, to: RANGES[:ab]),
    b: Color.translate_range(b, from: -100.0..100.0, to: RANGES[:ab])
  )
end

Instance Method Details

#coerce(other) ⇒ Object

Coerces the other Color object into CIELAB.



100
# File 'lib/color/cielab.rb', line 100

def coerce(other) = other.to_lab

#css(alpha: nil) ⇒ Object

Render the CSS ‘lab()` function for this CIELAB object, adding an `alpha` if provided.



159
160
161
162
163
164
# File 'lib/color/cielab.rb', line 159

def css(alpha: nil, **)
  params = [css_value(l, :percent), css_value(a), css_value(b)].join(" ")
  params = "#{params} / #{css_value(alpha)}" if alpha

  "lab(#{params})"
end

#delta_e2000(other, klch: {L: 1.0, C: 1.0, H: 1.0}) ⇒ Object

Implements the CIELAB ΔE* 2000 perceptual color distance metric with more reliable results over CIELAB ΔE* 1994.

See [CIEDE2000] for precise details on the mathematical formulas. The implementation here is based on Sharma, Wu, and Dala in [CIEDE2000.xls], published as supplementary materials for their paper “The CIEDE2000 Color-Difference Formula: Implementation Notes, Supplementary Test Data, and Mathematical Observations,”, G. Sharma, W. Wu, E. N. Dalal, Color Research and Application, vol.

  1. No. 1, pp. 21-30, February 2005.

Do not override the ‘klch` parameter unless you really know what you’re doing.

See also <www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE2000.html>

[ciede2000]: en.wikipedia.org/wiki/Color_difference#CIEDE2000 [ciede2000xls]: www.ece.rochester.edu/~gsharma/ciede2000/dataNprograms/CIEDE2000.xls



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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/color/cielab.rb', line 183

def delta_e2000(other, klch: {L: 1.0, C: 1.0, H: 1.0})
  other = coerce(other)
  klch => L: k_l, C: k_c, H: k_h
  self => l: l_star_1, a: a_star_1, b: b_star_1
  other => l: l_star_2, a: a_star_2, b: b_star_2

  v_25_pow_7 = 25**7

  c_star_1 = Math.sqrt(a_star_1**2 + b_star_1**2)
  c_star_2 = Math.sqrt(a_star_2**2 + b_star_2**2)

  c_mean = ((c_star_1 + c_star_2) / 2.0)
  c_mean_pow_7 = c_mean**7
  c_mean_g = (0.5 * (1.0 - Math.sqrt(c_mean_pow_7 / (c_mean_pow_7 + v_25_pow_7))))

  a_1_prime = ((1.0 + c_mean_g) * a_star_1)
  a_2_prime = ((1.0 + c_mean_g) * a_star_2)

  c_1_prime = Math.sqrt(a_1_prime**2 + b_star_1**2)
  c_2_prime = Math.sqrt(a_2_prime**2 + b_star_2**2)

  h_1_prime =
    if a_1_prime + b_star_1 == 0
      0
    else
      (to_degrees(Math.atan2(b_star_1, a_1_prime)) % 360.0)
    end
  h_2_prime =
    if a_2_prime + b_star_2 == 0
      0
    else
      (to_degrees(Math.atan2(b_star_2, a_2_prime)) % 360.0)
    end

  delta_lower_h_prime =
    if h_2_prime - h_1_prime < -180
      h_2_prime + 360 - h_1_prime
    elsif h_2_prime - h_1_prime > 180
      h_2_prime - h_1_prime - 360.0
    else
      h_2_prime - h_1_prime
    end

  delta_upper_l_prime = l_star_2 - l_star_1
  delta_upper_c_prime = c_2_prime - c_1_prime
  delta_upper_h_prime = (
    2.0 *
    Math.sqrt(c_1_prime * c_2_prime) *
    Math.sin(to_radians(delta_lower_h_prime / 2.0))
  )

  l_prime_mean = ((l_star_1 + l_star_2) / 2.0)
  c_prime_mean = ((c_1_prime + c_2_prime) / 2.0)
  h_prime_mean =
    if c_1_prime * c_2_prime == 0
      h_1_prime + h_2_prime
    elsif (h_2_prime - h_1_prime).abs <= 180
      ((h_1_prime + h_2_prime) / 2.0)
    elsif h_2_prime + h_1_prime <= 360
      ((h_1_prime + h_2_prime) / 2.0 + 180.0)
    else
      ((h_1_prime + h_2_prime) / 2.0 - 180.0)
    end

  l_prime_mean50sq = ((l_prime_mean - 50)**2)

  upper_s_l = (1 + (0.015 * l_prime_mean50sq / Math.sqrt(20 + l_prime_mean50sq)))
  upper_s_c = (1 + 0.045 * c_prime_mean)
  upper_t = (
    1 -
    0.17 * Math.cos(to_radians(h_prime_mean - 30)) +
    0.24 * Math.cos(to_radians(2 * h_prime_mean)) +
    0.32 * Math.cos(to_radians(3 * h_prime_mean + 6)) -
    0.2 * Math.cos(to_radians(4 * h_prime_mean - 63))
  )

  upper_s_h = (1 + 0.015 * c_prime_mean * upper_t)

  delta_theta = (30 * Math.exp(-1 * ((h_prime_mean - 275) / 25.0)**2))
  upper_r_c = (2 * Math.sqrt(c_prime_mean**7 / (c_prime_mean**7 + v_25_pow_7)))
  upper_r_t = (-Math.sin(to_radians(2 * delta_theta)) * upper_r_c)
  delta_l_prime_div_kl_div_sl = (delta_upper_l_prime / upper_s_l / k_l.to_f)
  delta_c_prime_div_kc_div_sc = (delta_upper_c_prime / upper_s_c / k_c.to_f)
  delta_h_prime_div_kh_div_sh = (delta_upper_h_prime / upper_s_h / k_h.to_f)

  Math.sqrt(
    delta_l_prime_div_kl_div_sl**2 +
    delta_c_prime_div_kc_div_sc**2 +
    delta_h_prime_div_kh_div_sh**2 +
    upper_r_t * delta_c_prime_div_kc_div_sc * delta_h_prime_div_kh_div_sh
  )
end

#delta_e94(other, weight: :graphic_arts) ⇒ Object

Implements the CIELAB ΔE* 1994 perceptual color distance metric. This version is an improvement over previous versions, but it does not handle perceptual discontinuities as well as CIELAB ΔE* 2000. This is implemented because some functions still require the 1994 algorithm for proper operation.

See [CIE94] for precise details on the mathematical formulas.

Different weights for ‘k_l`, `k_1`, and `k_2` may be applied via the `weight` keyword parameter. This may be provided either as a Hash with `k_l`, `k_1`, and `k_2` values or as a key to DE94_WEIGHTS. The default weight is `:graphic_arts`.

See also <www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html>.

[cie94]: en.wikipedia.org/wiki/Color_difference#CIE94

Raises:

  • (ArgumentError)


291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/color/cielab.rb', line 291

def delta_e94(other, weight: :graphic_arts)
  weight = DE94_WEIGHTS[weight] if DE94_WEIGHTS.key?(weight)
  raise ArgumentError, "Unsupported weight #{weight.inspect}." unless weight.is_a?(Hash)

  weight => k_1:, k_2:, k_l:

  # Under some circumstances in real computers, the computed value of ΔH could be an
  # imaginary number (it's a square root value), so instead of √(((ΔL/(kL*sL))²) +
  # ((ΔC/(kC*sC))²) + ((ΔH/(kH*sH))²)), we have implemented the final computation as
  # √(((ΔL/(kL*sL))²) + ((ΔC/(kC*sC))²) + (ΔH2/(kH*sH)²)) and not performing the square
  # root when computing ΔH2.

  k_c = k_h = 1.0

  other = coerce(other)

  self => l: l_1, a: a_1, b: b_1
  other => l: l_2, a: a_2, b: b_2

  delta_a = a_1 - a_2
  delta_b = b_1 - b_2

  cab_1 = Math.sqrt((a_1**2) + (b_1**2))
  cab_2 = Math.sqrt((a_2**2) + (b_2**2))

  delta_upper_l = l_1 - l_2
  delta_upper_c = cab_1 - cab_2

  delta_h2 = (delta_a**2) + (delta_b**2) - (delta_upper_c**2)

  s_upper_l = 1.0
  s_upper_c = 1 + k_1 * cab_1
  s_upper_h = 1 + k_2 * cab_1

  composite_upper_l = (delta_upper_l / (k_l * s_upper_l))**2
  composite_upper_c = (delta_upper_c / (k_c * s_upper_c))**2
  composite_upper_h = delta_h2 / ((k_h * s_upper_h)**2)
  Math.sqrt(composite_upper_l + composite_upper_c + composite_upper_h)
end

#inspectObject

:nodoc:



338
# File 'lib/color/cielab.rb', line 338

def inspect = "CIELAB [%.4f %.4f %.4f]" % [l, a, b] # :nodoc:

#pretty_print(q) ⇒ Object

:nodoc:



341
342
343
344
345
346
347
348
349
350
351
# File 'lib/color/cielab.rb', line 341

def pretty_print(q) # :nodoc:
  q.text "CIELAB"
  q.breakable
  q.group 2, "[", "]" do
    q.text "%.4f" % l
    q.fill_breakable
    q.text "%.4f" % a
    q.fill_breakable
    q.text "%.4f" % b
  end
end

#to_cmykObject

Converts CIELAB to Color::CMYK via Color::RGB.

See #to_rgb and Color::RGB#to_cmyk.



106
# File 'lib/color/cielab.rb', line 106

def to_cmyk(...) = to_rgb(...).to_cmyk(...)

#to_grayscaleObject

Converts CIELAB to Color::Grayscale via Color::RGB.

See #to_rgb and Color::RGB#to_grayscale.



112
# File 'lib/color/cielab.rb', line 112

def to_grayscale(...) = to_rgb(...).to_grayscale(...)

#to_hslObject

Converts CIELAB to Color::HSL via Color::RGB.

See #to_rgb and Color::RGB#to_hsl.



121
# File 'lib/color/cielab.rb', line 121

def to_hsl(...) = to_rgb(...).to_hsl(...)

#to_labObject



115
# File 'lib/color/cielab.rb', line 115

def to_lab(...) = self

#to_rgbObject

Converts CIELAB to Color::RGB via Color::XYZ.

See #to_xyz and Color::XYZ#to_rgb.



127
# File 'lib/color/cielab.rb', line 127

def to_rgb(...) = to_xyz(...).to_rgb(...)

#to_xyz(*args, **kwargs) ⇒ Object

Converts CIELAB to Color::XYZ based on a reference white.

Accepts a single keyword parameter, ‘white`, indicating the reference white used for conversion scaling. If none is provided, Color::XYZ::D65 is used.

:call-seq:

to_xyz(white: Color::XYZ::D65)


137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/color/cielab.rb', line 137

def to_xyz(*args, **kwargs)
  fy = (l + 16.0) / 116
  fz = fy - b / 200.0
  fx = a / 500.0 + fy

  xr = ((fx3 = fx**3) > Color::XYZ::E) ? fx3 : (116.0 * fx - 16) / Color::XYZ::K
  yr = (l > Color::XYZ::EK) ? ((l + 16.0) / 116)**3 : l
  zr = ((fz3 = fz**3) > Color::XYZ::E) ? fz3 : (116.0 * fz - 16) / Color::XYZ::K

  ref = kwargs[:white] || args.first
  ref = Color::XYZ::D65 unless ref.is_a?(Color::XYZ)

  ref.scale(xr, yr, zr)
end

#to_yiqObject

Converts CIELAB to Color::YIQ via Color::XYZ.



154
# File 'lib/color/cielab.rb', line 154

def to_yiq(...) = to_xyz(...).to_yiq(...)