Skip to content

Commit

Permalink
adjustments: add _ColorMaps.from_wavelength()
Browse files Browse the repository at this point in the history
  • Loading branch information
rpauszek committed Jul 4, 2023
1 parent 9c639a4 commit daaa1cf
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 1 deletion.
3 changes: 3 additions & 0 deletions docs/tutorial/figures/kymographs/kymo_wavelength_cmap.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions docs/tutorial/kymographs.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ with the cyan colormap::

.. image:: figures/kymographs/kymo_blue.png

We can also use the `lk.colormaps.from_wavelength()` method to generate a color map approximating the color of a particular wavelength::

adjustment = lk.ColorAdjustment(0, 99, mode="percentile")

plt.figure()
plt.subplot(2, 1, 1)
kymo.plot(channel="green", adjustment=adjustment)
plt.title("default 'green' colormap")
plt.subplot(2, 1, 2)
kymo.plot(
channel="green",
adjustment=adjustment,
cmap=lk.colormaps.from_wavelength(590)
)
plt.title("emission @ 590 nm")
plt.tight_layout()
plt.show()

.. image:: figures/kymographs/kymo_wavelength_cmap.png

The kymograph can also be exported to TIFF format::

kymo.export_tiff("image.tiff")
Expand Down
20 changes: 20 additions & 0 deletions docs/whatsnew/1.2.0/1_2_0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Pylake 1.2.0
============

.. only:: html

Here's a sneak peak at some of the highlights from the upcoming Pylake `v1.2.0` release...

Generate colormaps according to emission wavelength
---------------------------------------------------

By default, single-channel images arising from fluorophores excited with the red, green, and blue lasers
are plotted with the corresponding `lk.colormaps.red`, `lk.colormaps.green`, and `lk.colormaps.blue`
colormaps, respectively. However, the actual light emitted is always red-shifted from the excitation color.
Now you can plot single-channel images with the approximate color of the signal emitted based on the
emission wavelength using the `from_wavelength()` method of :data:`~lumicks.pylake.colormaps`.

.. figure:: wavelength_cmaps.png

