Simulation of color change over time requires a structural color physics model. In the general case of scattering colorants this is quite a challenge. First we need to know the optical absorption (K) and scattering (S) properties of all paints in the palette. Furthermore we need to know the distribution of all paints. Finally this stratigraphic model data then needs to be fed into the complicated mathematical machinery of Kubelka-Munk equations.
Pff, all together a rather complicated problem. We will deal with this later. Fortunately, the modeling of transparent (i.e. non-scattering) colors such as inks and dyes is much simpler. An object can be thought of as a background with a number of transparent colored layers on top. These layers act like filters. Their ordering does not matter. Such colors are called subtractive. The colors of a color photograph are the result of the combined filter action of specific amounts of cyan, magenta and yellow.
Creating a stratigraphic model
In a typical situation we are confronted with a drawing or photograph and we do not have information about the palette of inks or the set of dyes that constitute the primaries. Also we do not know how these colorants are distributed. We magically need to separate the optical properties of the colorants, and come up with a concentration or thickness map for each colorant layer.
Let's load the kaleidoscope()
test image, and learn how we can separate the cyan, magenta and yellow primary colors.
from inktime import kaleidoscope
test_img = kaleidoscope()
A first step in the construction of a stratigraphic model is the detection of primary colors as the corners of the normalized optical density palette histogram.
from inktime import palette_plot, primaries, optical_density, transmission
palette_plot(test_img);
Similar colors in the NOD map represent a single colorant mixing ratio. The coordinates of the corners (cyan, magenta and yellow) of the triangle in the NOD histogram are the ink primaries that we seek. They can be computed with the primaries()
function.
M = primaries(test_img)
M
The rows in this matrix contain the normalized optical densities for the three primary inks. Now we can continue and construct a three layer stratigraphic model. In other words, we will compute separate transmission images for the three inks that were used.
The essential idea for this computation is work in optical density space because quantities then scale linearly with concentrations. This allows us make use of the inverse transformation matrix of the primaries. The inverse matrix can be calculated with the numpy function numpy.linalg.inv()
.
M_inv = np.linalg.inv(M)
M_inv
If we now compute optical density image, we can multiply with the inverse transformation matrix and get the concentrations image.
od_img = optical_density(test_img, normalize=False)
# temporarily flatten image into list
img_shape = od_img.shape
od_list = od_img.reshape(-1, 3)
# matrix multiplication
conc_list = od_list @ M_inv
# unflatten list into image
concs_img = conc_list.reshape(img_shape)
# seperate images for cyan, magenta and yellow
cyan_conc_img, magenta_conc_img, yellow_conc_img = concs_img.transpose(2, 0, 1)
Now we can compute the transmission images from concentration images and their corresponding primaries and plot them.
T_cyan = transmission(cyan_conc_img, M[0])
T_cyan = np.clip(T_cyan, a_min=0, a_max=1)
T_magenta = transmission(magenta_conc_img, M[1])
T_magenta = np.clip(T_magenta, a_min=0, a_max=1)
T_yellow = transmission(yellow_conc_img, M[2])
T_yellow = np.clip(T_yellow, a_min=0, a_max=1)
fig, [ax0, ax1, ax2] = plt.subplots(ncols=3, figsize=[9, 3], sharex=True, sharey=True)
ax0.imshow(T_cyan)
ax0.set_title(f'Cyan layer')
ax1.imshow(T_magenta)
ax1.set_title('Magenta layer')
ax2.imshow(T_yellow);
ax2.set_title('Yellow layer');
That is it! We have separated the original test image into a stratigraphic model of three separate ink layers. Let's check if we can reconstruct the original test image by overlaying (multiplying transmissions) of the three ink layers.
cyan_x_magenta_x_yellow = T_cyan * T_magenta * T_yellow
fig, [ax0, ax1] = plt.subplots(ncols=2, figsize=[9, 5], sharex=True, sharey=True)
ax0.imshow(test_img)
ax1.imshow(cyan_x_magenta_x_yellow)
ax0.set_title('Original')
ax1.set_title('Reconstruction');
These computations above are implemented in the Stratigraphy()
class.
from inktime import Stratigraphy
s = Stratigraphy(test_img)
fig, ax = plt.subplots()
ax.imshow(s.T_cyan);
We are now in a position to develop a spectral model and implement chemical kinetics of fading. To be continued...