Tobia Cavalli

Calculating the CIE color of Chlorophyll A and B using Python

· Tobia Cavalli

Summary

This post provides a step-by-step guide on how to calculate CIE colors from UV-Vis spectra using Python. We’ll look at the CIE colorimetry system and the underlying principles behind color perception.

The core of the post focuses on practical implementation, guiding you through the Python code that performs the necessary calculations. If you’re primarily interested in the coding aspect, you can skip directly to the Python code section.

Table of Contents

What is color?

Color isn’t just a thing; it’s a feeling. It’s how our brains interpret light, and it’s pretty subjective. Think about it: the same light can look different to different people, even under the same conditions. From a pure physical and biological point of view, it is a phenomenon arising from the human eye’s interpretation of light within the visible spectrum (380–780 nm). It’s not a physical property of light itself, but a sensation created by the brain’s processing of visual information.

Factors like lighting conditions, color vision deficiencies, and individual variations can influence how we perceive color. This subjectivity makes traditional scientific measurements, like spectroscopy, insufficient for directly quantifying color perception, as they focus only on the physical properties of light without considering their corresponding perceptual aspects.

Psychophysics bridges this gap by quantifying the relationship between physical stimuli and the sensations they generate. As a field, it is primarily concerned with how humans perceive and interpret sensory inputs like colors, sounds, and textures. Guy Brindley’s work in 1970 was a significant contribution to this field.1 2

Brindley introduced the concept of observations to describe perceptual states during psychophysical tasks. He categorized observations into two types:

  1. Class A observations: When two physically different stimuli are perceived as identical. Despite their distinct physical properties, the observer cannot distinguish between them.
  2. Class B observations: All other cases where stimuli are distinguishable.

Color is a prime example of Class A observations. Two lights with different physical compositions can appear identical under certain conditions. For instance, a pure red light and a mixture of red, green, and blue light can be perceived as the same shade of red if viewed in low light or by someone with limited color sensitivity.

The CIE colorimetry system

Given the subjectivity of color perception, how can we objectively measure and communicate colors? The CIE colorimetry system provides a solution by using color matching experiments to establish a statistical representation of human color vision, which provides a standardized method for relating spectral light distributions to perceived colors.

In these experiments, observers compare two stimuli under controlled conditions. If they appear identical despite their physical differences, they are considered perceptually equivalent. The CIE system, developed by the Commission Internationale d’Éclairage, quantifies the relationship between wavelength distributions and perceived colors.3 4

CIE color matching experiment
CIE color matching experiment. Adapted from literature.

The CIE 1931 model, introduced in 1931, uses additive color mixing based on Color Matching Functions (CMFs). These functions represent the spectral sensitivity of the three types of cone cells in the human eye, denoted as \(\bar{x}\), \(\bar{y}\), and \(\bar{z}\).

CMFs are derived from standardized experiments involving foveal vision, specific field sizes, dark surroundings, and average observations from multiple individuals, providing a statistical measure of color receptor sensitivity.

By convolution of the sample spectrum \(M(\lambda)\) with the CMFs, we calculate tristimulus values \(X\), \(Y\), and \(Z\). These values represent the amounts of the three primary colors (red, green, and blue) required to match the given color.

$$ \begin{align} X &= \int_{380}^{780} M(\lambda) \bar{x}(\lambda) \, d\lambda \tag{1}\\ Y &= \int_{380}^{780} M(\lambda) \bar{y}(\lambda) \, d\lambda \tag{2}\\ Z &= \int_{380}^{780} M(\lambda) \bar{z}(\lambda) \, d\lambda \tag{3} \end{align} $$

The tristimulus values define a point in a three-dimensional color space. However, for practical purposes, this space is often reduced to two dimensions using the \(x\) and \(y\) chromaticity coordinates:

$$ \begin{align} x &= \frac{X}{X + Y + Z} \tag{4}\\ y &= \frac{Y}{X + Y + Z} \tag{5} \end{align} $$

The \(x\) and \(y\) coordinates uniquely specify a color within the CIE color space, enabling a standardized and objective representation of color perception. This system has been (and is!) used in various industries, including printing, photography, lighting design, and digital media, where accurate color reproduction and communication are essential.

Python code

