Skip to content

Commit 21e7a6b

Browse files
authored
Improve conversions and support ImageCore (#4)
These are required for compatibility with ImageView and generally fill out the package. This also changes the design to always use `RGB{N0f8}` for the colors.
1 parent f22ebe3 commit 21e7a6b

File tree

9 files changed

+119
-129
lines changed

9 files changed

+119
-129
lines changed

.github/workflows/CI.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ jobs:
1818
fail-fast: false
1919
matrix:
2020
version:
21-
- '1.6'
21+
- '1.6' # 1.6 lacks `@constprop :aggressive` so it's a bit poorly-supported
22+
- '1.7' # therefore make sure we directly test 1.7 as well as more current versions
2223
- '1'
2324
- 'nightly'
2425
os:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
*.jl.cov
33
*.jl.mem
44
/Manifest.toml
5+
/docs/Manifest.toml
56
/docs/build/

Project.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
1313
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
1414

1515
[compat]
16-
ColorTypes = "0.11"
16+
ColorTypes = "0.11.1"
1717
ColorVectorSpace = "0.9"
1818
Colors = "0.12"
19-
Compat = "3.36"
2019
FixedPointNumbers = "0.8"
2120
Reexport = "1"
2221
Requires = "1"
2322
julia = "1.6"
2423

2524
[extras]
25+
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
2626
StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
2727
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
2828

2929
[targets]
30-
test = ["StructArrays", "Test"]
30+
test = ["ImageCore", "StructArrays", "Test"]

README.md

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,33 @@ julia> convert(RGB, c)
3333
RGB{N0f16}(0.75,0.87549,0.09117)
3434
```
3535

36-
The latter is how this color would be rendered in a viewer; embedded in a function, the conversion is extremely well optimized (~2.2ns on the author's machine).
36+
The latter is how this color would be rendered in a viewer.
37+
38+
## Overflow protection
39+
40+
Depending on the colors you pick for conversion to RGB (e.g., `channels`), it is possible to exceed the 0-to-1 bounds of RGB.
41+
With the choice above,
42+
43+
```julia
44+
julia> c = ctemplate(0.99, 0.99)
45+
(0.99001N0f16₁, 0.99001N0f16₂)
46+
47+
julia> convert(RGB, c)
48+
ERROR: ArgumentError: component type N0f16 is a 16-bit type representing 65536 values from 0.0 to 1.0,
49+
but the values (0.9900053f0, 1.7664759f0, 0.36105898f0) do not lie within this range.
50+
See the READMEs for FixedPointNumbers and ColorTypes for more information.
51+
Stacktrace:
52+
[...]
53+
```
54+
55+
If you want to guard against such errors, one good choice would be
56+
57+
```julia
58+
julia> convert(RGB{Float32}, c)
59+
RGB{Float32}(0.9900053, 1.7664759, 0.36105898)
60+
```
61+
62+
Conversions to floating-point types also tend to be faster, since the values do not have to be checked.
3763

3864
## Advanced usage
3965

@@ -43,9 +69,15 @@ However, constructing `ctemplate` as above is an inherently non-inferrable opera
4369
inferrably, you can use the macro version:
4470

4571
```julia
46-
f(i1, i2) = ColorMixture{N0f8}((fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato"), (i1, i2))
72+
f(i1, i2) = ColorMixture{N0f16}((fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato"), (i1, i2))
4773
```
4874

49-
Note the absence of `[]` brackets around the fluorophore names. For such constructors, `N0f8` is the only option if you're
50-
looking up the RGB values with `fluorophore_rgb`; however, if you hard-code the RGB values there is no restriction
51-
on the element type.
75+
Note the absence of `[]` brackets around the fluorophore names.
76+
77+
## Why are the RGB colors encoded in the *type*? Why not a value field?
78+
79+
In many places, JuliaImages assumes that you can convert from one color space to another purely from knowing the type you want to convert to. This would not be possible if the RGB colors were encoded as a second field of the color.
80+
81+
## I wrote some code and got lousy performance. How can I fix it?
82+
83+
To achieve good performance, in some cases the RGB *values* must be aggressively constant-propagated, a feature available only on Julia 1.7 and higher. So if you're experiencing this problem on Julia 1.6, try a newer version.

docs/Manifest.toml

Lines changed: 0 additions & 97 deletions
This file was deleted.

src/FluorophoreColors.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module FluorophoreColors
22

3-
using Compat # for Compat.@constprop
3+
using Compat
44

55
using Reexport
66
@reexport using FixedPointNumbers
@@ -17,6 +17,7 @@ include("utils.jl")
1717

1818
function __init__()
1919
@require StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" include("structarrays.jl")
20+
@require ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" include("imagecore.jl")
2021
end
2122

2223
end

src/imagecore.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# clamp01 is designed to work on RGB colors (a major usage is for display), so convert first
2+
ImageCore.clamp01(c::ColorMixture{T}) where T = convert(RGB{T}, ImageCore.clamp01(convert(RGB{floattype(T)}, c)))
3+
ImageCore.clamp01nan(c::ColorMixture{T}) where T = convert(RGB{T}, ImageCore.clamp01nan(convert(RGB{floattype(T)}, c)))

src/types.jl

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ julia> c = ColorMixture{N0f16}(channelcolors, #= GFP intensity =# 0.2, #= tdToma
3232
(0.2N0f16₁, 0.85N0f16₂)
3333
3434
julia> convert(RGB, c)
35-
RGB{N0f16}(0.85,0.9151,0.07294)
35+
RGB{N0f16}(0.85, 0.9151, 0.07294)
3636
```
3737
3838
If you must construct colors inferrably inside a function body, use
@@ -51,30 +51,21 @@ struct ColorMixture{T,N,Cs} <: Color{T,N}
5151
channels::NTuple{N,T}
5252

5353
Compat.@constprop :aggressive function ColorMixture{T,N,Cs}(channels::NTuple{N}) where {T,N,Cs}
54-
Cs isa NTuple{N,RGB{T}} || throw(TypeError(:ColorMixture, "incompatible color types", NTuple{N,RGB{T}}, typeof(Cs)))
54+
Cs isa NTuple{N,RGB{N0f8}} || throw(TypeError(:ColorMixture, "", NTuple{N,RGB{N0f8}}, Cs))
5555
return new{T,N,Cs}(channels)
5656
end
5757
end
5858
ColorMixture{T,N,Cs}(channels::Vararg{Real,N}) where {T,N,Cs} = ColorMixture{T,N,Cs}(channels)
59-
Compat.@constprop :aggressive ColorMixture{T}(Cs::NTuple{N,RGB{T}}, channels::NTuple{N,Real}) where {T,N} = ColorMixture{T,N,Cs}(channels)
60-
ColorMixture{T}(Cs::NTuple{N,AbstractRGB}, channels::NTuple{N,Real}) where {T,N} = ColorMixture{T,N,RGB{T}.(Cs)}(channels)
61-
ColorMixture{T}(Cs::NTuple{N,AbstractRGB}, channels::Vararg{Real,N}) where {T,N} = ColorMixture{T}(Cs, channels)
59+
Compat.@constprop :aggressive ColorMixture{T}(Cs::NTuple{N,RGB{N0f8}}, channels::NTuple{N,Real}) where {T,N} = ColorMixture{T,N,Cs}(channels)
60+
Compat.@constprop :aggressive ColorMixture{T}(Cs::NTuple{N,AbstractRGB}, channels::NTuple{N,Real}) where {T,N} = ColorMixture{T,N,RGB{N0f8}.(Cs)}(channels)
61+
Compat.@constprop :aggressive ColorMixture{T}(Cs::NTuple{N,AbstractRGB}, channels::Vararg{Real,N}) where {T,N} = ColorMixture{T}(Cs, channels)
6262

63-
@inline _promote_typeof(::Type{C1}, ::Type{C2}) where {C1,C2} = promote_type(C1, C2)
64-
@inline _promote_typeof(::Type{C1}, ::Type{C2}, obj, objs...) where {C1,C2} =
65-
_promote_typeof(promote_type(C1, C2), typeof(obj), objs...)
66-
67-
@inline promote_typeof(obj) = typeof(obj)
68-
@inline promote_typeof(obj1, obj2) = promote_type(typeof(obj1), typeof(obj2))
69-
@inline promote_typeof(obj1, obj2, objs...) = _promote_type(typeof(obj1), typeof(obj2), objs...)
70-
71-
computeT(Cs::NTuple{N,AbstractRGB}, channels::NTuple{N,Real}) where {N} = eltype(promote_typeof(map(*, Cs, channels)...))
72-
ColorMixture(Cs::NTuple{N,AbstractRGB}, channels::NTuple{N,Real}) where {N} = ColorMixture{computeT(Cs, channels)}(Cs, channels)
73-
ColorMixture(Cs::NTuple{N,AbstractRGB}, channels::Vararg{Real,N}) where {N} = ColorMixture{computeT(Cs, channels)}(Cs, channels)
63+
Compat.@constprop :aggressive ColorMixture(Cs::NTuple{N,AbstractRGB}, channels::NTuple{N,Real}) where {N} = ColorMixture{eltype(map(z -> zero(N0f8)*z, channels))}(Cs, channels)
64+
Compat.@constprop :aggressive ColorMixture(Cs::NTuple{N,AbstractRGB}, channels::Vararg{Real,N}) where {N} = ColorMixture(Cs, channels)
7465

7566
"""
76-
cobj = ColorMixture((rgb₁, rgb₂)) # create an all-zeros ColorMixture
77-
cobj = ColorMixture{T}((rgb₁, rgb₂)) # same, but coerce the element type
67+
cobj = ColorMixture((rgb₁, rgb₂)) # create an all-zeros ColorMixture with N0f8 channel intensities
68+
cobj = ColorMixture{T}((rgb₁, rgb₂)) # same, but specify the element type
7869
c = cobj((i₁, i₂)) # Construct non-zero ColorMixture (inferrably)
7970
8071
Create a ColorMixture `c` from a "template" `cobj`. `c` will be the same type as `cobj`.
@@ -83,15 +74,15 @@ Create a ColorMixture `c` from a "template" `cobj`. `c` will be the same type as
8374
is known. In conjunction with a [function barrier](https://docs.julialang.org/en/v1/manual/performance-tips/#kernel-functions),
8475
this form can be used to circumvent performance problems due to poor inferrability.
8576
"""
86-
ColorMixture(Cs::NTuple{N,RGB{T}}) where {T,N} = ColorMixture{T}(Cs, ntuple(_ -> zero(T), N))
8777
ColorMixture{T}(Cs::NTuple{N,AbstractRGB}) where {T,N} = ColorMixture{T}(Cs, ntuple(_ -> zero(T), N))
78+
ColorMixture(Cs::NTuple{N,RGB{N0f8}}) where {N} = ColorMixture{N0f8}(Cs)
8879

8980
(::ColorMixture{T,N,Cs})(channels::NTuple{N,Real}) where {T,N,Cs} = ColorMixture{T,N,Cs}(channels)
9081
(::ColorMixture{T,N,Cs})(channels::Vararg{Real,N}) where {T,N,Cs} = ColorMixture{T,N,Cs}(channels)
9182

9283

93-
Base.:(==)(a::ColorMixture{Ta,N,Csa}, b::ColorMixture{Tb,N,Csb}) where {Ta,Tb,N,Csa,Csb} =
94-
Csa == Csb && a.channels == b.channels
84+
Base.:(==)(a::ColorMixture{Ta,N,Cs}, b::ColorMixture{Tb,N,Cs}) where {Ta,Tb,N,Cs} = a.channels == b.channels
85+
Base.:(==)(a::ColorMixture, b::ColorMixture) = false
9586

9687
function Base.show(io::IO, c::ColorMixture)
9788
print(io, '(')
@@ -103,7 +94,17 @@ function Base.show(io::IO, c::ColorMixture)
10394
print(io, ')')
10495
end
10596

97+
# These definitions use floats to avoid overflow
10698
function Base.convert(::Type{RGB{T}}, c::ColorMixture{T,N,Cs}) where {T,N,Cs}
107-
sum(map(*, c.channels, Cs))
99+
convert(RGB{T}, sum(map(*, c.channels, Cs); init=zero(RGB{floattype(T)})))
100+
end
101+
function Base.convert(::Type{RGB{T}}, c::ColorMixture{R,N,Cs}) where {T,R,N,Cs}
102+
convert(RGB{T}, sum(map((w, rgb) -> convert(RGB{floattype(T)}, w*rgb), c.channels, Cs)))
108103
end
109104
Base.convert(::Type{RGB}, c::ColorMixture{T}) where T = convert(RGB{T}, c)
105+
Base.convert(::Type{RGB24}, c::ColorMixture) = convert(RGB24, convert(RGB, c))
106+
107+
ColorTypes._comp(::Val{N}, c::ColorMixture) where N = c.channels[N]
108+
Compat.@constprop :aggressive ColorTypes.mapc(f, c::ColorMixture{T,N,Cs}) where {T,N,Cs} = ColorMixture(Cs, map(f, c.channels))
109+
Compat.@constprop :aggressive ColorTypes.mapreducec(f, op, v0, c::ColorMixture{T,N,Cs}) where {T,N,Cs} = mapreduce(f, op, v0, c.channels)
110+
Compat.@constprop :aggressive ColorTypes.reducec(op, v0, c::ColorMixture{T,N,Cs}) where {T,N,Cs} = reduce(op, c.channels; init=v0)

test/runtests.jl

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ using Test
33

44
# interacts via @require
55
using StructArrays
6+
using ImageCore
67

78
@testset "FluorophoreColors.jl" begin
89
@test isempty(detect_ambiguities(FluorophoreColors))
@@ -30,6 +31,27 @@ using StructArrays
3031
@test c.channels[1] == c.channels[2] == 0.5
3132
@test convert(RGB, c) 0.5*channels[1] + 0.5*channels[2]
3233

34+
fchannels = float.(channels)
35+
if Base.VERSION >= v"1.8.0-DEV.363"
36+
@test_throws r"ColorMixture.*expected Tuple{RGB{N0f8}, +RGB{N0f8}}.*got a value of type Tuple{RGB{Float32}, +RGB{Float32}}" ColorMixture{Float32,2,fchannels}(0.1, 0.2)
37+
else
38+
@test_throws TypeError ColorMixture{Float32,2,fchannels}(0.1, 0.2)
39+
end
40+
41+
# Overflow behavior
42+
ctemplate = ColorMixture{N0f8}((RGB(1, 0, 0), RGB(0.5, 0.5, 0)))
43+
c = ctemplate(0.8, 0.8)
44+
if Base.VERSION >= v"1.8.0-DEV.363"
45+
@test_throws "the values (1.2f0, 0.4f0, 0.0f0) do not lie within this range" convert(RGB{N0f8}, c)
46+
@test_throws "the values (1.2f0, 0.4f0, 0.0f0) do not lie within this range" convert(RGB24, c)
47+
@test_throws "the values (1.2f0, 0.4f0, 0.0f0) do not lie within this range" convert(RGB, c)
48+
else
49+
@test_throws ArgumentError convert(RGB{N0f8}, c)
50+
@test_throws ArgumentError convert(RGB24, c)
51+
@test_throws ArgumentError convert(RGB, c)
52+
end
53+
@test convert(RGB{Float32}, c) === RGB{Float32}(1.2, 0.4, 0)
54+
3355
# Macro syntax & inferrability
3456
f_infer(i1, i2) = ColorMixture{N0f8}((fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato"), (i1, i2))
3557
f_noinfer(i1, i2) = ColorMixture((fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"]), (i1, i2))
@@ -47,6 +69,19 @@ using StructArrays
4769
@test @inferred(ctmpl(1, 0)) === ColorMixture{N0f8}(channels, (1, 0))
4870
end
4971

72+
@testset "mapc etc" begin
73+
channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"])
74+
ctemplate = ColorMixture{N0f8}(channels)
75+
c = ctemplate(0.4, 0.2)
76+
if Base.VERSION >= v"1.7"
77+
@test @inferred(mapc(x->2x, c)) === ColorMixture{Float32}(channels, (0.8, 0.4))
78+
else
79+
@test_broken @inferred(mapc(x->2x, c)) === ColorMixture{Float32}(channels, (0.8, 0.4))
80+
end
81+
@test @inferred(mapreducec(x->2x, +, c)) === 1.2f0
82+
@test @inferred(reducec(+, c)) === reduce(+, (0.4N0f8, 0.2N0f8))
83+
end
84+
5085
@testset "StructArrays" begin
5186
channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"])
5287
ctemplate = ColorMixture(channels)
@@ -72,6 +107,19 @@ using StructArrays
72107
@test soa[1] == ctemplate(ntuple(i->(i-1)/32, 16))
73108
end
74109

110+
@testset "ImageCore" begin
111+
# Integration with the rest of the JuliaImages ecosystem
112+
ctemplate = ColorMixture{N0f8}((RGB(1, 0, 0), RGB(0.5, 0.5, 0)))
113+
c = ctemplate(0.8, 0.8)
114+
@test convert(RGB{Float32}, c) === RGB{Float32}(1.2, 0.4, 0)
115+
@test clamp01(c) === RGB{N0f8}(1, 0.4, 0)
116+
@test clamp01nan(c) === RGB{N0f8}(1, 0.4, 0)
117+
ctemplate = ColorMixture{Float32}((RGB(1, 0, 0), RGB(0.5, 0.5, 0)))
118+
c = ctemplate(0.8, NaN)
119+
@test isequal(convert(RGB, c), RGB{Float32}(NaN,NaN,NaN))
120+
@test clamp01nan(c) === RGB{Float32}(0, 0, 0)
121+
end
122+
75123
@testset "IO" begin
76124
channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"])
77125
c = ColorMixture(channels, (1, 0))

0 commit comments

Comments
 (0)