diff --git a/CHANGELOG.md b/CHANGELOG.md index c030fb2..8aca6d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ This is the changelog for Image version 0.54.0 released on ____, 2024. For olde ### Enhancements -* Adds `Image.vibrance/3` following the [libvips discussion](https://github.com/libvips/libvips/discussions/4039). +* Adds `Image.vibrance/3` and `Image.vibrance!/3` following the [libvips discussion](https://github.com/libvips/libvips/discussions/4039). ## Image 0.53.0 diff --git a/lib/image.ex b/lib/image.ex index 5ea746d..ad3735f 100644 --- a/lib/image.ex +++ b/lib/image.ex @@ -8647,6 +8647,9 @@ defmodule Image do * `{:error, reason}`. """ + + # See https://github.com/libvips/libvips/discussions/4039 + @doc since: "0.54.0" @doc subject: "Operation" @@ -8657,23 +8660,74 @@ defmodule Image do use Image.Math with {:ok, options} <- Options.Vibrance.validate_options(options) do - identity = Operation.identity!() - h2 = 255 - h3 = 6 * identity / h2 - h4 = 2 / (1 + exp!(-h3)) - 1 - h5 = h3 / 6 + h1 = Operation.identity!() * 1.0 + threshold = options.threshold * 1.0 + + h2 = 255.0 + h3 = 6.0 * h1 / h2 + h4 = 2.0 / (1.0 + exp!(-h3)) - 1.0 + h5 = h3 / 6.0 h6 = h4 - h5 h7 = vibrance h8 = h5 + h7 * h6 with_colorspace(image, :lch, fn image -> - g5 = h2 * image[1] / options.threshold - g7 = options.threshold * Operation.maplut!(g5, h8) + g5 = h2 * image[1] / threshold + g7 = threshold * Operation.maplut!(g5, h8) Image.join_bands([image[0], g7, image[2]]) end) end end + @doc """ + Apply an curve adjustment to an image's saturation + (chroma) such that less saturated colors are more + affected than more saturated colors. Raises on + error. + + This operation is similar to the vibrance function + in Adobe Lightroom. However this implementation does + not account for skin tones. + + ### Arguments + + * `image` is any `t:Vix.Vips.Image.t/0`. + + * `saturation` is any float greater than `0.0`. A number less + than `1.0` means reduce saturation. A number greater than `1.0` + means increase saturation. + + * `options` is a keyword list of options. + + ### Options + + * `:threshold` is the saturation level above which no + adjustment is made. The range is `1..100` with a default + of `70`. + + ### Returns + + * `adjusted_image` or + + * raises an exception. + + """ + + # See https://github.com/libvips/libvips/discussions/4039 + + @doc since: "0.54.0" + @doc subject: "Operation" + + @spec vibrance!(image :: Vimage.t(), vibrance :: float(), options :: Options.Vibrance.vibrance_options()) :: + Vimage.t() | no_return() + + def vibrance!(%Vimage{} = image, vibrance, options \\ []) when is_multiplier(vibrance) do + case vibrance(image, vibrance, options) do + {:ok, image} -> image + {:error, reason} -> raise Image.Error, reason + end + end + @doc """ Reduces noise in an image by applying a median filter. diff --git a/lib/image/math.ex b/lib/image/math.ex index 68cb67e..d443b4b 100644 --- a/lib/image/math.ex +++ b/lib/image/math.ex @@ -262,7 +262,7 @@ defmodule Image.Math do end @spec pow(number(), number()) :: {:ok, number()} - def pow(a, b) do + def pow(a, b) when is_number(a) and is_number(b) do {:ok, Kernel.**(a, b)} end @@ -359,7 +359,7 @@ defmodule Image.Math do end @spec multiply(number(), number()) :: {:ok, number} - def multiply(a, b) do + def multiply(a, b) when is_number(a) and is_number(b) do {:ok, Kernel.*(a, b)} end @@ -373,10 +373,13 @@ defmodule Image.Math do divide(image, [value]) end - # @spec divide(number(), Vimage.t()) :: {:ok, Vimage.t()} | {:error, Image.error_message()} - # def divide(value, %Vimage{} = image) when is_number(value) do - # - # end + # See https://github.com/libvips/libvips/blob/master/cplusplus/VImage.cpp#L1062-L1066 + @spec divide(number(), Vimage.t()) :: {:ok, Vimage.t()} | {:error, Image.error_message()} + def divide(value, %Vimage{} = image) when is_number(value) do + image + |> pow!(-1.0) + |> multiply(value) + end @spec divide(Vimage.t(), [number()]) :: {:ok, Vimage.t()} | {:error, Image.error_message()} def divide(%Vimage{} = image, value) when is_list(value) do @@ -621,8 +624,16 @@ defmodule Image.Math do end end + @spec divide!(Image.pixel(), Vimage.t()) :: Vimage.t() | no_return() + def divide!(value, %Vimage{} = image) do + case divide(value, image) do + {:ok, image} -> image + {:error, reason} -> raise ArgumentError, reason + end + end + @spec divide!(number(), number()) :: number() | no_return() - def divide!(a, b) do + def divide!(a, b) when is_number(a) and is_number(b) do Kernel./(a, b) end @@ -643,7 +654,7 @@ defmodule Image.Math do end @spec pow!(number(), number()) :: number() | no_return() - def pow!(a, b) do + def pow!(a, b) when is_number(a) and is_number(b) do Kernel.**(a, b) end