Python libraries for color analysis

Before diving into the code, let’s import the necessary Python libraries:

If you’d like to replicate this analysis on your machine, you can install these libraries using the following pip command in your terminal or PowerShell (assuming you don’t have them installed already):

1pip install numpy pandas matplotlib colour-science

With the libraries installed, we can now import them into our script or Jupyter notebook:

1import colour as cl
2import matplotlib.pyplot as plt
3import matplotlib.ticker as tck
4import numpy as np
5import pandas as pd
6
7# Disable some annoying warnings from colour library
8cl.utilities.filter_warnings(colour_usage_warnings=True)

Plot settings

I want the plot aesthetics to match the style of the blog, and therefore I’m adding the following settings to customize the appearance of the graphics I am going to generate. If you’re following along on your own machine (e.g., using Jupyter notebooks), you can skip this step.

Here, I am importing seaborn, a powerful library for statistical data visualization. I won’t be using its statistical functions, but I prefer its default presets over the ones provided by matplotlib. After importing the library, I am setting:

In addition, I am importing golden_ratio from the scipy library, which is equivalent to defining a variable containing the value 1.618. This is a mathematical constant also known as the divine proportion, which I will use to set the aspect ratio of the plots, specifically the ratio between the shorter and longer axes. It is absolutely not necessary, but I find that it helps create a more aesthetically pleasing visual balance.

 9import seaborn as sns
10from scipy.constants import golden_ratio
11
12# Set seaborn defaults
13sns.set_context("notebook")
14sns.set_style("ticks")
15sns.set_palette("colorblind", color_codes=True)
16
17# Use white color for elements that are typically black
18plt.style.use("dark_background")
19
20# Remove background from figures and axes
21plt.rcParams["figure.facecolor"] = "none"
22plt.rcParams["axes.facecolor"] = "none"
23
24# Default figure size to use throughout
25figure_size = (7, 7 / golden_ratio)

Plotting the CIE (2°) color space

Now that we have our tools in place, let’s create our first color plot. One thing that I like very much about the colour-science library is that it provides handy functions for plotting color spaces according to different colorimetric systems.

We’re going to use the plot_chromaticity_diagram_CIE1931 function to show the CIE color space for a 2° observer. This is like a map of all the colors humans can see within a small area of the eye. The CIE color space is a triangle where the corners represent pure red, green, and blue. Any color can be plotted somewhere inside this triangle by mixing different amounts of these primary colors.

By plotting this color space, we can get a better feel for the colors we can perceive and compare different colors to see how similar or different they are. We’ll use it later to analyze colors and create a color model.

26# Instantiate figure and axes
27fig, ax = plt.subplots(1, 1, figsize=(7, 7))
28
29# Plot CIE color space for a 2°
30cl.plotting.plot_chromaticity_diagram_CIE1931(
31    cmfs="CIE 1931 2 Degree Standard Observer",
32    axes=ax,
33    show=False,
34    title=None,
35    spectral_locus_colours="white",
36)
37
38# Axes labels
39ax.set_xlabel("x (2°)")
40ax.set_ylabel("y (2°)")
41
42# Axes limits
43ax.set_xlim(-0.1, 0.85)
44ax.set_ylim(-0.1, 0.95)
45
46# Ticks separation
47ax.xaxis.set_major_locator(tck.MultipleLocator(0.1))
48ax.xaxis.set_minor_locator(tck.MultipleLocator(0.01))
49ax.yaxis.set_major_locator(tck.MultipleLocator(0.1))
50ax.yaxis.set_minor_locator(tck.MultipleLocator(0.01))
51
52# Grid settings
53ax.grid(which="major", axis="both", linestyle="--", color="gray", alpha=0.8)
54
55# Padding adjustment
56plt.tight_layout()
57
58plt.show()
CIE color space for a 2° observer.
CIE color space for a 2° observer.

Importing and scaling data

Let’s see if we can calculate the actual color of some interesting molecules! In this example, we’ll determine the colors of Chlorophyll A and Chlorophyll B in solution, two pigments essential for photosynthesis in plants. The data used here consists of pre-recorded UV-Vis spectra for Chlorophyll A and B in both 70% and 90% acetone solutions, obtained from a scientific publication.5 You can download the .csv file containing this data here.

