zabop
zabop

Reputation: 7912

How to use Mapbox's raster-color to recolour pixels with a specific value in a specific colour channel?

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):

enter image description here

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 by raster-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:

enter image description here

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",
},

enter image description here

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

Answers (1)

zabop
zabop

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:

enter image description here

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>

enter image description here

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

Related Questions