diff --git a/docs/tutorial/figures/kymographs/kymo_wavelength_cmap.png b/docs/tutorial/figures/kymographs/kymo_wavelength_cmap.png new file mode 100644 index 000000000..1071efcc1 --- /dev/null +++ b/docs/tutorial/figures/kymographs/kymo_wavelength_cmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28f6bac24fe809a7a9107aa1c721975dffcd4816e3621e4874a4c3d5e5d5ee96 +size 936194 diff --git a/docs/tutorial/kymographs.rst b/docs/tutorial/kymographs.rst index a172a02bd..2f04ea3b9 100644 --- a/docs/tutorial/kymographs.rst +++ b/docs/tutorial/kymographs.rst @@ -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") diff --git a/docs/whatsnew/1.2.0/1_2_0.rst b/docs/whatsnew/1.2.0/1_2_0.rst new file mode 100644 index 000000000..142b05b18 --- /dev/null +++ b/docs/whatsnew/1.2.0/1_2_0.rst @@ -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). diff --git a/docs/whatsnew/1.2.0/wavelength_cmaps.png b/docs/whatsnew/1.2.0/wavelength_cmaps.png new file mode 100644 index 000000000..6f34e59f3 --- /dev/null +++ b/docs/whatsnew/1.2.0/wavelength_cmaps.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55e1e1ed3772798fd626b53a7882c7c5faa83b86af828b5982cfed31ed335137 +size 429759 diff --git a/docs/whatsnew/index.rst b/docs/whatsnew/index.rst index 6dd566e86..e98395d72 100644 --- a/docs/whatsnew/index.rst +++ b/docs/whatsnew/index.rst @@ -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 diff --git a/lumicks/pylake/adjustments.py b/lumicks/pylake/adjustments.py index da7ec9dc0..eddc39893 100644 --- a/lumicks/pylake/adjustments.py +++ b/lumicks/pylake/adjustments.py @@ -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 @@ -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]) @@ -157,6 +193,24 @@ 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 -------- :: @@ -164,6 +218,8 @@ class _ColorMaps( # 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): @@ -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) diff --git a/lumicks/pylake/tests/test_image.py b/lumicks/pylake/tests/test_image.py index 863727404..f3b1ef1cd 100644 --- a/lumicks/pylake/tests/test_image.py +++ b/lumicks/pylake/tests/test_image.py @@ -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, @@ -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)