59column_names = ["lambda", "chl_a_70", "chl_a_90", "chl_b_70", "chl_b_90"]
60measured_samples = pd.read_csv(
61    "chlorophyll_uv_vis.csv", names=column_names, header=0, index_col="lambda"
62)
63measured_samples
chl_a_70chl_a_90chl_b_70chl_b_90
lambda
350.026132.025552.029529.028301.0
350.426251.025804.029574.028114.0
350.826666.026083.029350.027946.0
351.226703.026227.029084.027660.0
351.626834.026473.028991.027632.0
748.4-63.0-269.0-15.0-159.0
748.8-80.0-289.0-2.0-141.0
749.2-95.0-283.0-13.0-157.0
749.6-92.0-292.0-2.0-150.0
750.082.0-198.069.0-172.0

1001 rows × 4 columns

The imported data represents the absorbance (\(A\)) of each chlorophyll type, which is measured at regular intervals of light (lambda). In simpler terms, absorbance indicates the amount of light absorbed by a specific molecule at a particular wavelength. Let’s take a quick look at these spectra:

60fig, ax = plt.subplots(1, 1, figsize=figure_size)
61
62# Define the labels for the plot's legend
63chl_labels = [
64    "Chlorophyll A (70 % Acetone)",
65    "Chlorophyll A (90 % Acetone)",
66    "Chlorophyll B (70 % Acetone)",
67    "Chlorophyll B (90 % Acetone)",
68]
69
70# Iterate over dataframe and plot each spectrum
71for col, sample_label in zip(measured_samples.columns, chl_labels):
72    ax.plot(measured_samples.index,
73            measured_samples[col],
74            label=sample_label)
75
76# Ticks separation
77ax.xaxis.set_major_locator(tck.MultipleLocator(50))
78ax.xaxis.set_minor_locator(tck.MultipleLocator(10))
79
80# Axes labels
81ax.set_xlabel("Wavelength [nm]")
82ax.set_ylabel("Absorbance [a.u.]")
83
84# Display legend
85ax.legend()
86
87plt.show()
UV-Vis absorption spectra of Chlorophyll A and B in 70% and 90 % acetone solutions.
UV-Vis absorption spectra of Chlorophyll A and B in 70% and 90 % acetone solutions.

From this initial inspection, we can see that Chlorophyll A absorbs light primarily in the blue and red regions of the spectrum, corresponding to the peaks at around 430 nm and 670 nm. Chlorophyll B also absorbs light in the blue and red regions, with peaks near 460 nm and 650 nm, but its absorption peak in the blue region is slightly shifted towards the green compared to chlorophyll A.

Additionally, the concentration of acetone in the solution seems to influence the peak intensities. While the overall spectral shapes remain consistent for each chlorophyll type, the intensity variations likely affect their perceived colors.

The current spectra also have significantly different absolute absorbance values. Therefore, some data pre-processing is required before calculating color. To enable meaningful comparisons, we need to normalize the data. This normalization will change the original absorbance values, which might be a disadvantage for quantitative analyses (e.g., concentration determination). However, since our focus here is qualitative (comparing spectra), these intensity scale differences would not provide an effective comparison.

The simplest approach for this is to normalize the spectra, transforming them into a range between 0 and 1 while preserving their overall shapes. We’ll use a technique called range scaling (also known as MinMax scaling), described by the following equation:6

$$ x_{scaled} = \frac{x - x_{min}}{x_{max} - x_{min}} \tag{6} $$

Here, \(x\) represents a single measured value, \(x_{min}\)​ and \(x_{max}\)​ represent the minimum and maximum values within the spectrum, respectively, and \(x_{scaled}\) represents the resulting normalized value.

While the scikit-learn library offers built-in functions for this task (see here), I prefer to avoid introducing unnecessary dependencies for a simple function. Therefore, we’ll define a custom function named normalize to perform the MinMax scaling:

86def normalize(x: pd.Series | np.ndarray) -> pd.Series | np.ndarray:
87    """MinMax scaling from 0 to 1
88
89    Args:
90        x (pd.Series | np.ndarray): series or array to normalize
91
92    Returns:
93        pd.Series | np.ndarray: series or array of normalized values
94    """
95    x_scaled = (x - x.min()) / (x.max() - x.min())
96    return x_scaled

