코딩 권장사항

이 문서에서는 복잡하거나 비용이 많이 드는 Earth Engine 계산의 성공 확률을 극대화하기 위한 코딩 관행을 설명합니다. 여기서 설명하는 메서드는 대화형 (예: Code 편집기) 및 일괄(Export) 계산 모두에 적용되지만 일반적으로 장기 실행 계산은 일괄 시스템에서 실행해야 합니다.

클라이언트 함수 및 객체를 서버 함수 및 객체와 혼합하지 않음

Earth Engine 서버 객체는 ee로 시작하는 생성자가 있는 객체(예: ee.Image, ee.Reducer)이며 이러한 객체의 모든 메서드는 서버 함수입니다. 이 방식으로 생성되지 않은 객체는 클라이언트 객체입니다. 클라이언트 객체는 코드 편집기 (예: Map, Chart) 또는 JavaScript 언어 (예: Date, Math, [], {})에서 가져올 수 있습니다.

의도치 않은 동작을 방지하려면 여기, 여기, 여기에 설명된 대로 스크립트에서 클라이언트 함수와 서버 함수를 혼합하지 마세요. Earth Engine의 클라이언트와 서버에 관한 자세한 설명은 이 페이지 또는 이 튜토리얼을 참고하세요. 다음 예는 클라이언트와 서버 기능을 혼합하는 위험성을 보여줍니다.

오류: 이 코드는 작동하지 않습니다.

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Won't work.
for(var i=0; i<table.size(); i++) {
  print('No!');
}

오류를 발견하셨나요? table.size()는 서버 객체의 서버 메서드이며 < 조건문과 같은 클라이언트 측 기능과 함께 사용할 수 없습니다.

Code Editor ui 객체와 메서드는 클라이언트 측이므로 UI 설정과 관련하여 for 루프를 사용해야 할 수 있습니다. (Earth Engine에서 사용자 인터페이스를 만드는 방법 자세히 알아보기) 예를 들면 다음과 같습니다.

UI 설정에 클라이언트 함수 사용

var panel = ui.Panel();
for(var i=1; i<8; i++) {
  panel.widgets().set(i, ui.Button('button ' + i))
}
print(panel);

반대로 map()는 서버 함수이므로 map()에 전달된 함수 내에서는 클라이언트 기능이 작동하지 않습니다. 예를 들면 다음과 같습니다.

오류: 이 코드는 작동하지 않습니다.

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Error:
var foobar = table.map(function(f) {
  print(f); // Can't use a client function here.
  // Can't Export, either.
});

컬렉션의 모든 요소에 대해 작업을 실행하려면 컬렉션에 관한 함수 map()와 속성 set()을 사용합니다.

map()set()를 사용하세요.

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');
print(table.first());

// Do something to every element of a collection.
var withMoreProperties = table.map(function(f) {
  // Set a property.
  return f.set('area_sq_meters', f.area())
});
print(withMoreProperties.first());

계산된 속성 또는 기존 속성을 기반으로 컬렉션을 filter()하고 결과를 print()할 수도 있습니다. 요소가 5,000개를 초과하는 컬렉션은 출력할 수 없습니다. '5, 000개가 넘는 요소가 누적된 후 컬렉션 쿼리가 중단됨' 오류가 발생하면 컬렉션을 filter() 또는 limit()한 후 출력합니다.

불필요하게 목록으로 변환하지 않음

Earth Engine의 컬렉션은 컬렉션을 List 또는 Array 유형으로 변환하여 중단되는 최적화를 사용하여 처리됩니다. 컬렉션 요소에 무작위로 액세스해야 하는 경우 (예: 컬렉션의 i번째 요소를 가져와야 )를 제외하고는 컬렉션의 필터를 사용하여 개별 컬렉션 요소에 액세스합니다. 다음 예는 컬렉션의 요소에 액세스하기 위한 유형 변환 (권장하지 않음)과 필터링 (권장)의 차이를 보여줍니다.

불필요하게 목록으로 변환하지 마세요.

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Do NOT do this!!
var list = table.toList(table.size());
print(list.get(13)); // User memory limit exceeded.

불필요하게 컬렉션을 목록으로 변환하면 오류가 쉽게 발생할 수 있습니다. filter()를 사용하는 것이 더 안전합니다.

filter() 사용

print(table.filter(ee.Filter.eq('country_na', 'Niger')).first());

분석에서 최대한 빨리 필터를 사용해야 합니다.

ee.Algorithms.If() 사용 자제

특히 매핑된 함수에서 ee.Algorithms.If()를 사용하여 분기 로직을 구현하지 마세요. 다음 예에서 볼 수 있듯이 ee.Algorithms.If()는 메모리 사용량이 많을 수 있으므로 권장하지 않습니다.

