Leaflet COG TIFF – COG TIFF Multiband in Leaflet

cloud-optimized-geotiffgeotiff-tiffleaflet

I am attempting to display a 4-band Sentinel image where the RGB bands are the first 3 bands, each represented as uint16.

Reason: Currently viewers like COGEO tinyurl.com/3jy9jjcy do stretch each COG tile COGEO : each tile is stretched and therefore,

Expected result: we need on stretching over the entire zoomlevel like in QGIS QGIS : zoomlevel is stretched

To achieve this, I am using Leaflet with GeoRaster, based on an existing example. My goal is to:
A) Enhance each RGB band using the formula band[i] = Math.cbrt(0.6 * band[i] – 0.035).
B) Convert the result to RGB 8-bit for display.

I am encountering difficulties in parsing the channels, enhancing them, and converting them for display. In my JavaScript code my.js, I am struggling to access each band individually with new GeoRasterLayer: The resulting image is all black Leaflet as in  my.js

var layer = new GeoRasterLayer({
    attribution: "Unknown",
    georaster: georaster,
    resolution: 128,
    pixelValuesToColorFn: function(values) {
        // Stretch values using cube root function for the first three bands
        var stretchedValues = [
            Math.cbrt(0.6 * values[0] - 0.035), // Stretch first band
            Math.cbrt(0.6 * values[1] - 0.035), // Stretch second band
            Math.cbrt(0.6 * values[2] - 0.035)  // Stretch third band
        ];
        // Scale stretched values to 8-bit RGB
        var scaledValues = stretchedValues.map(value => Math.round(value * 255));
        return scaledValues;
    }
});

There is no error thrown (exxept the one forovr which can be ignored)no error for the script above but black result

How can I properly access each band in the GeoTIFF file, apply the enhancement formula on zoomlevel, and show the result to RGB 8-bit for display using Leaflet and GeoRaster?

Best Answer

I have absolutely no knowledge about COG and GeoTIFF, so this answer is only how to make the code from question show at least something.

First problem: If you look at the content of georaster object, mins and maxs properties tell about min and max values for each channel. Since max values for all three channels are a little below 15000, calculated color values in scaledValues are way above 255.

Second problem: pixelValuesToColorFn option function should return CSS color definition as string, like for example: rgba(100, 50, 250, 1).

Third problem: Sentinel image is in EPSG:2056 projection, so to display it correctly in Leaflet map, this projection has to be used.

Fourth problem: Channel value 9999 stands for no value, so pixel should be transparent.

In the below code channel values are normalized to 255 value and some Swiss map with EPSG:2056 projection is used as a background:

var crs2056 = new L.Proj.CRS('EPSG:2056',
  "+proj=somerc +lat_0=46.95240555555556 +lon_0=7.439583333333333 +k_0=1 +x_0=2600000 +y_0=1200000 +ellps=bessel +towgs84=674.374,15.056,405.346,0,0,0,0 +units=m +no_defs",
  {
    resolutions: [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5, 0.25, 0.125, 0.0625],
    origin: [2485071.58, 1299941.79],
  }
 );

var map = L.map('map', {
  crs: crs2056
}).setView([46, 8], 1);

var map4 = new L.tileLayer.wms("https://wms0.geo.admin.ch/", {
    layers: 'ch.vbs.milairspacechart',
    format: 'image/png',
    transparent: true,
    crs: crs2056
}).addTo(map);

var url_to_geotiff_file = "/data/S2_L2A_SR_20240106T102329_20240106T102326_10M_run20240107.tif.png";

fetch(url_to_geotiff_file)
  .then(response => response.arrayBuffer())
  .then(arrayBuffer => {
    parseGeoraster(arrayBuffer).then(georaster => {      
      var layer = new GeoRasterLayer({
        georaster: georaster,
        resolution: 128,
        pixelValuesToColorFn: function(values) {
          var color, sv;
          if ((values[0] == 9999) && (values[1] == 9999) && (values[2] == 9999))
            color = 'rgba(0,0,0,0)';
          else {
            sv = [
              Math.round((values[0] / georaster.maxs[0]) * 255),
              Math.round((values[1] / georaster.maxs[1]) * 255),
              Math.round((values[2] / georaster.maxs[2]) * 255)
            ];
            color = 'rgba(' + sv[0] + ',' + sv[1] + ',' + sv[2] + ', 1)';
          }
          return color;
        }
      });
      layer.addTo(map);     
      map.fitBounds(layer.getBounds());    
  });
});

This is how the result looks like:

enter image description here

Now to get desired/correct image just correct formulas have to be used for converting channel values to RGB.

P.S.: Since image has 1.6 GB, it took ages to show this in Firefox on Windows 10. Chrome and Edge both failed.

Related Question