Kymographs showing tracks in three color channels using the default colormaps (left) and colormaps
corresponding to the actual emission colors (right).
3 changes: 3 additions & 0 deletions docs/whatsnew/1.2.0/wavelength_cmaps.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/whatsnew/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ For a full list of new features and changes, please refer to the :doc:`changelog
:caption: Contents
:maxdepth: 1

1.2.0/1_2_0
1.1.0/1_1_0
1.0.0/1_0_0
61 changes: 61 additions & 0 deletions lumicks/pylake/adjustments.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
import matplotlib as mpl
from matplotlib.colors import LinearSegmentedColormap
from skimage.color import xyz2rgb
from dataclasses import make_dataclass


Expand Down Expand Up @@ -125,6 +126,41 @@ def nothing(cls):
no_adjustment = ColorAdjustment.nothing()


def wavelength_to_xyz(wavelength):
"""Calculate XYZ components in CIE color space."""
conversion_coefficients = {
"x": {
"alpha": np.array([0.362, 1.056, -0.065]),
"beta": np.array([442.0, 599.8, 501.1]),
"gamma": np.array([0.0624, 0.0264, 0.0490]),
"delta": np.array([0.0374, 0.0323, 0.0382]),
},
"y": {
"alpha": np.array([0.821, 0.286]),
"beta": np.array([568.8, 530.9]),
"gamma": np.array([0.0213, 0.0613]),
"delta": np.array([0.0247, 0.0322]),
},
"z": {
"alpha": np.array([1.217, 0.681]),
"beta": np.array([437.0, 459.0]),
"gamma": np.array([0.0845, 0.0385]),
"delta": np.array([0.0278, 0.0725]),
},
}

def calculate_component(alpha, beta, gamma, delta):
lam_min_beta = wavelength - beta
s_func = np.where(lam_min_beta < 0, gamma, delta)
return np.sum(alpha * np.exp(-0.5 * (lam_min_beta * s_func) ** 2))

xyz = [
calculate_component(**conversion_coefficients[key])
for key in conversion_coefficients.keys()
]
return np.hstack(xyz)


def _make_cmap(name, color):
return LinearSegmentedColormap.from_list(name, colors=[(0, 0, 0), color])

Expand Down Expand Up @@ -157,13 +193,33 @@ class _ColorMaps(
yellow
cyan
Methods
-------
from_wavelength(wavelength)
Generate a colormap with a minimum of black and maximum color approximately
corresponding to a wavelength in nanometers.
RGB value approximating the given wavelength is calculated using Eq. 4 from [1]_.
Parameters
----------
wavelength: int
wavelength to approximate maximum color from
References
----------
.. [1] Chris Wyman, Peter-Pike Sloan, Peter Shirley. "Simple Analytic Approximations to the
CIE XYZ Color Matching Functions" Journal of Computer Graphics Techniques (2013) 2, 1-11.
Examples
--------
::
# plot the blue image from a kymograph with cyan colormap
kymo.plot(channel="blue", cmap=lk.colormaps.cyan)
# plot the blue image with the emission maximum of the fluorophore excited at 488 nm
kymo.plot(channel="blue", cmap=lk.colormaps.from_wavelength(521))
"""

def __str__(self):
Expand All @@ -173,5 +229,10 @@ def __str__(self):
def rgb(self):
return None

def from_wavelength(self, wavelength):
xyz = wavelength_to_xyz(wavelength)
rgb = xyz2rgb(xyz.reshape([1, 1, 3])).squeeze()
return _make_cmap(f"{wavelength}nm", rgb)


colormaps = _ColorMaps(**_available_colormaps)
34 changes: 33 additions & 1 deletion lumicks/pylake/tests/test_image.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
import re
import numpy as np
from lumicks.pylake.adjustments import ColorAdjustment
from lumicks.pylake.adjustments import ColorAdjustment, wavelength_to_xyz, colormaps
from lumicks.pylake.detail.image import (
reconstruct_image,
reconstruct_image_sum,
Expand Down Expand Up @@ -194,3 +194,35 @@ def test_no_adjust():

ca = ColorAdjustment.nothing()
np.testing.assert_allclose(ca._get_data_rgb(ref_img), ref_img / ref_img.max())


@pytest.mark.parametrize(
"wavelength, ref_xyz",
[
(300, [2.63637746e-14, 6.25334786e-08, 4.96638266e-09]),
(488, [0.04306751, 0.19571404, 0.52012862]),
(590, [1.02103920e00, 7.62586723e-01, 1.43479704e-04]),
(650, [2.83631873e-01, 1.10045343e-01, 2.96115850e-08]),
(850, [6.94669991e-15, 2.74627747e-11, 2.88661210e-29]),
],
)
def test_wavelength_to_xyz(wavelength, ref_xyz):
xyz = wavelength_to_xyz(wavelength)
np.testing.assert_allclose(xyz, ref_xyz)


@pytest.mark.parametrize(
"wavelength, ref",
[
# fmt: off
(300, [[0, 0, 0, 1], [0, 7.62146890e-07, 0, 1], [0, 1.51833951e-06, 0, 1]]),
(488, [[0, 0, 0, 1], [0, 0.31312012, 0.3731908, 1], [0, 0.623794, 0.74346605, 1]]),
(590, [[0, 0, 0, 1], [0.50196078, 0.34888505, 0, 1], [1, 0.69504443, 0, 1]]),
(650, [[0, 0, 0, 1], [0.44212589, 0, 0, 1], [0.88079768, 0, 0, 1]]),
(850, [[0, 0, 0, 1], [0, 3.34079999e-10, 0, 1], [0, 6.65549997e-10, 0, 1]]),
# fmt: on
],
)
def test_wavelength_to_cmap(wavelength, ref):
cmap = colormaps.from_wavelength(wavelength)
np.testing.assert_allclose(cmap([0, 0.5, 1]), ref)

0 comments on commit daaa1cf

Please sign in to comment.