If()를 사용하지 마세요.

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');

// Do NOT do this!
var veryBad = table.map(function(f) {
  return ee.Algorithms.If({
    condition: ee.String(f.get('country_na')).compareTo('Chad').gt(0),
    trueCase: f,      // Do something.
    falseCase: null   // Do something else.
  });
}, true);
print(veryBad); // User memory limit exceeded.

// If() may evaluate both the true and false cases.

map()의 두 번째 인수는 true입니다. 즉, 매핑된 함수가 null을 반환할 수 있으며 결과 컬렉션에서 null이 삭제됩니다. 이는 유용할 수 있지만 (If() 없이) 여기서는 필터를 사용하는 것이 가장 쉬운 해결 방법입니다.

filter() 사용

print(table.filter(ee.Filter.eq('country_na', 'Chad')));

이 튜토리얼에 나온 것처럼 필터를 사용하는 함수 프로그래밍 접근 방식은 컬렉션의 일부 요소에는 하나의 로직을 적용하고 컬렉션의 다른 요소에는 다른 로직을 적용하는 올바른 방법입니다.

reproject() 사용 자제

꼭 필요한 경우가 아니라면 재프로젝션을 사용하지 마세요. reproject()를 사용하는 한 가지 이유는 원하는 분석 규모에서 결과를 검사할 수 있도록 코드 편집기 계산이 특정 규모로 실행되도록 강제하기 위해서입니다. 다음 예에서는 핫 픽셀 패치가 계산되고 각 패치의 픽셀 수가 계산됩니다. 예시를 실행하고 패치 중 하나를 클릭합니다. 재프로젝션된 데이터와 재프로젝션되지 않은 데이터의 픽셀 수는 다릅니다.

var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');
var sf = ee.Geometry.Point([-122.405, 37.786]);
Map.centerObject(sf, 13);

// A reason to reproject - counting pixels and exploring interactively.
var image = l8sr.filterBounds(sf)
    .filterDate('2019-06-01', '2019-12-31')
    .first();

image = image.multiply(0.00341802).add(149);  // Apply scale factors.
Map.addLayer(image, {bands: ['ST_B10'], min: 280, max: 317}, 'image');

var hotspots = image.select('ST_B10').gt(317)
  .selfMask()
  .rename('hotspots');
var objectSize = hotspots.connectedPixelCount(256);

Map.addLayer(objectSize, {min: 1, max: 256}, 'Size No Reproject', false);

// Beware of reproject!  Don't zoom out on reprojected data.
var reprojected = objectSize.reproject(hotspots.projection());
Map.addLayer(reprojected, {min: 1, max: 256}, 'Size Reproject', false);

이러한 불일치는 분석 범위가 코드 편집기 확대/축소 수준에 따라 설정되기 때문입니다. reproject()를 호출하면 코드 편집기 대신 계산의 크기를 설정할 수 있습니다. reproject()이 문서에 설명된 이유로 매우 주의해서 사용하세요.

먼저 필터링하고 select()

일반적으로 컬렉션으로 다른 작업을 수행하기 전에 시간, 위치 또는 메타데이터별로 입력 컬렉션을 필터링합니다. 선택성이 낮은 필터보다 선택성이 높은 필터를 먼저 적용합니다. 공간 또는 시간 필터는 종종 더 선택적입니다. 예를 들어 select()filter()map()보다 먼저 적용됩니다.

var images = ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED');
var sf = ee.Geometry.Point([-122.463, 37.768]);

// Expensive function to reduce the neighborhood of an image.
var reduceFunction = function(image) {
  return image.reduceNeighborhood({
    reducer: ee.Reducer.mean(),
    kernel: ee.Kernel.square(4)
  });
};

var bands = ['B4', 'B3', 'B2'];
// Select and filter first!
var reasonableComputation = images
    .select(bands)
    .filterBounds(sf)
    .filterDate('2018-01-01', '2019-02-01')
    .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 1))
    .aside(print) // Useful for debugging.
    .map(reduceFunction)
    .reduce('mean')
    .rename(bands);
var viz = {bands: bands, min: 0, max: 10000};
Map.addLayer(reasonableComputation, viz, 'reasonableComputation');

mask() 대신 updateMask() 사용

updateMask()mask()의 차이점은 전자가 인수 (새 마스크)와 기존 이미지 마스크의 논리적 and()를 실행하는 반면 mask()는 이미지 마스크를 인수로 단순히 대체한다는 점입니다. 후자의 위험은 의도치 않게 픽셀을 노출할 수 있다는 점입니다. 이 예에서는 고도가 300미터 이하인 픽셀을 마스크 처리하는 것이 목표입니다. 보시다시피 (축소) mask()를 사용하면 관심 이미지에 속하지 않는 많은 픽셀의 마스크가 해제됩니다.

