colorSpec User Guide

Glenn Davis


colorSpec is an R package providing an S3 class with methods for color spectra. It supports the standard calculations with spectral properties of light sources, materials, cameras, eyes, scanners, etc.. And it works well with the more general action spectra. Many ideas are taken from packages hsdar [11], hyperSpec [3], pavo [12], photobiology [2], and zoo [20].

Some features: Some non-features:

1 Spectrum Types

Pick up any book on color physics (e.g. [19], [14], [13], or [8]) or color management (e.g. [5]) and you will see plots of many spectra. Let’s start with a simple division of these spectra into 4 basic types:

For the infinite-dimensional spaces, the interval [380,780] is used for illustration; in specific calculations it can vary. Note that of the 4 vector spaces, only \(L^*\) and \(M\) are isomorphic, but we take the mathematical point of view that although they are isomorphic, they are not the same. For a proof of this isomorphism, see Appendix D. Multiplication operators are the infinite-dimensional generalization of diagonal matrices. For more background on this functional analysis, see [18] and [9].

For the finite-dimensional spaces, it takes the full sequence of wavelengths and not just the endpoints. The wavelength sequence is typically regular not always. In this case all 4 vector spaces are isomorphic (since they are the same dimension), but we still take the mathematical point of view that they are not the same space.

The last type = 'responsivity.material' is perhaps the least common. There is an example in [5] (Figure 10.11a, page 141) of a scanner, where the 3 spectra are called the effective spectral responsivities. There is also a standard scanner from SMPTE, see [16].

Every colorSpec object has one of these types, but it is not stored with the object. The object stores a quantity which then determines the type; see the next section for more discussion. A synonym for type might be space, but this could be confused with color space.

colorSpec does not actually use the finite-dimensional representations in Table 1.1; the organization is flexible. And it would not be efficient memory use to store a diagonal matrix as such. For discussion of the organization, see section 4.

Given 2 finite-dimensional spectra of types 'light' and 'responsivity.light' the response (a real number) is their dot product multiplied by the step between wavelengths.

All materials in this document are non-fluorescent; i.e. the outgoing photons reflected (or transmitted) only come from incoming photons of the same wavelength. A transparent material transmits an incoming light spectrum and a new spectrum emerges on the other side. If the material is not fluorescent, the outgoing spectrum is the same as the incoming, except there is a reduction of power that depends only on the wavelength (and the material). If the light power were divided into N bins, the transmitted power spectrum would be a diagonal NxN matrix times the incoming spectrum.

A reflectance spectrum is mathematically the same as a transmittance spectrum, except we compare the outgoing light spectrum to that of a perfect reflecting diffuser. Such a material does not exist, like many concepts in physics, but it is a very useful idealization.

2 Spectrum Quantities

Unfortunately there are two common metrics for quantifying spectra with type='light' - power of photons and number of photons/sec. The former – radiometric - is the oldest, being used in the 19th century. The latter – actinometric - was not used until the 20th century (after the modern concept of photons was proposed in 1905). So colorimetry uses radiometric quantities by convention and actinometric ones are converted to radiometric automatically for calculations. The conversion is easy; see the function radiometric(), [14] pp. 93-94, and [13] p. 12.

Similarly, 'responsivity.light' can be radiometric (e.g. the CIE color matching functions) or actinometric (e.g. the quantum efficiency of a CMOS sensor). These actinometric spectra are also converted to radiometric on the fly.

For responsivity we distinguish between 3 types of response: electrical, neural, and action. In colorSpec this 3-way distinction is only used in two places: in the y label of the spectrum plot(), and to determine default adaption methods in calibrate(). Note that the action response is really a grab-bag for responses that are neither electrical (a modern solid-state photosensor) nor neural (a biological eye).

Here are the valid types and their quantities:

Note that 'photons' is an acceptable synonym for 'photons/sec' (although 'photons' is really a measure of energy).

3 Construction of colorSpec objects

The user constructs a colorSpec object x using the function colorSpec():

x <- colorSpec( data, wavelength, quantity='auto', organization='auto' )

The arguments are:

