Sentinel-2 fCover

I came across the custom script for fAPAR, which is really nice: https://github.com/sentinel-hub/custom-scripts/tree/master/sentinel-2/fapar
Does anyone have the alternative for fCOVER? It’s from the same SNAP toolbox, just different neural network. Could try to find out myself, but should someone have done this already, would be nice to know :slight_smile:

Hi Kristof,

we do no have a custom script for fCOVER (yet).
If you decide to implement it, we would be happy to help and would encourage you to then share it :slight_smile:

There are other existing scripts, which also implement NN algorithms (although I am not really familiar with fCOVER algorithm) and can thus be helpful:

Best, Anja

Hi Anja!

Ok so the fCOVER algorithm is actually very similar to the fAPAR one, implemented in the same SNAP biopar toolbox. It’s based on INRA’s neural network. So I took the fAPAR algorithm and updated all biases, weights and normalization scales, and at first sight it looks already quite good, though it would be good if you could give it a try in a custom layer as well, to verify empirically if this gives realistic results. fCOVER is also supposed to be between 0 and 1.
As soon as we decide this is useful, it would be great to share it as a custom script for everyone to use :slight_smile:

var degToRad = Math.PI / 180;

function evaluatePixel(samples) {
  var sample = samples[0];
  var b03_norm = normalize(sample.B03, 0, 0.253061520472);
  var b04_norm = normalize(sample.B04, 0, 0.290393577911);
  var b05_norm = normalize(sample.B05, 0, 0.305398915249);
  var b06_norm = normalize(sample.B06, 0.00663797254225, 0.608900395798);
  var b07_norm = normalize(sample.B07, 0.0139727270189, 0.753827384323);
  var b8a_norm = normalize(sample.B8A, 0.0266901380821, 0.782011770669);
  var b11_norm = normalize(sample.B11, 0.0163880741923, 0.493761397883);
  var b12_norm = normalize(sample.B12, 0, 0.49302598446);
  var viewZen_norm = normalize(Math.cos(sample.viewZenithMean * degToRad), 0.918595400582, 0.999999999991);
  var sunZen_norm  = normalize(Math.cos(sample.sunZenithAngles * degToRad), 0.342022871159, 0.936206429175);
  var relAzim_norm = Math.cos((sample.sunAzimuthAngles - sample.viewAzimuthMean) * degToRad)

  var n1 = neuron1(b03_norm,b04_norm,b05_norm,b06_norm,b07_norm,b8a_norm,b11_norm,b12_norm, viewZen_norm,sunZen_norm,relAzim_norm);
  var n2 = neuron2(b03_norm,b04_norm,b05_norm,b06_norm,b07_norm,b8a_norm,b11_norm,b12_norm, viewZen_norm,sunZen_norm,relAzim_norm);
  var n3 = neuron3(b03_norm,b04_norm,b05_norm,b06_norm,b07_norm,b8a_norm,b11_norm,b12_norm, viewZen_norm,sunZen_norm,relAzim_norm);
  var n4 = neuron4(b03_norm,b04_norm,b05_norm,b06_norm,b07_norm,b8a_norm,b11_norm,b12_norm, viewZen_norm,sunZen_norm,relAzim_norm);
  var n5 = neuron5(b03_norm,b04_norm,b05_norm,b06_norm,b07_norm,b8a_norm,b11_norm,b12_norm, viewZen_norm,sunZen_norm,relAzim_norm);
  
  var l2 = layer2(n1, n2, n3, n4, n5);
  
  var fcover = denormalize(l2, 0.000181230723879, 0.999638214715);
  return {
    default: [fcover]
  }
}

function neuron1(b03_norm,b04_norm,b05_norm,b06_norm,b07_norm,b8a_norm,b11_norm,b12_norm, viewZen_norm,sunZen_norm,relAzim_norm) {
  var sum =
	- 1.45261652206
	- 0.156854264841 * b03_norm
	+ 0.124234528462 * b04_norm
	+ 0.235625516229 * b05_norm
	- 1.8323910258 * b06_norm
	- 0.217188969888 * b07_norm
	+ 5.06933958064 * b8a_norm
	- 0.887578008155 * b11_norm
	- 1.0808468167 * b12_norm
	- 0.0323167041864 * viewZen_norm
	- 0.224476137359 * sunZen_norm
	- 0.195523962947 * relAzim_norm;

  return tansig(sum);
}

function neuron2(b03_norm,b04_norm,b05_norm,b06_norm,b07_norm,b8a_norm,b11_norm,b12_norm, viewZen_norm,sunZen_norm,relAzim_norm) {
  var sum =
	-1.70417477557
	- 0.220824927842 * b03_norm
	+ 1.28595395487 * b04_norm
	+ 0.703139486363 * b05_norm
	- 1.34481216665 * b06_norm
	- 1.96881267559 * b07_norm
	- 1.45444681639 * b8a_norm
	+ 1.02737560043 * b11_norm
	- 0.12494641532 * b12_norm
	+ 0.0802762437265 * viewZen_norm
	- 0.198705918577 * sunZen_norm
	+ 0.108527100527 * relAzim_norm;

  return tansig(sum);
}