var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');
var sf = ee.Geometry.Point([-122.40554461769182, 37.786807309873716]);
var aw3d30 = ee.Image('JAXA/ALOS/AW3D30_V1_1');

Map.centerObject(sf, 7);

var image = l8sr.filterBounds(sf)
    .filterDate('2019-06-01', '2019-12-31')
    .first();

image = image.multiply(0.0000275).subtract(0.2);  // Apply scale factors.
var vis = {bands: ['SR_B4', 'SR_B3', 'SR_B2'], min: 0, max: 0.3};
Map.addLayer(image, vis, 'image', false);

var mask = aw3d30.select('AVE').gt(300);
Map.addLayer(mask, {}, 'mask', false);

// NO!  Don't do this!
var badMask = image.mask(mask);
Map.addLayer(badMask, vis, 'badMask');

var goodMask = image.updateMask(mask);
Map.addLayer(goodMask, vis, 'goodMask', false);

리듀서 결합

단일 입력 (예: 이미지 영역)에서 여러 통계 (예: 평균 및 표준 편차)가 필요한 경우 리듀서를 결합하는 것이 더 효율적입니다. 예를 들어 이미지 통계를 가져오려면 다음과 같이 리듀서를 결합합니다.

var image = ee.Image(
  'COPERNICUS/S2_HARMONIZED/20150821T111616_20160314T094808_T30UWU');

// Get mean and SD in every band by combining reducers.
var stats = image.reduceRegion({
  reducer: ee.Reducer.mean().combine({
    reducer2: ee.Reducer.stdDev(),
    sharedInputs: true
  }),
  geometry: ee.Geometry.Rectangle([-2.15, 48.55, -1.83, 48.72]),
  scale: 10,
  bestEffort: true // Use maxPixels if you care about scale.
});

print(stats);

// Extract means and SDs to images.
var meansImage = stats.toImage().select('.*_mean');
var sdsImage = stats.toImage().select('.*_stdDev');

이 예에서 평균 감소기는 표준 편차 감소기와 결합되며 sharedInputs는 입력 픽셀을 한 번만 통과하도록 설정하기 위해 true입니다. 출력 사전에서 리듀서의 이름이 밴드 이름에 추가됩니다. 평균 및 표준편차 이미지를 가져오려면 (예: 입력 이미지를 정규화) 값을 이미지로 변환하고 정규식을 사용하여 예시에서 보여준 것처럼 평균과 표준편차를 개별적으로 추출하면 됩니다.

Export 사용

Code Editor에서 '사용자 메모리 한도 초과' 또는 '계산 시간 초과' 오류가 발생하는 계산의 경우 Export를 사용하면 동일한 계산이 성공할 수 있습니다. 이는 내보내기가 실행되는 일괄 처리 시스템에서 실행할 때 제한 시간이 더 길고 허용되는 메모리 공간이 더 크기 때문입니다. (디버깅 문서에 자세히 설명된 대로 먼저 시도해 볼 수 있는 다른 접근 방식이 있습니다.) 이전 예를 계속해서 사전이 오류를 반환했다고 가정해 보겠습니다. 다음과 같이 하면 결과를 얻을 수 있습니다.

var link = '86836482971a35a5e735a17e93c23272';
Export.table.toDrive({
  collection: ee.FeatureCollection([ee.Feature(null, stats)]),
  description: 'exported_stats_demo_' + link,
  fileFormat: 'CSV'
});

재현성을 위해 링크가 애셋 이름에 삽입됩니다. 또한 toAsset를 내보내려면 도형을 제공해야 합니다. 도형은 무엇이든 될 수 있으며, 예를 들어 이미지 중심점은 작고 계산이 저렴합니다. 즉, 복잡한 도형이 필요하지 않다면 사용하지 마세요.

Export를 사용하여 계산 시간 초과동시 집계가 너무 많음을 해결하는 예는 디버깅 페이지를 참고하세요. 일반적인 내보내기에 관한 자세한 내용은 이 문서를 참고하세요.

클립할 필요가 없는 경우 clip()를 사용하지 마세요.

clip()를 불필요하게 사용하면 계산 시간이 증가합니다. 분석에 필요하지 않은 한 clip()를 사용하지 마세요. 확실하지 않다면 클립하지 마세요. 클립을 잘못 사용한 예는 다음과 같습니다.

입력을 불필요하게 클립하지 마세요.

