2

I have a single-band raster (US landcover) where each pixel is assigned to one of a few classes. I want to get a random sample of points (lat-long) throughout an area such that each of those points is located within large neighborhoods of pixels of the same landcover class. Currently, I use EE's "randomPoints" to get a random sample of n points but many of those pixels may be isolated pixels belonging to one landcover class surrounded by pixels of other classes. What I need is a set of randomly selected points each surrounded by a neighborhood of radius r of pixels of the same class (as determined by the raster). Any ideas on how to approach this problem?

To clarify, I want points like the one on the right of the figure below (large yellow neighborhood) and not the point on the left. enter image description here Here is my code for the landcover raster within California:

import ee
ee.Initialize()
california = ee.FeatureCollection("TIGER/2018/States").filter(ee.Filter.eq("NAME", 'California')) 
landcover = ee.ImageCollection('USGS/NLCD_RELEASES/2016_REL')\
            .filter(ee.Filter.eq('system:index', '2004')).first().select('landcover')
landcover_ca = landcover.clip(california)
point_sample = ee.FeatureCollection.randomPoints(california, points=1000, seed=10)
2

I suppose you first could mask your landcover image, to remove "isolated" pixels, then sample that masked image. Here's an approach that masks based on ratio of pixels of the same class within a given radius:

var RADIUS = 300 // In meters
var MIN_RATIO = 0.5

var california = ee.FeatureCollection("TIGER/2018/States").filter(ee.Filter.eq("NAME", 'California')) 
var landcover = ee.ImageCollection('USGS/NLCD_RELEASES/2016_REL')
    .filter(ee.Filter.eq('system:index', '2004'))
    .first()
    .select('landcover')
var landcover_ca = landcover.clip(california)

// Hard code this if you have a an image without a class values property
var classValues = ee.List(landcover_ca.get('landcover_class_values'))

// Create a masked image for each class value, then combine into a single image
var maskedLandcover = ee.ImageCollection(
  classValues.map(onlyWithLargeNeighborhood)
).mosaic()

var point_sample = maskedLandcover.sample({
  region: california, 
  scale: 30,
  numPixels: 1000, 
  seed: 10,
  geometries: true
})

Map.addLayer(maskedLandcover.randomVisualizer(), null, 'landcover masked')
Map.addLayer(landcover_ca.randomVisualizer(), null, 'landcover', false)
Map.addLayer(point_sample, null, 'samples')


function onlyWithLargeNeighborhood(classValue) {
  var classValueImage = landcover_ca.updateMask(
    landcover_ca.eq(ee.Number(classValue))
  )
  var counts = classValueImage.reduceNeighborhood({
    reducer: ee.Reducer.count().combine(ee.Reducer.countEvery(), 'total_', false), 
    kernel: ee.Kernel.circle({radius: RADIUS, units: 'meters'})
  })
  var ratio = counts.select('landcover_count').divide(counts.select('landcover_total_count'))
  return classValueImage.updateMask(
    ratio.gte(MIN_RATIO)
  )
}

https://code.earthengine.google.com/3dcf1c81c18b4b979fb4e160d8c67383

UPDATE

If you are getting "Computation timed out" errors, you can try to split your sampling into smaller batches of points:

var BATCH_SIZE = 1000
var NUM_PIXELS = 10000
var point_sample = ee.FeatureCollection(
  ee.List.sequence(1, NUM_PIXELS / BATCH_SIZE)
    .map(function (batch) { // Samples batch of points
      return maskedLandcover
        .sample({
          region: california, 
          scale: 30,
          seed: batch,
          numPixels: 2 * BATCH_SIZE, // Sample some extra to make sure enough points fall on non-masked pixels
          geometries: true
        })
        .limit(BATCH_SIZE)
    })
    .iterate(function (acc, batchCollection) { // Merge batches into a single collection
      return ee.FeatureCollection(acc).merge(
        ee.FeatureCollection(batchCollection)
      )
    }, ee.FeatureCollection([]))
)

https://code.earthengine.google.com/0d602e28fd5f5b6d2e85ac358f382d5b

4
  • This seems to work. However, for slightly larger sample sizes (e.g. numPixels = 3,000), it takes a while and returns "Computation timed out". Is there any way to make this work for even larger sample sizes (e.g. 10,000)?
    – Mohamad
    Sep 6 '21 at 23:38
  • 1
    I updated my answer to show how you can workaround this. Sep 7 '21 at 10:02
  • Fantastic! This works like a charm! Just to understand it, the reason why I get fewer than the requested 10,000 points is that ee.Image.sample does a random sampling of points throughout the image and if it hits a set of pixels that are already masked, it won't return a point (hence the numPixels: 2 * BATCH_SIZE), right? This results in some classes that are naturally less frequent or belong to narrow shapes being undersampled. Is there a way to force it to sample more extensively in those smaller classes? In other words, can it be forced to return 500 points from each landcover class?
    – Mohamad
    Sep 10 '21 at 21:19
  • 1
    Looks at ee.Image.stratifiedSample() for this. Sep 11 '21 at 10:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.