แนวทางปฏิบัติแนะนำในการเขียนโค้ด

เอกสารนี้อธิบายแนวทางการเขียนโค้ดที่มีจุดประสงค์เพื่อเพิ่มโอกาสให้ประสบความสำเร็จสูงสุดในการประมวลผลของ Earth Engine ที่ซับซ้อนหรือมีค่าใช้จ่ายสูง วิธีการที่อธิบายที่นี่ใช้ได้กับทั้งการคํานวณแบบอินเทอร์แอกทีฟ (เช่น ตัวแก้ไขโค้ด) และแบบเป็นกลุ่ม (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() เป็นเมธอดเซิร์ฟเวอร์บนออบเจ็กต์เซิร์ฟเวอร์ และใช้ร่วมกับฟังก์ชันฝั่งไคลเอ็นต์ไม่ได้ เช่น เงื่อนไข <

สถานการณ์ที่คุณอาจต้องใช้วงวน for คือการตั้งค่า UI เนื่องจากออบเจ็กต์และเมธอด ui ของ Code Editor อยู่ฝั่งไคลเอ็นต์ (ดูข้อมูลเพิ่มเติมเกี่ยวกับการสร้างอินเทอร์เฟซผู้ใช้ใน 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.

โปรดทราบว่าอาร์กิวเมนต์ที่ 2 ของ 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');

ใช้ updateMask() แทน mask()

ความแตกต่างระหว่าง updateMask() กับ mask() คือ updateMask() จะทำ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 เป็นจริงเพื่อเปิดใช้การเรียกใช้พิกเซลอินพุตเพียงครั้งเดียว ในพจนานุกรมเอาต์พุต ระบบจะเพิ่มชื่อตัวลดต่อท้ายชื่อกลุ่ม หากต้องการดูรูปภาพค่ามัธยฐานและ SD (เช่น เพื่อทำให้รูปภาพอินพุตเป็นมาตรฐาน) ให้เปลี่ยนค่าเป็นรูปภาพและใช้นิพจน์ทั่วไปเพื่อดึงค่ามัธยฐานและ SD แยกกันตามที่แสดงในตัวอย่าง

ใช้ Export

สําหรับการคํานวณที่ทําให้เกิดข้อผิดพลาด "ใช้หน่วยความจําของผู้ใช้เกินขีดจํากัด" หรือ "การคํานวณหมดเวลา" ในเครื่องมือแก้ไขโค้ด การคํานวณเดียวกันอาจทําสําเร็จได้โดยใช้ 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 โปรดอย่าป้อน featureCollection.geometry() เป็นอินพุต 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.

ใช้ errorMargin ที่ไม่ใช่ 0

สําหรับการดำเนินการเกี่ยวกับเรขาคณิตที่อาจใช้ทรัพยากรมาก ให้ใช้ส่วนต่างข้อผิดพลาดที่ใหญ่ที่สุดที่เป็นไปได้โดยพิจารณาจากความแม่นยำของการคำนวณที่ต้องการ ส่วนต่างของข้อผิดพลาดจะระบุข้อผิดพลาดสูงสุดที่อนุญาต (เป็นเมตร) ในระหว่างการดำเนินการกับเรขาคณิต (เช่น ในระหว่างการโปรเจ็กต์ใหม่) การระบุส่วนต่างของข้อผิดพลาดเล็กน้อยอาจส่งผลให้ต้องเพิ่มความหนาแน่นของรูปทรงเรขาคณิต (ที่มีพิกัด) ซึ่งอาจใช้หน่วยความจำมาก แนวทางปฏิบัติแนะนำคือระบุส่วนต่างของข้อผิดพลาดให้มากที่สุดเท่าที่จะเป็นไปได้สำหรับการคํานวณ

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');

ในตัวอย่างก่อนหน้านี้ โปรดสังเกตการใช้รูปหลายเหลี่ยมที่ไม่ใช่รูปทรงเรขาคณิตเชิงพื้นที่เพื่อใช้กับการลดระดับทั่วโลก

อย่าใช้ reduceToVectors() กับ reduceRegions()

อย่าใช้ FeatureCollection ที่ reduceToVectors() แสดงผลเป็นอินพุตสำหรับ reduceRegions() แต่ให้เพิ่มย่านความถี่ที่ต้องการลดก่อนโทร 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() และ/หรือ grouping reducers

ใช้ 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 ผลลัพธ์ขั้นกลาง

สมมติว่าวัตถุประสงค์ของคุณคือการสุ่มตัวอย่างจากภาพคอมพิวเตอร์ที่ซับซ้อน บ่อยครั้งที่การExportรูปภาพ toAsset() โหลดรูปภาพที่ส่งออก แล้วจึงนำตัวอย่างนั้นไปใช้จะมีประสิทธิภาพมากกว่า เช่น

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...

ในตัวอย่างนี้ โปรดทราบว่าระบบส่งออกภาพเป็นประเภทลอย อย่าส่งออกที่ความแม่นยำแบบเลขทศนิยม 2 ตำแหน่ง เว้นแต่จำเป็นจริงๆ เมื่อส่งออก โปรดทราบว่าระบบจะฝังลิงก์เครื่องมือแก้ไขโค้ด (ได้รับทันทีก่อนส่งออก) ไว้ในชื่อไฟล์เพื่อให้ทำซ้ำได้

เมื่อส่งออกเสร็จแล้ว ให้โหลดชิ้นงานอีกครั้งแล้วทำการสุ่มตัวอย่างจากชิ้นงาน โปรดทราบว่าระบบจะเรียกใช้ตัวอย่างจำนวนน้อยมากในพื้นที่ทดสอบขนาดเล็กมากก่อนเพื่อแก้ไขข้อบกพร่อง เมื่อการทดสอบแสดงผลสำเร็จ ให้นำตัวอย่างที่ใหญ่ขึ้นและส่งออก โดยทั่วไปแล้ว จะต้องส่งออกตัวอย่างขนาดใหญ่ดังกล่าว โปรดทราบว่าตัวอย่างดังกล่าวจะใช้งานแบบอินเทอร์แอกทีฟ (เช่น ผ่าน print()) หรือใช้งานได้ (เช่น เป็นอินพุตสำหรับตัวจัดหมวดหมู่) โดยไม่ต้องส่งออกก่อน

การรวมกับตัวกรองแผนที่

สมมติว่าคุณต้องการรวมคอลเล็กชันตามเวลา สถานที่ หรือพร็อพเพอร์ตี้ข้อมูลเมตาบางอย่าง โดยทั่วไปแล้ว การรวมข้อมูลจะมีประสิทธิภาพมากที่สุด ตัวอย่างต่อไปนี้ทำการรวมเชิงพื้นที่และเวลาระหว่างคอลเล็กชัน 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 หากจําเป็น) แต่บางครั้งfilter()ภายในmap()ก็อาจมีประสิทธิภาพเช่นกัน โดยเฉพาะสําหรับคอลเล็กชันขนาดใหญ่มาก

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-loop

การเรียกใช้ reduceRegions() ด้วย FeatureCollection ขนาดใหญ่หรือซับซ้อนมากเป็นอินพุตอาจทำให้เกิดข้อผิดพลาด "ค่าที่คำนวณได้ใหญ่เกินไป" วิธีแก้ปัญหาที่เป็นไปได้วิธีหนึ่งคือการแมป reduceRegion() ทับ FeatureCollection แทน อีกวิธีหนึ่งที่อาจช่วยแก้ปัญหาได้คือการใช้ (อุ๊ย) ลูป for แม้ว่าเราจะไม่แนะนำให้ใช้วิธีนี้ใน Earth Engine ตามที่อธิบายไว้ที่นี่ ที่นี่ และที่นี่ แต่คุณก็สามารถใช้ reduceRegion() ในวงวน for เพื่อลดจำนวนข้อมูลจำนวนมากได้

สมมติว่าวัตถุประสงค์ของคุณคือหาค่าเฉลี่ยของพิกเซล (หรือสถิติใดๆ) ในฟีเจอร์แต่ละรายการใน FeatureCollection สำหรับรูปภาพแต่ละรูปใน ImageCollection ตัวอย่างต่อไปนี้เปรียบเทียบแนวทางทั้ง 3 วิธีที่อธิบายไว้ก่อนหน้านี้

// 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-loop

ใช้การหาค่าต่างของอนุกรมเวลาถัดไปสำหรับองค์ประกอบใกล้เคียงกัน

สมมติว่าคุณมี 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));

ตรวจสอบคอลเล็กชันที่พิมพ์เพื่อยืนยันว่านำรายการที่ซ้ำกันออกแล้ว