Module: Retouch::Kernels::LUT

Defined in:
lib/retouch/kernels/lut.rb

Class Method Summary collapse

Class Method Details

.apply(image, operation, value = nil, amount: 1.0) ⇒ Object

Raises:

  • (ArgumentError)


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
# File 'lib/retouch/kernels/lut.rb', line 8

def apply(image, operation, value = nil, amount: 1.0)
  bytes = image.bytes.dup
  number = value.nil? || operation.to_sym == :tint ? nil : Float(value)
  raise ArgumentError, "value must be finite" if number && !number.finite?

  case operation.to_sym
  when :grayscale
    bytes.bytes.each_slice(4).with_index do |(red, green, blue, _alpha), index|
      gray = (((red * 2126) + (green * 7152) + (blue * 722)) / 10_000.0).round
      bytes.setbyte(index * 4, gray)
      bytes.setbyte((index * 4) + 1, gray)
      bytes.setbyte((index * 4) + 2, gray)
    end
  when :invert
    lut = Array.new(256) { |channel| 255 - channel }
    transform_rgb(bytes) { |channel| lut[channel] }
  when :brightness
    raise ArgumentError, "brightness must be between -255 and 255" unless number&.between?(-255, 255)

    lut = Array.new(256) { |channel| (channel + number).round.clamp(0, 255) }
    transform_rgb(bytes) { |channel| lut[channel] }
  when :contrast
    raise ArgumentError, "contrast must be between 0 and 10" unless number&.between?(0, 10)

    lut = Array.new(256) { |channel| (((channel - 127.5) * number) + 127.5).round.clamp(0, 255) }
    transform_rgb(bytes) { |channel| lut[channel] }
  when :gamma
    raise ArgumentError, "gamma must be positive and at most 10" unless number&.positive? && number <= 10

    lut = Array.new(256) { |channel| (255 * ((channel / 255.0)**(1.0 / number))).round }
    transform_rgb(bytes) { |channel| lut[channel] }
  when :saturate
    raise ArgumentError, "saturation must be between 0 and 10" unless number&.between?(0, 10)

    bytes.bytes.each_slice(4).with_index do |(red, green, blue, _alpha), index|
      gray = (red * 0.2126) + (green * 0.7152) + (blue * 0.0722)
      [red, green, blue].each_with_index do |channel, component|
        bytes.setbyte((index * 4) + component, (gray + ((channel - gray) * number)).round.clamp(0, 255))
      end
    end
  when :tint
    raise ArgumentError, "tint amount must be between 0 and 1" unless amount.is_a?(Numeric) && amount.finite? && amount.between?(0, 1)

    tint = Tessel::Color.pack(value).bytes.first(3)
    bytes.bytes.each_slice(4).with_index do |(red, green, blue, _alpha), index|
      [red, green, blue].each_with_index do |channel, component|
        bytes.setbyte((index * 4) + component, ((channel * (1 - amount)) + (tint[component] * amount)).round)
      end
    end
  when :opacity
    raise ArgumentError, "opacity must be between 0 and 1" unless number&.between?(0, 1)

    (3...bytes.bytesize).step(4) { |index| bytes.setbyte(index, (bytes.getbyte(index) * number).round) }
  else
    raise ArgumentError, "unknown LUT operation: #{operation}"
  end
  Tessel::Image.from_rgba(image.width, image.height, bytes, metadata: image.)
end