a vector or matrix of the spectrum values. In case data is a vector, x has a single spectrum and the number of points in that spectrum is the length of the vector. In case data is a matrix, the spectra are stored in the columns, so the number of points in each spectrum is the number of rows in data. It is OK for the matrix to have only 0 or 1 column. The column names (if any) are taken as the spectrum names. If no column names are given, then dummy names 'S1', 'S2', … are used, and a warning is issued. Names can also be assigned after construction too; see specnames(). Compare colorSpec() with the function ts() in package stats.

a numeric vector of wavelengths for all the spectra in x. The length of this vector must be equal to NROW(data).

a character string giving the quantity of all spectra; see Table 2.1 for a list of valid values. In case quantity='auto', a guess is made from the column names. The quantity of x can be changed later.

a character string giving the desired organization of the returned colorSpec object. In case organization='auto', the organization is 'vector' or 'matrix' depending on data. The organization of x can be changed later; see the next section for discussion of all 4 possible organizations.

4 colorSpec object organization

A spectrum is similar to a time-series (with time replaced by wavelength), and so the organization of a colorSpec object is similar to that of the time-series objects in package stats. A single time-series is organized as a vector with class ts, and a multiple time series is organized as a matrix (with the series in the columns) with class mts. We decided to use a single class name colorSpec, continue the idea of different organizations, and allow 2 more organizations. Here are the 4 possible organizations, in order of increasing complexity:

The object is a numeric vector with attributes but no dimensions, like a time-series ts. This organization works for a single spectrum only, which is very common. The common arithmetic operations work well with this organization. The length of the vector is the number of wavelengths. The class of the object is c('colorSpec','numeric').

The object is a matrix with attributes, like a multiple time-series mts. This is probably the most suitable organization in most cases, but it does not support extra data (see 'df.row' below). The common arithmetic and subsetting operations work well; and even round() works. The number of columns is the number of spectra, and the spectrum names are stored as the column names. This organization can be used for any number of spectra, including 0 or 1. The class of the object is c('colorSpec', 'matrix').

The object is a data frame with attributes. The spectra are stored in the columns. But the first column is always the wavelength sequence, so the spectra are in columns 2:(M+1), where M is the number of spectra. This organization mirrors the most common organization in text files and spreadsheets. The common arithmetic operations do not work, and the initial wavelength column is awkward to handle. The spectrum names are stored as the column names of the data frame. This organization can be used for any number of spectra, including 0 or 1. This organization imitates the “long” format in package hyperSpec. The class of the object is c('colorSpec', 'data.frame').

The object is a data frame with attributes. The last (right-most) column is a matrix with spectra in the rows. This matrix is the transpose of the matrix used when the organization is 'matrix'. The common arithmetic operations do not work. The spectrum names are stored as the row names of the data frame. This organization can be used for any number of spectra, including 0 or 1. This organization imitates the “tall” format in package hyperSpec. This is the only organization that supports extra data associated with each spectrum, such as physical parameters, time parameters, descriptive strings, or whatever. This extra data occupies the initial columns of the data frame that come before the spectra, and can be any data frame with the right number of rows. This extra data can be assigned to any spectrum with the 'df.row' organization. The class of the object is c('colorSpec', 'data.frame').

5 colorSpec object attributes

The attribute list is kept as small as possible. Here it is: These are considered internals, and user should never have to access these directly.

6 Spectrum File Import

There are 5 text file formats that can be imported; no binary formats are supported yet. The function readSpectra() reads a few lines from the top of the file to try and determine the type. If successful, it then calls the appropriate read function; see the colorSpec reference guide for details. The file formats are:

There is a line matching '^(wave|wl)' (not case sensitive) followed by the the names of the spectra. This is the column header line. All lines above this one are taken to be metadata. This is probably the most common file format; see the sample file ciexyz31_1.csv.

There is a line matching '^(ID|SAMPLE|Time)'. This line and lines below must be tab-separated. Fields matching '^[A-Z]+([0-9.]+)nm$' are taken to be spectral data and other fields are taken to be extradata. All lines above this one are taken to be metadata. The organization of the returned object is 'df.row'. This is a good format for automated acquisition of many spectra, using a spectrometer. See the sample file E131102.txt.

