geebeam 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {geebeam-0.3.2 → geebeam-0.3.3}/PKG-INFO +32 -29
  2. {geebeam-0.3.2 → geebeam-0.3.3}/README.md +30 -28
  3. {geebeam-0.3.2 → geebeam-0.3.3}/_version.py +2 -2
  4. {geebeam-0.3.2 → geebeam-0.3.3}/pyproject.toml +6 -0
  5. geebeam-0.3.3/src/geebeam/__init__.py +13 -0
  6. geebeam-0.3.2/src/geebeam/ee_utils.py → geebeam-0.3.3/src/geebeam/_ee_utils.py +3 -3
  7. geebeam-0.3.3/src/geebeam/_pipeline.py +334 -0
  8. geebeam-0.3.2/src/geebeam/tf_utils.py → geebeam-0.3.3/src/geebeam/_tf_utils.py +25 -23
  9. geebeam-0.3.2/src/geebeam/tfds_writer.py → geebeam-0.3.3/src/geebeam/_tfds_writer.py +47 -73
  10. geebeam-0.3.2/src/geebeam/tfrecord_writer.py → geebeam-0.3.3/src/geebeam/_tfrecord_writer.py +30 -26
  11. geebeam-0.3.2/src/geebeam/tiff_writer.py → geebeam-0.3.3/src/geebeam/_tiff_writer.py +24 -36
  12. geebeam-0.3.2/src/geebeam/transforms.py → geebeam-0.3.3/src/geebeam/_transforms.py +13 -13
  13. geebeam-0.3.3/src/geebeam/_wds_writer.py +122 -0
  14. {geebeam-0.3.2 → geebeam-0.3.3}/src/geebeam/climate_indices.py +11 -0
  15. geebeam-0.3.3/src/geebeam/sampler.py +261 -0
  16. geebeam-0.3.3/tests/conftest.py +7 -0
  17. geebeam-0.3.3/tests/test_ee_utils.py +102 -0
  18. geebeam-0.3.3/tests/test_integration.py +182 -0
  19. geebeam-0.3.3/tests/test_pipeline.py +94 -0
  20. geebeam-0.3.3/tests/test_sampler.py +155 -0
  21. {geebeam-0.3.2 → geebeam-0.3.3}/tests/test_tf_utils.py +2 -2
  22. geebeam-0.3.3/tests/test_tiff_writer.py +54 -0
  23. geebeam-0.3.3/tests/test_transforms.py +120 -0
  24. geebeam-0.3.3/tests/test_wds_writer.py +56 -0
  25. geebeam-0.3.2/src/geebeam/__init__.py +0 -10
  26. geebeam-0.3.2/src/geebeam/pipeline.py +0 -200
  27. geebeam-0.3.2/src/geebeam/sampler.py +0 -97
  28. geebeam-0.3.2/tests/test_ee_utils.py +0 -46
  29. geebeam-0.3.2/tests/test_pipeline.py +0 -30
  30. geebeam-0.3.2/tests/test_sampler.py +0 -48
  31. geebeam-0.3.2/tests/test_transforms.py +0 -57
  32. {geebeam-0.3.2 → geebeam-0.3.3}/.gitignore +0 -0
  33. {geebeam-0.3.2 → geebeam-0.3.3}/LICENSE +0 -0
  34. {geebeam-0.3.2 → geebeam-0.3.3}/tests/test_climate_indices.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geebeam
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Earth Engine + Apache Beam
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -12,6 +12,7 @@ Requires-Dist: earthengine-api>=1.7.10
12
12
  Requires-Dist: geopandas>=1.0.1
13
13
  Requires-Dist: grpcio>=1.62.3
14
14
  Requires-Dist: rasterio>=1.3.10
15
+ Requires-Dist: webdataset>=1.0.2
15
16
  Provides-Extra: all
16
17
  Requires-Dist: importlib-resources>=6.5.2; extra == 'all'
17
18
  Requires-Dist: pytest; extra == 'all'
@@ -75,49 +76,51 @@ PROJECT_ID = google.auth.default()[1]
75
76
  # Initialize ee client, replace with your GCP project ID