This function takes a pandas Series or NumPy array (x) as input and performs the MinMax scaling operation. It subtracts the minimum value (x.min()) from each element in x and then divides the result by the difference between the maximum (x.max()) and minimum values. This ensures all values in the output (x_scaled) fall within the range of 0 to 1.

Now, we can leverage vectorization to efficiently apply the normalize function to the entire DataFrame containing the absorbance values. This will create a new DataFrame with the normalized values, stored in the variable abs_norm:

97abs_norm = normalize(measured_samples)
98abs_norm
chl_a_70chl_a_90chl_b_70chl_b_90
lambda
350.00.3243980.2854050.2275560.207087
350.40.3258690.2881880.2279030.205727
350.80.3310010.2912690.2261790.204505
351.20.3314580.2928590.2241330.202425
351.60.3330780.2955760.2234170.202221
748.40.0005070.0002540.0002620.000095
748.80.0002970.0000330.0003620.000225
749.20.0001110.0000990.0002770.000109
749.60.0001480.0000000.0003620.000160
750.00.0023000.0010380.0009080.000000

1001 rows × 4 columns

The resulting DataFrame (abs_norm) now has absorbance values between 0 and 1, which can be verified by plotting them:

 99fig, ax = plt.subplots(1, 1, figsize=figure_size)
100
101for col, sample_label in zip(abs_norm.columns, chl_labels):
102    ax.plot(abs_norm.index,
103            normalize(abs_norm[col]),
104            label=f"{sample_label} norm")
105
106# Ticks separation
107ax.xaxis.set_major_locator(tck.MultipleLocator(50))
108ax.xaxis.set_minor_locator(tck.MultipleLocator(10))
109ax.yaxis.set_major_locator(tck.MultipleLocator(0.1))
110ax.yaxis.set_minor_locator(tck.MultipleLocator(0.025))
111
112# Axes labels
113ax.set_xlabel("Wavelength [nm]")
114ax.set_ylabel("Absorbance [a.u.]")
115
116# Display legend
117ax.legend()
118
119plt.show()
Normalized UV-Vis absorption spectra of Chlorophyll A and B in 70% and 90 % acetone solutions.
Normalized UV-Vis absorption spectra of Chlorophyll A and B in 70% and 90 % acetone solutions.

Converting absorbance to transmittance

Before calculating the colors from our spectra, it’s important to remember that these spectra represent the light absorbed by the molecules. To determine the color we actually perceive, we need to convert absorbance to transmittance. Transmittance represents the light that passes through the molecules and reaches our eyes.

The conversion from absorbance (\(A\)) to transmittance (\(T\)) is straightforward and follows this equation:

$$ T = 10^{(2 - A)} \tag{7} $$

Using the normalized absorbance values, we can easily perform this conversion by defining the abs_to_trans function:

120def abs_to_trans(A: pd.Series | np.ndarray) -> pd.Series | np.ndarray:
121    """Convert absorbance to transmittance
122
123    Args:
124        A (pd.Series | np.ndarray): series or array of absorbance values
125
126    Returns:
127        pd.Series | np.ndarray: series or array of transmittance values
128    """
129    T = 10 ** (2 - A)
130    return T

This function takes a Series or array of absorbance values as input and returns a Series or array of corresponding transmittance values.

Now, we can apply this function to the entire abs_norm DataFrame to efficiently calculate the transmittance for each spectrum. This will create a new DataFrame (transm_norm) containing the normalized transmittance values for each chlorophyll sample.

131transm_norm = abs_to_trans(abs_norm)
132transm_norm
chl_a_70chl_a_90chl_b_70chl_b_90
lambda
350.047.38077551.83163759.21662762.074481
350.447.22052051.50056559.16944062.269182
350.846.66588051.13648859.40469862.444622
351.246.61674750.94958559.68528162.744426
351.646.44320750.63187159.78369262.773854
748.499.88333999.94153299.93978899.978231
748.899.93169499.99237299.91677599.948098
749.299.97438099.97711799.93624799.974883
749.699.965841100.00000099.91677599.963164
750.099.47184799.76125999.791184100.000000

