diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f68af2b..6700b4d 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -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)' diff --git a/Project.toml b/Project.toml index 80e8b4b..d16e33e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,4 +1,4 @@ -name = "FluorophoreColors" +name = "MultiChannelColors" uuid = "d4071afc-4203-49ee-90bc-13ebeb18d604" authors = ["Tim Holy and contributors"] version = "0.1.0" @@ -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" diff --git a/README.md b/README.md index 11ba28f..edfd3a1 100644 --- a/README.md +++ b/README.md @@ -1,83 +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) +[![](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaImages.github.io/MultiChannelColors.jl/stable) -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. - -## Basic usage - -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> channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"]) -(RGB{N0f8}(0.0,0.925,0.365), RGB{N0f8}(1.0,0.859,0.0)) - -julia> ctemplate = ColorMixture{N0f16}(channels) -(0.0N0f16₁, 0.0N0f16₂) -``` - -This creates an all-zero "template" color object. Note that we've specified the element type, `N0f16`, for 16-bit color depth. -The subscripts `₁` and `₂` are hints that this is not an ordinary tuple; each represents the intensity in the corresponding channel. - -We use `ctemplate` to construct any other color: - -```julia -julia> c = ctemplate(0.25, 0.75) -(0.25N0f16₁, 0.75N0f16₂) - -julia> convert(RGB, c) -RGB{N0f16}(0.75,0.87549,0.09117) -``` - -The latter is how this color would be rendered in a viewer. - -## Overflow protection - -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. -With the choice above, - -```julia -julia> c = ctemplate(0.99, 0.99) -(0.99001N0f16₁, 0.99001N0f16₂) - -julia> convert(RGB, c) -ERROR: ArgumentError: component type N0f16 is a 16-bit type representing 65536 values from 0.0 to 1.0, - but the values (0.9900053f0, 1.7664759f0, 0.36105898f0) do not lie within this range. - See the READMEs for FixedPointNumbers and ColorTypes for more information. -Stacktrace: -[...] -``` - -If you want to guard against such errors, one good choice would be - -```julia -julia> convert(RGB{Float32}, c) -RGB{Float32}(0.9900053, 1.7664759, 0.36105898) -``` - -Conversions to floating-point types also tend to be faster, since the values do not have to be checked. - -## Advanced usage - -`ctemplate` stores the RGB *values* for each fluorophore as a type-parameter. This allows efficient conversion to RGB -without running into world-age problems that might otherwise arise from auto-generated conversion methods. -However, constructing `ctemplate` as above is an inherently non-inferrable operation. If you want to construct such colors -inferrably, you can use the macro version: - -```julia -f(i1, i2) = ColorMixture{N0f16}((fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato"), (i1, i2)) -``` - -Note the absence of `[]` brackets around the fluorophore names. - -## Why are the RGB colors encoded in the *type*? Why not a value field? - -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. - -## I wrote some code and got lousy performance. How can I fix it? - -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. +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. diff --git a/docs/Project.toml b/docs/Project.toml index 9d0aa53..bad7f51 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,3 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -FluorophoreColors = "d4071afc-4203-49ee-90bc-13ebeb18d604" +MultiChannelColors = "d4071afc-4203-49ee-90bc-13ebeb18d604" diff --git a/docs/make.jl b/docs/make.jl index 1d7019e..21b9f20 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,24 +1,26 @@ -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 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.org/MultiChannelColors.jl", assets=String[], ), pages=[ "Home" => "index.md", + "FAQ" => "faq.md", + "Reference" => "api.md", ], ) deploydocs(; - repo="github.com/JuliaImages/FluorophoreColors.jl", + repo="github.com/JuliaImages/MultiChannelColors.jl", devbranch="main", ) diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..de58fee --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,18 @@ +# API reference + +## Type hierarchy and construction + +```@docs +AbstractMultiChannelColor +MultiChannelColor +ColorMixture +GreenMagenta +MagentaGreen +``` + +## Fluorophores + +```@docs +fluorophore_rgb +@fluorophore_rgb_str +``` diff --git a/docs/src/faq.md b/docs/src/faq.md new file mode 100644 index 0000000..b0175a9 --- /dev/null +++ b/docs/src/faq.md @@ -0,0 +1,26 @@ +# FAQ + +## Why are the RGB colors encoded in the `ColorMixture` *type*? Why not a value field? + +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. + +If you consider an entire image `Array{<:ColorMixture}` (the preferred default representation for code in JuliaImages), it becomes clear that storing the RGB colors as a value field would also require additional memory for each pixel. + +## I wrote some code and got lousy performance. How can I fix it? + +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. + +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"]`. + +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): + +```julia +ctemplate = ColorMixture((rgb1, rgb2)) + +@noinline function make_image_and_do_something(ctemplate, sz) + img = [ctemplate(rand(), rand()) for i = 1:sz[1], j = 1:sz[2]] + ... +end +``` + +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 its creation scope. diff --git a/docs/src/index.md b/docs/src/index.md index 30d4dc7..3292fb5 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,14 +1,187 @@ ```@meta -CurrentModule = FluorophoreColors +CurrentModule = MultiChannelColors ``` -# FluorophoreColors +# MultiChannelColors -Documentation for [FluorophoreColors](https://github.com/JuliaImages/FluorophoreColors.jl). +[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. -```@index +## Installation + +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. + +## Usage + +Use the package interactively or in code with + +```jldoctest demo +julia> using MultiChannelColors +``` + +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)). + +The color types in this package support two fundamental categories of operations: + +- arithmetic operations such as `+` and `-` and multiplying or dividing by a scalar. You can also scale each color channel independently with `⊙` (obtained with `\odot`) 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"). +- extracting the independent channel intensities as a tuple with `Tuple(c)`. + +When creating `c`, you have two choices which primarily affect visualization: + +- to use ["bare" colors](@ref index_multichannelcolor) 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. +- to use [colors with built-in conversion to RGB](@ref index_colormixture), making them work automatically in standard visualization tools. This may be most appropriate when you have 3 or fewer channels. + +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. + +### ["Bare" colors: `MultiChannelColor`](@id index_multichannelcolor) + +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 +[11 wavelength bands](https://landsat.gsfc.nasa.gov/satellites/landsat-8/landsat-8-bands/), one might create a pixel this way: + +```jldoctest demo +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) +(0.2N4f12₀₁, 0.1001N4f12₀₂, 0.2N4f12₀₃, 0.2N4f12₀₄, 0.2501N4f12₀₅, 0.2N4f12₀₆, 0.2N4f12₀₇, 0.2N4f12₀₈, 0.2N4f12₀₉, 0.2N4f12₁₀, 0.2N4f12₁₁) +``` + +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). + +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) +and render positive values in green and negative values in magenta: + +```jldoctest demo +julia> function evi(c::MultiChannelColor{T,11}) where T<:FixedPoint + # Valid for Landsat 8 with 11 spectral bands + b = Tuple(c) # extract the bands + evi = 2.5f0 * (b[5] - b[4]) / (b[5] + 6*b[4] - 7.5f0*b[2] + eps(T)) + return evi > 0 ? RGB(0, evi, 0) : RGB(-evi, 0, -evi) + end; + +julia> evi(c) +RGB{Float32}(0.0f0,0.17894554f0,0.0f0) ``` -```@autodocs -Modules = [FluorophoreColors] +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: + +```julia +julia> using MappedArrays + +julia> imgrgb = mappedarray(evi, img); ``` + +### [RGB-convertible colors: `ColorMixture`](@id index_colormixture) + +`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ⱼ`. +`rgbⱼ` is a feature of the *type* (shared by all objects of the same type) whereas `iⱼ` is a property of *objects*. + +`ColorMixture` objects 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 `RGB{T}` where `T` is limited to 0-to-1. +It is also faster, as the result does not have to be checked for whether it exceeds the bounds of the type. +(To prevent overflow, all internal operations are performed using floating-point intermediates even if you want a `FixedPoint` output.) + +!!! note + While `ColorMixture` objects can be converted to RGB, they are *not* AbstractRGB + colors: `red(c)`, `green(c)`, and `blue(c)` are not defined for `c::ColorMixture`, and low-level utilities + like `mapc` operate on the raw channel intensities rather than the RGB values. + + +There are several ways you can create these colors. An easy approach is to define the type through a "template" object: + +```jldoctest demo +julia> ctemplate = ColorMixture{Float32}((RGB(0,1,0), RGB(1,0,0))) +(0.0₁, 0.0₂) +``` + +`ctemplate` is an all-zeros `ColorMixture` object, but can be used to construct arbitrary `c` with specified intensities: + +```jldoctest demo +julia> typeof(ctemplate) +ColorMixture{Float32, 2, (RGB{N0f8}(0.0,1.0,0.0), RGB{N0f8}(1.0,0.0,0.0))} + +julia> c = ctemplate(0.2, 0.4) +(0.2₁, 0.4₂) + +julia> Tuple(c) +(0.2f0, 0.4f0) +``` + +You can also create them with a single call `ColorMixture(rgbs, intensities)`: + +```jldoctest demo +julia> c = ColorMixture{Float32}((RGB(0,1,0), RGB(1,0,0)), (0.2, 0.4)) +(0.2₁, 0.4₂) +``` + +or even by explicit type construction: + +```jldoctest demo +julia> ColorMixture{Float32, 2, (RGB{N0f8}(0.0,1.0,0.0), RGB{N0f8}(1.0,0.0,0.0))}(0.2, 0.4) +(0.2₁, 0.4₂) +``` + +!!! tip + All but the last form require [constant propagation](https://en.wikipedia.org/wiki/Constant_folding) for inferrability. + Julia 1.7 and higher can use "aggressive" constant propagation to solve inference problems that may reduce performance on Julia 1.6. + +### Importing external data + +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: + +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: + + ```julia + A = rand(0x0000:0x0fff, 11, 100, 100); + img = reinterpret(reshape, MultiChannelColor{N4f12,11}, A); + ``` + +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: + + ```julia + A = rand(0x0000:0x0fff, 100, 100, 11); + img = StructArray{MultiChannelColor{N4f12,11}}(A; dims=3) + ``` + +It is possible that simpler syntaxes will be developed in future releases. + +## Additional features + +### Fluorophores + +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: + +```jldoctest demo +julia> channels = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"]) +(RGB{N0f8}(0.0,0.925,0.365), RGB{N0f8}(1.0,0.859,0.0)) + +julia> ctemplate = ColorMixture{N0f16}(channels) +(0.0N0f16₁, 0.0N0f16₂) +``` + +If you'll be hard-coding the name of the fluorophore, consider using a slightly different syntax: + +```jldoctest demo +julia> channels = (fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato") +(RGB{N0f8}(0.0,0.925,0.365), RGB{N0f8}(1.0,0.859,0.0)) +``` + +Note the absence of `[]` brackets around the fluorophore names. This form creates types inferrably, but the fluorophore name must be a literal string constant. + +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. + +### Green/magenta coloration + +For good separability in two-color imaging, the `GreenMagenta{T}` and `MagentaGreen{T}` types are convenient: + +```jldoctest demo +julia> c = GreenMagenta{N0f8}(0.2, 0.4) +(0.2N0f8₁, 0.4N0f8₂) + +julia> convert(RGB, c) +RGB{N0f8}(0.4,0.2,0.4) +``` + +Green and magenta are distinguishable even by individuals with common forms of color blindness, and is thus a good default for two-color imaging. diff --git a/src/FluorophoreColors.jl b/src/MultiChannelColors.jl similarity index 71% rename from src/FluorophoreColors.jl rename to src/MultiChannelColors.jl index 2cc2f9b..085ab47 100644 --- a/src/FluorophoreColors.jl +++ b/src/MultiChannelColors.jl @@ -1,4 +1,4 @@ -module FluorophoreColors +module MultiChannelColors using Compat @@ -9,7 +9,8 @@ using Colors using ColorVectorSpace using Requires -export fluorophore_rgb, @fluorophore_rgb_str, ColorMixture +export AbstractMultiChannelColor, MultiChannelColor, ColorMixture, GreenMagenta, MagentaGreen +export fluorophore_rgb, @fluorophore_rgb_str include("types.jl") include("fluorophores.jl") diff --git a/src/types.jl b/src/types.jl index 2314492..fcf7c08 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,53 +1,63 @@ -# 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. - """ - ColorMixture((rgb₁, rgb₂), (i₁, i₂)) # store intensities - ColorMixture{T}((rgb₁, rgb₂), (i₁, i₂)) # same, but coerce to element type T for colors and intensities + AbstractMultiChannelColor{T<:Number,N} -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ⱼ`. +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::AbstractMultiChannelColor)`. +""" +abstract type AbstractMultiChannelColor{T<:Number,N} <: Color{T,N} end -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`. +ColorTypes.comp1(c::AbstractMultiChannelColor) = c.channels[1] +ColorTypes.comp2(c::AbstractMultiChannelColor) = c.channels[2] +ColorTypes.comp3(c::AbstractMultiChannelColor) = c.channels[3] +ColorTypes.comp4(c::AbstractMultiChannelColor) = c.channels[4] +ColorTypes.comp5(c::AbstractMultiChannelColor) = c.channels[5] -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. +Base.Tuple(c::AbstractMultiChannelColor) = c.channels -# Examples +function Base.show(io::IO, c::AbstractMultiChannelColor) + 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 -To construct a 16-bit "pixel" from a dual-channel EGFP (peak emission 507nm)/tdTomato (peak emission 581nm) image, -you might do the following: +""" + MultiChannelColor(i₁, i₂, ...) + MultiChannelColor((i₁, i₂, ...)) + MultiChannelColor{T}(...) # coerce to element type T -```jldoctest -julia> using FluorophoreColors +Represent multichannel "raw" colors, which lack `convert` methods to standard color spaces. +If `c` is a `MultiChannelColor` object, then `Tuple(c)` is a tuple of intensities (one per channel). -julia> channelcolors = (fluorophore_rgb["EGFP"], fluorophore_rgb["tdTomato"]); +[`ColorMixture`](@ref) is an alternative with a built-in conversion to RGB. +""" +struct MultiChannelColor{T<:Number,N} <: AbstractMultiChannelColor{T,N} + channels::NTuple{N,T} +end -julia> c = ColorMixture{N0f16}(channelcolors, #= GFP intensity =# 0.2, #= tdTomato intensity =# 0.85) -(0.2N0f16₁, 0.85N0f16₂) +MultiChannelColor{T}(channels::NTuple{N,Any}) where {T<:Number,N} = MultiChannelColor{T,N}(channels) +MultiChannelColor{T}(channels::Vararg{Any,N}) where {T<:Number,N} = MultiChannelColor{T}(channels) -julia> convert(RGB, c) -RGB{N0f16}(0.85, 0.9151, 0.07294) -``` +MultiChannelColor(channels::NTuple{N,Number}) where {N} = MultiChannelColor(promote(channels...)) +MultiChannelColor(channels::Vararg{Number,N}) where {N} = MultiChannelColor(channels) -If you must construct colors inferrably inside a function body, use -```jldoctest; setup=:(using FluorophoreColors) -julia> channelcolors = (fluorophore_rgb"EGFP", fluorophore_rgb"tdTomato"); +""" + ColorMixture((rgb₁, rgb₂, ...), (i₁, i₂, ...)) + ColorMixture((rgb₁, rgb₂, ...), i₁, i₂, ...) + ColorMixture{T}(...) # coerce intensities to element type -julia> c = ColorMixture{N0f8}(channelcolors, #= GFP intensity =# 0.2, #= tdTomato intensity =# 0.85) -(0.2N0f8₁, 0.851N0f8₂) -``` +Represent a multichannel color 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, `convert(RGB, c)`, using intensity-weighting: +`rgb = sum(ivalues .* rgbvalues)`. -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". +[`MultiChannelColor`](@ref) is an alternative that does not require an `rgb` list and has no built-in conversion to RGB. """ -struct ColorMixture{T,N,Cs} <: Color{T,N} +struct ColorMixture{T<:Number,N,Cs} <: AbstractMultiChannelColor{T,N} channels::NTuple{N,T} Compat.@constprop :aggressive function ColorMixture{T,N,Cs}(channels::NTuple{N}) where {T,N,Cs} @@ -60,51 +70,61 @@ 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) """ - cobj = ColorMixture((rgb₁, rgb₂)) # create an all-zeros ColorMixture with N0f8 channel intensities - cobj = ColorMixture{T}((rgb₁, rgb₂)) # same, but specify the element type - c = cobj((i₁, i₂)) # Construct non-zero ColorMixture (inferrably) + ctemplate = ColorMixture((rgb₁, rgb₂)) # create an all-zeros ColorMixture with N0f8 channel intensities + ctemplate = ColorMixture{T}((rgb₁, rgb₂)) # same, but specify the element type + c = ctemplate((i₁, i₂)) # Construct non-zero ColorMixture of the same type as `ctemplate` -Create a ColorMixture `c` from a "template" `cobj`. `c` will be the same type as `cobj`. +Create a ColorMixture "template" `ctemplate` from which other non-zero colors `c` may be created. -`cobj((i...,))` is a constructor form that is performance-favorable, if the type of `cobj` +`ctemplate((i...,))` is a constructor form that is performance-favorable, if the type of `ctemplate` 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)) +Base.convert(::Type{C}, c::ColorMixture{T}) where {C<:Colorant,T} = convert(C, convert(RGB{floattype(T)}, 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) + +# Default mappings + +""" + GreenMagenta{T}(intensities) + GreenMagenta(intensities) + +Construct a [`ColorMixture`](@ref) with the specified intensities that colorizes the first channel with green and the second with magenta. +""" +const GreenMagenta{T} = ColorMixture{T,2,(RGB(0,1,0), RGB(1,0,1))} +""" + MagentaGreen{T}(intensities) + MagentaGreen(intensities) + +Construct a [`ColorMixture`](@ref) with the specified intensities that colorizes the first channel with magenta and the second with green. +""" +const MagentaGreen{T} = ColorMixture{T,2,(RGB(1,0,1), RGB(0,1,0))} diff --git a/test/runtests.jl b/test/runtests.jl index 2e2c291..3ff1ab6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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"] @@ -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 @@ -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) @@ -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₁₆)"