Reputation: 7912
I am using Mapbox to add a red rectangle to a map. I would like to modify the colours of my rectangle. I use the raster-color
Paint property to achieve this.
Mapbox provides the example Add a raster image to a map layer. Based on this, I create the folowing:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="initial-scale=1,maximum-scale=1,user-scalable=no"
/>
<link
href="https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css"
rel="stylesheet"
/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken =
"pk.eyJ1IjoicGFsc3phYm8iLCJhIjoiY2xrNWY3cDhuMGpiajNwbzdlNzlscHc1eSJ9.Sppso1puTUCwR03aaWUHsQ";
const map = new mapboxgl.Map({
container: "map",
maxZoom: 10,
minZoom: 0,
zoom: 6,
center: [-82, 0],
style: "mapbox://styles/mapbox/dark-v11",
});
map.on("load", () => {
map.addSource("image", {
type: "image",
url: "https://upload.wikimedia.org/wikipedia/commons/8/89/Alizarin_crimson_(color).jpg",
coordinates: [
[-83, 1],
[-81, 1],
[-81, -1],
[-83, -1],
],
});
map.addLayer({
id: "radar-layer",
type: "raster",
source: "image",
paint: {
"raster-fade-duration": 0,
},
});
});
</script>
</body>
</html>
A red square does indeed show up on the Ecuadorian shores (location specified by coordinates
above):
To obtain the exact RGB values of the red rectangle, I download it (curl "https://upload.wikimedia.org/wikipedia/commons/8/89/Alizarin_crimson_(color).jpg" --output shape.jpg
), then process it with Python:
import imageio.v3 as iio
import numpy as np
array = iio.imread('shape.jpg')
print(np.unique(array[:,:,0],return_counts=True))
print(np.unique(array[:,:,1],return_counts=True))
print(np.unique(array[:,:,2],return_counts=True))
Output:
(array([226], dtype=uint8), array([686460]))
(array([38], dtype=uint8), array([686460]))
(array([53], dtype=uint8), array([686460]))
This tells us that every pixel in the rectangle has colour RGB(226,38,53)
. Knowing this, I can modify paint
so that all source pixels which have R values between 221 and 229 are shown as rgb(123,222,111,255)
(an example colour).
paint: {
"raster-color": [
"interpolate",
["linear"],
["raster-value"],
220 / 255,
"rgba(0,0,0,0)",
221 / 255,
"rgba(123,222,111,255)",
229 / 255,
"rgba(123,222,111,255)",
230 / 255,
"rgba(0,0,0,0)",
],
"raster-color-mix": [1, 0, 0, 0],
"raster-color-range": [220 / 255, 230 / 255],
"raster-resampling": "nearest",
},
Specifying raster-color-mix
to be [1, 0, 0, 0]
ensures that the so-called raster-value
is equal to the R channel. This then serves as a parameter of raster-color
, which is:
Defines a color map by which to colorize a raster layer, parameterized by the
["raster-value"]
expression and evaluated at 256 uniformly spaced steps over the range specified byraster-color-range
.
raster-color-range
specifies the range I want raster-color
to be parametrized over. Keeping it as narrow as possible helps, as the parametrisation happens in 256 steps (source: raster-color
line 2).
Editing paint
does indeed result in a differently coloured square:
However, if I want to use a narrower range of R values, the square does not show up. According to Python (see above), R channel is always 226, so remapping all values between 225 and 227 should indeed work:
paint: {
"raster-color": [
"interpolate",
["linear"],
["raster-value"],
224 / 255,
"rgba(0,0,0,0)",
225 / 255,
"rgba(123,222,111,255)",
227 / 255,
"rgba(123,222,111,255)",
228 / 255,
"rgba(0,0,0,0)",
],
"raster-color-mix": [1, 0, 0, 0],
"raster-color-range": [224 / 255, 228 / 255],
"raster-resampling": "nearest",
},
But no square shows up. After a few trial and error, I find that the correct range to remap is somewhere between 223.3 and 223.4. If I use these values, the square does indeed show up again:
paint: {
"raster-color": [
"interpolate",
["linear"],
["raster-value"],
223.3 / 255,
"rgba(0,0,0,0)",
223.31 / 255,
"rgba(123,222,111,255)",
223.39 / 255,
"rgba(123,222,111,255)",
223.4 / 255,
"rgba(0,0,0,0)",
],
"raster-color-mix": [1, 0, 0, 0],
"raster-color-range": [223.3 / 255, 223.4 / 255],
"raster-resampling": "nearest",
},
I can conclude that if I treat the pixels (all of them have R=226) as if their R value is somewhere between 223.3 and 223.4, I can recolour them.
Based on experiments on a different image: when R is 118, the range I need to specify for recolouring is centered roughly on 116.631.
Finding the correct range by trial and error is quite tedious, and prone to error. Using a wide range is problematic as well, as I often want to recolour pixels with similar R values differently. Wide, overlapping ranges prevent this, so I want to use as narrow ranges as possible.
How can I find out the values I need to use in raster-color
to recolour those (and only those) pixels with a given R
value?
Upvotes: 3
Views: 840
Reputation: 7912
One possible fix is to normalise the colours by dividing them by 258, and not 255.
Here are WEBP images. Each of them have a unique R value indicated in the filename, while all their G and B values are 0. (Images were created using this script.) For example, R151.webp:
All its pixels have RGB=(151,0,0)
.
So, the HTML page which displays a green square on the Ecuadorian shores (using images having whichever R
we want, without us having to guess the correct values for raster-color
), is:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="initial-scale=1,maximum-scale=1,user-scalable=no"
/>
<link
href="https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css"
rel="stylesheet"
/>
<script src="https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
mapboxgl.accessToken =
"pk.eyJ1IjoicGFsc3phYm8iLCJhIjoiY2xrNWY3cDhuMGpiajNwbzdlNzlscHc1eSJ9.Sppso1puTUCwR03aaWUHsQ";
const map = new mapboxgl.Map({
container: "map",
maxZoom: 10,
minZoom: 0,
zoom: 6,
center: [-82, 0],
style: "mapbox://styles/mapbox/dark-v11",
});
const r = 151; // choose any integer between 0 and 255 both inclusive, and a square will show up
const normalizer = 258; // choose any other integer, and no square will show up
map.on("load", () => {
map.addSource("image", {
type: "image",
url:
"https://raw.githubusercontent.com/zabop/mapboxDebug/master/webps/R" +
r +
".webp",
coordinates: [
[-83, 1],
[-81, 1],
[-81, -1],
[-83, -1],
],
});
map.addLayer({
id: "radar-layer",
type: "raster",
source: "image",
paint: {
"raster-color": [
"interpolate",
["linear"],
["raster-value"],
(r - 0.5) / normalizer,
"rgba(0,0,0,0)",
(r - 0.4) / normalizer,
"rgba(123,222,111,255)",
(r + 0.4) / normalizer,
"rgba(123,222,111,255)",
(r + 0.5) / normalizer,
"rgba(0,0,0,0)",
],
"raster-color-mix": [1, 0, 0, 0],
"raster-color-range": [
(r - 0.5) / normalizer,
(r + 0.5) / normalizer,
],
"raster-resampling": "nearest",
},
});
});
</script>
</body>
</html>
The 2 most important parts of the script:
1.
This is where we can define which input image we want to use, and the number we want to divide the original R values by, before passing them to mapbox:
const r = 151; // choose any integer between 0 and 255 both inclusive, and a square will show up
const normalizer = 258; // choose any other integer, and no square will show up
Based on experiments, all values between 0
and 255
both inclusive work for r
, but if the normalizer is not 258
, then the square does not show up for all r
values.
2.
The paint
property now only depends on the chosen r
value, no need to define the "color map" (mentioned here) manually:
paint: {
"raster-color": [
"interpolate",
["linear"],
["raster-value"],
(r - 0.5) / normalizer,
"rgba(0,0,0,0)",
(r - 0.4) / normalizer,
"rgba(123,222,111,255)",
(r + 0.4) / normalizer,
"rgba(123,222,111,255)",
(r + 0.5) / normalizer,
"rgba(0,0,0,0)",
],
"raster-color-mix": [1, 0, 0, 0],
"raster-color-range": [
(r - 0.5) / normalizer,
(r + 0.5) / normalizer,
],
"raster-resampling": "nearest",
},
Using values close to r
(such as r - 0.5
and r + 0.5
) ensures that the recolouring only happens for the pixels with the specified R
value, and not to any other pixels. (This relies of course on R
being an integer, which is a a fair assumption in most cases I believe.)
Upvotes: 1