Skip to content

Commit 91364a4

Browse files
committed
Rename package and redesign types
This creates an abstract `AbstractColorChannels` type and two types, `ColorChannels` and `ColorMixture`. `ColorChannels` is a "bare" multichannel color that lacks conversion to other color types; it is intended to be used in conjunction with MappedArrays if one wishes to visualize these as RGB images. `ColorMixture` is the weighted-RGB color type.
1 parent 21e7a6b commit 91364a4

File tree

9 files changed

+139
-82
lines changed

9 files changed

+139
-82
lines changed

.github/workflows/CI.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ jobs:
5555
- run: |
5656
julia --project=docs -e '
5757
using Documenter: DocMeta, doctest
58-
using FluorophoreColors
59-
DocMeta.setdocmeta!(FluorophoreColors, :DocTestSetup, :(using FluorophoreColors); recursive=true)
60-
doctest(FluorophoreColors)'
58+
using MultichannelColors
59+
DocMeta.setdocmeta!(MultichannelColors, :DocTestSetup, :(using MultichannelColors); recursive=true)
60+
doctest(MultichannelColors)'

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name = "FluorophoreColors"
1+
name = "MultichannelColors"
22
uuid = "d4071afc-4203-49ee-90bc-13ebeb18d604"
33
authors = ["Tim Holy <tim.holy@gmail.com> and contributors"]
44
version = "0.1.0"
@@ -13,7 +13,7 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
1313
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
1414

1515
[compat]
16-
ColorTypes = "0.11.1"
16+
ColorTypes = "0.11.2"
1717
ColorVectorSpace = "0.9"
1818
Colors = "0.12"
1919
FixedPointNumbers = "0.8"

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# FluorophoreColors
1+
# MultichannelColors
22

