Skip to content

Commit

Permalink
fixes, updates, docs for #297
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanbeaudette committed Nov 17, 2023
1 parent b1bac7c commit 7410738
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 93 deletions.
2 changes: 1 addition & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# aqp 2.0.2 (2023-10-24)
# aqp 2.0.2 (2023-11-17)
* new function `col2Munsell()` generalizes and replaces `rgb2munsell()` (thanks Shawn Salley for the suggestion)
* new function `warpHorizons()` for warping horizon thickness (inflate/deflate) (thanks Shawn Salley for idea / inspiration)
* fixed minor bug in `plotColorMixture()` when final mixed color does not exist in spectral library
Expand Down
61 changes: 36 additions & 25 deletions R/aggregateColor.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
#' @param x a `SoilProfileCollection` object
#' @param groups the name of a horizon or site attribute used to group horizons, see examples
#' @param col the name of a horizon-level attribute with soil color specified in hexadecimal (i.e. "#rrggbb")
#' @param colorSpace the name of color space or color distance metric to use for conversion of aggregate colors to Munsell; either CIE2000 (color distance metric), LAB, or sRGB. Default = 'CIE2000'
#' @param colorSpace (now deprecated, removed in aqp 2.1) 'CIE2000' used for all cases
#' @param k single integer specifying the number of colors discretized via PAM ([cluster::pam()]), see details
#' @param profile_wt the name of a site-level attribute used to modify weighting, e.g. area
#'
Expand Down Expand Up @@ -98,77 +98,79 @@
#'
aggregateColor <- function(x, groups = 'genhz', col = 'soil_color', colorSpace = 'CIE2000', k = NULL, profile_wt = NULL, mixingMethod = c('estimate', 'exact')) {

# colorSpace argument is now deprecated
if(!missing(colorSpace)) {
message('colorSpace argument is now deprecated, CIE2000 distance is always used.')
}

# sanity check
mixingMethod <- match.arg(mixingMethod)

# sanity check
if(!is.null(k)) {
k <- round(k)

# sanity check, need this for color distance eval
if (!requireNamespace('farver', quietly = TRUE))
stop('please install the `farver` package.', call.=FALSE)

if(is.na(k)) {
stop('k must be a single integer > 0')
}
}

# sanity check: profile weight must be a valid site-level attribute
if(!is.null(profile_wt)) {
if(! profile_wt %in% siteNames(x)) {
stop('`profile_wt` must be a site-level attribute of `x`', call. = FALSE)
}
}

# sanity check: groups must be a horizon-level attribute
if(! groups %in% names(x))
stop('`groups` must be a site or horizon attribute of `x`', call. = FALSE)

# sanity check: col must be a horizon-level attribute
if(! col %in% horizonNames(x))
stop('`col` must be a horizon-level attribute of `x`', call. = FALSE)

if(!colorSpace %in% c("CIE2000","LAB","sRGB"))
stop('colorSpace must be either: CIE2000, LAB or sRGB', call. = FALSE)


## hack to make R CMD check --as-cran happy
top <- bottom <- thick <- NULL

# extract pieces
h <- as(x, 'data.frame')

# keep track of just those variables we are using
# profile_wt is NULL by default
vars <- c(groups, horizonDepths(x), col, profile_wt)

# remove missing data
# note: missing color data will result in 'white' when converting to sRGB, why?
h.no.na <- na.omit(h[, vars])

# re-name depths
names(h.no.na)[2:3] <- c('top', 'bottom')

# safely compute thickness
# abs() used in rare cases where horizon logic is wrong: e.g. old-style O horizons
h.no.na$thick <- abs(h.no.na$bottom - h.no.na$top)

# 0-thickness will result in NA weights
# replace with a 1-unit slice
idx <- which(h.no.na$thick == 0)
if(length(idx) > 0) {
h.no.na$thick[idx] <- 1
}

# apply site-level weighting here, if present
if(!is.null(profile_wt)) {
# multiply thickness by site-level weight
h.no.na$thick <- h.no.na$thick * h.no.na[[profile_wt]]
}

# drop empty group levels
h.no.na[[groups]] <- factor(h.no.na[[groups]])

# split by genhz
ss <- split(h.no.na, h.no.na[[groups]])

Expand Down Expand Up @@ -197,15 +199,22 @@ aggregateColor <- function(x, groups = 'genhz', col = 'soil_color', colorSpace =
v <- t(col2rgb(lut$col) / 255)

# convert to LAB
v <- convertColor(v, from='sRGB', to='Lab', from.ref.white='D65', to.ref.white = 'D65', clip = FALSE)
v <- convertColor(
v,
from = 'sRGB',
to = 'Lab',
from.ref.white = 'D65',
to.ref.white = 'D65',
clip = FALSE
)

# compute perceptually based distance matrix on unique colors
dE00 <- farver::compare_colour(v, v, from_space='lab', method='CIE2000', white_from='D65')
dE00 <- farver::compare_colour(v, v, from_space = 'lab', method = 'CIE2000', white_from = 'D65')
dE00 <- as.dist(dE00)

# clustering from distance matrix
# TODO: save clustering results for later
v.pam <- cluster::pam(dE00, k = k.adj, diss = TRUE, pamonce=5)
v.pam <- cluster::pam(dE00, k = k.adj, diss = TRUE, pamonce = 5)

# put clustering vector into LUT
lut$cluster <- v.pam$clustering
Expand Down Expand Up @@ -242,7 +251,9 @@ aggregateColor <- function(x, groups = 'genhz', col = 'soil_color', colorSpace =

## TODO: this is wasteful, as we likely "knew" the munsell notation before-hand
# back-calculate the closest Munsell color
m <- rgb2munsell(t(col2rgb(res[[col]])) / 255, colorSpace = colorSpace)
# m <- rgb2munsell(t(col2rgb(res[[col]])) / 255, colorSpace = colorSpace)
m <- col2Munsell(res[[col]])


# format as text
res$munsell <- paste0(m[, 1], ' ', m[, 2], '/', m[, 3])
Expand All @@ -254,14 +265,14 @@ aggregateColor <- function(x, groups = 'genhz', col = 'soil_color', colorSpace =
})



# rescale using the sum of the weights within the current horizon
s.scaled <- lapply(s, function(i) {
i$weight <- i$weight / sum(i$weight)
return(i)
})


# iteration over groups, estimation of:
# aggregate colors via mixing
# number of associated colors
Expand Down
74 changes: 48 additions & 26 deletions R/col2Munsell.R
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,16 @@ col2Munsell <- function(col, space = c('sRGB', 'CIELAB'), nClosest = 1) {
nClosest <- 20
}

# sacrafice to CRAN gods
# empty DF for NA-padding
.empty <- data.frame(
hue = NA,
value = NA,
chroma = NA,
sigma = NA,
stringsAsFactors = FALSE
)

# sacrifice to CRAN gods
munsell <- NULL

# note: this is incompatible with LazyData: true
Expand All @@ -110,10 +119,27 @@ col2Munsell <- function(col, space = c('sRGB', 'CIELAB'), nClosest = 1) {

# col can be either character vector or numeric matrix

# convert numeric vector of colors -> sRGB -> CIELAB
# a vector implies that it should be a character vector
if(is.vector(col)) {
col <- as.character(col)
}

# convert character vector of colors -> sRGB -> CIELAB
if(inherits(col, 'character')) {

# generate index to NA and not-NA
na.idx <- which(is.na(col))
not.na.idx <- setdiff(1:length(col), na.idx)

# all NA short-circuit
if(length(not.na.idx) < 1) {
res <- .empty[na.idx, ]
row.names(res) <- NULL
return(res)
}

# sRGB matrix [0,1]
# not NA-safe: NA are converted to [1,1,1]
col <- t(col2rgb(col) / 255)

# sRGB [0,1] -> CIELAB
Expand All @@ -126,6 +152,17 @@ col2Munsell <- function(col, space = c('sRGB', 'CIELAB'), nClosest = 1) {
col <- as.matrix(col)
}

# generate index to NA and not-NA
na.idx <- which(apply(col, 1, function(i) any(is.na(i))))
not.na.idx <- setdiff(1:nrow(col), na.idx)

# all NA short-circuit
if(length(not.na.idx) < 1) {
res <- .empty[na.idx, ]
row.names(res) <- NULL
return(res)
}

# interpret space argument
space <- match.arg(space)

Expand All @@ -134,7 +171,8 @@ col2Munsell <- function(col, space = c('sRGB', 'CIELAB'), nClosest = 1) {
if(space == 'sRGB') {

# detect 0-1 vs. 0-255 sRGB range
if(range(col)[2] > 2) {
# not NA-safe
if(range(col[not.na.idx])[2] > 2) {
col <- col / 255
}

Expand All @@ -144,21 +182,14 @@ col2Munsell <- function(col, space = c('sRGB', 'CIELAB'), nClosest = 1) {

# CIELAB input
# nothing left to do
# if(space == 'CIELAB') {
# message('no conversion required')
# }
}

# vectorize via for-loop
n <- nrow(col)
res <- vector(length = n, mode = 'list')

# accounting for the possibility of NA
# result should be an empty record
not.na.idx <- which(apply(col, 1, function(i) ! any(is.na(i))))


# iterate over colors
# iterate over non-NA colors
# this will leave NULL "gaps"
for(i in not.na.idx) {
# convert current color to matrix, this will allow matrix and DF as input
this.color <- as.matrix(col[i, , drop = FALSE])
Expand All @@ -185,19 +216,11 @@ col2Munsell <- function(col, space = c('sRGB', 'CIELAB'), nClosest = 1) {

}

# pad records with NA in the sRGB input
# https://github.com/ncss-tech/aqp/issues/160
na.idx <- which(sapply(res, is.null))

if(length(na.idx) > 0) {
for(i in na.idx){
res[[i]] <- data.frame(
hue = NA,
value = NA,
chroma = NA,
sigma = NA,
stringsAsFactors = FALSE
)
# fill NULL gaps with empty DF, all NA
null.idx <- which(sapply(res, is.null))
if(length(null.idx) > 0) {
for(i in null.idx){
res[[i]] <- .empty
}
}

Expand All @@ -210,7 +233,6 @@ col2Munsell <- function(col, space = c('sRGB', 'CIELAB'), nClosest = 1) {
attr(res, which = 'sigma') <- 'CIE delta-E 2000'

return(res)

}


Expand Down
20 changes: 2 additions & 18 deletions R/estimateSoilColor.R
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,8 @@ estimateSoilColor <- function(hue, value, chroma, sourceMoistureState = c('dry',
## CIELAB -> closest Munsel
res <- col2Munsell(Y, space = 'CIELAB', nClosest = 1)

## no longer required since col2Munsell() ##

# ## CIELAB -> sRGB
# ## TODO: why does farver give slightly different results?
#
# ## farver: results are scaled 0-255
# # .srgb <- farver::convert_colour(Y, from = 'lab', to = 'rgb', white_from = 'D65', white_to = 'D65')
#
# ## grDevices: results are scaled 0-1
# .srgb <- convertColor(Y, from = 'Lab', to = 'sRGB', from.ref.white='D65', to.ref.white = 'D65')
#
# ## sRGB -> Munsell
# res <- rgb2munsell(color = .srgb, colorSpace = 'CIE2000', nClosest = 1)

###


## additional diagnostics... ?

## TODO: post-processing or additional diagnostics?

return(res)

Expand Down
3 changes: 3 additions & 0 deletions R/munsell2rgb.R
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
#'
rgb2munsell <- function(color, colorSpace = c('CIE2000', 'LAB', 'sRGB'), nClosest = 1) {

# 2023-11-17
.Deprecated(new = 'col2Munsell', msg = 'please use col2Munsell() instead.')

# argument check
colorSpace <- match.arg(colorSpace)

Expand Down
2 changes: 1 addition & 1 deletion man/aggregateColor.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion misc/sandbox/colordistance-leon-example.R
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ lab_kmeans <- getKMeanColors('fragipan-soil-color-example.png', n = 7, sample.si
color.space = "lab", ref.white = "D65")


aqp::rgb2munsell(convertColor(lab_kmeans$centers, from='Lab', to='sRGB'))
aqp::col2Munsell(convertColor(lab_kmeans$centers, from = 'Lab', to = 'sRGB'))



Expand Down
2 changes: 1 addition & 1 deletion misc/sandbox/pretty-soil-jars.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ x <- structure(
class = "data.frame", row.names = c(NA, -16L)
)

m <- rgb2munsell(x)
m <- col2Munsell(x)
m$label <- sprintf("%s %s/%s", m$hue, m$value, m$chroma)
m$color <- rgb(x)

Expand Down
Loading

0 comments on commit 7410738

Please sign in to comment.