var table = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');
var l8sr = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var belgium = table.filter(ee.Filter.eq('country_na', 'Belgium')).first();

// Do NOT clip unless you need to.
var unnecessaryClip = l8sr
    .select('SR_B4')                          // Good.
    .filterBounds(belgium.geometry())         // Good.
    .filterDate('2019-01-01', '2019-04-01')   // Good.
    .map(function(image) {
      return image.clip(belgium.geometry());  // NO! Bad! Not necessary.
    })
    .median()
    .reduceRegion({
      reducer: ee.Reducer.mean(),
      geometry: belgium.geometry(),
      scale: 30,
      maxPixels: 1e10,
    });
print(unnecessaryClip);

영역이 reduceRegion() 호출에 지정되므로 입력 이미지 클리핑을 완전히 건너뛸 수 있습니다.

출력할 리전을 지정하세요.

var noClipNeeded = l8sr
    .select('SR_B4')                           // Good.
    .filterBounds(belgium.geometry())          // Good.
    .filterDate('2019-01-01', '2019-12-31') // Good.
    .median()
    .reduceRegion({
      reducer: ee.Reducer.mean(),
      geometry: belgium.geometry(), // Geometry is specified here.
      scale: 30,
      maxPixels: 1e10,
    });
print(noClipNeeded);

이 계산에 시간이 초과되면 이 예시와 같이 Export합니다.

복잡한 컬렉션으로 클립해야 하는 경우 clipToCollection()를 사용하세요.

실제로 클립해야 하며 클리핑에 사용할 도형이 컬렉션에 있는 경우 clipToCollection()를 사용합니다.

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');
var image = ee.Image('JAXA/ALOS/AW3D30_V1_1');

var complexCollection = ecoregions
    .filter(ee.Filter.eq('BIOME_NAME',
                         'Tropical & Subtropical Moist Broadleaf Forests'));
Map.addLayer(complexCollection, {}, 'complexCollection');

var clippedTheRightWay = image.select('AVE')
    .clipToCollection(complexCollection);
Map.addLayer(clippedTheRightWay, {}, 'clippedTheRightWay', false);

메모리를 더 많이 사용할 수 있는 크거나 복잡한 컬렉션에는 featureCollection.geometry() 또는 featureCollection.union()를 사용하지 마세요.

복잡한 컬렉션을 리듀서의 영역으로 사용하지 않음

리듀서가 FeatureCollection의 여러 영역에서 입력을 풀링하도록 공간 감소를 실행해야 하는 경우 리듀서의 geometry 입력으로 featureCollection.geometry()를 제공하지 마세요. 대신 clipToCollection()와 컬렉션의 경계를 포함할 만큼 큰 영역을 사용하세요. 예를 들면 다음과 같습니다.

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');
var image = ee.Image('JAXA/ALOS/AW3D30_V1_1');

var complexCollection = ecoregions
    .filter(ee.Filter.eq('BIOME_NAME', 'Tropical & Subtropical Moist Broadleaf Forests'));

var clippedTheRightWay = image.select('AVE')
    .clipToCollection(complexCollection);
Map.addLayer(clippedTheRightWay, {}, 'clippedTheRightWay');

var reduction = clippedTheRightWay.reduceRegion({
  reducer: ee.Reducer.mean(),
  geometry: ee.Geometry.Rectangle({
    coords: [-179.9, -50, 179.9, 50],  // Almost global.
    geodesic: false
  }),
  scale: 30,
  maxPixels: 1e12
});
print(reduction); // If this times out, export it.

0이 아닌 errorMargin 사용

비용이 많이 들 수 있는 도형 작업의 경우 계산에 필요한 정밀도에 따라 최대 오류 마진을 사용합니다. 오류 마진은 도형 작업 (예: 재프로젝션 중) 중에 허용되는 최대 오류 (미터)를 지정합니다. 오류 마진을 작게 지정하면 메모리 사용량이 많을 수 있는 좌표가 포함된 도형을 빽빽하게 채워야 할 수 있습니다. 계산에 최대한 큰 오류 마진을 지정하는 것이 좋습니다.

var ecoregions = ee.FeatureCollection('RESOLVE/ECOREGIONS/2017');

var complexCollection = ecoregions.limit(10);
Map.centerObject(complexCollection);
Map.addLayer(complexCollection);

var expensiveOps = complexCollection.map(function(f) {
  return f.buffer(10000, 200).bounds(200);
});
Map.addLayer(expensiveOps, {}, 'expensiveOps');

reduceToVectors()와 함께 너무 작은 크기를 사용하지 마세요.