This is a file format used by Ocean Optics spectrometer software. There is a line >>>>>Begin Processed Spectral Data<<<<<. The following lines contain wavelength and power separated by a tab. There is only 1 spectrum per file. The organization of the returned object is 'vector'. See the sample file pos1-20x.scope.

This is a standardized format for exchange of color data, covered by both ANSI and ISO standards, see [1] and [7]. It might be best understood by looking at some samples, such as inst/extdata/objects/Rosco.txt. Unfortunately these standards do not give a standard way to name the spectral data. The function readSpectra() considers field names that match the pattern "^(nm|SPEC_|SPECTRAL_)[_A-Z]*([0-9.]+)$" to be spectral data and other fields are considered extra data. The organization of the returned object is 'df.row'.

This is a personal format used for digitizing images of plots from manufacturer datasheets and academic papers. It is structured like a Microsoft .INI file. There is a [Control] section establishing a simple linear map from the image pixels in the file to the wavelength and spectrum quantities. Only 3 points are really necessary. It is OK for there to be a little rotation of the plot axes relative to the image. This is followed by a section for each spectrum, in XY pixel units only. Conversion to wavelength and spectral quantities happens during on-the-fly after read. The organization of the returned object is 'vector'.

7 Package Options

There is a function cs.options() for setting options private to the package. There are 3 such options, and all are related to the package logging mechanism. All messages go to the console.

There is an option for setting the logging level. The levels are the 6 standard ones taken from Log4J: FATAL, ERROR, WARN, INFO, DEBUG, and TRACE. One can set higher levels to see more info.

By default, when an ERROR event occurs, execution stops. But there is a colorSpec option to continue. The logging level FATAL is reserved for internal errors, when execution always stops.

Finally, there is an option for how the message is formatted - a layout option. For details see the help page for the function cs.options().

8 Future Work

Here are a few possible improvements and additions.

handling the wavelength sequence, e.g. for product() and resample(), is an annoyance. We might consider adding a global wavelength option that all spectra are automatically resampled to.

fluorescent materials
Recall that a non-fluorescent material corresponds to a diagonal matrix, which operates in a trivial way on light spectra. A diagonal matrix can be stored much more compactly as a plain vector, and multiplication of a diagonal matrix by a vector simplifies to entrywise (Hadamard) multiplication. A fluorescent material corresponds to a non-diagonal matrix – called the Excitation Emission Matrix or Donaldson Matrix. The product in Appendix C is still multilinear, but the material product in the middle is no longer symmetric, so enhancements to the product computations must be made. This is a new level of complexity and memory usage, and may require a new type of memory organization.

There should a metric of some kind that compares two material spectra. There should be a way to compare 2 colorSpec objects of the same type, especially 'responsivity.light'. For example, there would then be a way to evaluate how close an electronic camera comes to satisying the Maxwell-Ives Criterion. Possible metrics would be the principal angles between subspaces.

For optimal colors in 3D, better numerical handling of optimal colors near the cusps at black and white would be an improvement. For optimal colors in 2D, it should be possible to probe the true optimal colors, and also the 1-transition edge-colors, or Kantenfarben.

the product() function saves the terms with the product object, but the plot() function ignores them. It may be useful to have an option to plot the individual terms too.

extrapolation is inconsistent and poorly documented, it should be improved

9 References

[1] ANSI/CGATS.17. Graphic technology — Exchange format for colour and process control data using XML or ASCII text [online]. Standard. New York City, USA: American National Standards Institute. 2009. Available at:

[2] APHALO, Pedro J. The r4photobiology suite. UV4Plants Bulletin [online]. 2015, 2015(1), 21–29. Available at: doi:10.19232/uv4pb.2015.1.14

[3] BELEITES, Claudia and SERGO, Valter. hyperSpec: a package to handle hyperspectral data sets in R [online]. 2017. Available at:

[4] GAMA, Jose. colorscience: Color Science Methods and Data [online]. 2016. Available at:

[5] GIORGIANNI, E.J., MADDEN, T.E. and KRISS, M. Digital Color Management: Encoding Solutions. B.m.: Wiley, 2009. The Wiley-IS&T Series in Imaging Science and Technology. ISBN 9780470512449.

[6] IHAKA, Ross, MURRELL, Paul, HORNIK, Kurt, FISHER, Jason C. and ZEILEIS, Achim. colorspace: Color Space Manipulation [online]. 2016. Available at:

