4
\$\begingroup\$

I'm using Google Earth Engine (GEE) to calculate the Euclidean distance from sample points to the nearest brightest pixel above the threshold value "avg_rad > 10" in nighttime light data. Some of the point coordinates I use as the base shapefile are as below:

U   -14.335572  35.639824
U   -13.203245  37.501709
U   -15.198814  35.863956
R   -13.422739  34.870815
R   -13.203394  35.187214
R   -15.004614  36.633186
R   -14.895029  36.772968
R   -14.597268  36.350857
R   -14.488451  36.53352

My approach:

  • Process VIIRS Data: Compute the median of 2021 data and mask bright pixels (avg_rad > 10).
  • Compute Distance: Use fastDistanceTransform(1024).sqrt().
  • Find Nearest Bright Pixel: Convert bright pixels to centroids with reduceToVectors(), then match them to sample points.
  • Extract Distance: Sample distance_to_bright at sample points.

Am I coding it correctly?

// ==========================
// 1️⃣ Load & Process Nighttime Light Data (VIIRS)
// ==========================
var nightLights = ee.ImageCollection('NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG')
  .filterDate('2021-01-01', '2021-12-31')  // Use one year of data
  .median()  // Reduce noise by computing the median composite
  .select('avg_rad');  // Select the average radiance band
 
// ==========================
// 2️⃣ Compute Euclidean Distance to Nearest Bright Pixel (avg_rad > 10)
// ==========================
var brightPixels = nightLights.gt(10);  // Mask for bright pixels
 
// Compute Euclidean distance to the nearest bright pixel
var distToBright = brightPixels.fastDistanceTransform(1024).sqrt()
  .rename('distance_to_bright');
 
// ==========================
// 3️⃣ Define Sample Points
// ==========================
 
// Load sample points from shapefile
var samplePoints = ee.FeatureCollection("projects/ee-researchgamage/assets/base1");
 
// ==========================
// 4️⃣ Define Region and Extract Nearest Bright Pixel Coordinates
// ==========================
// Ensure processing is limited to the area where night lights exist
var nightLightsBounds = nightLights.geometry();
var region = nightLightsBounds.intersection(samplePoints.geometry().buffer(50000), ee.ErrorMargin(1));
 
var brightPixelPoints = brightPixels.selfMask()
  .reduceToVectors({
    geometryType: 'centroid',
    scale: 500,
    geometry: region,
    geometryInNativeProjection: false,
    maxPixels: 1e7,  // Set max pixels to 10 million
    bestEffort: true  // Allow flexible aggregation
  });
 
 
// Function to find nearest bright pixel for each sample point
var findNearestBrightPixel = function(feature) {
  var nearestBright = brightPixelPoints
    .filterBounds(feature.geometry())  // Search for nearby bright pixels
    .sort('distance_to_bright')  // Sort by distance
    .first();  // Get closest
 
  // Extract coordinates safely
  var brightLonLat = ee.Algorithms.If(
    nearestBright,
    ee.Feature(nearestBright).geometry().coordinates(),
    ee.List([null, null])  // Handle cases with no bright pixel nearby
  );
 
  // Set attributes
  return feature.set({
    'point_lon': feature.geometry().coordinates().get(0),
    'point_lat': feature.geometry().coordinates().get(1),
    'bright_lon': ee.List(brightLonLat).get(0),  // Extract longitude
    'bright_lat': ee.List(brightLonLat).get(1)   // Extract latitude
  });
};
 
// Apply function to sample points
var enrichedSamplePoints = samplePoints.map(findNearestBrightPixel);
 
// ==========================
// 5️⃣ Extract Distance for Sample Points
// ==========================
var sampledDistances = distToBright.sampleRegions({
  collection: enrichedSamplePoints,
  scale: 500,  // Adjust based on VIIRS resolution
  projection: 'EPSG:4326',
  geometries: true
});
 
// Print extracted distances
print("Sampled Distances:", sampledDistances);
 
// ==========================
// 6️⃣ Export CSV with Coordinates
// ==========================
Export.table.toDrive({ 
  collection: sampledDistances,
  description: 'Point_Distance_to_Bright_Night_Areas_2021',  // Naming it as 2020
  folder: 'GEE_Exports',
  fileNamePrefix: 'point_distance_2021',  // Forces the output name to 2020
  fileFormat: 'CSV'
});
//======================
\$\endgroup\$
1

1 Answer 1

5
\$\begingroup\$

Am I coding it correctly?

It may be correct, but it's not obvious to me that it's correct. Possibly avg_rad in the NOAA/VIIRS/DNB/MONTHLY_V1/VCMSLCFG collection has already accounted for time-of-day? I mean, are we essentially measuring human activity, which diminishes when most folks are asleep, or are we mostly measuring urban street lights that stay on all night?

cite a reference

Maintainers of this code would benefit from an URL mentioned in the source code which describes the data, perhaps https://developers.google.com/earth-engine/datasets/catalog/NOAA_VIIRS_DNB_MONTHLY_V1_VCMSLCFG .

Apparently persistent cloud cover can really matter.

it is recommended that users of these data utilize the 'cf_cvg' band

magic number

It seems that .gt(10) works for you, probably based on eyeballing GIS renderings and deciding it was good enough. It would be worth breaking this out as a named parameter, and perhaps adding a comment describing how it was chosen. As a maintenance engineer, it's not clear to me what I would break if I were to tweak this to some nearby value.

validation

I assume there are some invariants we'd like to be able to say about the output, like "Oceans are dark". It would be worth writing an isOcean(lat, lng) predicate based on another dataset, and then validating your results.

The U.S. Census Bureau, and other agencies, publish "population per sq. km" for lots of (lat, lng)'s. A more adventurous validation would verify there's at least a positive correlation between radiance and population density.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for taking the time to review my code and provide such valuable insights! Your points about the interpretation of avg_rad, the potential impact of persistent cloud cover, and the importance of validation are incredibly helpful. I appreciate the suggestion to incorporate the 'cf_cvg' band and define a clearer threshold for avg_rad > 10, ensuring transparency in parameter selection. Additionally, your idea of validating using ocean masks and population density correlations is helpful too! thank you again \$\endgroup\$ Commented Mar 17 at 6:25

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.