래스터를 벡터로 변환하려면 적절한 배율을 사용하세요. 매우 작은 규모를 지정하면 계산 비용이 크게 증가할 수 있습니다. 필요한 정밀도를 제공할 수 있도록 최대한 높은 값으로 설정합니다. 예를 들어 전 세계 육지 면적을 나타내는 다각형을 가져오려면 다음을 실행합니다.

var etopo = ee.Image('NOAA/NGDC/ETOPO1');

// Approximate land boundary.
var bounds = etopo.select(0).gt(-100);

// Non-geodesic polygon.
var almostGlobal = ee.Geometry.Polygon({
  coords: [[-180, -80], [180, -80], [180, 80], [-180, 80], [-180, -80]],
  geodesic: false
});
Map.addLayer(almostGlobal, {}, 'almostGlobal');

var vectors = bounds.selfMask().reduceToVectors({
  reducer: ee.Reducer.countEvery(),
  geometry: almostGlobal,
  // Set the scale to the maximum possible given
  // the required precision of the computation.
  scale: 50000,
});
Map.addLayer(vectors, {}, 'vectors');

이전 예에서는 전역 감소에 사용하기 위해 지오데시컬이 아닌 다각형을 사용합니다.

reduceRegions()와 함께 reduceToVectors()를 사용하지 않음

reduceToVectors()에서 반환된 FeatureCollectionreduceRegions()의 입력으로 사용하지 마세요. 대신 reduceToVectors()를 호출하기 전에 줄이려는 밴드를 추가합니다.

var etopo = ee.Image('NOAA/NGDC/ETOPO1');
var mod11a1 = ee.ImageCollection('MODIS/006/MOD11A1');

// Approximate land boundary.
var bounds = etopo.select(0).gt(-100);

// Non-geodesic polygon.
var almostGlobal = ee.Geometry.Polygon({
  coords: [[-180, -80], [180, -80], [180, 80], [-180, 80], [-180, -80]],
  geodesic: false
});

var lst = mod11a1.first().select(0);

var means = bounds.selfMask().addBands(lst).reduceToVectors({
  reducer: ee.Reducer.mean(),
  geometry: almostGlobal,
  scale: 1000,
  maxPixels: 1e10
});
print(means.limit(10));

다른 영역 내에서 한 이미지의 픽셀을 줄이는 다른 방법에는 reduceConnectedCommponents() 또는 그룹화 reducer가 있습니다.

이웃 작업에 fastDistanceTransform() 사용

일부 컨볼루션 작업의 경우 fastDistanceTransform()reduceNeighborhood() 또는 convolve()보다 더 효율적일 수 있습니다. 예를 들어 바이너리 입력의 에러션 또는 확장을 수행하려면 다음을 실행합니다.

var aw3d30 = ee.Image('JAXA/ALOS/AW3D30_V1_1');

// Make a simple binary layer from a threshold on elevation.
var mask = aw3d30.select('AVE').gt(300);
Map.setCenter(-122.0703, 37.3872, 11);
Map.addLayer(mask, {}, 'mask');

// Distance in pixel units.
var distance = mask.fastDistanceTransform().sqrt();
// Threshold on distance (three pixels) for a dilation.
var dilation = distance.lt(3);
Map.addLayer(dilation, {}, 'dilation');

// Do the reverse for an erosion.
var notDistance = mask.not().fastDistanceTransform().sqrt();
var erosion = notDistance.gt(3);
Map.addLayer(erosion, {}, 'erosion');

reduceNeighborhood()에서 최적화 사용

컨볼루션을 실행해야 하는데 fastDistanceTransform()를 사용할 수 없는 경우 reduceNeighborhood()의 최적화를 사용하세요.

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);

var bands = ['B4', 'B3', 'B2'];

var optimizedConvolution = composite.select(bands).reduceNeighborhood({
  reducer: ee.Reducer.mean(),
  kernel: ee.Kernel.square(3),
  optimization: 'boxcar' // Suitable optimization for mean.
}).rename(bands);

var viz = {bands: bands, min: 0, max: 72};
Map.setCenter(-122.0703, 37.3872, 11);
Map.addLayer(composite, viz, 'composite');
Map.addLayer(optimizedConvolution, viz, 'optimizedConvolution');

필요 이상으로 데이터를 샘플링하지 않음

학습 데이터 세트 크기를 불필요하게 늘리려는 충동을 억제하세요. 학습 데이터의 양을 늘리는 것이 경우에 따라 효과적인 머신러닝 전략이지만, 정확도가 비례하여 증가하지 않으면서 계산 비용이 늘어날 수도 있습니다. 학습 데이터 세트 크기를 늘릴 시기는 이 참조를 참고하세요. 다음 예는 학습 데이터를 너무 많이 요청하면 '계산된 값이 너무 큼' 오류가 발생할 수 있는 방법을 보여줍니다.