[7] ISO/28178. Graphic technology — Exchange format for colour and process control data using XML or ASCII text [online]. Standard. Geneva, CH: International Organization for Standardization. 2009. Available at:

[8] KOENDERINK, J.J. Color for the Sciences. B.m.: MIT Press, 2010. ISBN 9780262014281.

[9] LANG, Serge. Real Analysis. Reading, Massachusetts: Addison-Wesley Pub. Co., 1969. Addison-wesley series in mathematics. ISBN 0-201-04179-0.

[10] LANG, Serge. Linear Algebra. B.m.: Springer New York, 2013. Undergraduate texts in mathematics. ISBN 9781475719499.

[11] LEHNERT, Lukas W., MEYER, Hanna and BENDIX, Jörg. hsdar: Manage, analyse and simulate hyperspectral data in R [online]. 2017. Available at:

[12] MAIA, Rafael, ELIASON, Chad M., BITTON, Pierre-Paul, DOUCET, Stephanie M. and SHAWKEY, Matthew D. pavo: an R Package for the analysis, visualization and organization of spectral data. Methods in Ecology and Evolution [online]. 2013, 4(10), 609–613. Available at: doi:10.1111/2041-210X.12069

[13] OLEARI, Claudio. Standard Colorimetry: Definitions, Algorithms and Software. B.m.: Wiley, 2016. SDC-society of dyers and colourists. ISBN 9781118894446.

[14] PACKER, Orin and WILLIAMS, David R. Light, the Retinal Image, and Photoreceptors. In: Steven K. SHEVELL, ed. The Science of Color. B.m.: Optical Society of America, 2003.

[15] RUDIN, Walter. Real and Complex Analysis. Second. New York: McGraw-Hill, 1974. McGraw-hill series in higher mathematics. ISBN 0-07-054233-3.

[16] SMPTE/2065-2. ST 2065-2:2012 - SMPTE Standard - Academy Printing Density (APD) #x2014; Spectral Responsivities, Reference Measurement Device and Spectral Calculation. ST 2065-2:2012 [online]. 2012, 1–14. Available at: doi:10.5594/SMPTE.ST2065-2.2012

[17] USER163644. Multiplication operator on \(L^1\) [online]. B.m.: Mathematics Stack Exchange. Available at: URL: (version: 2015-07-29)

[18] WIKIPEDIA. Multiplication operator — wikipedia, the free encyclopedia [online]. 2016. Available at: [Online; accessed 3-November-2017]

[19] WYSZECKI, G. and STILES, W.S. Color Science: Concepts and Methods, Quantitative Data and Formulae. B.m.: Wiley, 2000. Wiley series in pure and applied optics. ISBN 9780471399186.

[20] ZEILEIS, Achim and GROTHENDIECK, Gabor. zoo: S3 Infrastructure for Regular and Irregular Time Series. Journal of Statistical Software [online]. 2005, 14(6), 1–27. Available at: doi:10.18637/jss.v014.i06

10 Appendix A - Built-in colorSpec Objects

The following are built-in colorSpec objects that are commonly used. They are global objects that are automatically available when colorSpec is loaded. For more details on each see the corresponding help topic.

11 Appendix B - Bonus Spectral Data

Each built-in colorSpec object in Appendix A takes time to fully document in .Rd help files. Here are some bonus spectra files under folder extdata that users may find interesting and useful. Use the function readSpectra() to construct a colorSpec object from the file, for example:

sunlight = readSpectra( system.file( 'extdata/illuminants/sunlight.txt', package='colorSpec' ) )
## colorSpec object.   The organization is 'df.col'.  Object size is 3672 bytes.
## the object describes a single source of light, and the quantity is 'power' (power of photons, which is radiometric).
## Wavelength range: 300 to 830 nm.  Step size is 10 nm.
## 1 spectra
## 54 data points / spectrum
##           Source Min  Max LambdaMax Integral
## 1 sunlight.Power   0 1167       480   472390

See the top of each file for sources, attribution, and other information. Alternatively, one can run summary() on the imported object. Some of the files in Control format have associated JPG or PNG images of plots.

