Skip to content

Commit

Permalink
feat: CIELAB colorspace and CIEDE2000 distance algorithm (#1)
Browse files Browse the repository at this point in the history
* feat: add CIELAB and DIN99 colorspaces and distance algorithms

* fix: a couple of sigil tests

* chore: bump version to 0.4.0

* test: add nearest color algorithm test to CIELAB colorspace struct

* feat: add `RGB.grayscale?/1`

* feat: Add color analytic functions

* docs: update readme with supported color spaces

* feat: add rose color category to RGB module

* fix: improve color categories

* WIP: add color table script

* WIP: add LCh color space

* WIP: update color table script

* feat: add Delta E CIEDE2000 algorithm

* fix: CIELAB to LCh conversion

* add CIEDE2000 distance algorithm

* fix: CIEDE2000 algorithm

* fix: CIEDE2000 distance calculation

* refactor: remove LCh colorspace

* refactor: move distance algorithms to another folder

* fix: tests

* refactor: clean up

* refactor: rename CIELAB to Lab

* refactor: restructure protocols

* docs: improve readme

* fix: format in test file

* refactor: distance algorithms behavior

* feat: add distance cache

* fix: improve performance and allow DistanceCache to be not running

* fix: improve precision for XYZ colorspace

* feat: add `HSV.grayscale?/1`

* feat: add `RGB.grayish?/2`

* fix: use `Mix.Config` rather than `Config`

* test: implement missing test for all modules

* fix: allow distance algorithms to get any color type as first arg

* docs: add missing docs

* chore: bump version

* chore: remove script

* fix: docs since

* chore: remove Elixir 1.7 from officially supported versions

* chore: temporarily remove Elixir 1.10 from travis config
  • Loading branch information
tlux committed Mar 30, 2020
1 parent 25cf960 commit da74af6
Show file tree
Hide file tree
Showing 65 changed files with 2,663 additions and 487 deletions.
8 changes: 3 additions & 5 deletions .travis.yml
Expand Up @@ -12,12 +12,10 @@ after_script:

matrix:
include:
- elixir: '1.7.4'
otp_release: '20.3.8'
- elixir: '1.8.1'
- elixir: '1.8.2'
otp_release: '21.3.8'
- elixir: '1.9.0'
otp_release: '22.0.4'
- elixir: '1.9.4'
otp_release: '22.0.7'

cache:
directories:
Expand Down
150 changes: 141 additions & 9 deletions README.md
Expand Up @@ -7,12 +7,17 @@
An Elixir library allowing calculations with colors and conversions between
different colorspaces.

Currently supports the [RGB](https://en.wikipedia.org/wiki/RGB_color_space),
[CMYK](https://en.wikipedia.org/wiki/CMYK_color_model) and [HSV](https://en.wikipedia.org/wiki/HSL_and_HSV) color models.
Currently supports the following color models:
* [RGB](https://en.wikipedia.org/wiki/RGB_color_space)
* [CMYK](https://en.wikipedia.org/wiki/CMYK_color_model)
* [HSV](https://en.wikipedia.org/wiki/HSL_and_HSV)
* L\*a\*b\* ([CIELAB](https://en.wikipedia.org/wiki/CIELAB_color_space))
* XYZ ([CIE 1931](https://en.wikipedia.org/wiki/CIE_1931_color_space))
* [DIN99](https://de.wikipedia.org/wiki/DIN99-Farbraum)

## Prerequisites

- Elixir 1.7 or greater
* Elixir 1.8 or greater

## Installation

Expand All @@ -22,34 +27,90 @@ The package can be installed by adding `tint` to your list of dependencies in
```elixir
def deps do
[
{:tint, "~> 0.3"}
{:tint, "1.0.0-rc.0"}
]
end
```

## Usage

### RGB
### Colorspaces

#### RGB

```elixir
iex> Tint.RGB.new(255, 0, 0)
#Tint.RGB<255,0,0>
#Tint.RGB<255,0,0 (#FF0000)>
```

or

```elixir
iex> import Tint.Sigil
...> ~K[255, 0, 0]r
#Tint.RGB<255,0,0 (#FF0000)>
```

### CMYK
#### CMYK

```elixir
iex> Tint.CMYK.new(0.55, 0.26, 0.24, 0.65)
#Tint.CMYK<55%,26%,24%,65%>
```

### HSV
or

```elixir
iex> import Tint.Sigil
...> ~K[0.55, 0.26, 0.24, 0.65]c
#Tint.CMYK<55%,26%,24%,65%>
```

#### HSV

```elixir
iex> Tint.HSV.new(25.8, 0.882, 1)
#Tint.HSV<25.8°,88.2%,100%>
```

or

```elixir
iex> import Tint.Sigil
...> ~K[25.8, 0.882, 1]h
#Tint.HSV<25.8°,88.2%,100%>
```

#### CIELAB

```elixir
iex> Tint.Lab.new(53.2329, 80.1068, 67.2202)
#Tint.Lab<53.2329,80.1068,67.2202>
```

or

```elixir
iex> import Tint.Sigil
...> ~K[53.2329, 80.1068, 67.2202]c
#Tint.Lab<53.2329,80.1068,67.2202>
```

#### DIN99

```elixir
iex> Tint.DIN99.new(53.2329, 80.1068, 67.2202)
#Tint.DIN99<53.2329,80.1068,67.2202>
```

or

```elixir
iex> import Tint.Sigil
...> ~K[53.2329, 80.1068, 67.2202]d
#Tint.DIN99<53.2329,80.1068,67.2202>
```

### Conversion

#### Between Colorspaces
Expand All @@ -60,11 +121,24 @@ iex> rgb = Tint.RGB.new(255, 0, 0)
#Tint.HSV<0°,100%,100%>
```

The complete list:

```elixir
Tint.to_cmyk(color)
Tint.to_din99(color)
Tint.to_hsv(color)
Tint.to_lab(color)
Tint.to_rgb(color)
Tint.to_xyz(color)
```

Currently, only RGB can be converted to any other colorspace.

#### Hex Code

```elixir
iex> Tint.RGB.from_hex("#FF0000")
{:ok, #Tint.RGB<255,0,0>}
{:ok, #Tint.RGB<255,0,0 (#FF0000)>}
```

```elixir
Expand All @@ -77,6 +151,64 @@ iex> Tint.RGB.to_hex(Tint.RGB.new(255, 0, 0))
"#FF0000"
```

Alternatively, you can use the sigil as a shortcut:

```elixir
iex> import Tint.Sigil
...> ~K[#FF0000]
#Tint.RGB<255,0,0 (#FF0000)>
```

### Color Distance

There are a couple of functions to calculate the distance between two colors.

#### Euclidean Distance

The
[Euclidean distance](https://en.wikipedia.org/wiki/Color_difference#Euclidean)
can be calculated on RGB colors. Calculating the Euclidean distance is very fast
but may not be very precise. If you want maximum precision use the CIEDE2000
algorithm.

```elixir
iex> Tint.RGB.euclidean_distance(~K[#FFFFFF], ~K[#000000])
441.6729559300637
```

You can also define weights for the individual red, green and blue color
channels:

```elixir
iex> Tint.RGB.euclidean_distance(~K[#FFFFFF], ~K[#000000], weights: {2, 4, 3})
765.0
```

To find the nearest color from a given palette:

```elixir
iex> Tint.RGB.nearest_color(~K[#CC0000], [~K[#009900], ~K[#FF0000]])
#Tint.RGB<255,0,0 (#FF0000)>
```

#### CIEDE2000

[CIEDE2000](https://en.wikipedia.org/wiki/Color_difference#CIEDE2000) is an
algorithm that operates on the CIELAB colorspace. It is very slow compared to
the Euclidean distance algorithm but it is optimized to human color perception.

```elixir
iex> Tint.Lab.ciede2000_distance(~K[#FF0000], ~K[#000000])
50.3905024704449
```

To find the nearest color from a given palette:

```elixir
iex> Tint.Lab.nearest_color(~K[#FF0000], [~K[#009900], ~K[#CC0000]])
#Tint.RGB<204,0,0 (#CC0000)>
```

## Docs

The API docs can be found at [HexDocs](https://hexdocs.pm/tint).
17 changes: 17 additions & 0 deletions benchmarks/distance.exs
@@ -0,0 +1,17 @@
import Tint.Sigil

alias Tint.Distance

color = ~K[#FF0000]
other_color = ~K[#00FF00]
color_in_lab = Tint.to_lab(color)
other_color_in_lab = Tint.to_lab(other_color)

Benchee.run(%{
"RGB Delta E" => fn ->
Distance.Euclidean.distance(color, other_color, [])
end,
"CIEDE2000" => fn ->
Distance.CIEDE2000.distance(color_in_lab, other_color_in_lab, [])
end
})
4 changes: 2 additions & 2 deletions config/config.exs
Expand Up @@ -26,5 +26,5 @@ use Mix.Config
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env()}.exs"

import_config "#{Mix.env()}.exs"
3 changes: 3 additions & 0 deletions config/dev.exs
@@ -0,0 +1,3 @@
use Mix.Config

config :tint, distance_cache_size: 0
3 changes: 3 additions & 0 deletions config/test.exs
@@ -0,0 +1,3 @@
use Mix.Config

config :tint, distance_cache_size: 100
3 changes: 2 additions & 1 deletion coveralls.json
@@ -1,6 +1,7 @@
{
"skip_files": [
"deps"
"lib/tint/application.ex",
"test/support"
],
"terminal_options": {
"file_column_width": 80
Expand Down
46 changes: 36 additions & 10 deletions lib/tint.ex
Expand Up @@ -4,45 +4,71 @@ defmodule Tint do
colorspaces.
"""

alias Tint.CMYK
alias Tint.HSV
alias Tint.RGB
alias Tint.{CMYK, DIN99, HSV, Lab, RGB, XYZ}

@typedoc """
A type representing a color.
"""
@type color :: CMYK.t() | HSV.t() | RGB.t()
@type color ::
CMYK.t()
| DIN99.t()
| HSV.t()
| Lab.t()
| RGB.t()
| XYZ.t()

@doc """
Converts the given color to CMYK colorspace.
## Example
iex> Tint.to_cmyk(Tint.RGB.new(40, 66, 67))
#Tint.CMYK<40.2%,1.4%,0%,73.7%>
iex> Tint.to_cmyk(Tint.RGB.new(40, 66, 67))
#Tint.CMYK<40.2%,1.4%,0%,73.7%>
"""
@doc since: "0.3.0"
@spec to_cmyk(color) :: CMYK.t()
defdelegate to_cmyk(color), to: CMYK.Convertible

@doc """
Converts the given color to DIN99 colorspace.
"""
@doc since: "1.0.0"
@spec to_din99(color) :: DIN99.t()
defdelegate to_din99(color), to: DIN99.Convertible

@doc """
Converts the given color to HSV colorspace.
## Example
iex> Tint.to_hsv(Tint.RGB.new(255, 127, 30))
#Tint.HSV<25.8°,88.2%,100%>
iex> Tint.to_hsv(Tint.RGB.new(255, 127, 30))
#Tint.HSV<25.9°,88.2%,100%>
"""
@spec to_hsv(color) :: HSV.t()
defdelegate to_hsv(color), to: HSV.Convertible

@doc """
Converts the given color to CIELAB colorspace.
"""
@doc since: "1.0.0"
@spec to_lab(color) :: Lab.t()
defdelegate to_lab(color), to: Lab.Convertible

@doc """
Converts the given color to RGB colorspace.
## Example
iex> Tint.to_rgb(Tint.HSV.new(25.8, 0.882, 1))
#Tint.RGB<255,127,30>
iex> Tint.to_rgb(Tint.HSV.new(25.8, 0.882, 1))
#Tint.RGB<255,127,30 (#FF7F1E)>
"""
@spec to_rgb(color) :: RGB.t()
defdelegate to_rgb(color), to: RGB.Convertible

@doc """
Converts the given color to CIE XYZ colorspace.
"""
@doc since: "1.0.0"
@spec to_xyz(color) :: XYZ.t()
defdelegate to_xyz(color), to: XYZ.Convertible
end
17 changes: 17 additions & 0 deletions lib/tint/application.ex
@@ -0,0 +1,17 @@
defmodule Tint.Application do
@moduledoc false

use Application

def start(_type, _args) do
children = [
Tint.DistanceCache
]

Supervisor.start_link(
children,
strategy: :one_for_one,
name: Tint.Supervisor
)
end
end

0 comments on commit da74af6

Please sign in to comment.