데이터를 너무 많이 샘플링하지 마세요.

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);
var labels = ee.FeatureCollection('projects/google/demo_landcover_labels');

// No!  Not necessary.  Don't do this:
labels = labels.map(function(f) { return f.buffer(100000, 1000); });

var bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7'];

var training = composite.select(bands).sampleRegions({
  collection: labels,
  properties: ['landcover'],
  scale: 30
});

var classifier = ee.Classifier.smileCart().train({
  features: training,
  classProperty: 'landcover',
  inputProperties: bands
});
print(classifier.explain()); // Computed value is too large

적당한 양의 데이터로 시작하고 분류기의 초매개변수를 조정하여 원하는 정확도를 달성할 수 있는지 확인하는 것이 좋습니다.

초매개변수 조정

var l8raw = ee.ImageCollection('LANDSAT/LC08/C02/T1_RT');
var composite = ee.Algorithms.Landsat.simpleComposite(l8raw);
var labels = ee.FeatureCollection('projects/google/demo_landcover_labels');

// Increase the data a little bit, possibly introducing noise.
labels = labels.map(function(f) { return f.buffer(100, 10); });

var bands = ['B2', 'B3', 'B4', 'B5', 'B6', 'B7'];

var data = composite.select(bands).sampleRegions({
  collection: labels,
  properties: ['landcover'],
  scale: 30
});

// Add a column of uniform random numbers called 'random'.
data = data.randomColumn();

// Partition into training and testing.
var training = data.filter(ee.Filter.lt('random', 0.5));
var testing = data.filter(ee.Filter.gte('random', 0.5));

// Tune the minLeafPopulation parameter.
var minLeafPops = ee.List.sequence(1, 10);

var accuracies = minLeafPops.map(function(p) {
  var classifier = ee.Classifier.smileCart({minLeafPopulation: p})
      .train({
        features: training,
        classProperty: 'landcover',
        inputProperties: bands
      });

  return testing
    .classify(classifier)
    .errorMatrix('landcover', 'classification')
    .accuracy();
});

print(ui.Chart.array.values({
  array: ee.Array(accuracies),
  axis: 0,
  xLabels: minLeafPops
}));

이 예에서 분류기는 이미 매우 정확하므로 조정할 필요가 거의 없습니다. 필요한 정확성을 유지하면서 가능한 한 작은 트리 (즉, 가장 큰 minLeafPopulation)를 선택하는 것이 좋습니다.

Export 중간 결과

비교적 복잡한 컴퓨팅된 이미지에서 샘플을 가져오는 것이 목표라고 가정해 보겠습니다. 이미지 toAsset()Export하고 내보낸 이미지를 로드한 다음 샘플링하는 것이 더 효율적인 경우가 많습니다. 예를 들면 다음과 같습니다.

var image = ee.Image('UMD/hansen/global_forest_change_2018_v1_6');
var geometry = ee.Geometry.Polygon(
    [[[-76.64069800085349, 5.511777325802095],
      [-76.64069800085349, -20.483938229362376],
      [-35.15632300085349, -20.483938229362376],
      [-35.15632300085349, 5.511777325802095]]], null, false);
var testRegion = ee.Geometry.Polygon(
    [[[-48.86726050085349, -3.0475996402515717],
      [-48.86726050085349, -3.9248707849303295],
      [-47.46101050085349, -3.9248707849303295],
      [-47.46101050085349, -3.0475996402515717]]], null, false);

// Forest loss in 2016, to stratify a sample.
var loss = image.select('lossyear');
var loss16 = loss.eq(16).rename('loss16');

// Scales and masks Landsat 8 surface reflectance images.
function prepSrL8(image) {
  var qaMask = image.select('QA_PIXEL').bitwiseAnd(parseInt('11111', 2)).eq(0);
  var opticalBands = image.select('SR_B.').multiply(0.0000275).add(-0.2);
  var thermalBands = image.select('ST_B.*').multiply(0.00341802).add(149.0);
  return image.addBands(opticalBands, null, true)
      .addBands(thermalBands, null, true)
      .updateMask(qaMask);
}

var collection = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2')
    .map(prepSrL8);

// Create two annual cloud-free composites.
var composite1 = collection.filterDate('2015-01-01', '2015-12-31').median();
var composite2 = collection.filterDate('2017-01-01', '2017-12-31').median();

// We want a strtatified sample of this stack.
var stack = composite1.addBands(composite2)
    .float(); // Export the smallest size possible.