12 Appendix C - Spectrum Products

This Appendix is a very formal mathematical treatment of spectra. In infinite dimensions we use the terminology of functional analysis. In finite dimensions we use the terminology of linear algebra. For easier reference here is a repeat of Table 1.1:

There are 5 natural binary products on these spaces:

An equivalent way to handle these material diagonal matrices is to represent them instead as simple vectors – the entries along the diagonal. The above products with diagonal matrices then become the much simpler entrywise or Hadamard product. This is how it is done in colorSpec, using R’s built-in entrywise product operation.

The first 4 products can be strung together to get an associative product: \[L \times M_1 \times ... \times M_m \times L^* \to R\] It is not hard to show that this product is multilinear. This means that if one fixes all terms except the \(i^{th}\) material location, then the composition: \[M \to L \times M_1 \times ... \times \bullet \times ... \times M_m \times L^* \to R\] is linear, see [10]. The first inclusion map means to place the material spectrum in \(M\) at the ith variable slot \(\bullet\) in the product. The composition map is a functional on \(M\) which is an element of \(M^*\), i.e. a material responder. This special method of creating a material responder - a spectrum in \(M^*\) - plus all the products in the above table, are available in the function product() in colorSpec. See that help page for examples.

The right-hand term \(R\) can be thought of as standing for Response or Real numbers. In colorSpec the light responders can have multiple channels, e.g. R, G, and B, and so there are conventions on the admissible numbers of spectra for each term in these products. See the help page for colorSpec::product() for details.

13 Appendix D - Proofs

This appendix gives some proofs of some earlier statements about infinite dimensional function spaces. It is not relevant to the software in any way, and is likely of interest only to mathematicians and physicists. This proof is not original and is largely an expanded version of a discussion on, see [17].

Throughout this appendix, \(L^1\) denotes \(L^1[0,1]\), which is isomorphic to \(L^1[ \lambda_{min}, \lambda_{max}]\) where \([ \lambda_{min}, \lambda_{max}]\) is an arbitrary interval of wavelengths. Furthermore, \(L^\infty\) denotes \(L^\infty[0,1]\), and \(\mu\) denotes Lebesgue measure on \([0,1]\).

Proposition: Suppose \(\phi :[0,1] \to \mathbb{R}\) is a measurable function, and that \(\phi f \in L^1\) whenever \(f \in L^1\). Define the multiplication operator \(M_{\phi} : L^1 \to L^1\) by \(M_{\phi}(f) = \phi f\). Then
  1. \(M_{\phi}\) is a continuous linear operator on \(L^1\)
  2. \(\phi \in L^\infty[0,1]\)
  3. \(\left\lVert M_\phi \right\rVert ~=~ \left\lVert \phi \right\rVert _ \infty\)

Lemma: Given \(f,g \in L^1\) and a sequence \({f_n} \in L^1\) and \(\phi\) as above. Suppose \[a) ~ f_n \to f ~~~~~~~\text{and} ~~~~~~~~ b) ~ \phi f_n \to g\]

where both convergences are in \(L^1\). Then \(\phi f = g\) almost everywhere.

Proof: From \(a)\), and Theorem 3.12 in [15], p. 70, \(f_n\) has a subsequence that converges to \(f\) a.e. Replace \(f_n\) by this subsequence and \(a)\) and \(b)\) are still true. From \(b)\), \(\phi f_n\) has a subsequence that converges to \(g\) a.e. Replace \(\phi f_n\) and \(f_n\) by this subsequence and \(a)\) and \(b)\) are still true. So we have \[a') ~ f_n \to f ~~~~~~~\text{and}~~~~~~~~ a'') ~ \phi f_n \to \phi f ~~~~~~~\text{and}~~~~~~~~ b') ~ \phi f_n \to g\] where all convergences are almost everywhere. From \(a'')\) and \(b')\) we conclude that \(\phi f = g\) a.e. \(\square\).

Proof of Proposition: Parts \(a)\) and \(b)\) of the Lemma state that \((f_n,\phi f_n) \to (f,g)\) in \(L^1 \times L^1\). Define the graph of \(M_{\phi}\) in \(L^1 \times L^1\) to be the set of all pairs \((f,\phi f)\), \(f \in L^1\). The conclusion of the Lemma states that this graph is closed. So by the closed graph theorem ([15] p. 122), \(M_\phi\) is continuous. This shows part \(1.\)

