Skip to content

Commit

Permalink
Rename package and redesign types
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
timholy committed May 15, 2022
1 parent 21e7a6b commit 91364a4
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 82 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@ jobs:
- run: |
julia --project=docs -e '
using Documenter: DocMeta, doctest
using FluorophoreColors
DocMeta.setdocmeta!(FluorophoreColors, :DocTestSetup, :(using FluorophoreColors); recursive=true)
doctest(FluorophoreColors)'
using MultichannelColors
DocMeta.setdocmeta!(MultichannelColors, :DocTestSetup, :(using MultichannelColors); recursive=true)
doctest(MultichannelColors)'
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name = "FluorophoreColors"
name = "MultichannelColors"
uuid = "d4071afc-4203-49ee-90bc-13ebeb18d604"
authors = ["Tim Holy <[email protected]> and contributors"]
version = "0.1.0"
Expand All @@ -13,7 +13,7 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"

[compat]
ColorTypes = "0.11.1"
ColorTypes = "0.11.2"
ColorVectorSpace = "0.9"
Colors = "0.12"
FixedPointNumbers = "0.8"
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# FluorophoreColors
# MultichannelColors

[![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)
[![Coverage](https://codecov.io/gh/JuliaImages/FluorophoreColors.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaImages/FluorophoreColors.jl)
[![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)
[![Coverage](https://codecov.io/gh/JuliaImages/MultichannelColors.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/JuliaImages/MultichannelColors.jl)

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
wavelength of each fluorophore.
Expand All @@ -11,7 +11,7 @@ wavelength of each fluorophore.
Perhaps the easiest way to learn the package is by example. Suppose we are imaging two fluorophores, EGFP and tdTomato.

```julia
julia> using FluorophoreColors
julia> using MultichannelColors

julia> channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"])
(RGB{N0f8}(0.0,0.925,0.365), RGB{N0f8}(1.0,0.859,0.0))
Expand Down
2 changes: 1 addition & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[deps]
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
FluorophoreColors = "d4071afc-4203-49ee-90bc-13ebeb18d604"
MultichannelColors = "d4071afc-4203-49ee-90bc-13ebeb18d604"
14 changes: 7 additions & 7 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
using FluorophoreColors
using MultichannelColors
using Documenter

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

makedocs(;
modules=[FluorophoreColors],
modules=[MultichannelColors],
authors="Tim Holy <[email protected]> and contributors",
repo="https://github.com/JuliaImages/FluorophoreColors.jl/blob/{commit}{path}#{line}",
sitename="FluorophoreColors.jl",
repo="https://github.com/JuliaImages/MultichannelColors.jl/blob/{commit}{path}#{line}",
sitename="MultichannelColors.jl",
format=Documenter.HTML(;
prettyurls=get(ENV, "CI", "false") == "true",
canonical="https://JuliaImages.github.io/FluorophoreColors.jl",
canonical="https://JuliaImages.github.io/MultichannelColors.jl",
assets=String[],
),
pages=[
Expand All @@ -19,6 +19,6 @@ makedocs(;
)

deploydocs(;
repo="github.com/JuliaImages/FluorophoreColors.jl",
repo="github.com/JuliaImages/MultichannelColors.jl",
devbranch="main",
)
8 changes: 4 additions & 4 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
```@meta
CurrentModule = FluorophoreColors
CurrentModule = MultichannelColors
```

# FluorophoreColors
# MultichannelColors

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

```@index
```

```@autodocs
Modules = [FluorophoreColors]
Modules = [MultichannelColors]
```
5 changes: 3 additions & 2 deletions src/FluorophoreColors.jl → src/MultichannelColors.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module FluorophoreColors
module MultichannelColors

using Compat

Expand All @@ -9,7 +9,8 @@ using Colors
using ColorVectorSpace
using Requires

export fluorophore_rgb, @fluorophore_rgb_str, ColorMixture
export AbstractColorChannels, ColorChannels, ColorMixture
export fluorophore_rgb, @fluorophore_rgb_str

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

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

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

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

# Examples
"""
ColorChannels(i₁, i₂, ...)
ColorChannels((i₁, i₂, ...))
ColorChannels{T}(...) # coerce to element type T
To construct a 16-bit "pixel" from a dual-channel EGFP (peak emission 507nm)/tdTomato (peak emission 581nm) image,
you might do the following:
Represent multichannel "raw" colors, which lack `convert` methods to standard color spaces.
If `c` is a `ColorChannels` object, then `Tuple(c)` is a tuple of intensities (one per channel).
```jldoctest
julia> using FluorophoreColors
[`ColorMixture`](@ref) is an alternative with a built-in conversion to RGB.
julia> channelcolors = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"]);
# Examples
julia> c = ColorMixture{N0f16}(channelcolors, #= GFP intensity =# 0.2, #= tdTomato intensity =# 0.85)
(0.2N0f16₁, 0.85N0f16₂)
## Hyperspectral/multiband colors
julia> convert(RGB, c)
RGB{N0f16}(0.85, 0.9151, 0.07294)
Images from the [Operational Land Imager](https://en.wikipedia.org/wiki/Operational_Land_Imager) have
[11 wavelength bands](https://landsat.gsfc.nasa.gov/satellites/landsat-8/landsat-8-bands/). A single pixel
could be represented as
```julia
julia> using MultichannelColors
julia> c = ColorChannels{N0f16}(0.25, 0.15, ...) # 11 entries in all
```
If you must construct colors inferrably inside a function body, use
See the [FixedPointNumbers](https://github.com/JuliaMath/FixedPointNumbers.jl) package for information about `N0f16`.
## Visualizing Images
You can create a custom function mapping a `ColorChannel` to a numeric or RGB value and apply it to the image using
broadcasting, or use [MappedArrays](https://github.com/JuliaArrays/MappedArrays.jl) to apply it "lazily"
(useful for large data sets).
Let's compute the [Enhanced Vegetation Index](https://www.usgs.gov/landsat-missions/landsat-enhanced-vegetation-index)
and render positive values in green and negative values in magenta:
```jldoctest; setup=:(using FluorophoreColors)
julia> channelcolors = (fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato");
```julia
julia> function evi(c::ColorChannels{T,11}) where T<:FixedPoint
# Valid for Landsat 8 with 11 spectral bands
b = Tuple(c) # extract the bands
evi = 2.5 * (b[5] - b[4]) / (b[5] + 6*b[4] - 7.5*b[2] + eps(T))
return evi > zero(evi) ? RGB(0, evi, 0) : RGB(-evi, 0, -evi)
end;
julia> c = ColorMixture{N0f8}(channelcolors, #= GFP intensity =# 0.2, #= tdTomato intensity =# 0.85)
(0.2N0f8₁, 0.851N0f8₂)
julia> evi.(img) # img is an array of ColorChannels{T,11} values
```
This allows the RGB *values* to be visible to the compiler. However, the fluorophore names must be hard-coded,
and you must preserve the `N0f8` element type of fluorophore_rgb"NAME".
If the images are too dark, you may wish to apply some additional scaling to the `evi` value.
"""
struct ColorMixture{T,N,Cs} <: Color{T,N}
struct ColorChannels{T<:Number,N} <: AbstractColorChannels{T,N}
channels::NTuple{N,T}
end

ColorChannels{T}(channels::NTuple{N,Any}) where {T<:Number,N} = ColorChannels{T,N}(channels)
ColorChannels{T}(channels::Vararg{Any,N}) where {T<:Number,N} = ColorChannels{T}(channels)

ColorChannels(channels::NTuple{N,Number}) where {N} = ColorChannels(promote(channels...))
ColorChannels(channels::Vararg{Number,N}) where {N} = ColorChannels(channels)


"""
ColorMixture((rgb₁, rgb₂, ...), (i₁, i₂, ...))
ColorMixture((rgb₁, rgb₂, ...), i₁, i₂, ...)
ColorMixture{T}(...) # coerce intensities to element type
Represent multichannel colors with a defined conversion to RGB. `rgbⱼ` is an RGB color corresponding
to channel `j`, and its intensity is `iⱼ`.
Colors are converted to RGB with intensity-weighting,
``
c_{rgb} = \\sum_j i_j \\mathrm{rgb}_j
``
Depending on the the `rgbⱼ` and `iⱼ`, values may exceed the 0-to-1 colorscale of RGBs.
Conversion to `RGB{Float32}` may be safer than ones limited to 0-to-1.
# Examples
Let's create a two-channel value where channel 1 is rendered in cyan and channel 2 is rendered in red:
```jldoctest
julia> using MultichannelColors
julia> channels = (RGB(0, 0.5, 0.5), RGB(1, 0, 0));
julia> c = ColorMixture(channels, (0.8, 0.2))
(0.8₁, 0.2₂)
```
"""
struct ColorMixture{T<:Number,N,Cs} <: AbstractColorChannels{T,N}
channels::NTuple{N,T}

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

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

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

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


Base.:(==)(a::ColorMixture{Ta,N,Cs}, b::ColorMixture{Tb,N,Cs}) where {Ta,Tb,N,Cs} = a.channels == b.channels
Base.:(==)(a::ColorMixture{Ta,N,Cs}, b::ColorMixture{Tb,N,Cs}) where {Ta<:Number,Tb<:Number,N,Cs} = a.channels == b.channels
Base.:(==)(a::ColorMixture, b::ColorMixture) = false

function Base.show(io::IO, c::ColorMixture)
print(io, '(')
for (j, intensity) in enumerate(c.channels)
j > 1 && print(io, ", ")
print(io, intensity)
print_subscript(io, length(c), j)
end
print(io, ')')
end
Base.isequal(a::ColorMixture{Ta,N,Cs}, b::ColorMixture{Tb,N,Cs}) where {Ta<:Number,Tb<:Number,N,Cs} = isequal(a.channels, b.channels)
Base.isequal(a::ColorMixture, b::ColorMixture) = false

# These definitions use floats to avoid overflow
function Base.convert(::Type{RGB{T}}, c::ColorMixture{T,N,Cs}) where {T,N,Cs}
convert(RGB{T}, sum(map(*, c.channels, Cs); init=zero(RGB{floattype(T)})))
end
function Base.convert(::Type{RGB{T}}, c::ColorMixture{R,N,Cs}) where {T,R,N,Cs}
function Base.convert(::Type{RGB{T}}, c::ColorMixture{R,N,Cs}) where {T,R<:Number,N,Cs}
convert(RGB{T}, sum(map((w, rgb) -> convert(RGB{floattype(T)}, w*rgb), c.channels, Cs)))
end
Base.convert(::Type{RGB}, c::ColorMixture{T}) where T = convert(RGB{T}, c)
Base.convert(::Type{RGB}, c::ColorMixture{T}) where T<:Number = convert(RGB{T}, c)
Base.convert(::Type{RGB24}, c::ColorMixture) = convert(RGB24, convert(RGB, c))

ColorTypes._comp(::Val{N}, c::ColorMixture) where N = c.channels[N]
Compat.@constprop :aggressive ColorTypes.mapc(f, c::ColorMixture{T,N,Cs}) where {T,N,Cs} = ColorMixture(Cs, map(f, c.channels))
Compat.@constprop :aggressive ColorTypes.mapreducec(f, op, v0, c::ColorMixture{T,N,Cs}) where {T,N,Cs} = mapreduce(f, op, v0, c.channels)
Compat.@constprop :aggressive ColorTypes.reducec(op, v0, c::ColorMixture{T,N,Cs}) where {T,N,Cs} = reduce(op, c.channels; init=v0)
Compat.@constprop :aggressive ColorTypes.mapc(f, c::ColorMixture{T,N,Cs}) where {T<:Number,N,Cs} = ColorMixture(Cs, map(f, c.channels))
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)
Compat.@constprop :aggressive ColorTypes.reducec(op, v0, c::ColorMixture{T,N,Cs}) where {T<:Number,N,Cs} = reduce(op, c.channels; init=v0)
16 changes: 8 additions & 8 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using FluorophoreColors
using MultichannelColors
using Test

# interacts via @require
using StructArrays
using ImageCore

@testset "FluorophoreColors.jl" begin
@test isempty(detect_ambiguities(FluorophoreColors))
@testset "MultichannelColors.jl" begin
@test isempty(detect_ambiguities(MultichannelColors))

@testset "Fluorophore lookup" begin
cfp = fluorophore_rgb["ECFP"]
Expand Down Expand Up @@ -78,8 +78,8 @@ using ImageCore
else
@test_broken @inferred(mapc(x->2x, c)) === ColorMixture{Float32}(channels, (0.8, 0.4))
end
@test @inferred(mapreducec(x->2x, +, c)) === 1.2f0
@test @inferred(reducec(+, c)) === reduce(+, (0.4N0f8, 0.2N0f8))
@test @inferred(mapreducec(x->2x, +, 0f0, c)) === 1.2f0
@test @inferred(reducec(+, 0N0f8, c)) === reduce(+, (0.4N0f8, 0.2N0f8))
end

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

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

# Hyperspectral
cols = FluorophoreColors.Colors.distinguishable_colors(16, [RGB(0,0,0)]; dropseed=true)
cols = MultichannelColors.Colors.distinguishable_colors(16, [RGB(0,0,0)]; dropseed=true)
ctemplate = ColorMixture{Float32}((cols...,))
c = ctemplate([i/16 for i = 0:15]...)
@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₁₆)"
Expand Down

0 comments on commit 91364a4

Please sign in to comment.