// Export the image.  This block is commented because the export is complete.
/*
var link = '0b8023b0af6c1b0ac7b5be649b54db06'
var desc = 'Logistic_regression_stack_' + link;
Export.image.toAsset({
  image: stack,
  description: desc,
  assetId: desc,
  region: geometry,
  scale: 30,
  maxPixels: 1e10
})
*/

// Load the exported image.
var exportedStack = ee.Image(
  'projects/google/Logistic_regression_stack_0b8023b0af6c1b0ac7b5be649b54db06');

// Take a very small sample first, to debug.
var testSample = exportedStack.addBands(loss16).stratifiedSample({
  numPoints: 1,
  classBand: 'loss16',
  region: testRegion,
  scale: 30,
  geometries: true
});
print(testSample); // Check this in the console.

// Take a large sample.
var sample = exportedStack.addBands(loss16).stratifiedSample({
  numPoints: 10000,
  classBand: 'loss16',
  region: geometry,
  scale: 30,
});

// Export the large sample...

이 예시에서는 이미지가 부동 소수점 수로 내보내집니다. 꼭 필요한 경우가 아니라면 배정밀도로 내보내지 마세요. 이 내보내기를 실행할 때 재현성을 위해 내보내기 직전에 가져온 Code 편집기 링크가 파일 이름에 삽입됩니다.

내보내기가 완료되면 애셋을 새로고침하고 애셋에서 샘플링을 진행합니다. 디버깅을 위해 매우 작은 테스트 영역에서 매우 작은 샘플이 먼저 실행됩니다. 성공한 것으로 표시되면 더 큰 샘플을 가져와 내보냅니다. 이러한 대규모 샘플은 일반적으로 내보내야 합니다. 이러한 샘플은 먼저 내보내지 않고는 대화식으로 (예: print()를 통해) 사용할 수 없으며 (예: 분류기에 입력으로) 사용할 수 없습니다.

join과 map-filter 비교

시간, 위치 또는 일부 메타데이터 속성을 기반으로 컬렉션을 결합하려고 한다고 가정해 보겠습니다. 일반적으로 조인을 사용하면 가장 효율적으로 이 작업을 수행할 수 있습니다. 다음 예에서는 Landsat 8 컬렉션과 Sentinel-2 컬렉션 간에 시공간 조인을 실행합니다.

var s2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED')
    .filterBounds(ee.Geometry.Point([-2.0205, 48.647]));

var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var joined = ee.Join.saveAll('landsat').apply({
  primary: s2,
  secondary: l8,
  condition: ee.Filter.and(
    ee.Filter.maxDifference({
      difference: 1000 * 60 * 60 * 24, // One day in milliseconds
      leftField: 'system:time_start',
      rightField: 'system:time_start',
    }),
    ee.Filter.intersects({
      leftField: '.geo',
      rightField: '.geo',
    })
  )
});
print(joined);

먼저 조인을 시도해야 하지만(필요한 경우 Export) 경우에 따라 map() 내의 filter()도 효과적일 수 있습니다(특히 매우 큰 컬렉션의 경우).

var s2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED')
    .filterBounds(ee.Geometry.Point([-2.0205, 48.647]));

var l8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2');

var mappedFilter = s2.map(function(image) {
  var date = image.date();
  var landsat = l8
      .filterBounds(image.geometry())
      .filterDate(date.advance(-1, 'day'), date.advance(1, 'day'));
  // Return the input image with matching scenes in a property.
  return image.set({
    landsat: landsat,
    size: landsat.size()
  });
}).filter(ee.Filter.gt('size', 0));
print(mappedFilter);

reduceRegion(), reduceRegions(), for 루프 비교

매우 크거나 복잡한 FeatureCollection를 입력으로 사용하여 reduceRegions()를 호출하면 '계산된 값이 너무 큽니다'라는 오류가 발생할 수 있습니다. 한 가지 해결 방법은 FeatureCollection 위에 reduceRegion()를 매핑하는 것입니다. 또 다른 해결 방법은 (헉) for 루프를 사용하는 것입니다. 여기, 여기, 여기에 설명된 대로 Earth Engine에서는 이를 권장하지 않지만, reduceRegion()를 for 루프에 구현하여 대규모 감소를 실행할 수 있습니다.

ImageCollection의 각 이미지에 대해 FeatureCollection의 각 지형지물에서 픽셀의 평균 (또는 통계)을 얻는 것이 목표라고 가정해 보겠습니다. 다음 예에서는 앞에서 설명한 세 가지 접근 방식을 비교합니다.

