Skip to content

Commit 04df112

Browse files
committed
Support abs2 via Future
To smoothly transition between the old `abs2` return value and the new one (which differs by a factor of 3 for RGB), this introduces an internal module `ColorVectorSpace.Future` permitting users to transition immediately to the new behavior. `Base.abs2` will return the old value with a depwarn; users can switch immediately to `ColorVectorSpace.Future.abs2` to avoid it.
1 parent 5504756 commit 04df112

File tree

3 files changed

+63
-14
lines changed

3 files changed

+63
-14
lines changed

README.md

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@ represents the "RGB vector space" version.
3838

3939
This package also defines `norm(c)` for RGB and grayscale colors.
4040
This makes these color spaces [normed vector spaces](https://en.wikipedia.org/wiki/Normed_vector_space).
41-
Note that `norm` has been designed to satisfy equivalence of grayscale and RGB representations: if
41+
Note that `norm` has been designed to satisfy **equivalence** of grayscale and RGB representations: if
4242
`x` is a scalar, then `norm(x) == norm(Gray(x)) == norm(RGB(x, x, x))`.
4343
Effectively, there's a division-by-3 in the `norm(::RGB)` case compared to the Euclidean interpretation of
4444
the RGB vector space.
4545
Equivalence is an important principle for the Colors ecosystem, and violations should be reported as likely bugs.
46+
One violation is `abs2`; see the section below for more detail.
4647

4748
## Usage
4849

@@ -117,15 +118,33 @@ The corresponding `stdmult` computes standard deviation.
117118

118119
To begin with, there is no general and straightforward definition of the
119120
absolute value of a vector.
120-
There are roughly two possible definitions of `abs`/`abs2`: as a channel-wise
121+
There are two reasonably intuitive definitions of `abs`/`abs2`: as a channel-wise
121122
operator or as a function which returns a real number based on the norm.
122123
For the latter, there are also variations in the definition of norm.
123124

124-
In ColorVectorSpace v0.9 and later, `abs` is defined as a channel-wise operator
125-
and `abs2` is undefined.
126-
The following are alternatives for the definitions in ColorVectorSpace v0.8 and
127-
earlier.
128-
```julia
129-
_abs(c) = mapreducec(v->abs(float(v)), +, 0, c)
130-
_abs2(c) = mapreducec(v->float(v)^2, +, 0, c)
131-
```
125+
In ColorVectorSpace v0.9 and later, `abs` is defined as a channel-wise operator.
126+
`abs2` returns a real-valued scalar. In previous versions of ColorVectorSpace,
127+
for `g = Gray(0.3)`, ColorVectorSpace returned different values for `abs2(g)` and
128+
`abs2(RGB(g))`. This breaks the equivalence of `g` and `RGB(g)`.
129+
This behavior is retained, with a deprecation warning, starting with
130+
ColorVectorSpace 0.9.6.
131+
132+
**In the future**, `abs2` will be defined as `abs2(c) == c⋅c ≈ norm(c)^2`.
133+
This effectively divides the old result by 3; code that imposes thresholds
134+
on `abs2(c)` may need to be updated.
135+
You can obtain that behavior now--and circumvent the deprecation warning--
136+
by using `ColorVectorSpace.Future.abs2(c)`.
137+
138+
We anticipate the following transition schedule:
139+
140+
- Sept 20, 2021: release ColorVectorSpace 0.9.6 with both `abs2` and `Future.abs2`.
141+
`abs2` will have a "quiet" deprecation warning (visible with `--depwarn=yes`
142+
or when running `Pkg.test`)
143+
- Jan 1, 2022: make the deprecation warning "noisy" (cannot be turned off).
144+
This is designed to catch user-level scripts that may need to update thresholds
145+
or other constants.
146+
- *Apr 1, 2022: transition `abs2` to the new definition
147+
and make `Future.abs2` give a "noisy" depwarn to revert to regular `abs2`.
148+
- *July 1, 2022: remove `Future.abs2`.
149+
150+
The two marked with `*` denote breaking releases.

src/ColorVectorSpace.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ if !hasmethod(zero, (Type{TransparentGray},))
4141
zero(p::Colorant) = zero(typeof(p))
4242
end
4343

44+
if !hasmethod(one, (Type{Gray24},))
45+
Base.one(::Type{Gray24}) = Gray24(1)
46+
end
47+
4448
if !hasmethod(one, (Type{TransparentGray},)) # specification change is planned for ColorTypes v0.12
4549
Base.one(::Type{C}) where {C<:TransparentGray} = C(1,1)
4650
Base.one(::Type{C}) where {C<:AbstractRGB} = C(1,1,1)
@@ -473,6 +477,24 @@ function stdmult(op, itr; kwargs...)
473477
return isa(result, Union{Real,Colorant,RGBRGB}) ? _sqrt(result) : _sqrt.(result)
474478
end
475479

480+
Base.abs2(g::AbstractGray) = abs2(gray(g))
481+
function Base.abs2(c::AbstractRGB)
482+
Base.depwarn("""
483+
The return value of `abs2` will change to ensure that `abs2(g::Gray) ≈ abs2(RGB(g::Gray))`.
484+
This corresponds to dividing the prevous output for `RGB` by 3.
485+
486+
To avoid this warning, use `ColorVectorSpace.Future.abs2` instead of `abs2` .
487+
If you are getting this from `var`, use `varmult` instead.
488+
""", :abs2)
489+
return mapreducec(v->float(v)^2, +, zero(eltype(c)), c)
490+
end
491+
492+
module Future
493+
using ..ColorTypes
494+
using ..ColorVectorSpace: , dot
495+
abs2(c::Union{Real,AbstractGray,AbstractRGB}) = c c
496+
end
497+
476498
function __init__()
477499
if isdefined(Base, :Experimental) && isdefined(Base.Experimental, :register_error_hint)
478500
Base.Experimental.register_error_hint(MethodError) do io, exc, argtypes, kwargs

test/runtests.jl

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ ColorTypes.gray( c::GrayA32) = reinterpret(N0f8, (c.color >> 0x18) % UInt8)
5151
ColorTypes.alpha(c::GrayA32) = reinterpret(N0f8, c.color % UInt8)
5252
ColorTypes.comp2(c::RGBA32) = alpha(c)
5353

54-
@testset "Colortypes" begin
54+
# @testset "Colortypes" begin
5555
@testset "ambiguities" begin
5656
@test isempty(detect_ambiguities(ColorVectorSpace))
5757
end
@@ -137,7 +137,7 @@ ColorTypes.comp2(c::RGBA32) = alpha(c)
137137
@test_throws MethodError cf ÷ cf
138138
@test cf + 0.1 === 0.1 + cf === Gray(Float64(0.1f0) + 0.1)
139139
@test cf64 - 0.1f0 === -(0.1f0 - cf64) === Gray( 0.2 - Float64(0.1f0))
140-
@test_throws MethodError abs2(ccmp)
140+
@test ColorVectorSpace.Future.abs2(ccmp) === ColorVectorSpace.Future.abs2(gray(ccmp))
141141
@test norm(cf) == norm(cf, 2) == norm(gray(cf))
142142
@test norm(cf, 1) == norm(gray(cf), 1)
143143
@test norm(cf, Inf) == norm(gray(cf), Inf)
@@ -405,7 +405,10 @@ ColorTypes.comp2(c::RGBA32) = alpha(c)
405405
@test isinf(RGB(1, Inf, 0.5))
406406
@test !isnan(RGB(1, Inf, 0.5))
407407
@test abs(RGB(0.1,0.2,0.3)) == RGB(0.1,0.2,0.3)
408-
@test_throws MethodError abs2(RGB(0.1,0.2,0.3))
408+
cv = RGB(0.1,0.2,0.3)
409+
@test ColorVectorSpace.Future.abs2(cv) == cv cv
410+
@test_logs (:warn, r"change to ensure") abs2(cv) > 2*ColorVectorSpace.Future.abs2(cv)
411+
@test ColorVectorSpace.Future.abs2(cv) norm(cv)^2
409412
@test_throws MethodError sum(abs2, RGB(0.1,0.2,0.3))
410413
@test norm(RGB(0.1,0.2,0.3)) sqrt(0.14)/sqrt(3)
411414

@@ -777,6 +780,7 @@ ColorTypes.comp2(c::RGBA32) = alpha(c)
777780
@test norm(x, p) == norm(g, p) norm(c, p)
778781
end
779782
@test dot(x, x) == dot(g, g) dot(c, c)
783+
@test abs2(x) == abs2(g) ColorVectorSpace.Future.abs2(c)
780784
@test_throws MethodError mapreduce(x->x^2, +, c) # this risks breaking equivalence & noniterability
781785
end
782786

@@ -804,6 +808,10 @@ ColorTypes.comp2(c::RGBA32) = alpha(c)
804808
sv2 = mapc(sqrt, v2)
805809
@test varmult(, cs, dims=1) [v2 v2]
806810
@test stdmult(, cs, dims=1) [sv2 sv2]
811+
812+
# When ColorVectorSpace.Future.abs2 becomes the default, delete the `@test_logs`
813+
# and change the `@test_broken` to `@test`
814+
@test_logs (:warn, r"will change to ensure") match_mode=:any @test_broken var(cs) == varmult(, cs)
807815
end
808816

809817
@testset "copy" begin
@@ -813,4 +821,4 @@ ColorTypes.comp2(c::RGBA32) = alpha(c)
813821
@test copy(c) === c
814822
end
815823

816-
end
824+
# end

0 commit comments

Comments
 (0)