Consider the functional \(f \mapsto \int \phi f \, d\mu\) on \(L^1\). It is the composition of \(M_\phi\) and a trivially continuous functional, and is therefore continous. Since \(L^1\) is \(\sigma\)-finite, the standard duality theorem ([15] p. 136), implies that there is a unique \(g \in L^\infty\) so that \(\int \phi f \, d\mu = \int g f \, d\mu\) for all \(f \in L^1\). Therefore \(\phi = g\), and this shows part \(2.\)

If \(\left\lVert \phi \right\rVert_\infty = 0\) then \(\phi=0\) and \(\left\lVert M_\phi \right\rVert = 0\), so part \(3.\) is trivially true. Assume now that \(\left\lVert \phi \right\rVert_\infty > 0\). Let \(f \in L^1\) with \(\left\lVert f \right\rVert = 1\). Then \[\left\lVert M_\phi (f) \right\rVert_1 ~=~ \int_0^1 \left\lvert \phi f \right\rvert \, d\mu ~=~ \int_0^1 \left\lvert \phi \right\rvert \left\lvert f \right\rvert \, d\mu ~\le~ \left\lVert \phi \right\rVert_\infty \int_0^1 \left\lvert f \right\rvert \, d\mu ~=~ \left\lVert \phi \right\rVert_\infty \] This shows \(\left\lVert M_\phi \right\rVert \le \left\lVert \phi \right\rVert_\infty\). For the other direction, let \(\alpha\) be any number with \(0 < \alpha < \left\lVert \phi \right\rVert_\infty\), and let \(E_\alpha := \left\lvert \phi \right\rvert ^{-1} ( [\alpha,\infty] )\). Then by the definition of \(\left\lVert \phi \right\rVert_\infty\), \(\mu( E_\alpha) > 0\). Let \(f_\alpha := \chi_{E_\alpha} / \mu( E_\alpha)\) (the \(L^1\)-normalized indicator function of \(E_\alpha\)). Then \[ \left\lVert M_\phi (f_\alpha) \right\rVert_1 ~:=~ \left\lVert \phi f_\alpha \right\rVert_1 ~:=~ \int_0^1 \left\lvert \phi \right\rvert f_\alpha \, d\mu ~\ge~ \int_0^1 \alpha f_\alpha \, d\mu ~=~ \alpha \int_0^1 f_\alpha \, d\mu ~=~ \alpha \left\lVert f_\alpha \right\rVert_1 ~=~ \alpha \] So \(\left\lVert M_\phi \right\rVert \ge \alpha\) for every \(\alpha < \left\lVert \phi \right\rVert_\infty\), which implies \(\left\lVert M_\phi \right\rVert \ge \left\lVert \phi \right\rVert_\infty\). This shows part \(3.\) \(\square\).

Corollary: Let \(M\) be the vector space of all multiplication operators on \(L^1\). Then the mapping \(L^\infty \to M\) given by \(\phi \mapsto M_\phi\) is a norm-preserving isomorphism.
Proof:The mapping is clearly injective. The Proposition shows that it is surjective and norm-preserving. \(\square\)

14 Session Information

R version 3.4.3 (2017-11-30)
Platform: i386-w64-mingw32/i386 (32-bit)
Running under: Windows 7 (build 7601) Service Pack 1

Matrix products: default

[1] LC_COLLATE=C                          
[2] LC_CTYPE=English_United States.1252   
[3] LC_MONETARY=English_United States.1252
[4] LC_NUMERIC=C                          
[5] LC_TIME=English_United States.1252    

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] colorSpec_0.6-2

loaded via a namespace (and not attached):
 [1] MASS_7.3-47     compiler_3.4.3  backports_1.1.1 magrittr_1.5   
 [5] rprojroot_1.2   htmltools_0.3.6 tools_3.4.3     yaml_2.1.14    
 [9] Rcpp_0.12.14    stringi_1.1.6   rmarkdown_1.8   knitr_1.17     
[13] stringr_1.2.0   digest_0.6.12   evaluate_0.10.1