1001 rows × 4 columns

Let’s verify that the conversion to transmittance worked as expected by plotting the new values:

133fig, ax = plt.subplots(1, 1, figsize=figure_size)
134
135for col, sample_label in zip(transm_norm.columns, chl_labels):
136    ax.plot(transm_norm.index, transm_norm[col], label=f"{sample_label}")
137
138# Ticks separation
139ax.xaxis.set_major_locator(tck.MultipleLocator(50))
140ax.xaxis.set_minor_locator(tck.MultipleLocator(10))
141ax.yaxis.set_major_locator(tck.MultipleLocator(10))
142ax.yaxis.set_minor_locator(tck.MultipleLocator(5))
143
144# Axes labels
145ax.set_xlabel("Wavelength [nm]")
146ax.set_ylabel("Transmittance [%]")
147
148# Display legend
149ax.legend()
150
151plt.show()
Normalized UV-Vis transmission spectra of Chlorophyll A and B in 70% and 90 % acetone solutions.
Normalized UV-Vis transmission spectra of Chlorophyll A and B in 70% and 90 % acetone solutions.

As expected, the normalized transmittance values range between 0 and 1 (or 0% and 100% transmittance). We can see the inverse relationship between absorbance and transmittance: higher absorbance corresponds to lower transmittance, and vice versa. For a more direct comparison, we can visualize both absorbance and transmittance in a single plot.

152fig, ax = plt.subplots(2, 1, sharex=True, figsize=figure_size)
153
154# Iterate over absorbance dataframe
155for col, sample_label in zip(abs_norm.columns, chl_labels):
156    ax[0].plot(abs_norm.index, normalize(abs_norm[col]), label=f"{sample_label}")
157
158# Iterate over transmittance dataframe
159for col, sample_label in zip(transm_norm.columns, chl_labels):
160    ax[1].plot(transm_norm.index, transm_norm[col], label=f"{sample_label}")
161
162# Axes labels
163ax[0].set_ylabel("Absorbance [a.u.]")
164
165ax[1].set_xlabel("Wavelength [nm]")
166ax[1].set_ylabel("Transmittance [%]")
167
168# Ticks separation
169for axis in ax:
170    axis.xaxis.set_major_locator(tck.MultipleLocator(50))
171    axis.xaxis.set_minor_locator(tck.MultipleLocator(10))
172    # Display legend
173    axis.legend()
174
175ax[0].yaxis.set_major_locator(tck.MultipleLocator(0.1))
176ax[0].yaxis.set_minor_locator(tck.MultipleLocator(0.025))
177ax[1].yaxis.set_major_locator(tck.MultipleLocator(10))
178ax[1].yaxis.set_minor_locator(tck.MultipleLocator(5))
179
180plt.show()
Normalized UV-Vis absorption and transmission spectra of Chlorophyll A and B in 70% and 90 % acetone solutions.
Normalized UV-Vis absorption and transmission spectra of Chlorophyll A and B in 70% and 90 % acetone solutions.

Calculating the CIE colors

Now that we have the normalized absorbance and transmittance spectra, we can finally calculate the corresponding CIE colors. To do this, we need to do the following:

This code iterates over each spectrum, calculates the CIE \(XYZ\) coordinates, converts them to \(xy\) coordinates, and stores the results in two lists: chl_abs_clr for the absorbance-based colors and chl_transm_clr for the transmittance-based colors. Finally, the results are merged into a single DataFrame for easier analysis.