// Table of countries.
var countriesTable = ee.FeatureCollection("USDOS/LSIB_SIMPLE/2017");
// Time series of images.
var mod13a1 = ee.ImageCollection("MODIS/006/MOD13A1");

// MODIS vegetation indices (always use the most recent version).
var band = 'NDVI';
var imagery = mod13a1.select(band);

// Option 1: reduceRegions()
var testTable = countriesTable.limit(1); // Do this outside map()s and loops.
var data = imagery.map(function(image) {
  return image.reduceRegions({
    collection: testTable,
    reducer: ee.Reducer.mean(),
    scale: 500
  }).map(function(f) {
    return f.set({
      time: image.date().millis(),
      date: image.date().format()
    });
  });
}).flatten();
print(data.first());

// Option 2: mapped reduceRegion()
var data = countriesTable.map(function(feature) {
  return imagery.map(function(image) {
    return ee.Feature(feature.geometry().centroid(100),
        image.reduceRegion({
          reducer: ee.Reducer.mean(),
          geometry: feature.geometry(),
          scale: 500
        })).set({
          time: image.date().millis(),
          date: image.date().format()
        }).copyProperties(feature);
  });
}).flatten();
print(data.first());

// Option 3: for-loop (WATCH OUT!)
var size = countriesTable.size();
// print(size); // 312
var countriesList = countriesTable.toList(1); // Adjust size.
var data = ee.FeatureCollection([]); // Empty table.
for (var j=0; j<1; j++) { // Adjust size.
  var feature = ee.Feature(countriesList.get(j));
  // Convert ImageCollection > FeatureCollection
  var fc = ee.FeatureCollection(imagery.map(function(image) {
    return ee.Feature(feature.geometry().centroid(100),
        image.reduceRegion({
          reducer: ee.Reducer.mean(),
          geometry: feature.geometry(),
          scale: 500
        })).set({
          time: image.date().millis(),
          date: image.date().format()
        }).copyProperties(feature);
  }));
  data = data.merge(fc);
}
print(data.first());

디버깅을 위해 각 컬렉션의 first() 항목이 출력됩니다. 전체 결과를 대화식으로 사용할 수 있을 것으로 기대해서는 안 됩니다. Export해야 합니다. 또한 for 루프는 최후의 수단으로만 극도로 주의해서 사용해야 합니다. 마지막으로 for 루프는 입력 컬렉션의 크기를 수동으로 가져와 적절한 위치에 하드코딩해야 합니다. 위 내용이 명확하지 않다면 for 루프를 사용하지 마세요.

시간의 이웃에 전방 차이를 사용합니다.

시간순으로 정렬된 ImageCollection (즉, 시계열)이 있고 각 이미지를 이전 (또는 다음) 이미지와 비교하려고 한다고 가정해 보겠습니다. 이 용도로 iterate()를 사용하는 대신 배열 기반 전방 차이를 사용하는 것이 더 효율적일 수 있습니다. 다음 예에서는 이 메서드를 사용하여 Sentinel-2 컬렉션의 중복을 삭제합니다. 여기서 중복은 일 수가 동일한 이미지로 정의됩니다.

var sentinel2 = ee.ImageCollection('COPERNICUS/S2_HARMONIZED');
var sf = ee.Geometry.Point([-122.47555371521855, 37.76884708376152]);
var s2 = sentinel2
    .filterBounds(sf)
    .filterDate('2018-01-01', '2019-12-31');

var withDoys = s2.map(function(image) {
  var ndvi = image.normalizedDifference(['B4', 'B8']).rename('ndvi');
  var date = image.date();
  var doy = date.getRelative('day', 'year');
  var time = image.metadata('system:time_start');
  var doyImage = ee.Image(doy)
      .rename('doy')
      .int();
  return ndvi.addBands(doyImage).addBands(time)
      .clip(image.geometry()); // Appropriate use of clip.
});

var array = withDoys.toArray();
var timeAxis = 0;
var bandAxis = 1;

var dedupe = function(array) {
  var time = array.arraySlice(bandAxis, -1);
  var sorted = array.arraySort(time);
  var doy = sorted.arraySlice(bandAxis, -2, -1);
  var left = doy.arraySlice(timeAxis, 1);
  var right = doy.arraySlice(timeAxis, 0, -1);
  var mask = ee.Image(ee.Array([[1]]))
      .arrayCat(left.neq(right), timeAxis);
  return array.arrayMask(mask);
};

var deduped = dedupe(array);

// Inspect these outputs to confirm that duplicates have been removed.
print(array.reduceRegion('first', sf, 10));
print(deduped.reduceRegion('first', sf, 10));

인화된 컬렉션을 검사하여 중복이 삭제되었는지 확인합니다.