The goal of this vignette is to give examples of rounding to the
closest official color chip in one or more Munsell chart sets. The five
chart sets are Soil [11],
Rock [12],
Bead [13],
Plant [14], and
The New Student Color Set [10]. For the color chip data we depend
completely on Ferguson [7]. The
supplementary material from this paper was put in computer readable form
by Willie Ondricek, who greatly improved the wording of this vignette.
One of the fields in this supplementary material is a color name for
each chip; we call this name the Ferguson Name
. In this
vignette, we only use examples from the Munsell Soil-Color Charts.
The key function in this vignette is roundHVC()
. Other
featured functions from munsellinterpol are
XYZtoMunsell()
, MunsellNameFromHVC()
, and
ColorBlockFromMunsell()
. Other packages that play a very
important part are OxSR [8], colorSpec [5], and spacesXYZ [4]. But we will not attach these and prefix
their functions with the namespace to make it easier to locate them. The
package flextable [9] is
used in one section to display some RGB color patches.
The package OxSR has some high resolution (0.5 nm)
reflectance spectra for 23 soil samples; these spectra are stored in the
object OxSR::soil_refle
. Of these 23 we have pre-selected a
subset of 6 samples with high variability. The following function also
adds an ideal neutral sample with constant reflectance of 18% - an 18%
gray card [16].
load_soil <- function( soil_raw=OxSR::soil_refle, subset=c( "a4", "a6", "a8", "a11", "a14", "a15" ),
wave=380:780 )
{
# locate only the wavelengths in OxSR::soil_refle that are relevant for color
# the given data.frame OxSR::soil_refle goes way into the infra-red and these are not needed
# We already know that all relevant multiples of 1nm are in soil_raw
idx = match( wave, soil_raw$wavelength_nm )
# extract just a subset of samples with high variability
# the reflectances in OxSR::soil_refle are percentages, so divide by 100
mat = as.matrix(soil_raw)[ idx, subset ] / 100
# convert the matrix mat to a colorSpec object
soil_spec = colorSpec::colorSpec( mat, wavelength=wave, quantity='reflectance' )
# add neutral gray as a test sample
gray = colorSpec::neutralMaterial( 0.18, wavelength=wave )
soil_spec = colorSpec::bind( soil_spec, gray )
return( soil_spec )
}
Load the spectra and plot them.
soil_spec = load_soil()
par( omi=c(0,0,0,0), mai=c(0.6,0.6,0.3,0.3) )
plot( soil_spec, legend="topleft" )
The graphed colors here are computed for Illuminant D65. This may not
always be appropriate, so here is another function that can create and
use illuminants of any Correlated Color Temperature (CCT).
The argument CCT
in the following function is the
desired Correlated Color Temperature of the illuminant.
soil_data <- function( soil_spec, CCT )
{
wave = colorSpec::wavelength(soil_spec)
# make illuminant from the CCT. This is the reference illuminant in IES Standard TM-30.
illum = colorSpec::referenceSpectraTM30( CCT, wavelength=wave )
# make scanner from this illuminant and the standard x,y,z responsivities of the human eye
scanner = colorSpec::product( illum, "soil", colorSpec::xyz1931.1nm, wavelength=wave )
# scale so the Perfect Reflecting Diffuser has Y=100; which is conventional for Munsell work
scanner = colorSpec::calibrate( scanner, response=c(NA,100,NA), method='scaling' )
# compute white point of illum
white = colorSpec::product( colorSpec::neutralMaterial(1,wavelength=wave), scanner )
# compute XYZ for all soil samples; this is XYZ under illum
XYZ = colorSpec::product( soil_spec, scanner )
# compute Lab for all soil samples, under illum
Lab = spacesXYZ::LabfromXYZ( XYZ, white )
# chromatically adapt soil sample XYZ under illum, to sample XYZ under Illuminant C
white.C = 100 * spacesXYZ::standardXYZ( "C.NBS" ) # use the original NBS variant of this white point
theCAT = spacesXYZ::CAT( white, white.C )
XYZ.C = spacesXYZ::adaptXYZ( theCAT, XYZ )
# convert XYZ.C (XYZ under Illuminant C) to Munsell HVC.
HVC = XYZtoMunsell( XYZ.C, xyC="NBS" ) # use the original NBS variant, as in standardXYZ() above
rownames(HVC) = colorSpec::specnames(soil_spec)
# create output data.frame and add 4 columns: HVC, Munsell notation, ISCC-NBS color name, and Lab
out = data.frame( row.names=rownames(HVC) )
out$HVC = HVC
out$Munsell = MunsellNameFromHVC( HVC )
out[[ "ISCC-NBS Name" ]] = ColorBlockFromMunsell( HVC )$Name
out$Lab = Lab
return( out )
}
This one is fairly long, but it does a lot of work.
When viewing soil samples, these are the official recommendations:
The visual impression of color from the standard color chips is accurate only under standard conditions of light intensity and quality. Comparison of soil color to the standard Munsell chips should be done without sunglasses and in normal sunlight. … If artificial light is used … the light source used must be as near to the white light of midday as possible. — from Conditions for measuring color in [11]
Instead of “normal sunlight” I believe the authors actually meant “normal daylight”, as in the next sentence “light of midday”. The American Cinematographer Manual, p 199 of [6], says that a reasonable Correlated Color Temperature (CCT) for “Average Summer Daylight” is 6500 K. So we’ll choose the CIE-standardized daylight illuminant with CCT=6500 for this calculation. Note that the white point of this illuminant is also the white point for the color space sRGB.
Some teams of soil scientists may use viewing conditions that are
different, and also want agreement between human and instrumental
Munsell colors. Here are some possible alternatives to the daylight
illuminant with CCT=6500 (D65). An unofficial page [15], recommends that viewing is done under
“Natural light conditions (preferably on a cloudy day or in shaded
area)”. Reference [6] states that a
reasonable CCT for “Average Summer Shade” is 8000K. So if viewing is
done in a shaded area, CCT=8000 might be more appropriate. For viewing
indoors under incandescent lamps, warm light with CCT=3000 K might be
more appropriate. For viewing indoors under LEDs, an actual LED spectrum
might be more appropriate. There are many such LED spectra in packages
photobiologyLEDs [2] and
photobiologyLamps [3].
These can be converted to colorSpec
objects using
photobiologyInOut::as.colorSpec()
[1].
## HVC.H HVC.V HVC.C Munsell ISCC-NBS Name
## a4 18.5080067 5.3137162 0.9324906 8.5YR 5.3/0.93 light brownish gray
## a6 13.4661350 5.7955799 4.1294108 3.5YR 5.8/4.1 light brown
## a8 12.9220689 5.4686500 3.9175170 2.9YR 5.5/3.9 light reddish brown
## a11 19.2796152 6.2947929 1.6599596 9.3YR 6.3/1.7 light grayish yellowish brown
## a14 17.1148841 6.8192429 3.1407503 7.1YR 6.8/3.1 light yellowish brown
## a15 13.4261954 5.0039322 3.3763618 3.4YR 5/3.4 light brown
## Neutral0.18 100.0000000 4.8519274 0.0000000 N 4.9/ medium gray
## Lab.L Lab.a Lab.b
## a4 54.186009 2.018138 5.414906
## a6 59.009553 13.256621 18.519024
## a8 55.734273 13.181574 16.807799
## a11 63.973658 2.786787 10.440434
## a14 69.112885 7.290566 17.315536
## a15 51.039279 11.337843 14.982868
## Neutral0.18 49.496108 0.000000 0.000000
Note that all 6 pre-selected soil samples are within the
YR (Yellow-Red) Hues. Actually all 23 soil samples are
YR, and not just these selected 6 ! Note that for the
Neutral0.18
test sample (not soil), C=a=b=0
as
they should be.
The above samples have high precision, but unfortunately that precision makes it inconvenient to report in a way that researchers familiar with the Munsell Soil Color Charts can relate to. For the sake of consistency with the traditional way of communicating colors, it is sometimes better to provide the Munsell notation for the closest color chip in the official Soil Charts.
dat_rnd = roundHVC( dat_soil$HVC, books="soil" )
dat_rnd$HVC = NULL # delete matrix HVC, because it is redundant in this context
dat_rnd
## ISCC-NBS Name MunsellRounded Ferguson Name
## a4 light brownish gray 7.5YR 5/1 Gray
## a6 light brown 2.5YR 6/4 Light reddish brown
## a8 light reddish brown 2.5YR 5/4 Reddish brown
## a11 light grayish yellowish brown 10YR 6/2 Light brownish gray
## a14 light yellowish brown 7.5YR 7/3 Pink
## a15 light brown 2.5YR 5/3 Reddish brown
## Neutral0.18 medium gray N 5/ Gray
The Munsell notations in the MunsellRounded
column are
guaranteed to be in the Soil Color Charts; note that all 6 are distinct.
The ISCC-NBS Name
s are computed from the original and
precise HVC, and the Ferguson Name
s are computed from
MunsellRounded
. Note that these names are all
different.
Note that 3 of the samples are on the same Hue chart 2.5YR. Plot that Hue chart and label those 3 samples:
par( omi=c(0,0,0,0), mai=c(0.4,0.4,0.25,0) )
plotPatchesH( "2.5YR", value=c(2.5,3:8), chroma=c(1,2,3,4,6,8) )
text( c(4,4,3), c(5,4,4), c("a6","a8","a15"), adj=c(0.5,0.5), col="white" )
The Value and Chroma vectors match the actual soil color chart, but the
chips here are a superset of those in the chart. The top-right chip,
2.5YR 8/8, is inside the object color gamut, but
outside the sRGB gamut. These colors are best viewed on a display that
is calibrated for sRGB.
Finally, we make a table with side-by-side color comparisons of the precisely computed color in Munsell notation, and the Munsell notation after rounding to the closest color chip in the Munsell Soil Color Charts.
tbl = data.frame( row.names=1:(2*nrow(dat_soil)) )
tbl[[ "Sample" ]] = as.character( rbind(rownames(dat_soil),'') )
tbl[[ "Precise" ]] = as.character( rbind('',MunsellNameFromHVC( dat_soil$HVC, digits=3 )) )
tbl[[ "Rounded" ]] = as.character( rbind('',dat_rnd$MunsellRounded ) )
library( flextable )
myrt <- regulartable( tbl )
myrt <- width( myrt, j=c(2,3), width=2 )
myrt <- align( myrt, j=1:3, align='center', part='all' )
myrt <- hline( myrt, i=seq( 2, nrow(tbl)-2, by=2 ), border=fp_border_default( color="black", width=2 ) )
myrt <- hrule( myrt, rule = "exact" )
idxcolor <- seq(1,nrow(tbl)-1,by=2)
myrt <- height( myrt, i=idxcolor, height=1 )
myrt <- bg( myrt, i=idxcolor, j=2, bg=rgb( round(MunsellTosRGB(dat_soil$HVC)$RGB), max=255 ) )
myrt <- bg( myrt, i=idxcolor, j=3, bg=rgb( round(MunsellTosRGB(dat_rnd$MunsellRounded)$RGB), max=255 ) )
myrt
Sample | Precise | Rounded |
---|---|---|
a4 | ||
8.51YR 5.31/0.932 | 7.5YR 5/1 | |
a6 | ||
3.47YR 5.8/4.13 | 2.5YR 6/4 | |
a8 | ||
2.92YR 5.47/3.92 | 2.5YR 5/4 | |
a11 | ||
9.28YR 6.29/1.66 | 10YR 6/2 | |
a14 | ||
7.11YR 6.82/3.14 | 7.5YR 7/3 | |
a15 | ||
3.43YR 5/3.38 | 2.5YR 5/3 | |
Neutral0.18 | ||
N 4.85/ | N 5/ |
These colors are best viewed on a display that is calibrated for sRGB.
R version 4.5.1 (2025-06-13 ucrt) Platform: x86_64-w64-mingw32/x64 Running under: Windows 11 x64 (build 26100) Matrix products: default LAPACK version 3.12.1 locale: [1] LC_COLLATE=C LC_CTYPE=English_United States.utf8 [3] LC_MONETARY=English_United States.utf8 LC_NUMERIC=C [5] LC_TIME=English_United States.utf8 time zone: America/Los_Angeles tzcode source: internal attached base packages: [1] stats graphics grDevices utils datasets methods base other attached packages: [1] flextable_0.9.7 spacesXYZ_1.6-0 spacesRGB_1.7-0 munsellinterpol_3.2-0 loaded via a namespace (and not attached): [1] katex_1.5.0 jsonlite_2.0.0 compiler_4.5.1 equatags_0.2.1 [5] Rcpp_1.0.14 zip_2.3.3 xml2_1.3.8 jquerylib_0.1.4 [9] fontquiver_0.2.1 systemfonts_1.2.3 textshaping_1.0.1 uuid_1.2-1 [13] yaml_2.3.10 fastmap_1.2.0 R6_2.6.1 gdtools_0.4.2 [17] microbenchmark_1.5.0 curl_6.2.2 knitr_1.50 colorSpec_1.8-0 [21] logger_0.4.0 openssl_2.3.2 bslib_0.9.0 rlang_1.1.6 [25] V8_6.0.3 cachem_1.1.0 xfun_0.52 sass_0.4.10 [29] cli_3.6.5 OxSR_1.0.1 digest_0.6.37 grid_4.5.1 [33] rootSolve_1.8.2.4 askpass_1.2.1 lifecycle_1.0.4 evaluate_1.0.3 [37] glue_1.8.0 data.table_1.17.2 fontLiberation_0.1.0 officer_0.6.8 [41] ragg_1.4.0 xslt_1.5.1 fontBitstreamVera_0.1.1 rmarkdown_2.29 [45] tools_4.5.1 htmltools_0.5.8.1