181# Define color matching functions
182cmfs = cl.MSDS_CMFS["cie_2_1931"]
183
184# Define illuminant
185illuminant = cl.SDS_ILLUMINANTS["D65"]
186
187chl_abs_clr = []
188
189# Iterate over each normalized absorbance spectrum
190for col in abs_norm.columns:
191    # Initialize spectral distribution
192    sd = cl.SpectralDistribution(data=abs_norm[col])
193
194    # Interpolate sd to conform to the CIE specifications
195    sd = sd.interpolate(cl.SpectralShape(350, 750, 1))
196
197    # Calculate CIE XYZ coordinates from spectral distribution
198    cie_XYZ = cl.sd_to_XYZ(sd, cmfs, illuminant)
199
200    # Convert to CIE xy coordinates
201    cie_xy = cl.XYZ_to_xy(cie_XYZ)
202
203    # Append the results to the list of sample colors
204    chl_abs_clr.append(
205        {"sample": col, "x_A": np.round(cie_xy[0], 4), "y_A": np.round(cie_xy[1], 4)}
206    )
207
208chl_transm_clr = []
209
210# Iterate over each normalized transmittance spectrum
211for col in transm_norm.columns:
212    # Initialize spectral distribution
213    sd = cl.SpectralDistribution(data=transm_norm[col])
214    sd = sd.interpolate(cl.SpectralShape(350, 750, 1))
215
216    # Calculate CIE XYZ coordinates from spectral distribution
217    cie_XYZ = cl.sd_to_XYZ(sd, cmfs, illuminant)
218
219    # Convert to CIE xy coordinates
220    cie_xy = cl.XYZ_to_xy(cie_XYZ)
221
222    # Append the results to the list of sample colors
223    chl_transm_clr.append(
224        {"sample": col, "x_T": np.round(cie_xy[0], 4), "y_T": np.round(cie_xy[1], 4)}
225    )
226
227# Convert dictionaries to dataframes and join them together
228colors = pd.merge(pd.DataFrame(chl_transm_clr), pd.DataFrame(chl_abs_clr))
229colors
samplex_Ty_Tx_Ay_A
0chl_a_700.30460.37770.29660.1330
1chl_a_900.30630.37210.29480.1345
2chl_b_700.35580.43650.20360.0930
3chl_b_900.35380.43340.20480.0904

Visualizing colors on the CIE color space

Now that we have the \(x\) and \(y\) values for both absorbed and transmitted colors, let’s plot them on the CIE 1931 color space. This code first plots the CIE color space as we have seen at the beginning. Then, it iterates through the colors DataFrame and plots the absorbed colors (based on the x_A and y_A columns) as scattered points on the color space with corresponding labels, colors, and edge colors.

230# Instantiate figure and axes
231fig, ax = plt.subplots(1, 1, figsize=figure_size)
232
233# Plot CIE color space for a 2°
234cl.plotting.plot_chromaticity_diagram_CIE1931(
235    cmfs="CIE 1931 2 Degree Standard Observer",
236    axes=ax,
237    show=False,
238    title=None,
239    spectral_locus_colours="white",
240)
241
242color_list = ["r", "g", "b", "m"]
243
244for i, c in enumerate(color_list):
245    ax.scatter(
246        colors["x_A"][i],
247        colors["y_A"][i],
248        label=chl_labels[i],
249        color=c,
250        edgecolors="k",
251        alpha=0.8,
252    )
253
254# Axes labels
255ax.set_xlabel("x (2°)")
256ax.set_ylabel("y (2°)")
257
258# Axes limits
259ax.set_xlim(0.18, 0.32)
260ax.set_ylim(0.06, 0.16)
261
262# Ticks separation
263ax.xaxis.set_major_locator(tck.MultipleLocator(0.01))
264ax.xaxis.set_minor_locator(tck.MultipleLocator(0.005))
265ax.yaxis.set_major_locator(tck.MultipleLocator(0.01))
266ax.yaxis.set_minor_locator(tck.MultipleLocator(0.005))
267
268# Grid settings
269ax.grid(which="major", axis="both", linestyle="--", color="gray", alpha=0.8)
270
271# Display legend
272ax.legend()
273
274# Adjust plot padding
275plt.tight_layout()
276
277plt.show()
CIE color space for a 2° observer and calculated absorbed colors for Chlorophyll A and B in 70 % and 90 % acetone with D65 illuminant.
CIE color space for a 2° observer and calculated absorbed colors for Chlorophyll A and B in 70 % and 90 % acetone with D65 illuminant.

To visualize the transmitted colors, we can reuse most of the existing code and simply modify the data source:

