Skip to content

Commit 64f0583

Browse files
committed
Adding new script from the Contest
1 parent 3b0792d commit 64f0583

File tree

7 files changed

+384
-0
lines changed

7 files changed

+384
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ Dedicated to supplying data for [Copernicus services](http://www.esa.int/Our_Act
186186
- [Homage to Mondrian](sentinel-2/homage_to_mondrian) - artistic script
187187
- [Index visualisation](sentinel-2/index_visualization) - universal script for visualisation of indices
188188
- [NDVI on L2A Vegetation and natural Colours](sentinel-2/ndvi-on-vegetation-natural_colours)
189+
- [PUCK](sentinel-2/puck) - Perceptually-Uniform Color Map Kit
189190

190191
#### Scripts including machine learning techniques (eo-learn)
191192

sentinel-2/puck/README.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# PUCK - Perceptually-Uniform Color Map Kit Script
2+
3+
<a href="#" id='togglescript'>Show</a> script or [download](script.js){:target="_blank"} it.
4+
<div id='script_view' style="display:none">
5+
{% highlight javascript %}
6+
{% include_relative script.js %}
7+
{% endhighlight %}
8+
</div>
9+
10+
## Evaluate and visualize
11+
- [EO Browser](https://tinyurl.com/y6b5hevs){:target="_blank"}
12+
13+
14+
## General description of the script
15+
16+
A set of visualization utilities that produce beautiful images designed for human perception from single-channel data (NDVI, spectral angle, etc). Generate quality figures that are true to the data.
17+
18+
Good visualizations depend on good color maps. However, it is very easy to unintentionally create data artifacts or obscure trends unless care is taken. For example, many have written about the problems with rainbow color maps (e.g. Borland and Taylor 2007 and Kovesi 2015, see references). Yet, rainbows remain prevalent in scientific publications (and occasionally on [SentinelHub](https://tinyurl.com/y34rkknf)).
19+
20+
SentinelHub custom scripts have limited support for custom color maps: colorBlend and ColorRampVisualizer. As a result, many users end up hardcoding their colors. Also, these functions use RGB colors instead of a color gamut designed around human perception. The goal of this project is to create tools for custom script developers to generate useful and perceptually uniform color maps. It provides visualization classes for users familiar with CIELAB color space and works to correct colors in RGB space to be perceptually uniform. The underlying mathematics is used by many scientific plotting applications, such as matplotlib, bokeh, chroma.js, and others.
21+
22+
## Details of the script
23+
24+
### How the Script Works
25+
This script supports 4 flavors of color map in addition to functions that inter-convert between the CIELAB, XYZ, and RGB color spaces in pure JavaScript. In each case, the user simply provides two arrays, one for anchor colors and one for data values, and the class will generate the correct color for data values calculated by the script. For example, to generate a brown-green color map for NDVI values in the range [0, 1], simply use:
26+
27+
var ndvi_map = [
28+
// https://www.google.com/search?q=rgb+color+picker
29+
[66/255, 50/255, 28/255],
30+
[0, 209/255, 3/255],
31+
]
32+
var data = [0, 1]
33+
var cmap = new LinearColormap(ndvi_map, data);
34+
35+
function setup() {
36+
return {
37+
input: ["B04", "B08", "dataMask"],
38+
output: { bands: 3 }
39+
}
40+
}
41+
42+
function evaluatePixel(sample) {
43+
let ndvi = (sample.B08 - sample.B04) / (sample.B08 + sample.B04)
44+
return cmap.get_color(ndvi)
45+
}
46+
47+
In addition to LinearColormap, there is DivergentColormap, IsoluminantColormap, and the base class Colormap. The base class expects colors in CIELAB color space and simply calls colorBlend to get in-between colors before converting back to RGB space. All other classes expect colors in RGB space, map them onto CIELAB space, and apply a lightness correction specific to the class. The corrections for each class are:
48+
- LinearColormap: Lightness monotonically changes across the whole map. Your standard color ramp.
49+
- DivergentColormap: Lightness reaches a minimum or maximum at the center of the color map and monotonically changes towards the edges of the map. Each edge has the same lightness. Useful for showing how values are distributed around a reference value. However, problems can arise if data features straddle the lightness peak.
50+
- IsoluminantColormap: Lightness is the same across the whole map. Not very pretty on its own, but useful when one wants to visualize data on top of relief shading.
51+
52+
### Applicability
53+
This script is useful for any continuous, single-channel data product.
54+
55+
### Known Issues
56+
The script is a visualization utility, so false detection issues are not applicable. However, since by definition not all combinations of CIELAB coordinates map onto valid RGB values, the lightness correction may be invalidated by especially poor RGB anchors. For this reason, it is usually best to implement an already-proven color map instead of implementing one from scratch. Also, having too many lines of code can break link sharing. To share your code with others, delete the classes you are not using so the code fits in a URL.
57+
58+
### Color Spaces
59+
RGB color space is defined in terms of the relative brightness of red, green, and blue color used in screens. XYZ color space was specified by the International Commission on Illumination in 1931 following a series of perception experiments with human observers (Smith and Guild 1931). CIELAB color space is the successor to XYZ space, defined in 1976 with the goal of being more perceptually uniform (Judd, CIE Publication No. 015). CIELAB, also known as L*a*b, is defined by three parameters: lightness (L), red-green (a), and blue-yellow (b). Conversion directly to and from RGB and CIELAB space in one step is generally not used. The script converts from RGB to XYZ and then to CIELAB using a D65, secondary-observer reference white. Mathematics of the conversion are available in pseudocode [here](https://www.easyrgb.com/en/math.php).
60+
61+
### Color Maps
62+
Issues with color maps generally stem from the fact that humans perceive differences in color lightness better than differences in color hue or saturation (Cleveland and McGill 1984). In addition, the colors used in a map should have intuitive meaning. Returning to Roy G. Biv, do you instantly understand yellow-green to be between orange and violet? Is it closer to orange or violet? For most people, the relationship is difficult to understand quantitatively.
63+
64+
To make your own color maps there are many battle-tested maps available at SciVisColor. If the distribution of data is unknown, it is best to use a perceptually-uniform color map with a strong overall lightness gradient. SciVisColor recommends a [blue-orange divergent map](https://sciviscolor.org/home/colormaps/contrasting-divergent-colormaps/) for general use.
65+
66+
## Author of the script
67+
68+
Keenan Ganz
69+
70+
## Description of representative images
71+
72+
1) Masked NDVI over Sequim, Washington, USA using a white-green Linear Colormap. The town of Sequim has low NDVI values, while Olympic National Park to the south has very dense vegetation.
73+
74+
![The script example 1](fig/sequim_ndvi.jpg)
75+
76+
2) Hurricane Matthew brightness temperature visualized with a blue-white-red Divergent Colormap. Range is 200-300 K.
77+
78+
![The script example 2](fig/hurricane_matthew_temperature.jpg)
79+
80+
3) Masked NDWI over the Lena River delta using a custom two-wave color map; white-blue-black-green. Lightness correction is not used here to allow for rise/fall. Custom data breaks show extent of detail in the data.
81+
82+
![The script example 3](fig/lena_ndwi.jpg)
83+
84+
4) Isoluminant NDVI over Troy, New York, USA overlaid with relief shading. Note that interpretation of terrain features is unhindered by NDVI data.
85+
86+
![The script example 4](fig/qgis_isoluminant.jpg)
87+
88+
## References
89+
90+
Not used specifically for defining color mapping, but useful reading for designing color maps:
91+
92+
OíDonoghue, Se·n I., et al. ìVisualization of Biomedical Data.î Annual Review of Biomedical Data Science, vol. 1, no. 1, 2018, pp. 275ñ304. Annual Reviews, doi:10.1146/annurev-biodatasci-080917-013424.
93+
94+
Moreland, Kenneth. ìWhy We Use Bad Color Maps and What You Can Do About It.î Electronic Imaging, vol. 2016, no. 16, Feb. 2016, pp. 1ñ6. IngentaConnect, doi:10.2352/ISSN.2470-1173.2016.16.HVEI-133.
95+
96+
Mason, Betsy. ìWhy Scientists Need to Be Better at Data Visualization.î Knowable Magazine | Annual Reviews, Annual Reviews, Nov. 2019. www.knowablemagazine.org, doi:10.1146/knowable-110919-1.
97+
98+
## Credits
99+
100+
Borland, David, and M. Russell Taylor. ìRainbow Color Map (Still) Considered Harmful.î IEEE Computer Graphics and Applications, vol. 27, no. 2, Apr. 2007, pp. 14ñ17. PubMed, doi:10.1109/mcg.2007.323435.
101+
102+
CIE Publication No. 015: Colorimetry. Central Bureau of the CIE, Vienna (2004)
103+
104+
Cleveland, William S., and McGill, Robert. ìGraphical Perception: Theory, Experimentation, and Application to the Development of Graphical Methods.î Journal of the American Statistical Association, vol. 79, no. 387, [American Statistical Association, Taylor & Francis, Ltd.], 1984, pp. 531ñ54. JSTOR, JSTOR, doi:10.2307/2288400.
105+
106+
Judd, Deane B., et al. ìSpectral Distribution of Typical Daylight as a Function of Correlated Color Temperature.î JOSA, vol. 54, no. 8, Optical Society of America, Aug. 1964, pp. 1031ñ40. www.osapublishing.org, doi:10.1364/JOSA.54.001031.
107+
108+
Kovesi, Peter. ìGood Colour Maps: How to Design Them.î ArXiv:1509.03700 [Cs], Sept. 2015. arXiv.org, http://arxiv.org/abs/1509.03700.
109+
110+
Smith, T., and J. Guild. ìThe C.I.E. Colorimetric Standards and Their Use.î Transactions of the Optical Society, vol. 33, no. 3, IOP Publishing, Jan. 1931, pp. 73ñ134. Institute of Physics, doi:10.1088/1475-4878/33/3/301.
Loading

sentinel-2/puck/fig/lena_ndwi.jpg

682 KB
Loading
295 KB
Loading

sentinel-2/puck/fig/sequim_ndvi.jpg

5 MB
Loading

sentinel-2/puck/script.js

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
//VERSION=3
2+
3+
/*
4+
Perceptually-Uniform Colormap Kit
5+
author: Keenan Ganz ([email protected])
6+
September 2020
7+
*/
8+
9+
/*
10+
Reference white values for D65 illuminant,
11+
secondary observer.
12+
*/
13+
var REF_X = 95.0489
14+
var REF_Y = 100.0
15+
var REF_Z = 108.8840
16+
17+
function percent_between(min, max, x){
18+
return (x - min) / (max - min)
19+
}
20+
21+
function clip(min, max, x) {
22+
return Math.min(max, Math.max(min, x))
23+
}
24+
25+
function rgb2lab(rgb) {
26+
/*
27+
Convert rgb coordinates to CIELAB coordinates via XYZ.
28+
Expects normalized RGB values.
29+
30+
Arithmetic from easyrgb.com
31+
*/
32+
let [r, g, b] = rgb
33+
34+
function to_linear(val) {
35+
if (val > 0.04045)
36+
return Math.pow((val + 0.055) / 1.055, 2.4)
37+
else
38+
return val / 12.92
39+
}
40+
41+
let r_lin = to_linear(r) * 100
42+
let g_lin = to_linear(g) * 100
43+
let b_lin = to_linear(b) * 100
44+
45+
let x = r_lin * 0.4124 + g_lin * 0.3576 + b_lin * 0.1805
46+
let y = r_lin * 0.2126 + g_lin * 0.7152 + b_lin * 0.0722
47+
let z = r_lin * 0.0193 + g_lin * 0.1192 + b_lin * 0.9505
48+
49+
let x_std = x / REF_X
50+
let y_std = y / REF_Y
51+
let z_std = z / REF_Z
52+
53+
function std_prep(val) {
54+
if (val > 0.008856)
55+
return Math.pow(val, 1.0 / 3.0)
56+
else
57+
return val * 7.787 + (16.0 / 116.0)
58+
}
59+
let L = 116.0 * (std_prep(y_std) - 16.0 / 116.0)
60+
let a = 500.0 * (std_prep(x_std) - std_prep(y_std))
61+
b = 200.0 * (std_prep(y_std) - std_prep(z_std))
62+
63+
return [L, a, b]
64+
}
65+
66+
function lab2rgb(Lab) {
67+
/*
68+
Convert CIELAB coordinates to RGB coordinates.
69+
70+
Arithmetic from easyrgb.com
71+
*/
72+
let [L, a, b] = Lab
73+
let var_y = (L + 16.0) / 116.0
74+
let var_x = (a / 500.0) + var_y
75+
let var_z = var_y - (b / 200.0)
76+
77+
function undo_std_prep(val) {
78+
if (Math.pow(val, 3.0) > 0.008856)
79+
return Math.pow(val, 3.0)
80+
else
81+
return (val - (16.0 / 116.0)) / 7.787
82+
}
83+
var_y = undo_std_prep(var_y)
84+
var_x = undo_std_prep(var_x)
85+
var_z = undo_std_prep(var_z)
86+
87+
let x = var_x * REF_X / 100
88+
let y = var_y * REF_Y / 100
89+
let z = var_z * REF_Z / 100
90+
91+
let var_r = x * 3.2406 + y * -1.5372 + z * -0.4986
92+
let var_g = x * -0.9689 + y * 1.8758 + z * 0.0415
93+
let var_b = x * 0.0557 + y * -0.2040 + z * 1.0570
94+
95+
function undo_linear(val) {
96+
if (val > 0.0031308)
97+
return 1.055 * Math.pow(val, (1.0 / 2.4)) - 0.055
98+
else
99+
return val * 12.92
100+
}
101+
102+
let r = Math.max(undo_linear(var_r), 0)
103+
let g = Math.max(undo_linear(var_g), 0)
104+
b = Math.max(undo_linear(var_b), 0)
105+
106+
// mapping isn't perfect, constrain to [0, 1]
107+
return [clip(0, 1, r), clip(0, 1, g), clip(0, 1, b)]
108+
}
109+
110+
class Colormap {
111+
/*
112+
Base class for making perceptually uniform color maps. Using this class on its own
113+
simply maps RGB coordinates to LAB space and linearly interpolates inbetween values.
114+
115+
Use SequentialColorMap, DivergentColorMap, etc. for more specific use cases.
116+
*/
117+
constructor(color_anchors, data_anchors, remap=true, uniform=false) {
118+
if (color_anchors.length < 1)
119+
throw "ColorMap requires at least one color."
120+
if (color_anchors.length != data_anchors.length)
121+
throw "Color and data anchors must be of same length."
122+
// verify that the data array is sorted low to high
123+
for (let i = 1; i < data_anchors.length; i++) {
124+
if (data_anchors[i] < data_anchors[i - 1])
125+
throw "Data anchors array must be sorted."
126+
}
127+
// map incoming rgb coordinates into LAB space
128+
this.data_anchors = data_anchors
129+
if (remap) this.color_anchors = color_anchors.map(rgb2lab)
130+
else this.color_anchors = color_anchors
131+
// do the lightness correction, if desired, and then check
132+
// if the correction moved colors outside of RGB space
133+
if (uniform) { this._lightness_correction() }
134+
}
135+
136+
_lightness_correction() {return}
137+
138+
get_color(data_value) {
139+
// return edge values if data value is oob
140+
if (data_value <= this.data_anchors[0])
141+
return lab2rgb(this.color_anchors[0])
142+
else if (data_value >= this.data_anchors[this.data_anchors.length - 1])
143+
return lab2rgb(this.color_anchors[this.color_anchors.length-1])
144+
145+
return lab2rgb(colorBlend(data_value, this.data_anchors, this.color_anchors))
146+
}
147+
}
148+
149+
class LinearColormap extends Colormap {
150+
/*
151+
Simple linear ramp color map class. Set uniform to true
152+
in the constructor to enforce constant lightness.
153+
*/
154+
constructor(color_anchors, data_anchors, uniform=true) {
155+
super(color_anchors, data_anchors, true, uniform)
156+
}
157+
158+
_lightness_correction() {
159+
// get overall change in lightness
160+
let L0 = this.color_anchors[0][0]
161+
let Lp = this.color_anchors[this.color_anchors.length-1][0]
162+
let dL = Lp - L0
163+
164+
// make the lightness values monotonically change
165+
for (let i = 1; i < this.color_anchors.length - 1; i++) {
166+
let percent_interval = percent_between(
167+
this.data_anchors[this.data_anchors.length - 1],
168+
this.data_anchors[0],
169+
this.data_anchors[i]
170+
)
171+
this.color_anchors[i][0] = L0 + (dL * percent_interval)
172+
}
173+
}
174+
};
175+
176+
class DivergentColormap extends Colormap {
177+
/*
178+
Color map that reaches a max/min lightness at its center and changes
179+
monotonically toward the edges of the data with equal lightness
180+
at each edge. If you don't want the lightness correction, just use
181+
LinearColormap with uniform=false.
182+
*/
183+
constructor(color_anchors, data_anchors) {
184+
if (color_anchors.length % 2 != 1)
185+
throw "DivergentColorMap must have an odd number of anchors."
186+
super(color_anchors, data_anchors, true, true)
187+
}
188+
189+
_lightness_correction() {
190+
// L0 is the mean lightness of the edge colors
191+
// Lp is the lightness of the center color
192+
let len = this.color_anchors.length
193+
let L0 = (this.color_anchors[0][0] + this.color_anchors[len - 1][0]) / 2
194+
// set the edge colors lightness to the mean
195+
this.color_anchors[len - 1][0] = this.color_anchors[0][0] = L0
196+
197+
// Calculate intermediate lightness values
198+
let Lp = this.color_anchors[Math.floor(len / 2)][0]
199+
let dL = Lp - L0
200+
201+
for (let i = 1; i < Math.floor(len / 2); i++) {
202+
let left_percent_interval = percent_between(
203+
this.data_anchors[Math.floor(len / 2)],
204+
this.data_anchors[0],
205+
this.data_anchors[i]
206+
)
207+
// Invert right % since lightness trends the other way now
208+
let right_percent_interval = 1 - percent_between(
209+
this.data_anchors[len - 1],
210+
this.data_anchors[Math.floor(len / 2)],
211+
this.data_anchors[len - 1 - i]
212+
)
213+
this.color_anchors[i][0] = L0 + (dL * left_percent_interval)
214+
this.color_anchors[len - 1 - i][0] = L0 + (dL * right_percent_interval)
215+
}
216+
}
217+
}
218+
219+
class IsoluminantColormap extends Colormap {
220+
/*
221+
Color map that enforces constant lightness. Not particularly pretty on its own,
222+
but can be overlaid on relief shading to visualize data on top of topography.
223+
*/
224+
constructor(color_anchors, data_anchors) {
225+
super(color_anchors, data_anchors, true, true)
226+
}
227+
_lightness_correction() {
228+
// get the mean lightness of all colors
229+
let L_sum = 0
230+
for (let i = 0; i < this.color_anchors.length; i++) {
231+
L_sum += this.color_anchors[i][0]
232+
}
233+
let L_avg = L_sum / this.color_anchors.length
234+
235+
// set all anchors to have lightness of this value
236+
for (let i = 0; i < this.color_anchors.length; i++) {
237+
this.color_anchors[i][0] = L_avg
238+
}
239+
}
240+
}
241+
242+
/*
243+
Example usage: Masked NDVI on a white-green
244+
color map.
245+
*/
246+
247+
var map = [
248+
[217/255, 229/255, 206/255],
249+
[12/255, 22/255, 3/255],
250+
]
251+
252+
var data = [-0.1, 1]
253+
254+
var cmap = new LinearColormap(map, data);
255+
256+
function setup() {
257+
return {
258+
input: ["B02", "B03", "B04", "B08", "dataMask"],
259+
output: {
260+
bands: 3
261+
}
262+
};
263+
}
264+
265+
function trueColor(sample){
266+
return [sample.B04 * 2.5, sample.B03 * 2.5, sample.B02 * 2.5]
267+
}
268+
269+
function evaluatePixel(sample) {
270+
let ndvi = (sample.B08 - sample.B04) / (sample.B04 + sample.B08)
271+
if (ndvi > -.1) {return cmap.get_color(ndvi)}
272+
else {return trueColor(sample)}
273+
}

0 commit comments

Comments
 (0)