Add wavelengths_strip to plotting functions with wavelengths on x axis.#1401
Open
lassefschmidt wants to merge 2 commits into
Open
Add wavelengths_strip to plotting functions with wavelengths on x axis.#1401lassefschmidt wants to merge 2 commits into
lassefschmidt wants to merge 2 commits into
Conversation
Member
|
Hi @lassefschmidt, Looking at this, and given how we tend to compose plots together, how about something like this: from __future__ import annotations
import typing
from typing import Any
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Polygon
from mpl_toolkits.axes_grid1 import make_axes_locatable
from colour.algebra import LinearInterpolator, normalise_maximum, sdiv, sdiv_mode
from colour.colorimetry import (
CCS_ILLUMINANTS,
SDS_ILLUMINANTS,
MultiSpectralDistributions,
SpectralDistribution,
sd_ones,
wavelength_to_XYZ,
)
from colour.plotting import (
CONSTANTS_COLOUR_STYLE,
XYZ_to_plotting_colourspace,
artist,
filter_cmfs,
override_style,
render,
)
from colour.colorimetry import sd_to_XYZ, sds_and_msds_to_sds
from colour.plotting import filter_illuminants, update_settings_collection
from colour.utilities import domain_range_scale, first_item, ones, tstack
if typing.TYPE_CHECKING:
from collections.abc import Sequence
from matplotlib.axes import Axes
from matplotlib.figure import Figure
def wavelengths_to_RGB(
wavelengths,
cmfs: MultiSpectralDistributions,
out_of_gamut_clipping: bool = True,
):
RGB = XYZ_to_plotting_colourspace(
wavelength_to_XYZ(wavelengths, cmfs),
CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["E"],
apply_cctf_encoding=False,
)
if not out_of_gamut_clipping:
RGB += np.abs(np.min(RGB))
return normalise_maximum(RGB)
@override_style()
def plot_visible_spectrum_colours(
cmfs: (
MultiSpectralDistributions | str | Sequence[
MultiSpectralDistributions | str]
) = "CIE 1931 2 Degree Standard Observer",
out_of_gamut_clipping: bool = True,
**kwargs: Any,
):
_figure, axes = artist(**kwargs)
cmfs = typing.cast(
"MultiSpectralDistributions",
first_item(filter_cmfs(cmfs).values()),
)
x_min, x_max, y_min, y_max = kwargs.get(
"bounding_box",
(
float(np.min(cmfs.wavelengths)),
float(np.max(cmfs.wavelengths)),
0.0,
1.0,
),
)
x_visible_min = max(x_min, float(np.min(cmfs.wavelengths)))
x_visible_max = min(x_max, float(np.max(cmfs.wavelengths)))
for x_start, x_end in (
(x_min, min(x_max, x_visible_min)),
(max(x_min, x_visible_max), x_max),
):
if x_start >= x_end:
continue
axes.bar(
x_start,
y_max - y_min,
width=x_end - x_start,
bottom=y_min,
color=CONSTANTS_COLOUR_STYLE.colour.bright,
alpha=0.5,
align="edge",
hatch=CONSTANTS_COLOUR_STYLE.hatch.patterns[-1],
edgecolor=CONSTANTS_COLOUR_STYLE.colour.light,
linewidth=0,
zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
)
if x_visible_min < x_visible_max:
edges = np.linspace(
x_visible_min,
x_visible_max,
max(int(np.ceil(x_visible_max - x_visible_min)), 1) + 1,
)
wavelengths = 0.5 * (edges[:-1] + edges[1:])
RGB = CONSTANTS_COLOUR_STYLE.colour.colourspace.cctf_encoding(
wavelengths_to_RGB(wavelengths, cmfs, out_of_gamut_clipping)
)
axes.bar(
edges[:-1],
np.full(wavelengths.shape, y_max - y_min),
width=np.diff(edges),
bottom=y_min,
color=RGB,
align="edge",
antialiased=False,
linewidth=0,
edgecolor="none",
zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
)
axes.set_yticks([])
settings: dict[str, Any] = {
"axes": axes,
"bounding_box": (x_min, x_max, y_min, y_max),
"y_label": None,
}
settings.update(kwargs)
return render(**settings)
@override_style()
def plot_visible_spectrum(
cmfs: (
MultiSpectralDistributions | str | Sequence[
MultiSpectralDistributions | str]
) = "CIE 1931 2 Degree Standard Observer",
out_of_gamut_clipping: bool = True,
**kwargs: Any,
) -> tuple[Figure, Axes]:
cmfs = typing.cast(
"MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values())
)
bounding_box = (min(cmfs.wavelengths), max(cmfs.wavelengths), 0, 1)
settings: dict[str, Any] = {"bounding_box": bounding_box, "y_label": None}
settings.update(kwargs)
settings["show"] = False
settings["show_visible_spectrum"] = False
_figure, axes = plot_single_sd(
sd_ones(cmfs.shape),
cmfs=cmfs,
out_of_gamut_clipping=out_of_gamut_clipping,
**settings,
)
settings = {
"axes": axes,
"show": True,
"title": f"The Visible Spectrum - {cmfs.display_name}",
"x_label": "Wavelength $\\lambda$ (nm)",
}
settings.update(kwargs)
return render(**settings)
@override_style()
def plot_single_sd(
sd: SpectralDistribution,
cmfs="CIE 1931 2 Degree Standard Observer",
out_of_gamut_clipping: bool = True,
modulate_colours_with_sd_amplitude: bool = False,
equalize_sd_amplitude: bool = False,
show_visible_spectrum: bool = False,
**kwargs: Any,
):
_figure, axes = artist(**kwargs)
cmfs = typing.cast(
"MultiSpectralDistributions",
first_item(filter_cmfs(cmfs).values()),
)
sd = sd.copy()
sd.interpolator = LinearInterpolator
wavelengths = cmfs.wavelengths[
np.logical_and(
cmfs.wavelengths >= max(min(cmfs.wavelengths),
min(sd.wavelengths)),
cmfs.wavelengths <= min(max(cmfs.wavelengths),
max(sd.wavelengths)),
)
]
values = sd[wavelengths]
RGB = wavelengths_to_RGB(wavelengths, cmfs, out_of_gamut_clipping)
if modulate_colours_with_sd_amplitude:
with sdiv_mode():
RGB *= sdiv(values, np.max(values))[..., None]
RGB = CONSTANTS_COLOUR_STYLE.colour.colourspace.cctf_encoding(RGB)
if equalize_sd_amplitude:
values = ones(values.shape)
margin = 0 if equalize_sd_amplitude else 0.05
x_min, x_max = min(wavelengths), max(wavelengths)
y_min, y_max = 0, max(values) + max(values) * margin
polygon = Polygon(
np.vstack(
[
(x_min, 0),
tstack([wavelengths, values]),
(x_max, 0),
]
),
facecolor="none",
edgecolor="none",
zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
)
axes.add_patch(polygon)
padding = 0.1
axes.bar(
x=wavelengths - padding,
height=max(values),
width=1 + padding,
color=RGB,
align="edge",
clip_path=polygon,
zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
)
axes.plot(
wavelengths,
values,
color=CONSTANTS_COLOUR_STYLE.colour.dark,
zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
)
settings: dict[str, Any] = {
"axes": axes,
"bounding_box": (x_min, x_max, y_min, y_max),
"title": f"{sd.display_name} - {cmfs.display_name}",
"x_label": "Wavelength $\\lambda$ (nm)",
"y_label": "Spectral Distribution",
}
settings.update(kwargs)
if show_visible_spectrum:
visible_spectrum_axes = make_axes_locatable(axes).append_axes(
"bottom", size="5%", pad=0, sharex=axes
)
plot_visible_spectrum_colours(
cmfs=cmfs,
out_of_gamut_clipping=out_of_gamut_clipping,
axes=visible_spectrum_axes,
bounding_box=(x_min, x_max, 0, 1),
x_label=settings.pop("x_label", None),
show=False,
)
axes.tick_params(
axis="x", bottom=False, labelbottom=False, which="both"
)
axes.set_xlabel(None)
return render(**settings)
@override_style()
def plot_multi_sds(
sds,
plot_kwargs: dict | list[dict] | None = None,
show_visible_spectrum: bool = False,
**kwargs: Any,
):
_figure, axes = artist(**kwargs)
sds_converted = sds_and_msds_to_sds(sds)
plot_settings_collection = [
{
"label": f"{sd.display_name}",
"zorder": CONSTANTS_COLOUR_STYLE.zorder.midground_line,
"cmfs": "CIE 1931 2 Degree Standard Observer",
"illuminant": SDS_ILLUMINANTS["E"],
"use_sd_colours": False,
"normalise_sd_colours": False,
}
for sd in sds_converted
]
if plot_kwargs is not None:
update_settings_collection(
plot_settings_collection, plot_kwargs, len(sds_converted)
)
x_limit_min, x_limit_max, y_limit_min, y_limit_max = [], [], [], []
for i, sd in enumerate(sds_converted):
plot_settings = plot_settings_collection[i]
cmfs = typing.cast(
"MultiSpectralDistributions",
first_item(filter_cmfs(plot_settings.pop("cmfs")).values()),
)
illuminant = typing.cast(
"SpectralDistribution",
first_item(
filter_illuminants(plot_settings.pop("illuminant")).values()),
)
normalise_sd_colours = plot_settings.pop("normalise_sd_colours")
use_sd_colours = plot_settings.pop("use_sd_colours")
wavelengths, values = sd.wavelengths, sd.values
shape = sd.shape
x_limit_min.append(shape.start)
x_limit_max.append(shape.end)
y_limit_min.append(min(values))
y_limit_max.append(max(values))
if use_sd_colours:
with domain_range_scale("1"):
XYZ = sd_to_XYZ(sd, cmfs, illuminant)
if normalise_sd_colours:
XYZ /= XYZ[..., 1]
plot_settings["color"] = np.clip(
XYZ_to_plotting_colourspace(XYZ), 0, 1
)
axes.plot(wavelengths, values, **plot_settings)
bounding_box = (
min(x_limit_min),
max(x_limit_max),
min(y_limit_min),
max(y_limit_max) * 1.05,
)
settings: dict[str, Any] = {
"axes": axes,
"bounding_box": bounding_box,
"legend": True,
"x_label": "Wavelength $\\lambda$ (nm)",
"y_label": "Spectral Distribution",
}
settings.update(kwargs)
if show_visible_spectrum:
visible_spectrum_axes = make_axes_locatable(axes).append_axes(
"bottom", size="5%", pad=0, sharex=axes
)
plot_visible_spectrum_colours(
cmfs=cmfs,
axes=visible_spectrum_axes,
bounding_box=(bounding_box[0], bounding_box[1], 0, 1),
x_label=settings.pop("x_label", None),
show=False,
)
axes.tick_params(
axis="x", bottom=False, labelbottom=False, which="both"
)
axes.set_xlabel(None)
return render(**settings)
def gaussian(x, mu, sigma):
x = np.asarray(x, dtype=float)
return np.exp(-0.5 * ((x - mu) / sigma) ** 2)
def make_red_reflectance(start=300, stop=1000, step=10):
wavelengths = np.arange(start, stop + step, step)
values = np.clip(
0.03 + 0.82 * gaussian(wavelengths, mu=660, sigma=40), 0.0, 1.0
)
return {int(w): float(v) for w, v in zip(wavelengths, values)}
if __name__ == "__main__":
sd = SpectralDistribution(make_red_reflectance(), name="Red Reflectance")
plot_single_sd(sd, show=False)
plt.savefig("/tmp/pr1401_baseline.png", dpi=120, bbox_inches="tight")
plt.close()
plot_single_sd(sd, show_visible_spectrum=True, show=False)
plt.savefig("/tmp/pr1401_with_strip.png", dpi=120, bbox_inches="tight")
plt.close()
sd_visible = SpectralDistribution(
SDS_ILLUMINANTS["D65"].copy().normalise().align(
SDS_ILLUMINANTS["D65"].shape
),
name="D65 (visible domain)",
)
plot_single_sd(sd_visible, show_visible_spectrum=True, show=False)
plt.savefig("/tmp/pr1401_visible_only.png", dpi=120, bbox_inches="tight")
plt.close()
plot_visible_spectrum(show=False)
plt.savefig("/tmp/pr1401_visible_spectrum.png", dpi=120,
bbox_inches="tight")
plt.close()
sd_extended = SpectralDistribution(
make_red_reflectance(start=300, stop=1000, step=10),
name="Red (300-1000 nm)",
)
sd_visible_only = SpectralDistribution(
{
int(w): float(0.4 + 0.4 * gaussian(w, mu=480, sigma=30))
for w in np.arange(400, 701, 10)
},
name="Blue (400-700 nm)",
)
plot_multi_sds(
[sd_extended, sd_visible_only],
plot_kwargs={"linewidth": 2},
show_visible_spectrum=True,
show=False,
)
plt.savefig("/tmp/pr1401_multi_sds.png", dpi=120, bbox_inches="tight")
plt.close()
print("wrote:")
print(" /tmp/pr1401_baseline.png")
print(" /tmp/pr1401_with_strip.png")
print(" /tmp/pr1401_visible_only.png")
print(" /tmp/pr1401_visible_spectrum.png")
print(" /tmp/pr1401_multi_sds.png")
|
896d106 to
0e3187d
Compare
Contributor
Author
|
@KelSolaar Thanks for the feedback. I agree it is much better to e.g. use a new axes for the visible spectrum ! Never saw make_axes_locatable(axes) before, thanks for the trick :) I made one adjustment compared to your implementation. I set pad in make_axes_locatable(axes) to 0.08 to have some distance between the plot and the spectrum. I find it confusing otherwise, see below examples. This also avoids that the spectrum bar has an outline that might "spill over" into the plot. See here
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Summary
Add a boolean parameter to plotting functions with wavelength on x-axis called wavelengths_strip to add the visible spectrum to the xaxis. If wavelengths go beyond visible interval (UV or IR), those areas are shown for visual cleanliness with a striped gray filling.
See wavelengths_strip_plots.html for how plots would look like with the argument activated.
As you can see e.g. for the very first plot (below minimal code example), the hues of the wavelengths strip might be slightly different to that of the spectrum itself as I wanted to desaturate the RGB colours of the wavelengths strip a bit. This can be easily changed but I felt that this made the plots visually too heavy. Happy to revert if wanted.
Preflight
Code Style and Quality
colour,colour.models.Documentation