278# Instantiate figure and axes
279fig, ax = plt.subplots(1, 1, figsize=figure_size)
280
281# Plot CIE color space for a 2°
282cl.plotting.plot_chromaticity_diagram_CIE1931(
283    cmfs="CIE 1931 2 Degree Standard Observer",
284    axes=ax,
285    show=False,
286    title=None,
287    spectral_locus_colours="white",
288)
289
290color_list = ["r", "g", "b", "m"]
291
292for i, c in enumerate(color_list):
293    ax.scatter(
294        colors["x_T"][i],
295        colors["y_T"][i],
296        label=chl_labels[i],
297        color=c,
298        edgecolors="k",
299        alpha=0.8,
300    )
301
302# Axes labels
303ax.set_xlabel("x (2°)")
304ax.set_ylabel("y (2°)")
305
306# Axes limits
307ax.set_xlim(0.25, 0.4)
308ax.set_ylim(0.35, 0.45)
309
310# Ticks separation
311ax.xaxis.set_major_locator(tck.MultipleLocator(0.01))
312ax.xaxis.set_minor_locator(tck.MultipleLocator(0.005))
313ax.yaxis.set_major_locator(tck.MultipleLocator(0.01))
314ax.yaxis.set_minor_locator(tck.MultipleLocator(0.005))
315
316# Grid settings
317ax.grid(which="major", axis="both", linestyle="--", color="gray", alpha=0.8)
318
319# Display legend
320ax.legend(labelcolor="k", edgecolor="k")
321
322# Adjust plot padding
323plt.tight_layout()
324
325plt.show()
CIE color space for a 2° observer and calculated transmitted colors for Chlorophyll A and B in 70 % and 90 % acetone with D65 illuminant.
CIE color space for a 2° observer and calculated transmitted colors for Chlorophyll A and B in 70 % and 90 % acetone with D65 illuminant.

From the CIE color space plots, we can observe some key differences between the perceived colors of Chlorophyll A and B under both absorbance and transmittance conditions. Chlorophyll A primarily absorbs light in the red and blue regions of the spectrum, reflecting green-yellow light. Chlorophyll B absorbs a greater proportion of blue light than Chlorophyll A, reflecting a more yellowish hue.

Conclusion

While both chlorophyll A and B are primarily responsible for the green color of plants, their slight differences in absorption spectra can lead to subtle variations in the exact shade of green. Despite being closely related pigments, they show distinct spectral absorption profiles, which ultimately influence their perceived colors.

Since both chlorophyll A and B absorb light in the blue and red regions, the light that is transmitted is primarily in the green region, which is why leaves appear green to our eyes. However, due to the slight difference in absorption peaks, Chlorophyll A tends to give a more deep green or olive green color, while Chlorophyll B gives a slightly lighter green or yellowish-green hue.


  1. Kingdom, F. A. A.; Prins, N. Psychophysics: A Practical Introduction, 2nd ed.; Academic Press: Amsterdam, NL, 2016; pp. 19–20. https://doi.org/10.1016/C2012-0-01278-1↩︎

  2. Schanda, J. Colorimetry: Understanding the CIE System; John Wiley & Sons: Hoboken, NJ, USA, 2007; pp. 56–59. https://doi.org/10.1002/9780470175637↩︎

  3. Guild, J. The Colorimetric Properties of the Spectrum. Phil. Trans. R. Soc. Lond. A 1931, 230 (681-693), 149–187. https://doi.org/10.1098/rsta.1932.0005↩︎

  4. Smith, T.; Guild, J. The C.I.E. Colorimetric Standards and Their Use. Trans. Opt. Soc. 1931, 33 (3), 73–134. https://doi.org/10.1088/1475-4878/33/3/301↩︎

  5. Chazaux, M.; Schiphorst, C.; Lazzari, G.; Caffarri, S. Precise Estimation of Chlorophyll a , b and Carotenoid Content by Deconvolution of the Absorption Spectrum and New Simultaneous Equations for Chl Determination. Plant J. 2022, 109 (6), 1630–1648. https://doi.org/10.1111/tpj.15643↩︎

  6. Otto, M. Chemometrics: Statistics and Computer Application in Analytical Chemistry, 4th ed.; Wiley-VCH Verlag: Weinheim, DE, 2024; pp. 137–140. https://doi.org/10.1002/9783527843800↩︎

Tags for this post

#colorimetry #python #spectroscopy

Did you like my work?

The content I share on this website is free from ads and free to read for everyone. I do not ask for donations or contributions, but if you like what I wrote or you found a mistake, consider letting me know!