function neuron3(b03_norm,b04_norm,b05_norm,b06_norm,b07_norm,b8a_norm,b11_norm,b12_norm, viewZen_norm,sunZen_norm,relAzim_norm) {
  var sum =
	+ 1.02168965849
	- 0.409688743281 * b03_norm
	+ 1.08858884766 * b04_norm
	+ 0.36284522554 * b05_norm
	+ 0.0369390509705 * b06_norm
	- 0.348012590003 * b07_norm
	- 2.0035261881 * b8a_norm
	+ 0.0410357601757 * b11_norm
	+ 1.22373853174 * b12_norm
	+ -0.0124082778287 * viewZen_norm
	- 0.282223364524 * sunZen_norm
	+ 0.0994993117557 * relAzim_norm;

  return tansig(sum);
}

function neuron4(b03_norm,b04_norm,b05_norm,b06_norm,b07_norm,b8a_norm,b11_norm,b12_norm, viewZen_norm,sunZen_norm,relAzim_norm) {
  var sum =
	- 0.498002810205
	- 0.188970957866 * b03_norm
	- 0.0358621840833 * b04_norm
	+ 0.00551248528107 * b05_norm
	+ 1.35391570802 * b06_norm
	- 0.739689896116 * b07_norm
	- 2.21719530107 * b8a_norm
	+ 0.313216124198 * b11_norm
	+ 1.5020168915 * b12_norm
	+ 1.21530490195 * viewZen_norm
	- 0.421938358618 * sunZen_norm
	+ 1.48852484547 * relAzim_norm;

  return tansig(sum);
}

function neuron5(b03_norm,b04_norm,b05_norm,b06_norm,b07_norm,b8a_norm,b11_norm,b12_norm, viewZen_norm,sunZen_norm,relAzim_norm) {
  var sum =
	- 3.88922154789
	+ 2.49293993709 * b03_norm
	- 4.40511331388 * b04_norm
	- 1.91062012624 * b05_norm
	- 0.703174115575 * b06_norm
	- 0.215104721138 * b07_norm
	- 0.972151494818 * b8a_norm
	- 0.930752241278 * b11_norm
	+ 1.2143441876 * b12_norm
	- 0.521665460192 * viewZen_norm
	- 0.445755955598 * sunZen_norm
	+ 0.344111873777 * relAzim_norm;
	
  return tansig(sum);
}

function layer2(neuron1, neuron2, neuron3, neuron4, neuron5) {
  var sum =
	- 0.0967998147811
	+ 0.23080586765 * neuron1
	- 0.333655484884 * neuron2
	- 0.499418292325 * neuron3
	+ 0.0472484396749 * neuron4
	- 0.0798516540739 * neuron5;

  return sum;
}

function normalize(unnormalized, min, max) {
  return 2 * (unnormalized - min) / (max - min) - 1;
}
function denormalize(normalized, min, max) {
  return 0.5 * (normalized + 1) * (max - min) + min;
}
function tansig(input) {
  return 2 / (1 + Math.exp(-2 * input)) - 1; 
}

function setup(ds) {
    return {
        components: [ds.B03, ds.B04, ds.B05, ds.B06, ds.B07, ds.B8A, ds.B11, ds.B12, ds.viewZenithMean, ds.viewAzimuthMean, ds.sunZenithAngles, ds.sunAzimuthAngles],
        output: [
            {
                id: "default",
                sampleType: SampleType.AUTO,
                componentCount: 1
            }
        ]
    }
}
1 Like

Hi Kristof,

nice!

I’ve checked quickly and it looks good. At least from the technical view, I would not dare to interpret the results. The only thing I had to add to the script was “//VERSION=2” in the first line.

When you are happy with the script you can add it our github repository yourself https://github.com/sentinel-hub/custom-scripts (instructions: https://github.com/sentinel-hub/custom-scripts/tree/master/example) or, if you do not want to bother with this, let me know and we will push it to the repository.

P.S.: I have heard rumors that Sentinel Hub custom scripts contest, edition 2 will start soon. A script like this can take you far :slight_smile:

To the interested reader: after almost a one-year delay, this stuff is finally officially integrated in the custom-scripts library :sweat_smile:

3 Likes

Hi Kristof,

It looks like that custom script for FAPAR was committed to custom script GitHub repo instead of fCover: https://github.com/sentinel-hub/custom-scripts/blob/master/sentinel-2/fcover/script.js.

Could you check and commit the correct version? If the fCover script posted above is up-to-date, I can do it as well, if you wish.

Thanks,
Anze

Wow that’s bad! Apologies, that shouldn’t have happened. I opened a new PR with the correct version. Please double-check that I did it right this time. This whole process was my first experience with forking and creating PRs, so it’s a learning curve :wink:

Kristof

Wow. That was fast. Thank you again!