76
77
  ee.Initialize(project=PROJECT_ID)
77
78
 
78
- # Build image for download
79
- burned_2024 = (ee.ImageCollection('MODIS/061/MCD64A1')
80
- .select('BurnDate')
81
- .filter(ee.Filter.calendarRange(2024, 2024, 'year'))
82
- .min()
83
- .gt(0)
84
- .rename(['Burn'])
85
- )
79
+ ### Build image for download
80
+ # Load a raw Landsat 5 ImageCollection for a single year.
81
+ ls5_collection = ee.ImageCollection('LANDSAT/LT05/C02/T1').filterDate(
82
+ '2010-01-01', '2010-12-31'
83
+ )
84
+ # Create a (mostly) cloud-free Landsat composite
85
+ ls5_composite = ee.Algorithms.Landsat.simpleComposite(
86
+ ls5_collection,
87
+ asFloat=True,
88
+ cloudScoreRange=5)
86
89
 
87
90
  # Building and triggering the pipeline is done with a single command:
88
- geebeam.run_pipeline(
89
- image_list = [burned_2024],
90
- project=PROJECT_ID,
91
- patch_size=128, # Pixel dimensions in each direction
92
- scale=500, # Final export resolution in meters
91
+ geebeam.sample_and_run_pipeline(
92
+ image_list = [ls5_composite], # Important: has to be a list of images
93
+ sampling_region=ee.Geometry.Rectangle(-55.0, -12.0, -50.0, -16.0), # In central-west Brazil
93
94
  n_sample=10, # Number of tiles to sample
95
+ patch_size=128, # Number of pixels in each direction
96
+ scale=30, # Final export resolution in meters
97
+ crs='EPSG:4326', # CRS for final output
98
+ project=PROJECT_ID, # GCP Project ID
99
+ output_path='./test_data/', # Output path, local or on GCP
94
100
  validation_ratio=0.2, # Fraction to select as validation data
95
- output_path='./test_tf_data/',
96
- sampling_region=ee.Geometry.Rectangle(-63.0, -9.0, -56.0, -4.0)
97
101
  )
98
102
  ```
99
103
 
100
- Now let's add another dataset: MapBiomas Amazonia forest fraction
104
+ Now let's add another dataset: MapBiomas land-cover from same year.
105
+ For more info, and legend, see: [MapBiomas Brasil](https://brasil.mapbiomas.org/en/codigos-de-legenda/)
101
106
  ```python
102
- # MB Land-use/land-cover forest fraction
103
- # Note that LULC codes less than 10 area forest in MapBiomas Amazon Collection 6
104
- mb_amz_lulc = (
105
- ee.Image('projects/mapbiomas-public/assets/amazon/lulc/collection6/mapbiomas_collection60_integration_v1')
106
- .lt(10)
107
- .reduceResolution('mean', maxPixels=500)
107
+ # MB Land-use/land-cover
108
+ mb_lulc = (
109
+ ee.Image('projects/mapbiomas-public/assets/brazil/lulc/collection10_1/mapbiomas_brazil_collection10_1_coverage_v1')
110
+ .select('classification_2010')
108
111
  )
109
112
 
110
113
  # Exporting both together is as simple as this:
111
- geebeam.run_pipeline(
112
- image_list = [burned_2024, mb_amz_lulc],
114
+ geebeam.sample_and_run_pipeline(
115
+ image_list = [ls5_composite, mb_lulc],
113
116
  project=PROJECT_ID,
117
+ crs='EPSG:4326',
114
118
  patch_size=128,
115
- scale=500,
119
+ scale=30,
116
120
  n_sample=10,
117
121
  validation_ratio=0.2,
118
- output_path='./test_tf_data/',
119
- sampling_region=ee.Geometry.Rectangle(-63.0, -9.0, -56.0, -4.0),
120
- num_workers=1
122
+ output_path='./test_data_w_mb/',
123
+ sampling_region=ee.Geometry.Rectangle(-55.0, -12.0, -50.0, -16.0)
121
124
  )
122
125
  ```
123
126
 
@@ -37,49 +37,51 @@ PROJECT_ID = google.auth.default()[1]
37
37
  # Initialize ee client, replace with your GCP project ID
38
38
  ee.Initialize(project=PROJECT_ID)
39
39
 
40
- # Build image for download
41
- burned_2024 = (ee.ImageCollection('MODIS/061/MCD64A1')
42
- .select('BurnDate')
43
- .filter(ee.Filter.calendarRange(2024, 2024, 'year'))
44
- .min()
45
- .gt(0)
46
- .rename(['Burn'])
47
- )
40
+ ### Build image for download
41
+ # Load a raw Landsat 5 ImageCollection for a single year.
42
+ ls5_collection = ee.ImageCollection('LANDSAT/LT05/C02/T1').filterDate(
43
+ '2010-01-01', '2010-12-31'
44
+ )
45
+ # Create a (mostly) cloud-free Landsat composite
46
+ ls5_composite = ee.Algorithms.Landsat.simpleComposite(
47
+ ls5_collection,
48
+ asFloat=True,
49
+ cloudScoreRange=5)
48
50
 
49
51
  # Building and triggering the pipeline is done with a single command:
50
- geebeam.run_pipeline(
51
- image_list = [burned_2024],
52
- project=PROJECT_ID,
53
- patch_size=128, # Pixel dimensions in each direction
54
- scale=500, # Final export resolution in meters
52
+ geebeam.sample_and_run_pipeline(
53
+ image_list = [ls5_composite], # Important: has to be a list of images
54
+ sampling_region=ee.Geometry.Rectangle(-55.0, -12.0, -50.0, -16.0), # In central-west Brazil
55
55
  n_sample=10, # Number of tiles to sample
56
+ patch_size=128, # Number of pixels in each direction
57
+ scale=30, # Final export resolution in meters
58
+ crs='EPSG:4326', # CRS for final output
59
+ project=PROJECT_ID, # GCP Project ID
60
+ output_path='./test_data/', # Output path, local or on GCP
56
61
  validation_ratio=0.2, # Fraction to select as validation data
57
- output_path='./test_tf_data/',
58
- sampling_region=ee.Geometry.Rectangle(-63.0, -9.0, -56.0, -4.0)
59
62
  )
60
63
  ```
61
64
 
62
- Now let's add another dataset: MapBiomas Amazonia forest fraction
65
+ Now let's add another dataset: MapBiomas land-cover from same year.
66
+ For more info, and legend, see: [MapBiomas Brasil](https://brasil.mapbiomas.org/en/codigos-de-legenda/)
63
67
  ```python
64
- # MB Land-use/land-cover forest fraction
65
- # Note that LULC codes less than 10 area forest in MapBiomas Amazon Collection 6
66
- mb_amz_lulc = (
67
- ee.Image('projects/mapbiomas-public/assets/amazon/lulc/collection6/mapbiomas_collection60_integration_v1')
68
- .lt(10)
69
- .reduceResolution('mean', maxPixels=500)
68
+ # MB Land-use/land-cover
69
+ mb_lulc = (
70
+ ee.Image('projects/mapbiomas-public/assets/brazil/lulc/collection10_1/mapbiomas_brazil_collection10_1_coverage_v1')
71
+ .select('classification_2010')
70
72
  )
71
73
 
72
74
  # Exporting both together is as simple as this:
73
- geebeam.run_pipeline(
74
- image_list = [burned_2024, mb_amz_lulc],
75
+ geebeam.sample_and_run_pipeline(
76
+ image_list = [ls5_composite, mb_lulc],
75
77
  project=PROJECT_ID,
78
+ crs='EPSG:4326',
76
79
  patch_size=128,
77
- scale=500,
80
+ scale=30,
78
81
  n_sample=10,
79
82
  validation_ratio=0.2,
80
- output_path='./test_tf_data/',
81
- sampling_region=ee.Geometry.Rectangle(-63.0, -9.0, -56.0, -4.0),
82
- num_workers=1
83
+ output_path='./test_data_w_mb/',
84
+ sampling_region=ee.Geometry.Rectangle(-55.0, -12.0, -50.0, -16.0)
83
85
  )
84
86
  ```
85
87
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.3.2'
22
- __version_tuple__ = version_tuple = (0, 3, 2)
21
+ __version__ = version = '0.3.3'
22
+ __version_tuple__ = version_tuple = (0, 3, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -17,6 +17,7 @@ dependencies = [
17
17
  "geopandas>=1.0.1",
18
18
  "grpcio>=1.62.3",
19
19
  "rasterio>=1.3.10",
20
+ "webdataset>=1.0.2",
20
21
  ]
21
22
 
22
23
  [project.optional-dependencies]
@@ -57,3 +58,8 @@ include = [
57
58
  "pyproject.toml",
58
59
  "setup.py"
59
60
  ]
61
+
62
+ [tool.pytest.ini_options]
63
+ markers = [
64
+ "integration: marks tests as integration tests (slower)",
65
+ ]
@@ -0,0 +1,13 @@
1
+ """Beam and Earth Engine helpers for running data pipelines"""
2
+
3
+ from . import climate_indices, sampler
4
+
5
+ from ._pipeline import run_pipeline, sample_and_run_pipeline, grid_and_run_pipeline
6
+
7
+ __all__ = [
8
+ "climate_indices",
9
+ "sampler",
10
+ "run_pipeline",
11
+ "sample_and_run_pipeline",
12
+ "grid_and_run_pipeline"
13
+ ]
@@ -7,11 +7,11 @@ import io
7
7
  import ee
8
8
  import numpy as np
9
9
 
10
- def deserialize(obj_json):
10
+ def _deserialize(obj_json):
11
11
  """Deserialize Earth Engine JSON DAG"""
12
12
  return ee.deserializer.fromJSON(obj_json)
13
13
 
14
- def serialize(obj_ee):
14
+ def _serialize(obj_ee):
15
15
  """Serialize Earth Engine object to JSON for Dataflow workers"""
16
16
  return ee.serializer.toJSON(obj_ee)
17
17
 
@@ -39,7 +39,7 @@ def build_prepped_image(input_list, split_processing=False):
39
39
  return prepped_im, band_groups, band_names_flat.getInfo()
40
40
 
41
41
  def list_to_im(input_list):
42
- prepped_im, _ = build_prepped_image(input_list)
42
+ prepped_im, *_ = build_prepped_image(input_list)
43
43
  return prepped_im
44
44
 
45
45
  def get_pixels(im, point, patch_size, scale_x, scale_y, crs_code):
@@ -0,0 +1,334 @@
1
+ """Prepare and run Beam pipeline to download image 'chips' from Earth Engine"""
2
+
3
+ import warnings
4
+ import ee
5
+ import geopandas as gpd
6
+ import pandas as pd
7
+ from apache_beam.options.pipeline_options import PipelineOptions
8
+ import numpy as np
9
+
10
+ from geebeam import _ee_utils, sampler, _transforms
11
+
12
+
13
+ def _check_if_localrunner(pipeline_options):
14
+ """Fixes gRPC timeout issue for local runners."""
15
+ runner = pipeline_options.get_all_options()['runner']
16
+ if runner is None or runner in ['DirectRunner', 'PrismRunner']:
17
+ return True
18
+ else:
19
+ return False
20
+
21
+ def _type_inference(val):
22
+ if isinstance(val, int):
23
+ return 'int'
24
+ elif isinstance(val, float):
25
+ return 'float'
26
+ elif isinstance(val, list) or isinstance(val, np.ndarray):
27
+ return {'arraylike': np.array(val).shape}
28
+ elif isinstance(val, str):
29
+ return 'str'
30
+ else:
31
+ raise ValueError
32
+
33
+ def _build_md_feature_dict(record, extra_metadata):
34
+ md_feature_dict = {
35
+ 'id': 'int',
36
+ 'x': 'float',
37
+ 'y': 'float',
38
+ 'split':'str'
39
+ }
40
+ if extra_metadata is not None:
41
+ for key in extra_metadata.keys():
42
+ # Basic type inference for extra metadata
43
+ try:
44
+ md_type = _type_inference(extra_metadata[key])
45
+ except ValueError:
46
+ raise ValueError(f'Could not determine data type of extra_metadata feature {key}')
47
+ md_feature_dict[key] = md_type
48
+
49
+ for key in record.keys():
50
+ if key not in ['id','x','y','split']:
51
+ try:
52
+ md_type = _type_inference(record[key])
53
+ except ValueError:
54
+ raise ValueError(f'Could not determine data type of column feature {key}')
55
+ md_feature_dict[key] = md_type
56
+ return md_feature_dict
57
+
58
+ def _prepare_run_metadata(config):
59
+ ee.Initialize(project=config['project_id'])
60
+
61
+ proj = ee.Projection(config['crs']).atScale(config['scale'])
62
+ proj_dict = proj.getInfo()
63
+
64
+ scale_x = proj_dict['transform'][0]
65
+ scale_y = -proj_dict['transform'][4]
66
+
67
+ return scale_x, scale_y
68
+
69
+ def run_pipeline(
70
+ image_list: list[ee.Image],
71
+ output_path: str,
72
+ project: str,
73
+ patch_size: int,
74
+ scale: float,
75
+ sampling_points: pd.DataFrame | gpd.GeoDataFrame | ee.FeatureCollection,
76
+ output_type: str = 'tiff',
77
+ crs: str = 'EPSG:4326',
78
+ split_processing: bool = False,
79
+ extra_metadata: dict = {},
80
+ beam_options: dict[str] | list[str] | None = None,
81
+ dataset_version: str = '1.0.0',
82
+ dataset_name: str = 'geebeam_dataset'
83
+ ) -> None:
84
+ """Run a Beam pipeline to download image chips from Earth Engine.
85
+
86
+ Args:
87
+ image_list: A list of image identifiers to process.
88
+ sampling_points: Locations to sample from, specifying upper-left of box.
89
+ output_path: The path where output will be saved.
90
+ output_type: 'tiff' (tiffs with parquet for metadata),
91
+ 'webdataset' (tiffs with jsons, in sharded tars),
92
+ 'tfrecord' (raw tfrecords), or 'tfds' (tensorflow-dataset).
93
+ project: The Google Cloud project ID.
94
+ patch_size: The size of the patches to be processed.
95
+ scale: The scale factor for image processing.
96
+ crs: The coordinate reference system. Defaults to 'EPSG:4326'.
97
+ split_processing: Flag to indicate if processing should be split. Defaults to False.
98
+ extra_metadata: Additional metadata to include. Defaults to an empty dictionary.
99
+ beam_options_dict: Options for the Beam pipeline. Defaults to an empty dictionary.
100
+ """
101
+ import logging
102
+
103
+ logging.getLogger().setLevel(logging.INFO)
104
+
105
+ # Set up configuration dict to pass along
106
+ config = {
107
+ 'project_id': project,
108
+ 'patch_size': patch_size,
109
+ 'scale': scale,
110
+ 'crs': crs
111
+ }
112
+
113
+ # Parses from command line and/or retrieves from dict. Note that dict takes precedent.
114
+ if isinstance(beam_options, dict):
115
+ pipeline_options = PipelineOptions(
116
+ **beam_options,
117
+ project=config['project_id'],
118
+ save_main_session=True,
119
+ )
120
+ elif isinstance(beam_options, list):
121
+ warnings.warn('Creating PipelineOptions from beam_options list.'
122
+ ' Ignores command-line beam options.')
123
+ pipeline_options = PipelineOptions(
124
+ beam_options,
125
+ project=config['project_id'],
126
+ save_main_session=True,
127
+ )
128
+ else:
129
+ pipeline_options = PipelineOptions(
130
+ project=config['project_id'],
131
+ save_main_session=True,
132
+ )
133
+
134
+ # Get sample points
135
+ input_records, splits = sampler._process_sampling_points(sampling_points, target_crs=config['crs'])
136
+
137
+ # Get types of extra non-image data in extra_medtada or input_records
138
+ md_feature_dict = _build_md_feature_dict(input_records[0], extra_metadata)
139
+
140
+ # Pre-run info:
141
+ scale_x, scale_y = _prepare_run_metadata(config)
142
+
143
+ # Check if a local runner. If so, add longer job timeout to fix grpcio timeout issue
144
+ is_local = _check_if_localrunner(pipeline_options)
145
+ if is_local:
146
+ logging.warning('Running on local runner. Setting beam job_server_timeout'
147
+ ' to 9999999 seconds to avoid grpcio timeout errors.')
148
+ pipeline_options_dict = pipeline_options.get_all_options(drop_default=True)
149
+ pipeline_options_dict['job_server_timeout'] = 9999999
150
+ pipeline_options = PipelineOptions.from_dictionary(pipeline_options_dict)
151
+
152
+ # Prepare and serialize inputs
153
+ # band_groups is a list of lists containing bands to export
154
+ # if split_processing = False, will contain one list with all bands
155
+ # if split_processing = True, contains separate band_lists for each image in image_list
156
+ prepped_image, band_groups, all_bands = _ee_utils.build_prepped_image(image_list, split_processing=split_processing)
157
+ serialized_image = _ee_utils._serialize(prepped_image)
158
+
159
+ # Execute pipeline based on output type:
160
+ if output_type == 'tfrecord':
161
+ try:
162
+ from geebeam import _tfrecord_writer
163
+ except ImportError:
164
+ raise ImportError(
165
+ "Missing dependencies for tfrecord writer. "
166
+ "Install them with `pip install geebeam[tensorflow]`"
167
+ )
168
+ # Write sidecar schema before pipeline execution
169
+ extra_keys = list(extra_metadata.keys())
170
+ _transforms._write_sidecar_schema(output_path, all_bands, extra_keys,
171
+ is_gcs=output_path.startswith('gs://'))
172
+ _tfrecord_writer.run_tfrecord_export(
173
+ input_records=input_records,
174
+ splits=splits,
175
+ output_path=output_path,
176
+ config=config,
177
+ serialized_image=serialized_image,
178
+ band_groups=band_groups,
179
+ scale_x=scale_x,
180
+ scale_y=scale_y,
181
+ extra_metadata=extra_metadata,
182
+ md_feature_dict=md_feature_dict,
183
+ pipeline_options=pipeline_options
184
+ )
185
+ elif output_type == 'tfds':
186
+ try:
187
+ from geebeam import _tfds_writer
188
+ except ImportError:
189
+ raise ImportError(
190
+ "Missing dependencies for TFDS writer. "
191
+ "Install them with `pip install geebeam[tensorflow]`"
192
+ )
193
+ _tfds_writer.run_tfds_export(
194
+ input_records=input_records,
195
+ splits=splits,
196
+ output_path=output_path,
197
+ config=config,
198
+ serialized_image=serialized_image,
199
+ band_groups=band_groups,
200
+ all_bands=all_bands,
201
+ scale_x=scale_x,
202
+ scale_y=scale_y,
203
+ extra_metadata=extra_metadata,
204
+ md_feature_dict=md_feature_dict,
205
+ pipeline_options=pipeline_options,
206
+ dataset_name=dataset_name,
207
+ dataset_version=dataset_version
208
+ )
209
+ elif output_type == 'tif' or output_type == 'tiff':
210
+ from geebeam import _tiff_writer
211
+ _tiff_writer.run_tiff_export(
212
+ input_records=input_records,
213
+ splits=splits,
214
+ output_path=output_path,
215
+ config=config,
216
+ serialized_image=serialized_image,
217
+ band_groups=band_groups,
218
+ scale_x=scale_x,
219
+ scale_y=scale_y,
220
+ extra_metadata=extra_metadata,
221
+ md_feature_dict=md_feature_dict,
222
+ pipeline_options=pipeline_options
223
+ )
224
+ elif output_type == 'webdataset':
225
+ from geebeam import _wds_writer
226
+ _wds_writer.run_webdataset_export(
227
+ input_records=input_records,
228
+ splits=splits,
229
+ output_path=output_path,
230
+ config=config,
231
+ serialized_image=serialized_image,
232
+ band_groups=band_groups,
233
+ scale_x=scale_x,
234
+ scale_y=scale_y,
235
+ extra_metadata=extra_metadata,
236
+ pipeline_options=pipeline_options
237
+ )
238
+ else:
239
+ raise ValueError(f"output_type {output_type} not implemented. Options are ['tfds', 'tfrecord', 'tiff', 'webdataset']")
240
+
241
+ def sample_and_run_pipeline(
242
+ sampling_region: str | gpd.GeoDataFrame | ee.Geometry,
243
+ n_sample: int,
244
+ validation_ratio: float = 0,
245
+ random_seed: int = 0,
246
+ crs: str = 'EPSG:4326',
247
+ *args,
248
+ **kwargs
249
+ ) -> None:
250
+ """Sample random points and then run a Beam pipeline to download image chips from Earth Engine.
251
+
252
+ Args:
253
+ image_list: A list of image identifiers to process.
254
+ sampling_region: Region to sample from, polygon or group of polygons.
255
+ n_sample: Number of points to sample.
256
+ validation_ratio: Fraction of points to mark as validation.
257
+ random_seed: Seed for random sampling
258
+ output_path: The path where output will be saved.
259
+ output_type: 'tiff' (tiffs with parquet for metadata),
260
+ 'webdataset' (tiffs with jsons, in sharded tars),
261
+ 'tfrecord' (raw tfrecords), or 'tfds' (tensorflow-dataset).
262
+ project: The Google Cloud project ID.
263
+ patch_size: The size of the patches to be processed.
264
+ scale: The scale factor for image processing.
265
+ crs: The coordinate reference system. Defaults to 'EPSG:4326'.
266
+ split_processing: Flag to indicate if processing should be split. Defaults to False.
267
+ extra_metadata: Additional metadata to include. Defaults to an empty dictionary.
268
+ beam_options_dict: Options for the Beam pipeline. Defaults to an empty dictionary.
269
+ """
270
+
271
+ sample_points = sampler.sample_region_random(
272
+ roi=sampling_region,
273
+ n_sample=n_sample,
274
+ random_seed=random_seed,
275
+ crs=crs,
276
+ )
277
+
278
+ if validation_ratio > 0:
279
+ sample_points = sampler.split_sets(
280
+ sample_points, split_names=['train','validation'], split_ratios=[1-validation_ratio,validation_ratio],
281
+ random_seed=random_seed
282
+ )
283
+
284
+ return run_pipeline(sampling_points=sample_points, crs=crs, *args, **kwargs)
285
+
286
+ def grid_and_run_pipeline(
287
+ sampling_region: str | gpd.GeoDataFrame | ee.Geometry,
288
+ validation_ratio: float,
289
+ scale: float,
290
+ stride: int,
291
+ buffer_distance: float = 0,
292
+ crs: str = 'EPSG:4326',
293
+ random_seed: int = 0,
294
+ *args,
295
+ **kwargs
296
+ ) -> None:
297
+ """Sample points from regular grid and then run a Beam pipeline to download image chips from Earth Engine.
298
+
299
+ Args:
300
+ image_list: A list of image identifiers to process.
301
+ sampling_region: Region to sample from, polygon or group of polygons.
302
+ validation_ratio: Fraction of points to mark as validation.
303
+ stride: Number of pixels between consecutive samples. If want full coverage without overlaps,
304
+ stride should be equal to patch_size. If less than patch_size, will generate overlaps.
305
+ If greater, will be gaps between sampled patches.
306
+ random_seed: Seed for random sampling
307
+ output_path: The path where output will be saved.
308
+ output_type: 'tiff' (tiffs with parquet for metadata),
309
+ 'webdataset' (tiffs with jsons, in sharded tars),
310
+ 'tfrecord' (raw tfrecords), or 'tfds' (tensorflow-dataset).
311
+ project: The Google Cloud project ID.
312
+ patch_size: The size of the patches to be processed.
313
+ scale: The scale factor for image processing.
314
+ crs: The coordinate reference system. Defaults to 'EPSG:4326'.
315
+ split_processing: Flag to indicate if processing should be split. Defaults to False.
316
+ extra_metadata: Additional metadata to include. Defaults to an empty dictionary.
317
+ beam_options_dict: Options for the Beam pipeline. Defaults to an empty dictionary.
318
+ """
319
+
320
+ sample_points = sampler.sample_region_grid(
321
+ roi=sampling_region,
322
+ crs='EPSG:4326',
323
+ stride=stride,
324
+ scale=scale,
325
+ buffer_distance=buffer_distance,
326
+ )
327
+
328
+ if validation_ratio > 0:
329
+ sample_points = sampler.split_sets(
330
+ sample_points, split_names=['train','validation'], split_ratios=[1-validation_ratio,validation_ratio],
331
+ random_seed=random_seed
332
+ )
333
+
334
+ return run_pipeline(sampling_points=sample_points, crs=crs, scale=scale, *args, **kwargs)
@@ -2,8 +2,9 @@
2
2
 
3
3
  import tensorflow as tf
4
4
  import apache_beam as beam
5
+ import numpy as np
5
6
 
6
- from geebeam import transforms
7
+ from geebeam import _transforms
7
8
 
8
9
  def _bytes_feature(value):
9
10
  """Returns a bytes_list from a string / byte."""
@@ -17,28 +18,39 @@ def _int64_feature(value):
17
18
  """Returns an int64_list from a bool / enum / int / uint."""
18
19
  return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))
19
20
 
20
- def dict_to_example(element):
21
+ def _dict_to_example(record):
21
22
  """"Convert structured numpy array to tf.Example proto."""
22
23
  # First add metadata
23
- md_dict = {
24
- 'md_id': _int64_feature(element['metadata']['id']),
25
- 'md_y': _float_feature(element['metadata']['y']),
26
- 'md_x': _float_feature(element['metadata']['x']),
27
- }
28
- for md_key in element['metadata'].keys():
29
- if md_key not in ['id','y','x', 'split']:
30
- md_dict['md_' + md_key] = tf.train.Feature(float_list=
24
+ # Add metadata features
25
+ md_dict = {}
26
+ for md_key, md_val in record['metadata'].items():
27
+ if isinstance(md_val, str):
28
+ md_dict[f'md_{md_key}'] = _bytes_feature(md_val.encode('utf-8'))
29
+ elif np.isscalar(md_val):
30
+ if isinstance(md_val, int):
31
+ md_dict[f'md_{md_key}'] = _int64_feature(record['metadata'][md_key])
32
+ else:
33
+ md_dict[f'md_{md_key}'] = _float_feature(record['metadata'][md_key])
34
+ else:
35
+ md_dict[f'md_{md_key}'] = tf.train.Feature(float_list=
31
36
  tf.train.FloatList(
32
- value = transforms.convert_to_iterable(element['metadata'][md_key])
37
+ value = _transforms._convert_to_iterable(record['metadata'][md_key])
33
38
  )
34
39
  )
35
40
 
36
41
  # Build image feature with named bands
37
42
  array_dict = {}
38
- for im_feat in element['array'].keys():#.dtype.names:
43
+ for band_name, band_data in record['array'].items():
44
+ array_dict[f'{band_name}'] = band_data.flatten().astype('float32')
45
+
46
+ # Build image feature with named bands
47
+ array_dict = {}
48
+ for im_feat, im_data in record['array'].items():
39
49
  array_dict['im_'+im_feat] = tf.train.Feature(
40
50
  float_list = tf.train.FloatList(
41
- value = element['array'][im_feat].flatten()))
51
+ value = im_data.flatten()
52
+ )
53
+ )
42
54
 
43
55
  # Combine
44
56
  feature = {**md_dict, **array_dict}
@@ -47,16 +59,6 @@ def dict_to_example(element):
47
59
  return tf.train.Example(
48
60
  features = tf.train.Features(feature = feature)).SerializeToString()
49
61
 
50
- def array_to_example(structured_array):
51
- """"Convert structured numpy array to tf.Example proto."""
52
- feature = {}
53
- for f in structured_array.dtype.names:
54
- feature[f] = tf.train.Feature(
55
- float_list = tf.train.FloatList(
56
- value = structured_array[f].flatten()))
57
- return tf.train.Example(
58
- features = tf.train.Features(feature = feature))
59
-
60
62
  class WriteTFExample(beam.PTransform):
61
63
  """Write example"""
62
64
  def __init__(self, output_dir, file_name_suffix='.tfrecord.gz'):