3-
[![Build Status](https://github.com/JuliaImages/FluorophoreColors.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaImages/FluorophoreColors.jl/actions/workflows/CI.yml?query=branch%3Amain)
4-
[![Coverage](https://codecov.io/gh/JuliaImages/FluorophoreColors.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaImages/FluorophoreColors.jl)
3+
[![Build Status](https://github.com/JuliaImages/MultichannelColors.jl/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/JuliaImages/MultichannelColors.jl/actions/workflows/CI.yml?query=branch%3Amain)
4+
[![Coverage](https://codecov.io/gh/JuliaImages/MultichannelColors.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaImages/MultichannelColors.jl)
55

66
This package defines [color types](https://github.com/JuliaGraphics/ColorTypes.jl) for use with multichannel fluorescence imaging. Briefly, you can specify the intensity of each color channel plus an RGB value associated with the peak emission
77
wavelength of each fluorophore.
@@ -11,7 +11,7 @@ wavelength of each fluorophore.
1111
Perhaps the easiest way to learn the package is by example. Suppose we are imaging two fluorophores, EGFP and tdTomato.
1212

1313
```julia
14-
julia> using FluorophoreColors
14+
julia> using MultichannelColors
1515

1616
julia> channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"])
1717
(RGB{N0f8}(0.0,0.925,0.365), RGB{N0f8}(1.0,0.859,0.0))

docs/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[deps]
22
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
3-
FluorophoreColors = "d4071afc-4203-49ee-90bc-13ebeb18d604"
3+
MultichannelColors = "d4071afc-4203-49ee-90bc-13ebeb18d604"

docs/make.jl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
using FluorophoreColors
1+
using MultichannelColors
22
using Documenter
33

4-
DocMeta.setdocmeta!(FluorophoreColors, :DocTestSetup, :(using FluorophoreColors); recursive=true)
4+
DocMeta.setdocmeta!(MultichannelColors, :DocTestSetup, :(using MultichannelColors); recursive=true)
55

66
makedocs(;
7-
modules=[FluorophoreColors],
7+
modules=[MultichannelColors],
88
authors="Tim Holy <tim.holy@gmail.com> and contributors",
9-
repo="https://github.com/JuliaImages/FluorophoreColors.jl/blob/{commit}{path}#{line}",
10-
sitename="FluorophoreColors.jl",
9+
repo="https://github.com/JuliaImages/MultichannelColors.jl/blob/{commit}{path}#{line}",
10+
sitename="MultichannelColors.jl",
1111
format=Documenter.HTML(;
1212
prettyurls=get(ENV, "CI", "false") == "true",
13-
canonical="https://JuliaImages.github.io/FluorophoreColors.jl",
13+
canonical="https://JuliaImages.github.io/MultichannelColors.jl",
1414
assets=String[],
1515
),
1616
pages=[
@@ -19,6 +19,6 @@ makedocs(;
1919
)
2020

2121
deploydocs(;
22-
repo="github.com/JuliaImages/FluorophoreColors.jl",
22+
repo="github.com/JuliaImages/MultichannelColors.jl",
2323
devbranch="main",
2424
)

docs/src/index.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
```@meta
2-
CurrentModule = FluorophoreColors
2+
CurrentModule = MultichannelColors
33
```
44

5-
# FluorophoreColors
5+
# MultichannelColors
66

7-
Documentation for [FluorophoreColors](https://github.com/JuliaImages/FluorophoreColors.jl).
7+
Documentation for [MultichannelColors](https://github.com/JuliaImages/MultichannelColors.jl).
88

99
```@index
1010
```
1111

1212
```@autodocs
13-
Modules = [FluorophoreColors]
13+
Modules = [MultichannelColors]
1414
```

src/FluorophoreColors.jl renamed to src/MultichannelColors.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module FluorophoreColors
1+
module MultichannelColors
22

33
using Compat
44

@@ -9,7 +9,8 @@ using Colors
99
using ColorVectorSpace
1010
using Requires
1111

12-
export fluorophore_rgb, @fluorophore_rgb_str, ColorMixture
12+
export AbstractColorChannels, ColorChannels, ColorMixture
13+
export fluorophore_rgb, @fluorophore_rgb_str
1314

1415
include("types.jl")
1516
include("fluorophores.jl")

src/types.jl

Lines changed: 107 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,118 @@
1-
# For custom colortypes, the main things we need are
2-
# - utitlies for extracting channels
3-
# - conversion to RGB for display
4-
# Making the main representation by RGB means we can do the latter efficiently without requiring
5-
# world-age violations.
1+
"""
2+
AbstractColorChannels{T<:Number,N}
63
4+
An abstract type for multichannel/multiband/hyperspectral colors. Concrete derived types should have
5+
a field, `channels`, which is a `NTuple{N,T}`. The channels can be returned with `Tuple(c::AbstractColorChannels)`.
76
"""
8-
ColorMixture((rgb₁, rgb₂), (i₁, i₂)) # store intensities
9-
ColorMixture{T}((rgb₁, rgb₂), (i₁, i₂)) # same, but coerce to element type T for colors and intensities
7+
abstract type AbstractColorChannels{T<:Number,N} <: Color{T,N} end
108

11-
Represent the multichannel fluorescence intensity at a point. `rgbⱼ` is an RGB color corresponding
12-
to fluorophore `j` (e.g., see [`fluorophore_rgb`](@ref)) whose emission intensity is `iⱼ`.
9+
ColorTypes.comp1(c::AbstractColorChannels) = c.channels[1]
10+
ColorTypes.comp2(c::AbstractColorChannels) = c.channels[2]
11+
ColorTypes.comp3(c::AbstractColorChannels) = c.channels[3]
12+
ColorTypes.comp4(c::AbstractColorChannels) = c.channels[4]
13+
ColorTypes.comp5(c::AbstractColorChannels) = c.channels[5]
1314

14-
While the example shows two fluorophores, any number may be used, as long as the number of `rgb` colors
15-
matches the number of intensities `i`.
15+
Base.Tuple(c::AbstractColorChannels) = c.channels
1616

17-
If you're constructing such colors in a high-performance loop, there may be other methods that may
18-
yield better performance due to challenges with type-inference, unless the color is known
19-
at compile time.
17+
function Base.show(io::IO, c::AbstractColorChannels)
18+
print(io, '(')
19+
chans = Tuple(c)
20+
for (j, intensity) in enumerate(chans)
21+
j > 1 && print(io, ", ")
22+
print(io, intensity)
23+
print_subscript(io, length(chans), j)
24+
end
25+
print(io, ')')
26+
end
2027

21-
# Examples
28+
"""
29+
ColorChannels(i₁, i₂, ...)
30+
ColorChannels((i₁, i₂, ...))
31+
ColorChannels{T}(...) # coerce to element type T
2232
23-
To construct a 16-bit "pixel" from a dual-channel EGFP (peak emission 507nm)/tdTomato (peak emission 581nm) image,
24-
you might do the following:
33+
Represent multichannel "raw" colors, which lack `convert` methods to standard color spaces.
34+
If `c` is a `ColorChannels` object, then `Tuple(c)` is a tuple of intensities (one per channel).
2535
26-
```jldoctest
27-
julia> using FluorophoreColors
36+
[`ColorMixture`](@ref) is an alternative with a built-in conversion to RGB.
2837
29-
julia> channelcolors = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"]);
38+
# Examples
3039
31-
julia> c = ColorMixture{N0f16}(channelcolors, #= GFP intensity =# 0.2, #= tdTomato intensity =# 0.85)
32-
(0.2N0f16₁, 0.85N0f16₂)
40+
## Hyperspectral/multiband colors
3341
34-
julia> convert(RGB, c)
35-
RGB{N0f16}(0.85, 0.9151, 0.07294)
42+
Images from the [Operational Land Imager](https://en.wikipedia.org/wiki/Operational_Land_Imager) have
43+
[11 wavelength bands](https://landsat.gsfc.nasa.gov/satellites/landsat-8/landsat-8-bands/). A single pixel
44+
could be represented as
45+
46+
```julia
47+
julia> using MultichannelColors
48+
49+
julia> c = ColorChannels{N0f16}(0.25, 0.15, ...) # 11 entries in all
3650
```
3751
38-
If you must construct colors inferrably inside a function body, use
52+
See the [FixedPointNumbers](https://github.com/JuliaMath/FixedPointNumbers.jl) package for information about `N0f16`.
53+
54+
## Visualizing Images
55+
56+
You can create a custom function mapping a `ColorChannel` to a numeric or RGB value and apply it to the image using
57+
broadcasting, or use [MappedArrays](https://github.com/JuliaArrays/MappedArrays.jl) to apply it "lazily"
58+
(useful for large data sets).
59+
60+
Let's compute the [Enhanced Vegetation Index](https://www.usgs.gov/landsat-missions/landsat-enhanced-vegetation-index)
61+
and render positive values in green and negative values in magenta:
3962
40-
```jldoctest; setup=:(using FluorophoreColors)
41-
julia> channelcolors = (fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato");
63+
```julia
64+
julia> function evi(c::ColorChannels{T,11}) where T<:FixedPoint
65+
# Valid for Landsat 8 with 11 spectral bands
66+
b = Tuple(c) # extract the bands
67+
evi = 2.5 * (b[5] - b[4]) / (b[5] + 6*b[4] - 7.5*b[2] + eps(T))
68+
return evi > zero(evi) ? RGB(0, evi, 0) : RGB(-evi, 0, -evi)
69+
end;
4270
43-
julia> c = ColorMixture{N0f8}(channelcolors, #= GFP intensity =# 0.2, #= tdTomato intensity =# 0.85)
44-
(0.2N0f8₁, 0.851N0f8₂)
71+
julia> evi.(img) # img is an array of ColorChannels{T,11} values
4572
```
4673
47-
This allows the RGB *values* to be visible to the compiler. However, the fluorophore names must be hard-coded,
48-
and you must preserve the `N0f8` element type of fluorophore_rgb"NAME".
74+
If the images are too dark, you may wish to apply some additional scaling to the `evi` value.
4975
"""
50-
struct ColorMixture{T,N,Cs} <: Color{T,N}
76+
struct ColorChannels{T<:Number,N} <: AbstractColorChannels{T,N}
77+
channels::NTuple{N,T}
78+
end
79+
80+
ColorChannels{T}(channels::NTuple{N,Any}) where {T<:Number,N} = ColorChannels{T,N}(channels)
81+
ColorChannels{T}(channels::Vararg{Any,N}) where {T<:Number,N} = ColorChannels{T}(channels)
82+
83+
ColorChannels(channels::NTuple{N,Number}) where {N} = ColorChannels(promote(channels...))
84+
ColorChannels(channels::Vararg{Number,N}) where {N} = ColorChannels(channels)
85+
86+
87+
"""
88+
ColorMixture((rgb₁, rgb₂, ...), (i₁, i₂, ...))
89+
ColorMixture((rgb₁, rgb₂, ...), i₁, i₂, ...)
90+
ColorMixture{T}(...) # coerce intensities to element type
91+
92+
Represent multichannel colors with a defined conversion to RGB. `rgbⱼ` is an RGB color corresponding
93+
to channel `j`, and its intensity is `iⱼ`.
94+
95+
Colors are converted to RGB with intensity-weighting,
96+
``
97+
c_{rgb} = \\sum_j i_j \\mathrm{rgb}_j
98+
``
99+
Depending on the the `rgbⱼ` and `iⱼ`, values may exceed the 0-to-1 colorscale of RGBs.
100+
Conversion to `RGB{Float32}` may be safer than ones limited to 0-to-1.
101+
102+
# Examples
103+
104+
Let's create a two-channel value where channel 1 is rendered in cyan and channel 2 is rendered in red:
105+
106+
```jldoctest
107+
julia> using MultichannelColors
108+
109+
julia> channels = (RGB(0, 0.5, 0.5), RGB(1, 0, 0));
110+
111+
julia> c = ColorMixture(channels, (0.8, 0.2))
112+
(0.8₁, 0.2₂)
113+
```
114+
"""
115+
struct ColorMixture{T<:Number,N,Cs} <: AbstractColorChannels{T,N}
51116
channels::NTuple{N,T}
52117

53118
Compat.@constprop :aggressive function ColorMixture{T,N,Cs}(channels::NTuple{N}) where {T,N,Cs}
@@ -60,6 +125,7 @@ Compat.@constprop :aggressive ColorMixture{T}(Cs::NTuple{N,RGB{N0f8}}, channels:
60125
Compat.@constprop :aggressive ColorMixture{T}(Cs::NTuple{N,AbstractRGB}, channels::NTuple{N,Real}) where {T,N} = ColorMixture{T,N,RGB{N0f8}.(Cs)}(channels)
61126
Compat.@constprop :aggressive ColorMixture{T}(Cs::NTuple{N,AbstractRGB}, channels::Vararg{Real,N}) where {T,N} = ColorMixture{T}(Cs, channels)
62127

128+
Compat.@constprop :aggressive ColorMixture(Cs::NTuple{N,AbstractRGB}, channels::NTuple{N,Integer}) where {N} = ColorMixture{N0f8}(Cs, channels)
63129
Compat.@constprop :aggressive ColorMixture(Cs::NTuple{N,AbstractRGB}, channels::NTuple{N,Real}) where {N} = ColorMixture{eltype(map(z -> zero(N0f8)*z, channels))}(Cs, channels)
64130
Compat.@constprop :aggressive ColorMixture(Cs::NTuple{N,AbstractRGB}, channels::Vararg{Real,N}) where {N} = ColorMixture(Cs, channels)
65131

@@ -74,37 +140,27 @@ Create a ColorMixture `c` from a "template" `cobj`. `c` will be the same type as
74140
is known. In conjunction with a [function barrier](https://docs.julialang.org/en/v1/manual/performance-tips/#kernel-functions),
75141
this form can be used to circumvent performance problems due to poor inferrability.
76142
"""
77-
ColorMixture{T}(Cs::NTuple{N,AbstractRGB}) where {T,N} = ColorMixture{T}(Cs, ntuple(_ -> zero(T), N))
143+
ColorMixture{T}(Cs::NTuple{N,AbstractRGB}) where {T<:Number,N} = ColorMixture{T}(Cs, ntuple(_ -> zero(T), N))
78144
ColorMixture(Cs::NTuple{N,RGB{N0f8}}) where {N} = ColorMixture{N0f8}(Cs)
79145

80146
(::ColorMixture{T,N,Cs})(channels::NTuple{N,Real}) where {T,N,Cs} = ColorMixture{T,N,Cs}(channels)
81147
(::ColorMixture{T,N,Cs})(channels::Vararg{Real,N}) where {T,N,Cs} = ColorMixture{T,N,Cs}(channels)
82148

83149

84-
Base.:(==)(a::ColorMixture{Ta,N,Cs}, b::ColorMixture{Tb,N,Cs}) where {Ta,Tb,N,Cs} = a.channels == b.channels
150+
Base.:(==)(a::ColorMixture{Ta,N,Cs}, b::ColorMixture{Tb,N,Cs}) where {Ta<:Number,Tb<:Number,N,Cs} = a.channels == b.channels
85151
Base.:(==)(a::ColorMixture, b::ColorMixture) = false
86152

87-
function Base.show(io::IO, c::ColorMixture)
88-
print(io, '(')
89-
for (j, intensity) in enumerate(c.channels)
90-
j > 1 && print(io, ", ")
91-
print(io, intensity)
92-
print_subscript(io, length(c), j)
93-
end
94-
print(io, ')')
95-
end
153+
Base.isequal(a::ColorMixture{Ta,N,Cs}, b::ColorMixture{Tb,N,Cs}) where {Ta<:Number,Tb<:Number,N,Cs} = isequal(a.channels, b.channels)
154+
Base.isequal(a::ColorMixture, b::ColorMixture) = false
96155

97156
# These definitions use floats to avoid overflow
98-
function Base.convert(::Type{RGB{T}}, c::ColorMixture{T,N,Cs}) where {T,N,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}
157+
function Base.convert(::Type{RGB{T}}, c::ColorMixture{R,N,Cs}) where {T,R<:Number,N,Cs}
102158
convert(RGB{T}, sum(map((w, rgb) -> convert(RGB{floattype(T)}, w*rgb), c.channels, Cs)))
103159
end
104-
Base.convert(::Type{RGB}, c::ColorMixture{T}) where T = convert(RGB{T}, c)
160+
Base.convert(::Type{RGB}, c::ColorMixture{T}) where T<:Number = convert(RGB{T}, c)
105161
Base.convert(::Type{RGB24}, c::ColorMixture) = convert(RGB24, convert(RGB, c))
106162

107163
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)
164+
Compat.@constprop :aggressive ColorTypes.mapc(f, c::ColorMixture{T,N,Cs}) where {T<:Number,N,Cs} = ColorMixture(Cs, map(f, c.channels))
165+
Compat.@constprop :aggressive ColorTypes.mapreducec(f, op, v0, c::ColorMixture{T,N,Cs}) where {T<:Number,N,Cs} = mapreduce(f, op, c.channels; init=v0)
166+
Compat.@constprop :aggressive ColorTypes.reducec(op, v0, c::ColorMixture{T,N,Cs}) where {T<:Number,N,Cs} = reduce(op, c.channels; init=v0)

test/runtests.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
using FluorophoreColors
1+
using MultichannelColors
22
using Test
33

44
# interacts via @require
55
using StructArrays
66
using ImageCore
77

8-
@testset "FluorophoreColors.jl" begin
9-
@test isempty(detect_ambiguities(FluorophoreColors))
8+
@testset "MultichannelColors.jl" begin
9+
@test isempty(detect_ambiguities(MultichannelColors))
1010

1111
@testset "Fluorophore lookup" begin
1212
cfp = fluorophore_rgb["ECFP"]
@@ -78,8 +78,8 @@ using ImageCore
7878
else
7979
@test_broken @inferred(mapc(x->2x, c)) === ColorMixture{Float32}(channels, (0.8, 0.4))
8080
end
81-
@test @inferred(mapreducec(x->2x, +, c)) === 1.2f0
82-
@test @inferred(reducec(+, c)) === reduce(+, (0.4N0f8, 0.2N0f8))
81+
@test @inferred(mapreducec(x->2x, +, 0f0, c)) === 1.2f0
82+
@test @inferred(reducec(+, 0N0f8, c)) === reduce(+, (0.4N0f8, 0.2N0f8))
8383
end
8484

8585
@testset "StructArrays" begin
@@ -94,7 +94,7 @@ using ImageCore
9494
@test red[1] === 0.0N0f8
9595

9696
# Hyperspectral
97-
cols = FluorophoreColors.Colors.distinguishable_colors(16, [RGB(0,0,0)]; dropseed=true)
97+
cols = MultichannelColors.Colors.distinguishable_colors(16, [RGB(0,0,0)]; dropseed=true)
9898
ctemplate = ColorMixture{Float32}((cols...,))
9999
comps = collect(reshape((0:31)/32f0, 16, 2))
100100
compsr = reinterpret(reshape, typeof(ctemplate), comps)
@@ -123,10 +123,10 @@ using ImageCore
123123
@testset "IO" begin
124124
channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"])
125125
c = ColorMixture(channels, (1, 0))
126-
@test sprint(show, c) == "(1.0₁, 0.0₂)"
126+
@test sprint(show, c) == "(1.0N0f8₁, 0.0N0f8₂)"
127127

128128
# Hyperspectral
129-
cols = FluorophoreColors.Colors.distinguishable_colors(16, [RGB(0,0,0)]; dropseed=true)
129+
cols = MultichannelColors.Colors.distinguishable_colors(16, [RGB(0,0,0)]; dropseed=true)
130130
ctemplate = ColorMixture{Float32}((cols...,))
131131
c = ctemplate([i/16 for i = 0:15]...)
132132
@test sprint(show, c) == "(0.0₀₁, 0.0625₀₂, 0.125₀₃, 0.1875₀₄, 0.25₀₅, 0.3125₀₆, 0.375₀₇, 0.4375₀₈, 0.5₀₉, 0.5625₁₀, 0.625₁₁, 0.6875₁₂, 0.75₁₃, 0.8125₁₄, 0.875₁₅, 0.9375₁₆)"

0 commit comments

Comments
 (0)