Skip to content

Redesign color arithmetic #131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
name = "ColorVectorSpace"
uuid = "c3611d14-8923-5661-9e6a-0046d554d3a4"
version = "0.8.7"
version = "0.9.0"

[deps]
ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f"
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
TensorCore = "62fd8b95-f654-4bbd-a8a5-9c27f68ccd50"

[compat]
ColorTypes = "0.8, 0.9, 0.10"
Colors = "0.9, 0.10, 0.11, 0.12"
FixedPointNumbers = "0.6, 0.7, 0.8"
SpecialFunctions = "0.7, 0.8, 0.9, 0.10, 1.0"
StatsBase = "0.28, 0.29, 0.30, 0.31, 0.32, 0.33"
ColorTypes = "0.10"
FixedPointNumbers = "0.8"
SpecialFunctions = "0.7, 0.8, 0.9, 0.10"
TensorCore = "0.1"
julia = "1"

[extras]
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Statistics", "StatsBase", "LinearAlgebra", "Test"]
test = ["Colors", "Statistics", "LinearAlgebra", "Test"]
84 changes: 77 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,27 @@
This package is an add-on to [ColorTypes](https://github.com/JuliaGraphics/ColorTypes.jl), and provides fast
mathematical operations for objects with types such as `RGB` and
`Gray`.
Specifically, with this package both grayscale and `RGB` colors are treated as if they are points
in a normed vector space.

## Introduction

Colorspaces such as RGB, unlike XYZ, are technically non-linear; the
"colorimetrically correct" approach when averaging two RGBs is to
Colorspaces such as RGB, unlike XYZ, are technically non-linear;
perhaps the most "colorimetrically correct" approach when averaging two RGBs is to
first convert each to XYZ, average them, and then convert back to RGB.
Nor is there a clear definition of computing the sum of two colors.
As a consequence, Julia's base color package,
[ColorTypes](https://github.com/JuliaGraphics/ColorTypes.jl),
does not support mathematical operations on colors.

However, particularly in image processing it is common to ignore this
concern, and for the sake of performance treat an RGB as if it were a
3-vector. This package provides such operations.
3-vector. The role of this package is to extend ColorTypes to support such mathematical operations.
Specifically, it defines `+` and multiplication by a scalar (and by extension, `-` and division by a scalar) for grayscale and `AbstractRGB` colors.
These are the requirements of a [vector space](https://en.wikipedia.org/wiki/Vector_space).

If you're curious about how much difference it makes, the following
If you're curious about how much the "colorimetrically correct" and
"vector space" views differ, the following
diagram might help. The first 10 `distinguishable_colors` were
generated, and all pairs were averaged. Each box represents the
average of the pair of diagonal elements intersected by tracing
Expand All @@ -27,16 +36,77 @@ represents the "RGB vector space" version.

![ColorVectorSpace](images/comparison.png "Comparison")

This package also defines `norm(c)` for RGB and grayscale colors.
This makes these color spaces [normed vector spaces](https://en.wikipedia.org/wiki/Normed_vector_space).
Note that `norm` has been designed to satisfy equivalence of grayscale and RGB representations: if
`x` is a scalar, then `norm(x) == norm(Gray(x)) == norm(RGB(x, x, x))`.
Effectively, there's a division-by-3 in the `norm(::RGB)` case compared to the Euclidean interpretation of
the RGB vector space.
Equivalence is an important principle for the Colors ecosystem, and violations should be reported as likely bugs.

## Usage

```julia
using ColorTypes, ColorVectorSpace
```

That's it. Just by loading `ColorVectorSpace`, most basic mathematical
For the most part, that's it; just by loading `ColorVectorSpace`, most basic mathematical
operations will "just work" on `AbstractRGB`, `AbstractGray`
(`Color{T,1}`), `TransparentRGB`, and `TransparentGray` objects.
(See definitions for the latter inside of `ColorTypes`).

If you discover missing operations, please open an issue, or better
yet submit a pull request.
However, there are some additional operations that you may need to distinguish carefully.

### Multiplication

Grayscale values are conceptually similar to scalars, and consequently it seems straightforward to define multiplication of two grayscale values.
RGB values present more options.
This package supports three different notions of multiplication: the inner product, the hadamard (elementwise) product, and the tensor product.

```julia
julia> c1, c2 = RGB(0.2, 0.3, 0.4), RGB(0.5, 0.3, 0.2)
(RGB{Float64}(0.2,0.3,0.4), RGB{Float64}(0.5,0.3,0.1))

julia> c1⋅c2 # \cdot<TAB> # or dot(c1, c2)
0.09000000000000001

# This is equivelant to `mapc(*, c1, c2)`
julia> c1⊙c2 # \odot<TAB> # or hadamard(c1, c2)
RGB{Float64}(0.1,0.09,0.08000000000000002)

julia> c1⊗c2 # \otimes<TAB> # or tensor(c1, c2)
RGBRGB{Float64}(
0.1 0.06 0.04000000000000001
0.15 0.09 0.06
0.2 0.12 0.08000000000000002)
```

Note that `c1⋅c2 = (c1.r*c2.r + c1.g*c2.g + c1.b*c2.b)/3`, where the division by 3 ensures the equivalence `norm(x) == norm(Gray(x)) == norm(RGB(x, x, x))`.

It is designed to not support the ordinary multiplication operation `*` because it is not obvious which one of these should be the default option.

However, `*` is defined for grayscale since all these three multiplication operations (i.e., `⋅`, `⊙` and `⊗`) are equivalent in the 1D vector space.

### Variance

The variance `v = E((c - μ)^2)` (or its bias-corrected version) involves a multiplication,
and to be consistent with the above you must specify which sense of multiplication you wish to use:

```julia
julia> cs = [c1, c2]
2-element Array{RGB{Float64},1} with eltype RGB{Float64}:
RGB{Float64}(0.2,0.3,0.4)
RGB{Float64}(0.5,0.3,0.2)

julia> varmult(⋅, cs)
0.021666666666666667

julia> varmult(⊙, cs)
RGB{Float64}(0.045,0.0,0.020000000000000004)

julia> varmult(⊗, cs)
RGBRGB{Float64}(
0.045 0.0 -0.03
0.0 0.0 0.0
-0.03 0.0 0.020000000000000004)
```
Loading