Skip to content

Commit dc5a360

Browse files
committed
Rename package, redesign types, add docs
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. The package name has been changed to reflect the wider applicability. This also adds Documenter documentation and slims the README.
1 parent 21e7a6b commit dc5a360

File tree

11 files changed

+329
-169
lines changed

11 files changed

+329
-169
lines changed

.github/workflows/CI.yml

+3-3
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

+2-2
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 <[email protected]> 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

+5-81
Original file line numberDiff line numberDiff line change
@@ -1,83 +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)
5+
[![](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaImages.github.io/MultiChannelColors.jl/stable)
56

6-
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
7-
wavelength of each fluorophore.
8-
9-
## Basic usage
10-
11-
Perhaps the easiest way to learn the package is by example. Suppose we are imaging two fluorophores, EGFP and tdTomato.
12-
13-
```julia
14-
julia> using FluorophoreColors
15-
16-
julia> channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"])
17-
(RGB{N0f8}(0.0,0.925,0.365), RGB{N0f8}(1.0,0.859,0.0))
18-
19-
julia> ctemplate = ColorMixture{N0f16}(channels)
20-
(0.0N0f16₁, 0.0N0f16₂)
21-
```
22-
23-
This creates an all-zero "template" color object. Note that we've specified the element type, `N0f16`, for 16-bit color depth.
24-
The subscripts `` and `` are hints that this is not an ordinary tuple; each represents the intensity in the corresponding channel.
25-
26-
We use `ctemplate` to construct any other color:
27-
28-
```julia
29-
julia> c = ctemplate(0.25, 0.75)
30-
(0.25N0f16₁, 0.75N0f16₂)
31-
32-
julia> convert(RGB, c)
33-
RGB{N0f16}(0.75,0.87549,0.09117)
34-
```
35-
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.
63-
64-
## Advanced usage
65-
66-
`ctemplate` stores the RGB *values* for each fluorophore as a type-parameter. This allows efficient conversion to RGB
67-
without running into world-age problems that might otherwise arise from auto-generated conversion methods.
68-
However, constructing `ctemplate` as above is an inherently non-inferrable operation. If you want to construct such colors
69-
inferrably, you can use the macro version:
70-
71-
```julia
72-
f(i1, i2) = ColorMixture{N0f16}((fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato"), (i1, i2))
73-
```
74-
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.
7+
This package defines [color types](https://github.com/JuliaGraphics/ColorTypes.jl) for use with multichannel fluorescence and hyperspectral imaging. See the [documentation](https://JuliaImages.github.io/MultiChannelColors.jl/stable) for more information.

docs/Project.toml

+1-1
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

+9-7
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
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 <[email protected]> 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=[
1717
"Home" => "index.md",
18+
"FAQ" => "faq.md",
19+
"Reference" => "api.md",
1820
],
1921
)
2022

2123
deploydocs(;
22-
repo="github.com/JuliaImages/FluorophoreColors.jl",
24+
repo="github.com/JuliaImages/MultiChannelColors.jl",
2325
devbranch="main",
2426
)

docs/src/api.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# API reference
2+
3+
## Type hierarchy and construction
4+
5+
```@docs
6+
AbstractMultiChannelColor
7+
MultiChannelColor
8+
ColorMixture
9+
GreenMagenta
10+
MagentaGreen
11+
```
12+
13+
## Fluorophores
14+
15+
```@docs
16+
fluorophore_rgb
17+
@fluorophore_rgb_str
18+
```

docs/src/faq.md

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# FAQ
2+
3+
## Why are the RGB colors encoded in the `ColorMixture` *type*? Why not a value field?
4+
5+
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.
6+
7+
## I wrote some code and got lousy performance. How can I fix it?
8+
9+
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.
10+
11+
If you're using fluorophore colors with `fluorophore_rgb`, where possible make sure you're using the compile-time constant syntax `fluorophore_rgb"EGFP"` rather than the runtime syntax `fluorophore_rgb["EGFP"]`.
12+
13+
When you can't get good performance otherwise, your best option is to use a [function barrier](https://docs.julialang.org/en/v1/manual/performance-tips/#kernel-functions):
14+
15+
```julia
16+
ctemplate = ColorMixture((rgb1, rgb2))
17+
18+
@noinline function make_image_and_do_something(ctemplate, sz)
19+
img = [ctemplate(rand(), rand()) for i = 1:sz[1], j = 1:sz[2]]
20+
...
21+
end
22+
```
23+
24+
In this case `ctemplate` encodes the type and code in `make_image_and_do_something` will be inferrable even if the type of the created `ctemplate` is not inferrable in the calling scope.

docs/src/index.md

+177-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,185 @@
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+
[MultiChannelColors](https://github.com/JuliaImages/MultiChannelColors.jl) aims to support "unconventional colors," such as might arise in applications like multichannel fluorescence microscopy and hyperspectral imaging. Consistent with the philosophy of the [JuliaImages ecosystem](https://juliaimages.org/latest/), this package allows you to bundle together the different color channels into a "color object," and many color objects can be stored in an array. Having each entry of the array represent a complete pixel or voxel makes it much easier to write generic code supporting a wide range of image types.
88

9-
```@index
9+
## Installation
10+
11+
Install the package with `add MultiChannelColors` from the `pkg>` prompt, which you access by typing `]` from the `julia>` prompt. See the [Pkg documentation](https://pkgdocs.julialang.org/v1/getting-started/) for more information.
12+
13+
## Usage
14+
15+
Use the package interactively or in code with
16+
17+
```jldoctest demo
18+
julia> using MultiChannelColors
19+
```
20+
21+
In addition to giving access to specific types defined below, this will import the namespaces of [FixedPointNumbers](https://github.com/JuliaMath/FixedPointNumbers.jl) (which harmonizes the interpretation of "integer" and "floating-point" pixel-encodings) and [ColorTypes](https://github.com/JuliaGraphics/ColorTypes.jl) (which defines core color types and low-level manipulation). It will also define arithmetic for colors such as RGB (see [ColorVectorSpace](https://github.com/JuliaGraphics/ColorVectorSpace.jl)).
22+
23+
The color types in this package support two fundamental categories of operations:
24+
25+
- arithmetic operations such as `+` and `-` and multiplying or dividing by a scalar. You can also scale each color channel independently with `` (obtained with `\odotTAB`) or its synonym `hadamard`, e.g., `g ⊙ c` where `c` is a color object defined in this package and `g` is a tuple of real numbers (the "gains").
26+
- extracting the independent channel intensities as a tuple with `Tuple(c)`.
27+
28+
When creating `c`, you have two choices which primarily affect visualization:
29+
30+
- to use "bare" colors that store the multichannel data but lack any default conversion to other color spaces. This might be most appropriate if you have more than 3 channels, for which there may be many different ways to visualize the data they encode.
31+
- to use colors with built-in conversion to RGB, making them work automatically in standard visualization tools. This may be most appropriate when you have 3 or fewer channels.
32+
33+
Both options will be discussed below. See the [JuliaImages documentation on visualization](https://juliaimages.org/latest/install/#sec_visualization) for more information about tools for viewing images.
34+
35+
### "Bare" colors: `MultiChannelColor`
36+
37+
A `MultiChannelColor` object is essentially a glorified tuple, one that can be recognized as a [`Colorant`](https://github.com/JuliaGraphics/ColorTypes.jl#the-type-hierarchy-and-abstract-types) but with comparatively few automatic behaviors. For example, if you're working with [Landsat 8](https://en.wikipedia.org/wiki/Landsat_8) data with
38+
[11 wavelength bands](https://landsat.gsfc.nasa.gov/satellites/landsat-8/landsat-8-bands/), one might create a pixel this way:
39+
40+
```jldoctest demo
41+
julia> c = MultiChannelColor{N4f12}(0.2, 0.1, 0.2, 0.2, 0.25, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2)
42+
(0.2N4f12₀₁, 0.1001N4f12₀₂, 0.2N4f12₀₃, 0.2N4f12₀₄, 0.2501N4f12₀₅, 0.2N4f12₀₆, 0.2N4f12₀₇, 0.2N4f12₀₈, 0.2N4f12₀₉, 0.2N4f12₁₀, 0.2N4f12₁₁)
43+
```
44+
45+
See the [FixedPointNumbers](https://github.com/JuliaMath/FixedPointNumbers.jl) package for information about the 16-bit data type `N4f12` (Landsat 8 quantizes with 12 bits).
46+
47+
The usual way to visualize such an object is to define a custom function that converts such colors to more conventional colors (`RGB` or `Gray`). For example, we might compute the [Enhanced Vegetation Index](https://www.usgs.gov/landsat-missions/landsat-enhanced-vegetation-index)
48+
and render positive values in green and negative values in magenta:
49+
50+
```jldoctest demo
51+
julia> function evi(c::MultiChannelColor{T,11}) where T<:FixedPoint
52+
# Valid for Landsat 8 with 11 spectral bands
53+
b = Tuple(c) # extract the bands
54+
evi = 2.5f0 * (b[5] - b[4]) / (b[5] + 6*b[4] - 7.5f0*b[2] + eps(T))
55+
return evi > 0 ? RGB(0, evi, 0) : RGB(-evi, 0, -evi)
56+
end;
57+
58+
julia> evi(c)
59+
RGB{Float32}(0.0f0,0.17894554f0,0.0f0)
60+
```
61+
62+
If `img` is a whole image of such pixels, `evi.(img)` converts the entire array to RGB. For large data, you might prefer to use the [MappedArrays package](https://github.com/JuliaArrays/MappedArrays.jl) to do such conversions "lazily" (on an as-needed basis) to avoid exhausting computer memory:
63+
64+
```julia
65+
julia> using MappedArrays
66+
67+
julia> imgrgb = mappedarray(evi, img);
68+
```
69+
70+
### RGB-convertible colors: `ColorMixture`
71+
72+
`ColorMixture` objects are like `MultiChannelColor` objects except they have a built-in conversion to RGB. Each channel gets assigned a specific RGB color, say `rgbⱼ` for the `j`th channel, along with an intensity `iⱼ`.
73+
`rgbⱼ` is a feature of the *type* (shared by all objects of the same type) whereas `iⱼ` is a property of *objects*.
74+
75+
`ColorMixture` objects are converted to RGB with intensity-weighting,
76+
77+
``
78+
c_{rgb} = \sum_j i_j \mathrm{rgb}_j
79+
``
80+
81+
Depending on the the `rgbⱼ` and `iⱼ`, values may exceed the 0-to-1 colorscale of RGBs.
82+
Conversion to `RGB{Float32}` may be safer than `RGB{T}` where `T` is limited to 0-to-1.
83+
It is also faster, as the result does not have to be checked for whether it exceeds the bounds of the type.
84+
(To prevent overflow, all internal operations are performed using floating-point intermediates even if you want a `FixedPoint` output.)
85+
86+
!!! note
87+
While `ColorMixture` objects can be converted to RGB, they are *not* AbstractRGB
88+
colors: `red(c)`, `green(c)`, and `blue(c)` are not defined for `c::ColorMixture`, and low-level utilities
89+
like `mapc` operate on the raw channel intensities rather than the RGB values.
90+
91+
92+
There are several ways you can create these colors. An easy approach is to define the type through a "template" object:
93+
94+
```jldoctest demo
95+
julia> ctemplate = ColorMixture{Float32}((RGB(0,1,0), RGB(1,0,0)))
96+
(0.0₁, 0.0₂)
1097
```
1198

12-
```@autodocs
13-
Modules = [FluorophoreColors]
99+
`ctemplate` is an all-zeros `ColorMixture` object, but can be used to construct arbitrary `c` with specified intensities:
100+
101+
```jldoctest demo
102+
julia> typeof(ctemplate)
103+
ColorMixture{Float32, 2, (RGB{N0f8}(0.0,1.0,0.0), RGB{N0f8}(1.0,0.0,0.0))}
104+
105+
julia> c = ctemplate(0.2, 0.4)
106+
(0.2₁, 0.4₂)
107+
108+
julia> Tuple(c)
109+
(0.2f0, 0.4f0)
110+
```
111+
112+
You can also create them with a single call `ColorMixture(rgbs, intensities)`:
113+
114+
```jldoctest demo
115+
julia> c = ColorMixture{Float32}((RGB(0,1,0), RGB(1,0,0)), (0.2, 0.4))
116+
(0.2₁, 0.4₂)
117+
```
118+
119+
or even by explicit type construction:
120+
121+
```jldoctest demo
122+
julia> ColorMixture{Float32, 2, (RGB{N0f8}(0.0,1.0,0.0), RGB{N0f8}(1.0,0.0,0.0))}(0.2, 0.4)
123+
(0.2₁, 0.4₂)
14124
```
125+
126+
!!! tip
127+
All but the last form require [constant propagation](https://en.wikipedia.org/wiki/Constant_folding) for inferrability.
128+
Julia 1.7 and higher can use "aggressive" constant propagation to solve inference problems that may reduce performance on Julia 1.6.
129+
130+
### Importing external data
131+
132+
When objects are not created by code but instead loaded from an external source such as a file, you have several avenues for creating arrays of multichannel color objects. There are two particularly common cases:
133+
134+
1. If the imported data are an array `A` of size `(nc, m, n)`, where `nc` is the number of color channels (i.e., color is the fastest dimension), then use `reinterpret(reshape, C, A)` where `C` is the color type you want to use (e.g., `MultiChannelColor{T,nc}` or `ColorMixture{T,nc,rgbs}`). For instance, Landsat 8 data might look something like this:
135+
136+
```julia
137+
A = rand(0x0000:0x0fff, 11, 100, 100);
138+
img = reinterpret(reshape, MultiChannelColor{N4f12,11}, A);
139+
```
140+
141+
2. If the imported data have the color channel last, or use separate arrays for each channel, use the [StructArrays package](https://github.com/JuliaArrays/StructArrays.jl). For example:
142+
143+
```julia
144+
A = rand(0x0000:0x0fff, 100, 100, 11);
145+
img = StructArray{MultiChannelColor{N4f12,11}}(A; dims=3)
146+
```
147+
148+
## Additional features
149+
150+
### Fluorophores
151+
152+
This package also exports a lookup table for common [fluorophores](https://en.wikipedia.org/wiki/Fluorophore). If desired, these can be used as the `rgbⱼ` values for `ColorMixture` channels. For example:
153+
154+
```jldoctest demo
155+
julia> channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"])
156+
(RGB{N0f8}(0.0,0.925,0.365), RGB{N0f8}(1.0,0.859,0.0))
157+
158+
julia> ctemplate = ColorMixture{N0f16}(channels)
159+
(0.0N0f16₁, 0.0N0f16₂)
160+
```
161+
162+
If you'll be hard-coding the name of the fluorophore, consider using a slightly different syntax:
163+
164+
```jldoctest demo
165+
julia> channels = (fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato")
166+
(RGB{N0f8}(0.0,0.925,0.365), RGB{N0f8}(1.0,0.859,0.0))
167+
```
168+
169+
Note the absence of `[]` brackets around the fluorophore names. This form creates types inferrably, but the fluorophore name must be a literal string constant.
170+
171+
The RGB values are computed from the peak emission wavelength of each fluorophore; note, however, that the perceptual appearance is often more red-shifted due to the asymmetric shape of emission spectra.
172+
173+
### Green/magenta coloration
174+
175+
For good separability in two-color imaging, the `GreenMagenta{T}` and `MagentaGreen{T}` types are convenient:
176+
177+
```jldoctest demo
178+
julia> c = GreenMagenta{N0f8}(0.2, 0.4)
179+
(0.2N0f8₁, 0.4N0f8₂)
180+
181+
julia> convert(RGB, c)
182+
RGB{N0f8}(0.4,0.2,0.4)
183+
```
184+
185+
Green and magenta are distinguishable even by individuals with common forms of color blindness, and is thus a good default for two-color imaging.

0 commit comments

Comments
 (0)