diff --git a/CHANGES.md b/CHANGES.md index 4c988b22..b23087ec 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,17 @@ # Release History +## 0.20.0 (2023-MM-DD) + +### Breaking Changes + +- **BREAKING CHANGES: Switching from `resolution` to `pixel_size` to avoid confusion about the definitions (especially for SAR data)** ([#82](https://github.com/sertit/eoreader/issues/82)) + +### Other + +- INTERNAL: Better management of logs for deprecation warnings +- INTERNAL: Refactoring `simplify_footprint` in `sertit` library +- DEPS: Pin sertit to 1.25.0 + ## 0.19.4 (2023-04-12) ### Bug Fixes @@ -17,6 +29,7 @@ ### Bug Fixes +- OPTIM: Don't recompute stacks if already existing on disk - FIX: Fixing `Custom Stacks` when specifying `datetime=None` on creation - FIX: Fix regression for multi-swath DGM CSK data (huge region) ([#78](https://github.com/sertit/eoreader/issues/78)) - FIX: Fix calibration issues with CSK HR data (using fallback GPT graph by default) diff --git a/CI/SCRIPTS/test_broken_s2.py b/CI/SCRIPTS/test_broken_s2.py index a1c3ae1b..5f382482 100644 --- a/CI/SCRIPTS/test_broken_s2.py +++ b/CI/SCRIPTS/test_broken_s2.py @@ -17,7 +17,7 @@ @dask_env def test_broken_s2(): """Function testing the support of broken Sentinel-2 constellation""" - res = 10.0 * 100 + pixel_size = 10.0 * 100 # ----------- Broken MTD ----------- broken_mtd = broken_s2_path().joinpath( @@ -30,8 +30,8 @@ def test_broken_s2(): LOGGER.info(broken_mtd_prod) LOGGER.info(broken_mtd_prod.bands) - broken_mtd_prod.load(RED, resolution=res, clean_optical="clean") - broken_mtd_prod.load(NIR, resolution=res, clean_optical="nodata") + broken_mtd_prod.load(RED, pixel_size=pixel_size, clean_optical="clean") + broken_mtd_prod.load(NIR, pixel_size=pixel_size, clean_optical="nodata") # Invalid tests with pytest.raises(InvalidProductError): @@ -52,14 +52,14 @@ def test_broken_s2(): broken_detfoo_prod.footprint() broken_detfoo_prod.load( - RED, resolution=res, clean_optical="clean" + RED, pixel_size=pixel_size, clean_optical="clean" ) # Not corrupted band # Invalid tests # WARNING: This doesn't fail anymore! # with pytest.raises(InvalidProductError): # broken_detfoo_prod.load( - # NIR, resolution=res, clean_optical="nodata" + # NIR, pixel_size=pixel_size, clean_optical="nodata" # ) # Corrupted band # ----------- Broken MSK ----------- @@ -67,5 +67,5 @@ def test_broken_s2(): "S2B_MSIL2A_20220201T104149_N0400_R008_T31UFP_20220201T122857.SAFE" ) broken_msk_prod = READER.open(broken_msk) - broken_msk_prod.load(RED, resolution=res, clean_optical="clean") - broken_mtd_prod.load(NIR, resolution=res, clean_optical="nodata") + broken_msk_prod.load(RED, pixel_size=pixel_size, clean_optical="clean") + broken_mtd_prod.load(NIR, pixel_size=pixel_size, clean_optical="nodata") diff --git a/CI/SCRIPTS/test_custom.py b/CI/SCRIPTS/test_custom.py index 1f91035b..232a5b88 100644 --- a/CI/SCRIPTS/test_custom.py +++ b/CI/SCRIPTS/test_custom.py @@ -46,7 +46,7 @@ def test_custom_optical(): sensor_type=SensorType.OPTICAL, constellation="WV02", instrument="WW110", - resolution=2.0, + pixel_size=2.0, product_type="Ortho", band_map={BLUE: 1, GREEN: 2, RED: 3, NIR: 4, SWIR_1: 5}, ) @@ -118,7 +118,7 @@ def test_custom_optical(): extent_some = prod_some.extent() footprint_some = prod_some.footprint() crs_some = prod_some.crs() - bands = prod_some.load([HILLSHADE], resolution=200.0) + bands = prod_some.load([HILLSHADE], pixel_size=200.0) # Check attributes assert bands[HILLSHADE].attrs["long_name"] == "HILLSHADE" @@ -160,7 +160,7 @@ def test_custom_sar(): datetime="20210827T162210", constellation="ICEYE", instrument="SAR X-band", - resolution=6.0, + pixel_size=6.0, product_type="GRD", band_map={VV: 1, VV_DSPK: 2}, ) @@ -168,7 +168,7 @@ def test_custom_sar(): extent_sar = prod_sar.extent() footprint_sar = prod_sar.footprint() crs_sar = prod_sar.crs() - stack_sar = prod_sar.stack([VV, VV_DSPK], prod_sar.resolution * 10) + stack_sar = prod_sar.stack([VV, VV_DSPK], prod_sar.pixel_size * 10) # Errors with pytest.raises(AssertionError): @@ -195,13 +195,13 @@ def test_custom_sar(): product_type=None, instrument=None, datetime=None, - resolution=6.0, + pixel_size=6.0, ) LOGGER.info(prod_wtf) extent_wtf = prod_wtf.extent() footprint_wtf = prod_wtf.footprint() crs_wtf = prod_wtf.crs() - stack_wtf = prod_wtf.stack([HH, RH], prod_wtf.resolution * 10) + stack_wtf = prod_wtf.stack([HH, RH], prod_wtf.pixel_size * 10) ci.assert_geom_equal(extent_sar, extent_wtf) ci.assert_geom_equal(footprint_sar, footprint_wtf) @@ -223,7 +223,7 @@ def test_custom_wgs84(): name="SPOT6_WGS84", datetime="20181218T090308", constellation="SPOT6", - resolution=1.5 * 15, + pixel_size=1.5 * 15, instrument="NAOMI", product_type="ORT", band_map={RED: 1, GREEN: 2, BLUE: 3, NIR: 4}, @@ -247,7 +247,7 @@ def test_custom_wgs84(): assert root.findtext("datetime") == "2018-12-18T09:03:08" assert root.findtext("sensor_type") == "Optical" assert root.findtext("constellation") == "Spot-6" - assert root.findtext("resolution") == str(1.5 * 15) + assert root.findtext("pixel_size") == str(1.5 * 15) assert root.findtext("product_type") == "ORT" assert root.findtext("band_map") == "{'BLUE': 3, 'GREEN': 2, 'RED': 1, 'NIR': 4}" assert root.findtext("sun_azimuth") == "None" diff --git a/CI/SCRIPTS/test_end_to_end.py b/CI/SCRIPTS/test_end_to_end.py index 1fc1c982..f3931e0b 100644 --- a/CI/SCRIPTS/test_end_to_end.py +++ b/CI/SCRIPTS/test_end_to_end.py @@ -37,7 +37,12 @@ VV_DSPK, Oa01, ) -from eoreader.env_vars import DEM_PATH, S3_DB_URL_ROOT, SAR_DEF_RES, TEST_USING_S3_DB +from eoreader.env_vars import ( + DEM_PATH, + S3_DB_URL_ROOT, + SAR_DEF_PIXEL_SIZE, + TEST_USING_S3_DB, +) from eoreader.products.product import Product, SensorType from eoreader.reader import CheckMethod @@ -174,12 +179,12 @@ def _test_core( is_zip = "_ZIP" if prod.is_archived else "" prod.output = os.path.join(output, f"{prod.condensed_name}{is_zip}") - # Manage S3 resolution to speed up processes + # Manage S3 pixel_size to speed up processes if prod.sensor_type == SensorType.SAR: - res = 1000.0 - os.environ[SAR_DEF_RES] = str(res) + pixel_size = 1000.0 + os.environ[SAR_DEF_PIXEL_SIZE] = str(pixel_size) else: - res = prod.resolution * 50 + pixel_size = prod.pixel_size * 50 # BAND TESTS LOGGER.info("Checking load and stack") @@ -195,7 +200,7 @@ def _test_core( curr_path = os.path.join(tmp_dir, f"{prod.condensed_name}_stack.tif") stack = prod.stack( stack_bands, - resolution=res, + pixel_size=pixel_size, stack_path=curr_path, clean_optical="clean", **kwargs, diff --git a/CI/SCRIPTS/test_index.py b/CI/SCRIPTS/test_index.py index 6c7c98e6..76b7193d 100644 --- a/CI/SCRIPTS/test_index.py +++ b/CI/SCRIPTS/test_index.py @@ -63,7 +63,7 @@ def test_index(): idx_list = [ idx for idx in spyndex_list + get_eoreader_indices() if prod.has_band(idx) ] - idx = prod.load(idx_list, resolution=RES) + idx = prod.load(idx_list, pixel_size=RES) for idx_name, idx_arr in idx.items(): LOGGER.info("Write and compare: %s", idx_name) diff --git a/CI/SCRIPTS/test_others.py b/CI/SCRIPTS/test_others.py index ab4aea58..4bfb19c5 100644 --- a/CI/SCRIPTS/test_others.py +++ b/CI/SCRIPTS/test_others.py @@ -170,7 +170,7 @@ def test_products(): stack_path = os.path.join(tmp_dir, "stack.tif") stack = prod1.stack( BLUE, - resolution=prod1.resolution * 100, + pixel_size=prod1.pixel_size * 100, save_as_int=True, stack_path=stack_path, ) @@ -278,10 +278,10 @@ def test_dems_https(): # Loading same DEM from two different sources (one hosted locally and the other hosted on S3 compatible storage) with tempenv.TemporaryEnvironment({DEM_PATH: local_path}): # Local DEM dem_local = prod.load( - [DEM], resolution=30 + [DEM], pixel_size=30 ) # Loading same DEM from two different sources (one hosted locally and the other hosted on S3 compatible storage) with tempenv.TemporaryEnvironment({DEM_PATH: remote_path}): # Remote DEM - dem_remote = prod.load([DEM], resolution=30) + dem_remote = prod.load([DEM], pixel_size=30) xr.testing.assert_equal(dem_local[DEM], dem_remote[DEM]) @@ -309,10 +309,10 @@ def test_dems_S3(): # Loading same DEM from two different sources (one hosted locally and the other hosted on S3 compatible storage) with tempenv.TemporaryEnvironment({DEM_PATH: local_path}): # Local DEM dem_local = prod.load( - [DEM], resolution=30 + [DEM], pixel_size=30 ) # Loading same DEM from two different sources (one hosted locally and the other hosted on S3 compatible storage) with tempenv.TemporaryEnvironment({DEM_PATH: s3_path}): # S3 DEM - dem_s3 = prod.load([DEM], resolution=30) + dem_s3 = prod.load([DEM], pixel_size=30) xr.testing.assert_equal(dem_local[DEM], dem_s3[DEM]) diff --git a/CI/SCRIPTS/test_satellites.py b/CI/SCRIPTS/test_satellites.py index 18c51b60..9edca60a 100644 --- a/CI/SCRIPTS/test_satellites.py +++ b/CI/SCRIPTS/test_satellites.py @@ -35,7 +35,7 @@ CI_EOREADER_BAND_FOLDER, DEM_PATH, S3_DB_URL_ROOT, - SAR_DEF_RES, + SAR_DEF_PIXEL_SIZE, TEST_USING_S3_DB, ) from eoreader.keywords import SLSTR_RAD_ADJUST @@ -200,14 +200,14 @@ def _test_core( get_ci_data_dir().joinpath(prod.condensed_name) ) - # Manage S3 resolution to speed up processes + # Manage S3 pixel_size to speed up processes if prod.sensor_type == SensorType.SAR: - res = 1000.0 - os.environ[SAR_DEF_RES] = str(res) + pixel_size = 1000.0 + os.environ[SAR_DEF_PIXEL_SIZE] = str(pixel_size) elif prod.constellation_id in ["S2", "S2_THEIA"]: - res = 20.0 * 50 # Legacy + pixel_size = 20.0 * 50 # Legacy else: - res = prod.resolution * 50 + pixel_size = prod.pixel_size * 50 # Extent LOGGER.info("Checking extent") @@ -281,12 +281,12 @@ def _test_core( # Check that band loaded 2 times gives the same results (disregarding float uncertainties) assert prod.load([]) == {} band_arr_raw = prod.load( - first_band.value, resolution=res, clean_optical="raw" + first_band.value, pixel_size=pixel_size, clean_optical="raw" )[first_band] band_arr1 = prod.load( - first_band, resolution=res, clean_optical="nodata" + first_band, pixel_size=pixel_size, clean_optical="nodata" )[first_band] - band_arr2 = prod.load(first_band, resolution=res)[first_band] + band_arr2 = prod.load(first_band, pixel_size=pixel_size)[first_band] np.testing.assert_array_almost_equal(band_arr1, band_arr2) ci.assert_val(band_arr_raw.dtype, np.float32, "band_arr_raw dtype") ci.assert_val(band_arr1.dtype, np.float32, "band_arr1 dtype") @@ -302,7 +302,7 @@ def _test_core( curr_path = os.path.join(tmp_dir, f"{prod.condensed_name}_stack.tif") stack = prod.stack( stack_bands, - resolution=res, + pixel_size=pixel_size, stack_path=curr_path, clean_optical="clean", **kwargs, diff --git a/CI/SCRIPTS/test_stac.py b/CI/SCRIPTS/test_stac.py index f003aaa0..e3bb81c8 100644 --- a/CI/SCRIPTS/test_stac.py +++ b/CI/SCRIPTS/test_stac.py @@ -203,7 +203,7 @@ def _test_core( f"{CONSTELLATION} (item.properties)", ) compare( - item.properties[GSD], prod.resolution, f"{GSD} (item.properties)" + item.properties[GSD], prod.pixel_size, f"{GSD} (item.properties)" ) compare( item.properties[DATETIME], diff --git a/CI/SCRIPTS_SNAP/test_all_sat_end_to_end_on_disk.py b/CI/SCRIPTS_SNAP/test_all_sat_end_to_end_on_disk.py index e5cb442d..20d4e008 100644 --- a/CI/SCRIPTS_SNAP/test_all_sat_end_to_end_on_disk.py +++ b/CI/SCRIPTS_SNAP/test_all_sat_end_to_end_on_disk.py @@ -42,7 +42,12 @@ VV_DSPK, Oa01, ) -from eoreader.env_vars import DEM_PATH, S3_DB_URL_ROOT, SAR_DEF_RES, TEST_USING_S3_DB +from eoreader.env_vars import ( + DEM_PATH, + S3_DB_URL_ROOT, + SAR_DEF_PIXEL_SIZE, + TEST_USING_S3_DB, +) from eoreader.keywords import SLSTR_RAD_ADJUST from eoreader.products import S2Product, SlstrRadAdjust from eoreader.products.product import Product, SensorType @@ -200,12 +205,12 @@ def _test_core( is_zip = "_ZIP" if prod.is_archived else "" prod.output = os.path.join(output, f"{prod.condensed_name}{is_zip}") - # Manage S3 resolution to speed up processes + # Manage S3 pixel_size to speed up processes if prod.sensor_type == SensorType.SAR: - res = 1000.0 - os.environ[SAR_DEF_RES] = str(res) + pixel_size = 1000.0 + os.environ[SAR_DEF_PIXEL_SIZE] = str(pixel_size) else: - res = prod.resolution * 50 + pixel_size = prod.pixel_size * 50 # BAND TESTS LOGGER.info("Checking load and stack") @@ -221,7 +226,7 @@ def _test_core( curr_path = os.path.join(tmp_dir, f"{prod.condensed_name}_stack.tif") stack = prod.stack( stack_bands, - resolution=res, + pixel_size=pixel_size, stack_path=curr_path, clean_optical="clean", **kwargs, diff --git a/docs/custom.md b/docs/custom.md index 5e3b9956..30dc8c75 100644 --- a/docs/custom.md +++ b/docs/custom.md @@ -40,7 +40,7 @@ If you know them, it is best to give **EOReader** all the data you know about yo be used - `constellation`: product constellation. If not provided, `CUSTOM` will be set. Either a string of a `Constellation` enum. - `product_type`: product type. If not provided, `CUSTOM` will be set. -- `resolution`: product default resolution. If not provided, the stack resolution will be used. +- `pixel_size`: product default pixel size. If not provided, the stack pixel size will be used. For optical products, two additional keyword can be set to compute the hillshade band: @@ -60,7 +60,7 @@ custom_prod = Reader().open( sensor_type="OPTICAL", constellation="WV02", product_type="Ortho", - resolution=2.0, + pixel_size=2.0, sun_azimuth=10.0, sun_zenith=20.0, band_map={ diff --git a/docs/faq.md b/docs/faq.md index ae57e4c9..489fbd5a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -43,6 +43,10 @@ Sentinel-1 or other SAR constellations may fail to load KML extent files. The cause is unknown, but a workaround based on `ogr2ogr` has been written. Please be sure to have `ogr2ogr` (and other `GDAL` scripts available in your PATH) +For example, if you downloaded QGIS on Windows, you could simply put in your PATH: +![qgis](https://zupimages.net/up/23/13/njvv.png) +All GDAL scripts, exe, DLL, etc. are stored in the `bin` folder. + ## SNAP > ⚠ Be sure to use SNAP 8.0 or more, and please verify that your software is up-to-date. diff --git a/docs/main_features.md b/docs/main_features.md index d04132ec..8765eeb2 100644 --- a/docs/main_features.md +++ b/docs/main_features.md @@ -101,8 +101,8 @@ ok_bands = to_str([band for band in band_list if prod.has_band(band)]) # Sentinel-2 cannot produce satellite band TIR_1 and cloud band SHADOWS # Load bands -# if resolution is not specified -> load at default resolution (10.0 m for S2 data) -bands = prod.load(ok_bands, resolution=20.) +# if pixel_size is not specified -> load at default pixel_size (10.0 m for S2 data) +bands = prod.load(ok_bands, pixel_size=20.) # NOTE: every array that comes out `load` are collocated, which isn't the case if you load arrays separately # (important for DEM data as they may have different grids) ``` @@ -143,8 +143,8 @@ If the same band is asked several time, its order will be the one of the last de ```python # Create a stack with the previous OK bands stack = prod.stack( - ok_bands, - resolution=300., + ok_bands, + pixel_size=300., stack_path=os.path.join(prod.output, "stack.tif") ) ``` diff --git a/docs/notebooks/SAR.ipynb b/docs/notebooks/SAR.ipynb index dc42d78c..5e9584f3 100644 --- a/docs/notebooks/SAR.ipynb +++ b/docs/notebooks/SAR.ipynb @@ -706,8 +706,8 @@ } ], "source": [ - "# Load those bands as a dict of xarray.DataArray, with a 20m resolution\n", - "band_dict = prod.load(ok_bands, resolution=20.)\n", + "# Load those bands as a dict of xarray.DataArray, with a 20m pixel size\n", + "band_dict = prod.load(ok_bands, pixel_size=20.)\n", "band_dict[HH]" ] }, @@ -728,7 +728,7 @@ "from eoreader.keywords import SAR_INTERP_NA\n", "band_dict = prod.load(\n", " ok_bands, \n", - " resolution=20., \n", + " pixel_size=20.,\n", " **{SAR_INTERP_NA: True}\n", ")\n", "```" diff --git a/docs/notebooks/custom.ipynb b/docs/notebooks/custom.ipynb index a05e7791..695b2557 100644 --- a/docs/notebooks/custom.ipynb +++ b/docs/notebooks/custom.ipynb @@ -1356,7 +1356,7 @@ "- `datetime`: product acquisition datetime. If not provided, the datetime of the creation of the object will be used\n", "- `constellation`: product constellation. If not provided, `CUSTOM` will be set. Either a string of a Constellation enum.\n", "- `product_type`: product type. If not provided, `CUSTOM` will be set.\n", - "- `resolution`: product default resolution. If not provided, the stack resolution will be used.\n", + "- `pixel_size`: product default pixel size. If not provided, the stack pixel size will be used.\n", "\n", "For optical products, two additional keyword can be set to compute the hillshade band:\n", "- `sun_azimuth`\n", @@ -1389,7 +1389,7 @@ " sensor_type=SensorType.OPTICAL,\n", " constellation=\"WV02\",\n", " product_type=\"Ortho\",\n", - " resolution=2.0,\n", + " pixel_size=2.0,\n", " sun_azimuth=10.0,\n", " sun_zenith=20.0,\n", " band_map={BLUE: 1, GREEN: 2, RED: 3, NIR: 4, SWIR_1: 5},\n", @@ -1930,7 +1930,7 @@ " datetime=\"20210827T162210\",\n", " constellation=\"ICEYE\",\n", " product_type=\"GRD\",\n", - " resolution=6.0,\n", + " pixel_size=6.0,\n", " band_map={VV: 1, VV_DSPK: 2},\n", ")" ] diff --git a/docs/notebooks/dem.ipynb b/docs/notebooks/dem.ipynb index ea736a50..3d82d8cf 100644 --- a/docs/notebooks/dem.ipynb +++ b/docs/notebooks/dem.ipynb @@ -325,7 +325,7 @@ ], "source": [ "# Orthorectifying with COPDEM-30\n", - "vv = prod.load(VV, resolution=prod.resolution*100)[VV]\n", + "vv = prod.load(VV, pixel_size=prod.pixel_size*100)[VV]\n", "vv.plot()" ] }, @@ -401,7 +401,7 @@ " SNAP_DEM_NAME: SnapDems.GETASSE30.value\n", " }\n", "):\n", - " vv2 = prod.load(VV, resolution=prod.resolution*100)[VV]\n", + " vv2 = prod.load(VV, pixel_size=prod.pixel_size*100)[VV]\n", " vv2.plot()" ] }, @@ -418,7 +418,7 @@ " DEM_PATH: dem_tif\n", " }\n", "):\n", - " vv = prod.load(VV, resolution=prod.resolution*100)[VV]\n", + " vv = prod.load(VV, pixel_size=prod.pixel_size*100)[VV]\n", "```" ] }, @@ -463,7 +463,7 @@ "output_type": "stream", "text": [ "2022-10-10 10:26:45,276 - [DEBUG] - Loading bands ['RED']\n", - "2022-10-10 10:26:45,277 - [INFO] - Manually orthorectified stack not given by the user. Reprojecting whole stack, this may take a while. (May be inaccurate on steep terrain, depending on the DEM resolution)\n", + "2022-10-10 10:26:45,277 - [INFO] - Manually orthorectified stack not given by the user. Reprojecting whole stack, this may take a while. (May be inaccurate on steep terrain, depending on the DEM pixel size)\n", "2022-10-10 10:26:45,279 - [ERROR] - As you are using a non orthorectified VHR product (/home/data/DATA/PRODS/PLEIADES/3302499201/IMG_PHR1A_MS_004), you must provide a valid DEM through the EOREADER_DEM_PATH environment variable\n" ] } @@ -487,7 +487,7 @@ "output_type": "stream", "text": [ "2022-10-10 10:26:45,289 - [DEBUG] - Loading bands ['RED']\n", - "2022-10-10 10:26:45,290 - [INFO] - Manually orthorectified stack not given by the user. Reprojecting whole stack, this may take a while. (May be inaccurate on steep terrain, depending on the DEM resolution)\n", + "2022-10-10 10:26:45,290 - [INFO] - Manually orthorectified stack not given by the user. Reprojecting whole stack, this may take a while. (May be inaccurate on steep terrain, depending on the DEM pixel size)\n", "2022-10-10 10:26:46,516 - [DEBUG] - Orthorectifying data with /home/data/DS2/BASES_DE_DONNEES/GLOBAL/COPDEM_30m/COPDEM_30m.vrt\n", "2022-10-10 10:28:29,434 - [DEBUG] - Read RED\n", "2022-10-10 10:28:30,442 - [DEBUG] - Converting RED to reflectance\n", diff --git a/docs/notebooks/water_detection.ipynb b/docs/notebooks/water_detection.ipynb index 39338d7f..94eda276 100644 --- a/docs/notebooks/water_detection.ipynb +++ b/docs/notebooks/water_detection.ipynb @@ -50,12 +50,12 @@ "metadata": {}, "outputs": [], "source": [ - "def load_ndwi(prod, res):\n", + "def load_ndwi(prod, pixel_size):\n", " \"\"\"\n", " Load NDWI index (and rename the array)\n", " \"\"\"\n", " # Read NDWI index\n", - " ndwi = prod.load(NDWI, resolution=res)[NDWI]\n", + " ndwi = prod.load(NDWI, pixel_size=pixel_size)[NDWI]\n", " ndwi_name = f\"{ndwi.attrs['constellation']} NDWI\"\n", " return ndwi.rename(ndwi_name)\n", "\n", @@ -280,8 +280,8 @@ " extents.append(prod.extent())\n", " \n", " # Read NDWI index\n", - " # Let's say we want a 60. meters resolution\n", - " ndwi = load_ndwi(prod, res=60.)\n", + " # Let's say we want a 60. meters pixel_size\n", + " ndwi = load_ndwi(prod, pixel_size=60.)\n", " ndwi_arrays.append(ndwi)\n", " \n", " # Extract water\n", diff --git a/docs/optical.md b/docs/optical.md index 9750dfc4..5e0827ef 100644 --- a/docs/optical.md +++ b/docs/optical.md @@ -4,9 +4,10 @@ The product resolution is the one given in [Data Access Portfolio (2014-2022, section 6.2)](https://spacedata.copernicus.eu/documents/20126/0/DAP+Release+phase2+V2_8.pdf/82297817-2b96-d3de-c397-776292336434?t=1633508426589). The Data Access Portfolio Document presents the offer of the datasets and data access services that are made available to the Copernicus Users in response to their Earth Observation data requirements. +However, especially for SAR data, the default pixel size of GRD bands is not the same as the product resolution! ``` {container} full-width -| Constellations | Class | Product Types | Default Resolution | Use archive | +| Constellations | Class | Product Types | Default Pixel Size | Use archive | |------------------------------|---------------------------------------------------------|---------------------------|--------------------------------------|------------------------------| | Sentinel-2 | {meth}`~eoreader.products.optical.s2_product.S2Product` | L1C & L2A & L2Ap | 10m | ✅ | | Sentinel-2 Theia | {meth}`~eoreader.products.S2TheiaProduct` | L2A | 10m | ✅ | diff --git a/docs/sar.md b/docs/sar.md index 9826ad52..fa0afcee 100644 --- a/docs/sar.md +++ b/docs/sar.md @@ -121,11 +121,10 @@ The default resolution of SAR products is the one given in For resolutions not available in this document, we are using the pixel spacing given by the constellation's provider. Complex data are **always** converted back to ground range to be used, so the complex resolution is **never** used by EOReader. -SAR default resolution is the **pixel spacing** given by the constellation provider, to follow common habits. -Sometimes, especially when converting complex data to ground range, this resolution needs to be adaptated. +The default pixel size of GRD bands is not the same as the product resolution! (i.e. pixel size of 10m with a resolution of 20m for Sentinel-1 IW data) > ⚠ Pay attention that for a pixel spacing of 10 meters and a rg x az resolution of 23m, objects under 23m won't be resolved ! -> As this may be counter-intuitive, it is recommanded to **always** specify the resolution when loading SAR data. +> As this may be counter-intuitive, it is recommanded to **always** specify the pixel size when loading SAR data. ### Sentinel-1 @@ -401,10 +400,10 @@ The default `Terrain Correction` step is: ### Default SNAP resolution -You can override default SNAP resolution (in meters) when geocoding SAR bands by setting the following environment +You can override default SNAP pixel spacing (in meters) when geocoding SAR bands by setting the following environment variable: -- `EOREADER_SAR_DEFAULT_RES`: 0.0 by default, which means using the product's default resolution +- `EOREADER_SAR_DEFAULT_RES`: 0.0 by default, which means using the product's default pixel spacing ## Documentary Sources diff --git a/eoreader/__meta__.py b/eoreader/__meta__.py index efc1220e..c5a7ff6b 100644 --- a/eoreader/__meta__.py +++ b/eoreader/__meta__.py @@ -17,7 +17,7 @@ """ **EOReader** library """ -__version__ = "0.19.4" +__version__ = "0.20.0.dev0" __title__ = "eoreader" __description__ = ( "Remote-sensing opensource python library reading optical and SAR constellations, " diff --git a/eoreader/env_vars.py b/eoreader/env_vars.py index 782a6b46..a5f96664 100644 --- a/eoreader/env_vars.py +++ b/eoreader/env_vars.py @@ -22,8 +22,8 @@ DSPK_GRAPH = "EOREADER_DSPK_GRAPH" """Environment variable for overriding default despeckling graph path""" -SAR_DEF_RES = "EOREADER_SAR_DEFAULT_RES" -"""Environment variable for SAR default resolution, used for SNAP orthorectification to override default resolution.""" +SAR_DEF_PIXEL_SIZE = "EOREADER_SAR_DEFAULT_PIXEL_SIZE" +"""Environment variable for SAR default pixel ize, used for SNAP orthorectification to override default pixel size.""" DEM_PATH = "EOREADER_DEM_PATH" """Environment variable for overriding default DEM path""" diff --git a/eoreader/products/custom_product.py b/eoreader/products/custom_product.py index ddde6bcb..2f01a2ce 100644 --- a/eoreader/products/custom_product.py +++ b/eoreader/products/custom_product.py @@ -30,7 +30,7 @@ from lxml.builder import E from rasterio import crs from rasterio.enums import Resampling -from sertit import files, misc, rasters +from sertit import files, logs, misc, rasters from sertit.misc import ListEnum from eoreader import DATETIME_FMT, EOREADER_NAME, cache, utils @@ -64,6 +64,7 @@ class CustomFields(ListEnum): CONSTELLATION = "constellation" INSTRUMENT = "instrument" RES = "resolution" + PIX_SIZE = "pixel_size" PROD_TYPE = "product_type" SUN_AZ = "sun_azimuth" SUN_ZEN = "sun_zenith" @@ -212,16 +213,22 @@ def _get_constellation(self) -> Constellation: const = CUSTOM return Constellation.convert_from(const)[0] - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ - resolution = self.kwargs.get(CustomFields.RES.value) - if resolution is None: + pixel_size = self.kwargs.get(CustomFields.PIX_SIZE.value) + if pixel_size is None and CustomFields.RES.value in self.kwargs: + logs.deprecation_warning( + "`resolution` is deprecated in favor of `pixel_size` to avoid confusion." + ) + pixel_size = self.kwargs.pop(CustomFields.RES.value) + + if pixel_size is None: with rasterio.open(str(self.get_default_band_path())) as ds: - return ds.res[0] + self.pixel_size = ds.res[0] else: - return resolution + self.pixel_size = pixel_size def _set_instrument(self) -> None: """ @@ -315,14 +322,14 @@ def crs(self) -> crs.CRS: return def_crs def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Get the stack path for each asked band Args: band_list (list): List of the wanted bands - resolution (float): Band resolution + pixel_size (float): Band pixel size kwargs: Other arguments used to load bands Returns: @@ -358,7 +365,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -371,8 +378,8 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray @@ -380,7 +387,7 @@ def _read_band( """ return utils.read( path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, indexes=[self.bands[band].id], @@ -390,17 +397,17 @@ def _read_band( def _load_bands( self, bands: Union[list, BandNames], - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: bands (list, BandNames): List of the wanted bands - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -413,16 +420,16 @@ def _load_bands( if not isinstance(bands, list): bands = [bands] - if resolution is None and size is not None: - resolution = self._resolution_from_size(size) + if pixel_size is None and size is not None: + pixel_size = self._pixel_size_from_img_size(size) - band_paths = self.get_band_paths(bands, resolution, **kwargs) + band_paths = self.get_band_paths(bands, pixel_size, **kwargs) # Open bands and get array (resampled if needed) band_arrays = {} for band_name, band_path in band_paths.items(): band_arrays[band_name] = self._read_band( - band_path, band=band_name, resolution=resolution, size=size, **kwargs + band_path, band=band_name, pixel_size=pixel_size, size=size, **kwargs ) return band_arrays @@ -452,7 +459,7 @@ def get_mean_sun_angles(self) -> (float, float): def _compute_hillshade( self, dem_path: str = "", - resolution: Union[float, tuple] = None, + pixel_size: Union[float, tuple] = None, size: Union[list, tuple] = None, resampling: Resampling = Resampling.bilinear, ) -> Union[Path, CloudPath]: @@ -461,16 +468,16 @@ def _compute_hillshade( Args: dem_path (str): DEM path, using EUDEM/MERIT DEM if none - resolution (Union[float, tuple]): Resolution in meters. If not specified, use the product resolution. + pixel_size (Union[float, tuple]): Pixel size in meters. If not specified, use the product pixel size. resampling (Resampling): Resampling method - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: Union[Path, CloudPath]: Hillshade mask path """ sun_az, sun_zen = self.get_mean_sun_angles() if sun_az is not None and sun_zen is not None: # Warp DEM - warped_dem_path = self._warp_dem(dem_path, resolution, size, resampling) + warped_dem_path = self._warp_dem(dem_path, pixel_size, size, resampling) # Get Hillshade path hillshade_name = ( diff --git a/eoreader/products/optical/dimap_v1_product.py b/eoreader/products/optical/dimap_v1_product.py index acefd9c5..44d2e034 100644 --- a/eoreader/products/optical/dimap_v1_product.py +++ b/eoreader/products/optical/dimap_v1_product.py @@ -150,7 +150,7 @@ def footprint(self) -> gpd.GeoDataFrame: footprint_dezoom = 10 arr = rasters.read( self.get_default_band_path(), - resolution=self.resolution * footprint_dezoom, + resolution=self.pixel_size * footprint_dezoom, indexes=[1], ) @@ -323,7 +323,7 @@ def _has_cloud_band(self, band: BandNames) -> bool: def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -332,8 +332,8 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} diff --git a/eoreader/products/optical/dimap_v2_product.py b/eoreader/products/optical/dimap_v2_product.py index b92c3316..4b20b433 100644 --- a/eoreader/products/optical/dimap_v2_product.py +++ b/eoreader/products/optical/dimap_v2_product.py @@ -305,9 +305,9 @@ def _post_init(self, **kwargs) -> None: # Post init done by the super class super()._post_init(**kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ # Not Pansharpened images if self.band_combi in [ @@ -316,10 +316,10 @@ def _get_resolution(self) -> float: DimapV2BandCombination.MS_N, DimapV2BandCombination.MS_FS, ]: - return self._ms_res + self.pixel_size = self._ms_res # Pansharpened images else: - return self._pan_res + self.pixel_size = self._pan_res def _map_bands_core(self, **kwargs) -> None: """ @@ -850,7 +850,7 @@ def _has_cloud_band(self, band: BandNames) -> bool: def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -859,8 +859,8 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -873,18 +873,18 @@ def _open_clouds( has_vec = len(cld_vec) > 0 # Load default xarray as a template - def_utm_path = self._get_default_utm_band(resolution=resolution, size=size) + def_utm_path = self._get_default_utm_band(pixel_size=pixel_size, size=size) with rasterio.open(str(def_utm_path)) as dst: if dst.count > 1: def_xarr = utils.read( dst, - resolution=resolution, + pixel_size=pixel_size, size=size, indexes=[self.bands[self.get_default_band()].id], ) else: - def_xarr = utils.read(dst, resolution=resolution, size=size) + def_xarr = utils.read(dst, pixel_size=pixel_size, size=size) # Load nodata width = def_xarr.rio.width @@ -1043,7 +1043,7 @@ def open_mask(self, mask_str: str, **kwargs) -> gpd.GeoDataFrame: ) # Do not keep pixelized mask - mask = utils.simplify_footprint(mask, self.resolution) + mask = vectors.simplify_footprint(mask, self.pixel_size) # Sometimes the GML mask lacks crs (why ?) elif ( diff --git a/eoreader/products/optical/gs2_product.py b/eoreader/products/optical/gs2_product.py index 27435056..56e2da2b 100644 --- a/eoreader/products/optical/gs2_product.py +++ b/eoreader/products/optical/gs2_product.py @@ -175,9 +175,9 @@ def _post_init(self, **kwargs) -> None: # Post init done by the super class super()._post_init(**kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ if self.band_combi in [ Gs2BandCombination.PAN, @@ -201,10 +201,10 @@ def _get_resolution(self) -> float: # Bundle: return MS resolution if self.product_type == Gs2BandCombination.PM4: - return self._ms_res + self.pixel_size = self._ms_res # One res product else: - return resol[self.product_type][is_psh] + self.pixel_size = resol[self.product_type][is_psh] def _set_instrument(self) -> None: """ diff --git a/eoreader/products/optical/hls_product.py b/eoreader/products/optical/hls_product.py index e8fd0255..bfff089a 100644 --- a/eoreader/products/optical/hls_product.py +++ b/eoreader/products/optical/hls_product.py @@ -156,21 +156,21 @@ def _get_fmask_path(self) -> Union[CloudPath, Path]: return self._get_path("Fmask") - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ - return 30.0 + self.pixel_size = 30.0 def open_mask( - self, resolution: float = None, size: Union[list, tuple] = None, **kwargs + self, pixel_size: float = None, size: Union[list, tuple] = None, **kwargs ) -> Union[xr.DataArray, None]: """ Open a HLS Fmask Args: - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: Union[xarray.DataArray, None]: Mask array @@ -181,7 +181,7 @@ def open_mask( # Open mask band return utils.read( mask_path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.nearest, # Nearest to keep the flags masked=False, @@ -189,7 +189,7 @@ def open_mask( ).astype(np.uint8) def _load_nodata( - self, resolution: float = None, size: Union[list, tuple] = None, **kwargs + self, pixel_size: float = None, size: Union[list, tuple] = None, **kwargs ) -> Union[xr.DataArray, None]: """ Load nodata (unimaged pixels) as a numpy array. @@ -199,8 +199,8 @@ def _load_nodata( (unusable data mask) for more information. Args: - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: Union[xarray.DataArray, None]: Nodata array @@ -622,7 +622,7 @@ def _get_split_name(self) -> list: return [x for x in self.name.split(".") if x] def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -643,7 +643,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Useless here + pixel_size (float): Useless here kwargs: Other arguments used to load bands Returns: @@ -659,7 +659,7 @@ def get_band_paths( # Get clean band path clean_band = self._get_clean_band_path( - band, resolution=resolution, **kwargs + band, pixel_size=pixel_size, **kwargs ) if clean_band.is_file(): band_paths[band] = clean_band @@ -744,7 +744,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -757,15 +757,15 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray """ band_arr = utils.read( path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, **kwargs, @@ -841,17 +841,17 @@ def _manage_nodata( def _load_bands( self, bands: Union[list, BandNames], - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: bands (list, BandNames): List of the wanted bands - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -864,13 +864,13 @@ def _load_bands( if not isinstance(bands, list): bands = [bands] - if resolution is None and size is not None: - resolution = self._resolution_from_size(size) - band_paths = self.get_band_paths(bands, resolution=resolution, **kwargs) + if pixel_size is None and size is not None: + pixel_size = self._pixel_size_from_img_size(size) + band_paths = self.get_band_paths(bands, pixel_size=pixel_size, **kwargs) # Open bands and get array (resampled if needed) band_arrays = self._open_bands( - band_paths, resolution=resolution, size=size, **kwargs + band_paths, pixel_size=pixel_size, size=size, **kwargs ) return band_arrays @@ -921,7 +921,7 @@ def _has_cloud_band(self, band: BandNames) -> bool: def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -932,8 +932,8 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -942,7 +942,7 @@ def _open_clouds( if bands: # Open Fmask - fmask = self.open_mask(resolution, size, **kwargs) + fmask = self.open_mask(pixel_size, size, **kwargs) # Don't use load_nodata in order not to load a 2nd time fmask nodata = np.where(fmask == self._mask_nodata, 1, 0) diff --git a/eoreader/products/optical/landsat_product.py b/eoreader/products/optical/landsat_product.py index 83fafa63..4e2d88d5 100644 --- a/eoreader/products/optical/landsat_product.py +++ b/eoreader/products/optical/landsat_product.py @@ -246,9 +246,9 @@ def _get_path(self, band_id: str) -> Union[CloudPath, Path]: return path - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ if self.constellation in [ Constellation.L8, @@ -258,11 +258,11 @@ def _get_resolution(self) -> float: self.constellation in [Constellation.L4, Constellation.L5] and self.instrument == LandsatInstrument.TM ): - res = 30.0 + pixel_size = 30.0 else: - res = 60.0 + pixel_size = 60.0 - return res + self.pixel_size = pixel_size @cache @simplify @@ -302,7 +302,7 @@ def footprint(self) -> gpd.GeoDataFrame: # Read the file with a very low resolution nodata_band = utils.read( self._get_path(self._pixel_quality_id), - resolution=self.resolution * footprint_dezoom, + pixel_size=self.pixel_size * footprint_dezoom, masked=False, ) @@ -916,7 +916,7 @@ def _get_name_constellation_specific(self) -> str: return name def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -937,7 +937,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Useless here + pixel_size (float): Useless here kwargs: Other arguments used to load bands Returns: @@ -954,7 +954,7 @@ def get_band_paths( # Get clean band path clean_band = self._get_clean_band_path( - band, resolution=resolution, **kwargs + band, pixel_size=pixel_size, **kwargs ) if clean_band.is_file(): band_paths[band] = clean_band @@ -1055,7 +1055,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -1068,8 +1068,8 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray @@ -1082,7 +1082,7 @@ def _read_band( if self._pixel_quality_id in filename or self._radsat_id in filename: band_arr = utils.read( path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.nearest, # NEAREST TO KEEP THE FLAGS masked=False, @@ -1092,7 +1092,7 @@ def _read_band( # Read band (call superclass generic method) band_arr = utils.read( path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, **kwargs, @@ -1360,17 +1360,17 @@ def _manage_nodata( def _load_bands( self, bands: Union[list, BandNames], - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: bands (list, BandNames): List of the wanted bands - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -1383,13 +1383,13 @@ def _load_bands( if not isinstance(bands, list): bands = [bands] - if resolution is None and size is not None: - resolution = self._resolution_from_size(size) - band_paths = self.get_band_paths(bands, resolution=resolution, **kwargs) + if pixel_size is None and size is not None: + pixel_size = self._pixel_size_from_img_size(size) + band_paths = self.get_band_paths(bands, pixel_size=pixel_size, **kwargs) # Open bands and get array (resampled if needed) band_arrays = self._open_bands( - band_paths, resolution=resolution, size=size, **kwargs + band_paths, pixel_size=pixel_size, size=size, **kwargs ) return band_arrays @@ -1478,7 +1478,7 @@ def _e_tm_has_cloud_band(band: BandNames) -> bool: def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -1494,8 +1494,8 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -1505,7 +1505,7 @@ def _open_clouds( if bands: # Open QA band landsat_qa_path = self._get_path(self._pixel_quality_id) - qa_arr = self._read_band(landsat_qa_path, resolution=resolution, size=size) + qa_arr = self._read_band(landsat_qa_path, pixel_size=pixel_size, size=size) if self.instrument in [ LandsatInstrument.OLI, diff --git a/eoreader/products/optical/maxar_product.py b/eoreader/products/optical/maxar_product.py index 223790e8..8191d9e5 100644 --- a/eoreader/products/optical/maxar_product.py +++ b/eoreader/products/optical/maxar_product.py @@ -466,19 +466,19 @@ def _post_init(self, **kwargs) -> None: super()._post_init(**kwargs) @abstractmethod - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ # Band combination root, _ = self.read_mtd() - resol = root.findtext(".//MAP_PROJECTED_PRODUCT/PRODUCTGSD") - if not resol: + gsd = root.findtext(".//MAP_PROJECTED_PRODUCT/PRODUCTGSD") + if not gsd: raise InvalidProductError( "Cannot find PRODUCTGSD type in the metadata file" ) - return float(resol) + self.pixel_size = float(gsd) def _get_spectral_bands(self) -> dict: """ @@ -719,67 +719,67 @@ def _get_band_map(self, **kwargs) -> dict: MaxarBandId.Y, MaxarBandId.C, ]: - band_map = {PAN: pan.update(id=1, gsd=self.resolution)} + band_map = {PAN: pan.update(id=1, gsd=self.pixel_size)} elif self.band_combi == MaxarBandId.RGB: band_map = { - RED: red.update(id=1, gsd=self.resolution), - GREEN: green.update(id=2, gsd=self.resolution), - BLUE: blue.update(id=3, gsd=self.resolution), + RED: red.update(id=1, gsd=self.pixel_size), + GREEN: green.update(id=2, gsd=self.pixel_size), + BLUE: blue.update(id=3, gsd=self.pixel_size), } elif self.band_combi == MaxarBandId.NRG: band_map = { - NIR: nir.update(id=1, gsd=self.resolution), - NARROW_NIR: nir.update(id=1, gsd=self.resolution), - RED: red.update(id=2, gsd=self.resolution), - GREEN: green.update(id=3, gsd=self.resolution), + NIR: nir.update(id=1, gsd=self.pixel_size), + NARROW_NIR: nir.update(id=1, gsd=self.pixel_size), + RED: red.update(id=2, gsd=self.pixel_size), + GREEN: green.update(id=3, gsd=self.pixel_size), } elif self.band_combi == MaxarBandId.BGRN: band_map = { - BLUE: blue.update(id=1, gsd=self.resolution), - GREEN: green.update(id=2, gsd=self.resolution), - RED: red.update(id=3, gsd=self.resolution), - NIR: nir.update(id=4, gsd=self.resolution), - NARROW_NIR: nir.update(id=4, gsd=self.resolution), + BLUE: blue.update(id=1, gsd=self.pixel_size), + GREEN: green.update(id=2, gsd=self.pixel_size), + RED: red.update(id=3, gsd=self.pixel_size), + NIR: nir.update(id=4, gsd=self.pixel_size), + NARROW_NIR: nir.update(id=4, gsd=self.pixel_size), } elif self.band_combi == MaxarBandId.MS1: band_map = { - NIR: nir.update(id=1, gsd=self.resolution), - NARROW_NIR: nir.update(id=1, gsd=self.resolution), - RED: red.update(id=2, gsd=self.resolution), - GREEN: green.update(id=3, gsd=self.resolution), - BLUE: blue.update(id=4, gsd=self.resolution), + NIR: nir.update(id=1, gsd=self.pixel_size), + NARROW_NIR: nir.update(id=1, gsd=self.pixel_size), + RED: red.update(id=2, gsd=self.pixel_size), + GREEN: green.update(id=3, gsd=self.pixel_size), + BLUE: blue.update(id=4, gsd=self.pixel_size), } elif self.band_combi == MaxarBandId.MS2: band_map = { - WV: wv.update(id=1, gsd=self.resolution), - VRE_1: vre.update(id=2, gsd=self.resolution), - VRE_2: vre.update(id=2, gsd=self.resolution), - VRE_3: vre.update(id=2, gsd=self.resolution), - YELLOW: yellow.update(id=3, gsd=self.resolution), - CA: ca.update(id=4, gsd=self.resolution), + WV: wv.update(id=1, gsd=self.pixel_size), + VRE_1: vre.update(id=2, gsd=self.pixel_size), + VRE_2: vre.update(id=2, gsd=self.pixel_size), + VRE_3: vre.update(id=2, gsd=self.pixel_size), + YELLOW: yellow.update(id=3, gsd=self.pixel_size), + CA: ca.update(id=4, gsd=self.pixel_size), } elif self.band_combi == MaxarBandId.Multi: if self.constellation_id in (MaxarSatId.WV02.name, MaxarSatId.WV03.name): band_map = { - CA: ca.update(id=1, gsd=self.resolution), - BLUE: blue.update(id=2, gsd=self.resolution), - GREEN: green.update(id=3, gsd=self.resolution), - YELLOW: yellow.update(id=4, gsd=self.resolution), - RED: red.update(id=5, gsd=self.resolution), - VRE_1: vre.update(id=6, gsd=self.resolution), - VRE_2: vre.update(id=6, gsd=self.resolution), - VRE_3: vre.update(id=6, gsd=self.resolution), - NIR: nir.update(id=7, gsd=self.resolution), - NARROW_NIR: nir.update(id=7, gsd=self.resolution), - WV: wv.update(id=8, gsd=self.resolution), + CA: ca.update(id=1, gsd=self.pixel_size), + BLUE: blue.update(id=2, gsd=self.pixel_size), + GREEN: green.update(id=3, gsd=self.pixel_size), + YELLOW: yellow.update(id=4, gsd=self.pixel_size), + RED: red.update(id=5, gsd=self.pixel_size), + VRE_1: vre.update(id=6, gsd=self.pixel_size), + VRE_2: vre.update(id=6, gsd=self.pixel_size), + VRE_3: vre.update(id=6, gsd=self.pixel_size), + NIR: nir.update(id=7, gsd=self.pixel_size), + NARROW_NIR: nir.update(id=7, gsd=self.pixel_size), + WV: wv.update(id=8, gsd=self.pixel_size), } else: band_map = { - NIR: nir.update(id=1, gsd=self.resolution), - NARROW_NIR: nir.update(id=1, gsd=self.resolution), - RED: red.update(id=2, gsd=self.resolution), - GREEN: green.update(id=3, gsd=self.resolution), - BLUE: blue.update(id=4, gsd=self.resolution), + NIR: nir.update(id=1, gsd=self.pixel_size), + NARROW_NIR: nir.update(id=1, gsd=self.pixel_size), + RED: red.update(id=2, gsd=self.pixel_size), + GREEN: green.update(id=3, gsd=self.pixel_size), + BLUE: blue.update(id=4, gsd=self.pixel_size), } else: raise InvalidProductError( @@ -908,7 +908,7 @@ def footprint(self) -> gpd.GeoDataFrame: footprint_dezoom = 10 arr = rasters.read( self.get_default_band_path(), - resolution=self.resolution * footprint_dezoom, + resolution=self.pixel_size * footprint_dezoom, indexes=[1], ) @@ -1124,7 +1124,7 @@ def _has_cloud_band(self, band: BandNames) -> bool: def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -1133,8 +1133,8 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} diff --git a/eoreader/products/optical/optical_product.py b/eoreader/products/optical/optical_product.py index e4ee17cf..9fb43de9 100644 --- a/eoreader/products/optical/optical_product.py +++ b/eoreader/products/optical/optical_product.py @@ -116,6 +116,8 @@ def __init__( # Initialization from the super class super().__init__(product_path, archive_path, output_path, remove_tmp, **kwargs) + self.pixel_spacing = self.pixel_size + def _pre_init(self, **kwargs) -> None: """ Function used to pre_init the products @@ -287,7 +289,7 @@ def _to_reflectance( def _open_bands( self, band_paths: dict, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -296,8 +298,8 @@ def _open_bands( Args: band_paths (dict): Band dict: {band_enum: band_path} - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: @@ -310,13 +312,13 @@ def _open_bands( # Read band LOGGER.debug(f"Read {band.name}") band_arr = self._read_band( - band_path, band=band, resolution=resolution, size=size, **kwargs + band_path, band=band, pixel_size=pixel_size, size=size, **kwargs ) - if not resolution: - resolution = band_arr.rio.resolution()[0] + if not pixel_size: + pixel_size = band_arr.rio.resolution()[0] clean_band_path = self._get_clean_band_path( - band, resolution=resolution, writable=True, **kwargs + band, pixel_size=pixel_size, writable=True, **kwargs ) # If raw data, clean it ! if AnyPath(band_path).name != clean_band_path.name: @@ -370,7 +372,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -383,8 +385,8 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray @@ -490,7 +492,7 @@ def get_mean_viewing_angles(self) -> (float, float, float): def _compute_hillshade( self, dem_path: str = "", - resolution: Union[float, tuple] = None, + pixel_size: Union[float, tuple] = None, size: Union[list, tuple] = None, resampling: Resampling = Resampling.bilinear, ) -> Union[Path, CloudPath]: @@ -499,8 +501,8 @@ def _compute_hillshade( Args: dem_path (str): DEM path, using EUDEM/MERIT DEM if none - resolution (Union[float, tuple]): Resolution in meters. If not specified, use the product resolution. - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[float, tuple]): Pixel size in meters. If not specified, use the product pixel size. + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. resampling (Resampling): Resampling method Returns: @@ -508,7 +510,7 @@ def _compute_hillshade( """ # Warp DEM - warped_dem_path = self._warp_dem(dem_path, resolution, size, resampling) + warped_dem_path = self._warp_dem(dem_path, pixel_size, size, resampling) # Get Hillshade path hillshade_name = ( @@ -538,7 +540,7 @@ def _compute_hillshade( def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -547,8 +549,8 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -558,7 +560,7 @@ def _open_clouds( def _load_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -567,8 +569,8 @@ def _load_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -579,7 +581,7 @@ def _load_clouds( bands_to_load = [] for band in bands: cloud_path = self._construct_band_path( - band, resolution, size, writable=False, **kwargs + band, pixel_size, size, writable=False, **kwargs ) if cloud_path.is_file(): band_dict[band] = utils.read(cloud_path) @@ -587,12 +589,12 @@ def _load_clouds( bands_to_load.append(band) # Then load other bands that haven't been loaded before - loaded_bands = self._open_clouds(bands_to_load, resolution, size, **kwargs) + loaded_bands = self._open_clouds(bands_to_load, pixel_size, size, **kwargs) # Write them on disk for band_id, band_arr in loaded_bands.items(): cloud_path = self._construct_band_path( - band_id, resolution, size, writable=True, **kwargs + band_id, pixel_size, size, writable=True, **kwargs ) utils.write(band_arr, cloud_path) @@ -623,7 +625,7 @@ def _create_mask( def _get_clean_band_path( self, band: BandNames, - resolution: float = None, + pixel_size: float = None, writable: bool = False, **kwargs, ) -> Union[CloudPath, Path]: @@ -634,7 +636,7 @@ def _get_clean_band_path( Args: band (BandNames): Wanted band - resolution (float): Band resolution in meters + pixel_size (float): Band pixel size in meters writable (bool): True if we want the band folder to be writeable kwargs: Additional arguments @@ -645,7 +647,7 @@ def _get_clean_band_path( kwargs.get(CLEAN_OPTICAL, DEF_CLEAN_METHOD) ) - res_str = self._resolution_to_str(resolution) + res_str = self._pixel_size_to_str(pixel_size) # Radiometric processing rad_proc = "" if kwargs.get(TO_REFLECTANCE, True) else "_as_is" diff --git a/eoreader/products/optical/pla_product.py b/eoreader/products/optical/pla_product.py index bb8e8881..6afb69f0 100644 --- a/eoreader/products/optical/pla_product.py +++ b/eoreader/products/optical/pla_product.py @@ -207,16 +207,16 @@ def _post_init(self, **kwargs) -> None: # Post init done by the super class super()._post_init(**kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ # Ortho Tiles if self.product_type == PlaProductType.L3A: - return 3.125 + self.pixel_size = 3.125 # Ortho Scene else: - return 3.0 + self.pixel_size = 3.0 def _set_instrument(self) -> None: """ @@ -468,7 +468,7 @@ def _get_stack_path(self, as_list: bool = False) -> Union[str, list]: return stack_path def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -489,7 +489,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Band resolution + pixel_size (float): Band pixel size kwargs: Other arguments used to load bands Returns: diff --git a/eoreader/products/optical/planet_product.py b/eoreader/products/optical/planet_product.py index 2181659c..3609a880 100644 --- a/eoreader/products/optical/planet_product.py +++ b/eoreader/products/optical/planet_product.py @@ -393,7 +393,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -406,8 +406,8 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray @@ -416,7 +416,7 @@ def _read_band( # Read band band_arr = utils.read( path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, indexes=[self.bands[band].id], @@ -506,17 +506,17 @@ def _manage_nodata( def _load_bands( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: bands list: List of the wanted bands - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -530,7 +530,7 @@ def _load_bands( # Open bands and get array (resampled if needed) band_arrays = self._open_bands( - band_paths, resolution=resolution, size=size, **kwargs + band_paths, pixel_size=pixel_size, size=size, **kwargs ) return band_arrays @@ -561,7 +561,7 @@ def _has_cloud_band(self, band: BandNames) -> bool: def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -572,22 +572,22 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} """ if self._mask_type == PlanetMaskType.UDM2: - return self._open_clouds_udm2(bands, resolution, size, **kwargs) + return self._open_clouds_udm2(bands, pixel_size, size, **kwargs) else: # UDM - return self._open_clouds_udm(bands, resolution, size, **kwargs) + return self._open_clouds_udm(bands, pixel_size, size, **kwargs) def _open_clouds_udm2( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -598,8 +598,8 @@ def _open_clouds_udm2( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -610,13 +610,13 @@ def _open_clouds_udm2( def_xarr = self._read_band( self.get_default_band_path(), band=self.get_default_band(), - resolution=resolution, + pixel_size=pixel_size, size=size, **kwargs, ) # Load nodata - nodata = self._load_nodata(resolution, size, **kwargs).data + nodata = self._load_nodata(pixel_size, size, **kwargs).data if bands: for band in bands: @@ -624,10 +624,10 @@ def _open_clouds_udm2( cloud = self._create_mask( def_xarr.rename(ALL_CLOUDS.name), ( - self.open_mask("CLOUD", resolution, size, **kwargs).data - & self.open_mask("SHADOW", resolution, size, **kwargs).data + self.open_mask("CLOUD", pixel_size, size, **kwargs).data + & self.open_mask("SHADOW", pixel_size, size, **kwargs).data & self.open_mask( - "HEAVY_HAZE", resolution, size, **kwargs + "HEAVY_HAZE", pixel_size, size, **kwargs ).data ), nodata, @@ -635,24 +635,24 @@ def _open_clouds_udm2( elif band == SHADOWS: cloud = self._create_mask( def_xarr.rename(SHADOWS.name), - self.open_mask("SHADOW", resolution, size, **kwargs).data, + self.open_mask("SHADOW", pixel_size, size, **kwargs).data, nodata, ) elif band == CLOUDS: cloud = self._create_mask( def_xarr.rename(CLOUDS.name), - self.open_mask("CLOUD", resolution, size, **kwargs).data, + self.open_mask("CLOUD", pixel_size, size, **kwargs).data, nodata, ) elif band == CIRRUS: cloud = self._create_mask( def_xarr.rename(CIRRUS.name), - self.open_mask("HEAVY_HAZE", resolution, size, **kwargs).data, + self.open_mask("HEAVY_HAZE", pixel_size, size, **kwargs).data, nodata, ) elif band == RAW_CLOUDS: cloud = utils.read( - self._get_udm2_path(), resolution, size, **kwargs + self._get_udm2_path(), pixel_size, size, **kwargs ) else: raise InvalidTypeError( @@ -672,7 +672,7 @@ def _open_clouds_udm2( def _open_clouds_udm( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -683,8 +683,8 @@ def _open_clouds_udm( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -695,12 +695,12 @@ def _open_clouds_udm( def_xarr = self._read_band( self.get_default_band_path(), band=self.get_default_band(), - resolution=resolution, + pixel_size=pixel_size, size=size, **kwargs, ) # Open mask (here we know we have a UDM file, as the product is supposed to have the band) - udm = self.open_mask_udm(resolution, size, **kwargs) + udm = self.open_mask_udm(pixel_size, size, **kwargs) if bands: for band in bands: @@ -733,7 +733,7 @@ def _open_clouds_udm( def open_mask( self, mask_id: str, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> Union[xr.DataArray, None]: @@ -768,22 +768,22 @@ def open_mask( Args: mask_id: Mask ID - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: Union[xarray.DataArray, None]: Mask array """ if self._mask_type == PlanetMaskType.UDM2: - mask = self.open_mask_udm2(mask_id, resolution, size, **kwargs) + mask = self.open_mask_udm2(mask_id, pixel_size, size, **kwargs) elif self._mask_type == PlanetMaskType.UDM: - mask = self.open_mask_udm(resolution, size, **kwargs) + mask = self.open_mask_udm(pixel_size, size, **kwargs) else: def_xarr = self._read_band( self.get_default_band_path(), band=self.get_default_band(), - resolution=resolution, + pixel_size=pixel_size, size=size, **kwargs, ).astype(np.uint8) @@ -794,7 +794,7 @@ def open_mask( def open_mask_udm2( self, mask_id: str, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> Union[xr.DataArray, None]: @@ -829,8 +829,8 @@ def open_mask_udm2( Args: mask_id: Mask ID - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: Union[xarray.DataArray, None]: Mask array @@ -853,7 +853,7 @@ def open_mask_udm2( # Open mask band mask = utils.read( mask_path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.nearest, # Nearest to keep the flags masked=False, @@ -864,11 +864,11 @@ def open_mask_udm2( return mask.astype(np.uint8) def open_mask_udm( - self, resolution: float = None, size: Union[list, tuple] = None, **kwargs + self, pixel_size: float = None, size: Union[list, tuple] = None, **kwargs ) -> Union[xr.DataArray, None]: """ Open a Planet UDM (Unusable Data Mask) mask as a xarray. - For RapidEye, the mask is subsampled to 50m, so this function will interpolate to make it to the correct resolution + For RapidEye, the mask is subsampled to 50m, so this function will interpolate to make it to the correct pixel size Returns None if the mask is not available. Do not open cloud mask with this function. Use :code:`load` instead. @@ -877,8 +877,8 @@ def open_mask_udm( information. Args: - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: Union[xarray.DataArray, None]: Mask array @@ -889,7 +889,7 @@ def open_mask_udm( # Open mask band return utils.read( mask_path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.nearest, # Nearest to keep the flags masked=False, @@ -897,7 +897,7 @@ def open_mask_udm( ).astype(np.uint8) def _load_nodata( - self, resolution: float = None, size: Union[list, tuple] = None, **kwargs + self, pixel_size: float = None, size: Union[list, tuple] = None, **kwargs ) -> Union[xr.DataArray, None]: """ Load nodata (unimaged pixels) as a numpy array. @@ -907,14 +907,14 @@ def _load_nodata( (unusable data mask) for more information. Args: - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: Union[xarray.DataArray, None]: Nodata array """ - udm = self.open_mask("UNUSABLE", resolution, size, **kwargs) + udm = self.open_mask("UNUSABLE", pixel_size, size, **kwargs) nodata = udm.copy(data=rasters.read_bit_array(udm.compute(), 0)) return nodata.rename("NODATA") diff --git a/eoreader/products/optical/re_product.py b/eoreader/products/optical/re_product.py index 4a038c1b..24f118ed 100644 --- a/eoreader/products/optical/re_product.py +++ b/eoreader/products/optical/re_product.py @@ -117,11 +117,11 @@ def _post_init(self, **kwargs) -> None: # Post init done by the super class super()._post_init(**kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ - return 5.0 + self.pixel_size = 5.0 def _set_instrument(self) -> None: """ @@ -262,7 +262,7 @@ def _get_stack_path(self, as_list: bool = False) -> Union[str, list]: return stack_path def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -283,7 +283,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Band resolution + pixel_size (float): Band pixel size kwargs: Other arguments used to load bands Returns: diff --git a/eoreader/products/optical/s2_product.py b/eoreader/products/optical/s2_product.py index 7084d421..971f44a7 100644 --- a/eoreader/products/optical/s2_product.py +++ b/eoreader/products/optical/s2_product.py @@ -205,13 +205,13 @@ def _post_init(self, **kwargs) -> None: # Post init done by the super class super()._post_init(**kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ # S2: use 10m resolution, even if we have 60m and 20m resolution # In the future maybe use one resolution per band ? - return 10.0 + self.pixel_size = 10.0 def _get_tile_name(self) -> str: """ @@ -486,22 +486,22 @@ def _get_name_constellation_specific(self) -> str: return name - def _get_res_band_folder(self, band_list: list, resolution: float = None) -> dict: + def _get_res_band_folder(self, band_list: list, pixel_size: float = None) -> dict: """ Return the folder containing the bands of a proper S2 products. (IMG_DATA for L1C, IMG_DATA/Rx0m for L2A) Args: band_list (list): Wanted bands (listed as 01, 02...) - resolution (float): Band resolution for Sentinel-2 products {R10m, R20m, R60m}. + pixel_size (float): Band resolution for Sentinel-2 products {R10m, R20m, R60m}. The wanted bands will be chosen in this proper folder. Returns: dict: Dictionary containing the folder path for each queried band """ - if resolution is not None: - if isinstance(resolution, (list, tuple)): - resolution = resolution[0] + if pixel_size is not None: + if isinstance(pixel_size, (list, tuple)): + pixel_size = pixel_size[0] # Open the band directory names s2_bands_folder = {} @@ -518,8 +518,8 @@ def _get_res_band_folder(self, band_list: list, resolution: float = None) -> dic # If L2A products, we care about the resolution if self.product_type == S2ProductType.L2A: # If we got a true S2 resolution, open the corresponding band - if resolution and f"R{int(resolution)}m" in band_dir[band_id]: - dir_name = f"R{int(resolution)}m" + if pixel_size and f"R{int(pixel_size)}m" in band_dir[band_id]: + dir_name = f"R{int(pixel_size)}m" # Else open the first one, it will be resampled when the band will be read else: @@ -555,7 +555,7 @@ def _get_res_band_folder(self, band_list: list, resolution: float = None) -> dic return s2_bands_folder def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -574,18 +574,18 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Band resolution + pixel_size (float): Band pixel size kwargs: Other arguments used to load bands Returns: dict: Dictionary containing the path of each queried band """ - band_folders = self._get_res_band_folder(band_list, resolution) + band_folders = self._get_res_band_folder(band_list, pixel_size) band_paths = {} for band in band_list: # Get clean band path clean_band = self._get_clean_band_path( - band, resolution=resolution, **kwargs + band, pixel_size=pixel_size, **kwargs ) if clean_band.is_file(): band_paths[band] = clean_band @@ -614,7 +614,7 @@ def _read_band( self, path: Union[str, CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -627,8 +627,8 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray @@ -689,7 +689,7 @@ def _read_band( # Read band return utils.read( geocoded_path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, **kwargs, @@ -865,7 +865,7 @@ def _open_mask_gt_4_0( self, mask_id: Union[str, S2Jp2Masks], band: Union[BandNames, str] = None, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -881,8 +881,8 @@ def _open_mask_gt_4_0( Args: mask_id (Union[str, S2GmlMasks]): Mask ID band (Union[BandNames, str]): Band number as an SpectralBandNames or str (for clouds: 00) - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: gpd.GeoDataFrame: Mask as a DataArray @@ -913,7 +913,7 @@ def _open_mask_gt_4_0( # Read mask mask = utils.read( mask_path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.nearest, **kwargs, @@ -1161,17 +1161,17 @@ def _manage_nodata_gt_4_0( def _load_bands( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: bands (list): List of the wanted bands - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -1180,13 +1180,13 @@ def _load_bands( if not bands: return {} - if resolution is None and size is not None: - resolution = self._resolution_from_size(size) - band_paths = self.get_band_paths(bands, resolution=resolution, **kwargs) + if pixel_size is None and size is not None: + pixel_size = self._pixel_size_from_img_size(size) + band_paths = self.get_band_paths(bands, pixel_size=pixel_size, **kwargs) # Open bands and get array (resampled if needed) band_arrays = self._open_bands( - band_paths, resolution=resolution, size=size, **kwargs + band_paths, pixel_size=pixel_size, size=size, **kwargs ) return band_arrays @@ -1297,7 +1297,7 @@ def _has_cloud_band(self, band: BandNames) -> bool: def _open_clouds_lt_4_0( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -1309,8 +1309,8 @@ def _open_clouds_lt_4_0( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -1324,7 +1324,7 @@ def _open_clouds_lt_4_0( def_band = self._read_band( self.get_default_band_path(), self.get_default_band(), - resolution=resolution, + pixel_size=pixel_size, size=size, ) nodata = np.where(np.isnan(def_band), 1, 0) @@ -1366,7 +1366,7 @@ def _open_clouds_lt_4_0( def _open_clouds_gt_4_0( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -1378,8 +1378,8 @@ def _open_clouds_gt_4_0( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -1388,7 +1388,7 @@ def _open_clouds_gt_4_0( if bands: cloud_vec = self._open_mask_gt_4_0( - S2Jp2Masks.CLOUDS, "00", resolution=resolution, size=size, **kwargs + S2Jp2Masks.CLOUDS, "00", pixel_size=pixel_size, size=size, **kwargs ).astype(np.uint8) for band in bands: @@ -1422,7 +1422,7 @@ def _open_clouds_gt_4_0( def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -1434,16 +1434,16 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} """ if self._processing_baseline < 4.0: - return self._open_clouds_lt_4_0(bands, resolution, size, **kwargs) + return self._open_clouds_lt_4_0(bands, pixel_size, size, **kwargs) else: - return self._open_clouds_gt_4_0(bands, resolution, size, **kwargs) + return self._open_clouds_gt_4_0(bands, pixel_size, size, **kwargs) def _rasterize( self, xds: xr.DataArray, geometry: gpd.GeoDataFrame, nodata: np.ndarray diff --git a/eoreader/products/optical/s2_theia_product.py b/eoreader/products/optical/s2_theia_product.py index 8121eef5..a55e602a 100644 --- a/eoreader/products/optical/s2_theia_product.py +++ b/eoreader/products/optical/s2_theia_product.py @@ -107,13 +107,13 @@ def _post_init(self, **kwargs) -> None: # Post init done by the super class super()._post_init(**kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ # S2: use 10m resolution, even if we have 60m and 20m resolution # In the future maybe set one resolution per band ? - return 10.0 + self.pixel_size = 10.0 def _get_tile_name(self) -> str: """ @@ -295,7 +295,7 @@ def _get_name_constellation_specific(self) -> str: return name def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -317,7 +317,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Band resolution + pixel_size (float): Band pixel size kwargs: Other arguments used to load bands Returns: @@ -326,7 +326,7 @@ def get_band_paths( band_paths = {} for band in band_list: # Get clean band path clean_band = self._get_clean_band_path( - band, resolution=resolution, **kwargs + band, pixel_size=pixel_size, **kwargs ) if clean_band.is_file(): band_paths[band] = clean_band @@ -352,7 +352,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -365,15 +365,15 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray """ band_arr = utils.read( path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, **kwargs, @@ -527,7 +527,7 @@ def open_mask( self, mask_id: str, band: Union[BandNames, str], - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> np.ndarray: @@ -563,8 +563,8 @@ def open_mask( Args: mask_id: Mask ID band (Union[BandNames, str]): Band name as an SpectralBandNames or resolution ID: ['R1', 'R2'] - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: np.ndarray: Mask array @@ -596,7 +596,7 @@ def open_mask( # Open SAT band mask = utils.read( mask_path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.nearest, # Nearest to keep the flags masked=False, @@ -613,17 +613,17 @@ def open_mask( def _load_bands( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: bands list: List of the wanted bands - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -633,13 +633,13 @@ def _load_bands( return {} # Get band paths - if resolution is None and size is not None: - resolution = self._resolution_from_size(size) - band_paths = self.get_band_paths(bands, resolution=resolution, **kwargs) + if pixel_size is None and size is not None: + pixel_size = self._pixel_size_from_img_size(size) + band_paths = self.get_band_paths(bands, pixel_size=pixel_size, **kwargs) # Open bands and get array (resampled if needed) band_arrays = self._open_bands( - band_paths, resolution=resolution, size=size, **kwargs + band_paths, pixel_size=pixel_size, size=size, **kwargs ) return band_arrays @@ -713,7 +713,7 @@ def _has_cloud_band(self, band: BandNames) -> bool: def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -735,21 +735,21 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} """ band_dict = {} - if resolution: - res_file = resolution + if pixel_size: + res_file = pixel_size else: if size: - res_file = self._resolution_from_size(size)[0] + res_file = self._pixel_size_from_img_size(size)[0] else: - res_file = self.resolution + res_file = self.pixel_size if bands: # Open 20m cloud file if resolution >= 20m @@ -758,13 +758,13 @@ def _open_clouds( cloud_path = self.get_mask_path("CLM", res_id) clouds_mask = utils.read( cloud_path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.nearest, ).astype(np.float32) # Get nodata mask - nodata = self.open_mask("EDG", res_id, resolution=resolution, size=size) + nodata = self.open_mask("EDG", res_id, pixel_size=pixel_size, size=size) # Bit ids clouds_shadows_id = 0 diff --git a/eoreader/products/optical/s3_olci_product.py b/eoreader/products/optical/s3_olci_product.py index 29e55c78..db35bb8c 100644 --- a/eoreader/products/optical/s3_olci_product.py +++ b/eoreader/products/optical/s3_olci_product.py @@ -120,7 +120,7 @@ def __init__( def _get_preprocessed_band_path( self, band: Union[BandNames, str], - resolution: Union[float, tuple, list] = None, + pixel_size: Union[float, tuple, list] = None, writable=True, ) -> Union[CloudPath, Path]: """ @@ -128,13 +128,13 @@ def _get_preprocessed_band_path( Args: band (band: Union[BandNames, str]): Wanted band (quality flags accepted) - resolution (Union[float, tuple, list]): Resolution of the wanted UTM band + pixel_size (Union[float, tuple, list]): Resolution of the wanted UTM band writable (bool): Do we need to write the pre-processed band ? Returns: Union[CloudPath, Path]: Pre-processed band path """ - res_str = self._resolution_to_str(resolution) + res_str = self._pixel_size_to_str(pixel_size) band_str = band.name if isinstance(band, BandNames) else band return self._get_band_folder(writable=writable).joinpath( @@ -178,11 +178,11 @@ def _pre_init(self, **kwargs) -> None: # Post init done by the super class super()._pre_init(**kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ - return 300.0 + self.pixel_size = 300.0 def _set_product_type(self) -> None: """ @@ -208,7 +208,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa01", ID: "Oa01", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 400, FWHM: 15, DESCRIPTION: "Aerosol correction, improved water constituent retrieval", @@ -219,7 +219,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa02", ID: "Oa02", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 412.5, FWHM: 10, DESCRIPTION: "Yellow substance and detrital pigments (turbidity)", @@ -230,7 +230,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa03", ID: "Oa03", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 442.5, FWHM: 10, DESCRIPTION: "Chlorophyll absorption maximum, biogeochemistry, vegetation", @@ -241,7 +241,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa04", ID: "Oa04", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 490, FWHM: 10, DESCRIPTION: "High Chlorophyll", @@ -252,7 +252,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa05", ID: "Oa05", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 510, FWHM: 10, DESCRIPTION: "Chlorophyll, sediment, turbidity, red tide", @@ -263,7 +263,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa06", ID: "Oa06", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 560, FWHM: 10, DESCRIPTION: "Chlorophyll reference (Chlorophyll minimum)", @@ -274,7 +274,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa07", ID: "Oa07", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 620, FWHM: 10, DESCRIPTION: "Sediment loading", @@ -285,7 +285,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa08", ID: "Oa08", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 665, FWHM: 10, DESCRIPTION: "Chlorophyll (2nd Chlorophyll absorption maximum), sediment, yellow substance / vegetation", @@ -296,7 +296,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa09", ID: "Oa09", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 673.75, FWHM: 7.5, DESCRIPTION: "For improved fluorescence retrieval and to better account for smile together with the bands 665 and 680 nm", @@ -307,7 +307,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa10", ID: "Oa10", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 681.25, FWHM: 7.5, DESCRIPTION: "Chlorophyll fluorescence peak, red edge", @@ -318,7 +318,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa11", ID: "Oa11", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 708.75, FWHM: 10, DESCRIPTION: "Chlorophyll fluorescence baseline, red edge transition", @@ -329,7 +329,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa12", ID: "Oa12", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 753.75, FWHM: 7.5, DESCRIPTION: "O2 absorption/clouds, vegetation", @@ -340,7 +340,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa13", ID: "Oa13", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 761.25, FWHM: 2.5, DESCRIPTION: "O2 absorption band/aerosol correction.", @@ -351,7 +351,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa14", ID: "Oa14", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 764.375, FWHM: 3.75, DESCRIPTION: "Atmospheric correction", @@ -362,7 +362,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa15", ID: "Oa15", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 767.5, FWHM: 2.5, DESCRIPTION: "O2A used for cloud top pressure, fluorescence over land", @@ -373,7 +373,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa16", ID: "Oa16", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 778.75, FWHM: 15, DESCRIPTION: "Atmos. corr./aerosol corr.", @@ -384,7 +384,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa17", ID: "Oa17", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 865, FWHM: 20, DESCRIPTION: "Atmospheric correction/aerosol correction, clouds, pixel co-registration", @@ -395,7 +395,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa17", ID: "Oa17", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 865, FWHM: 20, DESCRIPTION: "Atmospheric correction/aerosol correction, clouds, pixel co-registration", @@ -406,7 +406,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa18", ID: "Oa18", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 885, FWHM: 10, DESCRIPTION: "Water vapour absorption reference band. Common reference band with SLSTR instrument. Vegetation monitoring", @@ -417,7 +417,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa19", ID: "Oa19", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 900, FWHM: 10, DESCRIPTION: "Water vapour absorption/vegetation monitoring (maximum reflectance)", @@ -428,7 +428,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa20", ID: "Oa20", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 940, FWHM: 20, DESCRIPTION: "Water vapour absorption, Atmospheric correction/aerosol correction", @@ -439,7 +439,7 @@ def _map_bands(self) -> None: **{ NAME: "Oa21", ID: "Oa21", - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 1020, FWHM: 40, DESCRIPTION: "Atmospheric correction/aerosol correction", @@ -477,7 +477,7 @@ def get_raw_band_paths(self, **kwargs) -> dict: def _preprocess( self, band: Union[BandNames, str], - resolution: float = None, + pixel_size: float = None, to_reflectance: bool = True, subdataset: str = None, **kwargs, @@ -489,7 +489,7 @@ def _preprocess( Args: band (Union[BandNames, str]): Band to preprocess (quality flags or others are accepted) - resolution (float): Resolution + pixel_size (float): Pixl size to_reflectance (bool): Convert band to reflectance subdataset (str): Subdataset kwargs: Other arguments used to load bands @@ -500,12 +500,12 @@ def _preprocess( band_str = band if isinstance(band, str) else band.name path = self._get_preprocessed_band_path( - band, resolution=resolution, writable=False + band, pixel_size=pixel_size, writable=False ) if not path.is_file(): path = self._get_preprocessed_band_path( - band, resolution=resolution, writable=True + band, pixel_size=pixel_size, writable=True ) # Get band regex @@ -530,7 +530,7 @@ def _preprocess( # Geocode LOGGER.debug(f"Geocoding {band_str}") - pp_arr = self._geocode(band_arr, resolution=resolution, **kwargs) + pp_arr = self._geocode(band_arr, pixel_size=pixel_size, **kwargs) # Write on disk utils.write(pp_arr, path) @@ -712,7 +712,7 @@ def _manage_invalid_pixels( qual_flags_path = self._preprocess( qual_regex, subdataset=subds, - resolution=band_arr.rio.resolution(), + pixel_size=band_arr.rio.resolution(), to_reflectance=False, ) @@ -744,7 +744,7 @@ def _has_cloud_band(self, band: BandNames) -> bool: def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -753,8 +753,8 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} diff --git a/eoreader/products/optical/s3_product.py b/eoreader/products/optical/s3_product.py index 3cfa0794..93663d9f 100644 --- a/eoreader/products/optical/s3_product.py +++ b/eoreader/products/optical/s3_product.py @@ -392,7 +392,7 @@ def _get_name_constellation_specific(self) -> str: return name def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -415,7 +415,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Useless here + pixel_size (float): Useless here kwargs: Other arguments used to load bands Returns: @@ -425,7 +425,7 @@ def get_band_paths( for band in band_list: # Get clean band path clean_band = self._get_clean_band_path( - band, resolution=resolution, **kwargs + band, pixel_size=pixel_size, **kwargs ) if clean_band.is_file(): band_paths[band] = clean_band @@ -433,7 +433,7 @@ def get_band_paths( # Pre-process the wanted band (does nothing if existing) band_paths[band] = self._preprocess( band, - resolution=resolution, + pixel_size=pixel_size, **kwargs, ) @@ -444,7 +444,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -457,8 +457,8 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray @@ -466,7 +466,7 @@ def _read_band( """ band = utils.read( path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, **kwargs, @@ -536,17 +536,17 @@ def _manage_nodata( def _load_bands( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: bands (list): List of the wanted bands - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -559,13 +559,13 @@ def _load_bands( if not isinstance(bands, list): bands = [bands] - if resolution is None and size is not None: - resolution = self._resolution_from_size(size) - band_paths = self.get_band_paths(bands, resolution=resolution, **kwargs) + if pixel_size is None and size is not None: + pixel_size = self._pixel_size_from_img_size(size) + band_paths = self.get_band_paths(bands, pixel_size=pixel_size, **kwargs) # Open bands and get array (resampled if needed) band_arrays = self._open_bands( - band_paths, resolution=resolution, size=size, **kwargs + band_paths, pixel_size=pixel_size, size=size, **kwargs ) return band_arrays @@ -574,7 +574,7 @@ def _load_bands( def _preprocess( self, band: Union[BandNames, str], - resolution: float = None, + pixel_size: float = None, to_reflectance: bool = True, subdataset: str = None, **kwargs, @@ -586,7 +586,7 @@ def _preprocess( Args: band (Union[BandNames, str]): Band to preprocess (quality flags or others are accepted) - resolution (float): Resolution + pixel_size (float): Pixel size to_reflectance (bool): Convert band to reflectance subdataset (str): Subdataset kwargs: Other arguments used to load bands @@ -600,7 +600,7 @@ def _geocode( self, band_arr: xr.DataArray, suffix: str = None, - resolution: float = None, + pixel_size: float = None, resampling: Resampling = Resampling.nearest, **kwargs, ) -> xr.DataArray: @@ -610,7 +610,7 @@ def _geocode( Args: band_arr (xr.DataArray): Band array suffix (str): Suffix (for the grid) - resolution (float): Resolution + pixel_size (float): Pixel size kwargs: Other arguments Returns: @@ -651,13 +651,13 @@ def _geocode( area_def = create_area_def( area_id=f"{self.condensed_name}_grid{suffix_str}", projection=self.crs(), - resolution=self.resolution, + resolution=self.pixel_size, area_extent=self.extent().bounds.values[0], ) # Resampling Nearest if resampling == Resampling.nearest: - resampler = XArrayResamplerNN(swath_def, area_def, self.resolution * 3) + resampler = XArrayResamplerNN(swath_def, area_def, self.pixel_size * 3) resampler.get_neighbour_info() band_arr_resampled = resampler.get_sample_from_neighbour_info( band_arr.squeeze(), fill_value=nodata @@ -904,7 +904,7 @@ def _read_nc( def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -913,21 +913,14 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} """ raise NotImplementedError - @abstractmethod - def _get_resolution(self) -> float: - """ - Get product default resolution (in meters) - """ - raise NotImplementedError - @abstractmethod def _set_product_type(self) -> None: """ diff --git a/eoreader/products/optical/s3_slstr_product.py b/eoreader/products/optical/s3_slstr_product.py index 3b3e7887..6469c34a 100644 --- a/eoreader/products/optical/s3_slstr_product.py +++ b/eoreader/products/optical/s3_slstr_product.py @@ -270,7 +270,7 @@ def _get_preprocessed_band_path( self, filename: str, suffix: str, - resolution: Union[float, tuple, list] = None, + pixel_size: Union[float, tuple, list] = None, writable: bool = True, ) -> Union[CloudPath, Path]: """ @@ -278,13 +278,13 @@ def _get_preprocessed_band_path( Args: filename (str): Filename - resolution (Union[float, tuple, list]): Resolution of the wanted UTM band + pixel_size (Union[float, tuple, list]): Pixel size of the wanted UTM band writable (bool): Do we need to write the pre-processed band ? Returns: Union[CloudPath, Path]: Pre-processed band path """ - res_str = self._resolution_to_str(resolution) + res_str = self._pixel_size_to_str(pixel_size) if filename.endswith(suffix): pp_name = f"{self.condensed_name}_{filename}_{res_str}.tif" else: @@ -325,11 +325,11 @@ def _set_preprocess_members(self): # Other self._exception_name = "{band}_exception_{suffix}" - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ - return 500.0 + self.pixel_size = 500.0 def _set_product_type(self) -> None: """Set products type""" @@ -352,7 +352,7 @@ def _map_bands(self) -> None: **{ NAME: SLSTR_A_BANDS[0], ID: SLSTR_A_BANDS[0], - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 554.27, FWHM: 19.26, DESCRIPTION: "Cloud screening, vegetation monitoring, aerosol", @@ -363,7 +363,7 @@ def _map_bands(self) -> None: **{ NAME: SLSTR_A_BANDS[1], ID: SLSTR_A_BANDS[1], - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 659.47, FWHM: 19.25, DESCRIPTION: "NDVI, vegetation monitoring, aerosol", @@ -374,7 +374,7 @@ def _map_bands(self) -> None: **{ NAME: SLSTR_A_BANDS[2], ID: SLSTR_A_BANDS[2], - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 868.00, FWHM: 20.60, DESCRIPTION: "NDVI, cloud flagging, pixel co-registration", @@ -385,7 +385,7 @@ def _map_bands(self) -> None: **{ NAME: SLSTR_A_BANDS[2], ID: SLSTR_A_BANDS[2], - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 868.00, FWHM: 20.60, DESCRIPTION: "NDVI, cloud flagging, pixel co-registration", @@ -396,7 +396,7 @@ def _map_bands(self) -> None: **{ NAME: SLSTR_ABC_BANDS[0], ID: SLSTR_ABC_BANDS[0], - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 1374.80, FWHM: 20.80, DESCRIPTION: "Cirrus detection over land", @@ -407,7 +407,7 @@ def _map_bands(self) -> None: **{ NAME: SLSTR_ABC_BANDS[1], ID: SLSTR_ABC_BANDS[1], - GSD: self.resolution, + GSD: self.pixel_size, CENTER_WV: 1613.40, FWHM: 60.68, DESCRIPTION: "Cloud clearing, ice, snow, vegetation monitoring", @@ -539,7 +539,7 @@ def _get_raw_band_path(self, band: BandNames, **kwargs) -> Union[Path, CloudPath def _preprocess( self, band: Union[BandNames, str], - resolution: float = None, + pixel_size: float = None, to_reflectance: bool = True, subdataset: str = None, **kwargs, @@ -552,7 +552,7 @@ def _preprocess( Args: band (Union[BandNames, str]): Band to preprocess (quality flags or others are accepted) - resolution (float): Resolution + pixel_size (float): Pixl size to_reflectance (bool): Convert band to reflectance subdataset (str): Subdataset kwargs: Other arguments used to load bands @@ -587,12 +587,12 @@ def _preprocess( # Get the pre-processed path path = self._get_preprocessed_band_path( - pp_name, suffix=suffix, resolution=resolution, writable=False + pp_name, suffix=suffix, pixel_size=pixel_size, writable=False ) if not path.is_file(): path = self._get_preprocessed_band_path( - pp_name, suffix=suffix, resolution=resolution, writable=True + pp_name, suffix=suffix, pixel_size=pixel_size, writable=True ) # Get raw band @@ -634,7 +634,7 @@ def _preprocess( LOGGER.debug(f"Geocoding {pp_name}") kwargs.pop("suffix", None) pp_arr = self._geocode( - band_arr, resolution=resolution, suffix=suffix, **kwargs + band_arr, pixel_size=pixel_size, suffix=suffix, **kwargs ) # Write on disk @@ -936,7 +936,7 @@ def _manage_invalid_pixels( band, suffix=suffix, subdataset=self._replace(self._exception_name, band=band, suffix=suffix), - resolution=band_arr.rio.resolution(), + pixel_size=band_arr.rio.resolution(), to_reflectance=False, flags=True, dtype=np.uint8, @@ -983,7 +983,7 @@ def _has_cloud_band(self, band: BandNames) -> bool: def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -1014,8 +1014,8 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -1035,14 +1035,14 @@ def _open_clouds( flags_file, suffix=suffix, subdataset=cloud_name, - resolution=resolution, + pixel_size=pixel_size, to_reflectance=False, ) # Open cloud file clouds_array = utils.read( cloud_path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.nearest, masked=False, @@ -1141,7 +1141,7 @@ def get_mean_sun_angles(self, view: SlstrView = SlstrView.NADIR) -> (float, floa def _get_clean_band_path( self, band: BandNames, - resolution: float = None, + pixel_size: float = None, writable: bool = False, **kwargs, ) -> Union[CloudPath, Path]: @@ -1152,7 +1152,7 @@ def _get_clean_band_path( Args: band (BandNames): Wanted band - resolution (float): Band resolution in meters + pixel_size (float): Band pixel size in meters kwargs: Additional arguments Returns: @@ -1163,7 +1163,7 @@ def _get_clean_band_path( ) suffix = self._get_suffix(band, **kwargs) - res_str = self._resolution_to_str(resolution) + res_str = self._pixel_size_to_str(pixel_size) return self._get_band_folder(writable).joinpath( f"{self.condensed_name}_{band.name}_{suffix}_{res_str.replace('.', '-')}_{cleaning_method.value}.tif", diff --git a/eoreader/products/optical/sky_product.py b/eoreader/products/optical/sky_product.py index 09ceea51..2b561fac 100644 --- a/eoreader/products/optical/sky_product.py +++ b/eoreader/products/optical/sky_product.py @@ -219,13 +219,13 @@ def footprint(self) -> gpd.GeoDataFrame: arr = rasters.read(self.get_default_band_path(), indexes=[1]) return rasters.get_valid_vector(arr, default_nodata=0) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ # Optical data: resolution = pixel_resolution (not gsd) root, _ = self.read_mtd() - return float(root.findtext(".//pixel_resolution")) + self.pixel_size = float(root.findtext(".//pixel_resolution")) def _set_instrument(self) -> None: """ @@ -247,7 +247,7 @@ def _map_bands(self): **{ NAME: "PAN", ID: 1, - GSD: self.resolution, + GSD: self.pixel_size, WV_MIN: 450, WV_MAX: 900, }, @@ -260,7 +260,7 @@ def _map_bands(self): **{ NAME: "Blue", ID: 1, - GSD: self.resolution, + GSD: self.pixel_size, WV_MIN: 450, WV_MAX: 515, }, @@ -270,7 +270,7 @@ def _map_bands(self): **{ NAME: "Green", ID: 2, - GSD: self.resolution, + GSD: self.pixel_size, WV_MIN: 515, WV_MAX: 595, }, @@ -280,7 +280,7 @@ def _map_bands(self): **{ NAME: "Red", ID: 3, - GSD: self.resolution, + GSD: self.pixel_size, WV_MIN: 605, WV_MAX: 695, }, @@ -290,7 +290,7 @@ def _map_bands(self): **{ NAME: "NIR", ID: 4, - GSD: self.resolution, + GSD: self.pixel_size, WV_MIN: 740, WV_MAX: 900, }, @@ -376,7 +376,7 @@ def _get_stack_path(self, as_list: bool = False) -> Union[str, list]: return stack_path def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -397,7 +397,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Band resolution + pixel_size (float): Band pixel size kwargs: Other arguments used to load bands Returns: diff --git a/eoreader/products/optical/spot45_product.py b/eoreader/products/optical/spot45_product.py index 147d8f47..88667d6d 100644 --- a/eoreader/products/optical/spot45_product.py +++ b/eoreader/products/optical/spot45_product.py @@ -251,9 +251,9 @@ def _post_init(self, **kwargs) -> None: # Post init done by the super class super()._post_init(**kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ # Not Pansharpened images if self.band_combi in [ @@ -262,7 +262,7 @@ def _get_resolution(self) -> float: Spot5BandCombination.X, Spot5BandCombination.J, ]: - return self._ms_res + self.pixel_size = self._ms_res # Pansharpened images elif self.band_combi in [ Spot4BandCombination.M, @@ -271,10 +271,10 @@ def _get_resolution(self) -> float: Spot5BandCombination.HM, Spot5BandCombination.HMX, ]: - return self._pan_res + self.pixel_size = self._pan_res # Supermode images else: - return self._supermode_res + self.pixel_size = self._supermode_res def _set_instrument(self) -> None: """ diff --git a/eoreader/products/optical/sv1_product.py b/eoreader/products/optical/sv1_product.py index 90687d20..2ddf82e4 100644 --- a/eoreader/products/optical/sv1_product.py +++ b/eoreader/products/optical/sv1_product.py @@ -153,17 +153,17 @@ def _post_init(self, **kwargs) -> None: # Post init done by the super class super()._post_init(**kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ # Not Pansharpened images if self.band_combi == Sv1BandCombination.PMS: # TODO: manage default resolution for PAN band ? - return self._ms_res + self.pixel_size = self._ms_res # Pansharpened images else: - return self._pan_res + self.pixel_size = self._pan_res def _set_instrument(self) -> None: """ @@ -527,7 +527,7 @@ def _has_cloud_band(self, band: BandNames) -> bool: def _open_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -536,8 +536,8 @@ def _open_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -560,7 +560,7 @@ def get_raw_band_paths(self, **kwargs) -> dict: return raw_band_paths def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -581,7 +581,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Band resolution + pixel_size (float): Band pixel size kwargs: Other arguments used to load bands Returns: @@ -592,14 +592,14 @@ def get_band_paths( for band in band_list: # Get clean band path clean_band = self._get_clean_band_path( - band, resolution=resolution, **kwargs + band, pixel_size=pixel_size, **kwargs ) if clean_band.is_file(): band_paths[band] = clean_band else: # First look for reprojected bands reproj_path = self._get_utm_band_path( - band=band.name, resolution=resolution + band=band.name, pixel_size=pixel_size ) if not reproj_path.is_file(): # Then for original data diff --git a/eoreader/products/optical/vhr_product.py b/eoreader/products/optical/vhr_product.py index f8de3c55..b67da8b9 100644 --- a/eoreader/products/optical/vhr_product.py +++ b/eoreader/products/optical/vhr_product.py @@ -126,7 +126,7 @@ def get_default_band_path(self, **kwargs) -> Union[CloudPath, Path]: Returns: Union[CloudPath, Path]: Default band path """ - return self._get_default_utm_band(self.resolution, **kwargs) + return self._get_default_utm_band(self.pixel_size, **kwargs) @abstractmethod def _get_raw_crs(self) -> CRS: @@ -153,7 +153,7 @@ def _get_ortho_path(self, **kwargs) -> Union[CloudPath, Path]: LOGGER.info( "Manually orthorectified stack not given by the user. " "Reprojecting whole stack, this may take a while. " - "(May be inaccurate on steep terrain, depending on the DEM resolution)" + "(May be inaccurate on steep terrain, depending on the DEM pixel size)" ) # Reproject and write on disk data @@ -181,7 +181,7 @@ def _get_ortho_path(self, **kwargs) -> Union[CloudPath, Path]: return ortho_path def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -202,7 +202,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Band resolution + pixel_size (float): Band pixel size kwargs: Other arguments used to load bands Returns: @@ -216,14 +216,14 @@ def get_band_paths( for band in band_list: # Get clean band path clean_band = self._get_clean_band_path( - band, resolution=resolution, **kwargs + band, pixel_size=pixel_size, **kwargs ) if clean_band.is_file(): band_paths[band] = clean_band else: # First look for reprojected bands reproj_path = self._get_utm_band_path( - band=band.name, resolution=resolution + band=band.name, pixel_size=pixel_size ) if not reproj_path.is_file(): # Then for original data @@ -264,7 +264,7 @@ def _reproject( self, src_arr: np.ndarray, src_meta: dict, rpcs: rpc.RPC, dem_path, **kwargs ) -> (np.ndarray, dict): """ - Reproject using RPCs (cannot use another resolution than src to ensure RPCs are valid) + Reproject using RPCs (cannot use another pixel size than src to ensure RPCs are valid) Args: src_arr (np.ndarray): Array to reproject @@ -286,14 +286,14 @@ def _reproject( } # Reproject - # WARNING: may not give correct output resolution + # WARNING: may not give correct output pixel size out_arr, dst_transform = warp.reproject( src_arr, rpcs=rpcs, src_crs=self._get_raw_crs(), src_nodata=self._raw_nodata, dst_crs=self.crs(), - dst_resolution=self.resolution, + dst_resolution=self.pixel_size, dst_nodata=self._raw_nodata, # input data should be in integer num_threads=MAX_CORES, resampling=Resampling.bilinear, @@ -319,7 +319,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -332,8 +332,8 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray @@ -341,31 +341,31 @@ def _read_band( with rasterio.open(str(path)) as dst: dst_crs = dst.crs - # Compute resolution from size (if needed) - if resolution is None and size is not None: - resolution = self._resolution_from_size(size) + # Compute pixel_size from size (if needed) + if pixel_size is None and size is not None: + pixel_size = self._pixel_size_from_img_size(size) # Reproj path in case - reproj_path = self._get_utm_band_path(band=band.name, resolution=resolution) + reproj_path = self._get_utm_band_path(band=band.name, pixel_size=pixel_size) # Manage the case if we got a LAT LON product if not dst_crs.is_projected: if not reproj_path.is_file(): reproj_path = self._get_utm_band_path( - band=band.name, resolution=resolution, writable=True + band=band.name, pixel_size=pixel_size, writable=True ) # Warp band if needed self._warp_band( path, reproj_path=reproj_path, - resolution=resolution, + pixel_size=pixel_size, ) # Read band LOGGER.debug(f"Reading warped {band.name}.") band_arr = utils.read( reproj_path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, indexes=[self.bands[band].id], @@ -377,7 +377,7 @@ def _read_band( # Read band band_arr = utils.read( path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, **kwargs, @@ -388,7 +388,7 @@ def _read_band( # Read band band_arr = utils.read( path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, indexes=[self.bands[band].id], @@ -404,17 +404,17 @@ def _read_band( def _load_bands( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: bands list: List of the wanted bands - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -424,13 +424,13 @@ def _load_bands( return {} # Get band paths - if resolution is None and size is not None: - resolution = self._resolution_from_size(size) - band_paths = self.get_band_paths(bands, resolution=resolution, **kwargs) + if pixel_size is None and size is not None: + pixel_size = self._pixel_size_from_img_size(size) + band_paths = self.get_band_paths(bands, pixel_size=pixel_size, **kwargs) # Open bands and get array (resampled if needed) band_arrays = self._open_bands( - band_paths, resolution=resolution, size=size, **kwargs + band_paths, pixel_size=pixel_size, size=size, **kwargs ) return band_arrays @@ -517,7 +517,7 @@ def _get_path( def _get_utm_band_path( self, band: str, - resolution: Union[float, tuple, list] = None, + pixel_size: Union[float, tuple, list] = None, writable: bool = False, ) -> Union[CloudPath, Path]: """ @@ -525,13 +525,13 @@ def _get_utm_band_path( Args: band (str): Band in string as written on the filepath - resolution (Union[float, tuple, list]): Resolution of the wanted UTM band + pixel_size (Union[float, tuple, list]): Pixel size of the wanted UTM band writable (bool): Do we need to write the UTM band ? Returns: Union[CloudPath, Path]: UTM band path """ - res_str = self._resolution_to_str(resolution) + res_str = self._pixel_size_to_str(pixel_size) return self._get_band_folder(writable).joinpath( f"{self.condensed_name}_{band}_{res_str}.vrt" @@ -541,7 +541,7 @@ def _warp_band( self, path: Union[str, CloudPath, Path], reproj_path: Union[str, CloudPath, Path], - resolution: float = None, + pixel_size: float = None, ) -> None: """ Warp band to UTM @@ -549,17 +549,17 @@ def _warp_band( Args: path (Union[str, CloudPath, Path]): Band path to warp reproj_path (Union[str, CloudPath, Path]): Path where to write the reprojected band - resolution (int): Band resolution in meters + pixel_size (int): Band pixel size in meters """ # Do not warp if existing file if reproj_path.is_file(): return - if not resolution: - resolution = self.resolution + if not pixel_size: + pixel_size = self.pixel_size - LOGGER.info(f"Warping stack to UTM with a {resolution} m resolution.") + LOGGER.info(f"Warping stack to UTM with a {pixel_size} m pixel size.") # Read band with rasterio.open(str(path)) as src: @@ -571,7 +571,7 @@ def _warp_band( src.width, src.height, *src.bounds, - resolution=resolution, + resolution=pixel_size, ) vrt_options = { @@ -596,7 +596,7 @@ def _warp_band( rio_shutil.copy(vrt, reproj_path, driver="vrt") def _get_default_utm_band( - self, resolution: float = None, size: Union[list, tuple] = None + self, pixel_size: float = None, size: Union[list, tuple] = None ) -> Union[CloudPath, Path]: """ Get the default UTM band: @@ -604,23 +604,23 @@ def _get_default_utm_band( - If not, create reproject (if needed) the GREEN band Args: - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: str: Default UTM path """ - # Manage resolution - if resolution is None and size is not None: - resolution = self._resolution_from_size(size) - def_res = resolution if resolution else self.resolution + # Manage pixel_size + if pixel_size is None and size is not None: + pixel_size = self._pixel_size_from_img_size(size) + def_res = pixel_size if pixel_size else self.pixel_size # Get default band path default_band = self.get_default_band() - def_path = self.get_band_paths([default_band], resolution=def_res)[default_band] + def_path = self.get_band_paths([default_band], pixel_size=def_res)[default_band] # First look for reprojected bands - res_str = self._resolution_to_str(resolution) + res_str = self._pixel_size_to_str(pixel_size) warped_regex = f"*{self.condensed_name}_*_{res_str}.tif" reproj_bands = list(self._get_band_folder().glob(warped_regex)) @@ -637,18 +637,18 @@ def _get_default_utm_band( if not dst_crs.is_projected: def_band = self.get_default_band() path = self._get_utm_band_path( - band=def_band.name, resolution=resolution + band=def_band.name, pixel_size=pixel_size ) # Warp band if needed if not path.is_file(): path = self._get_utm_band_path( - band=def_band.name, resolution=resolution, writable=True + band=def_band.name, pixel_size=pixel_size, writable=True ) self._warp_band( def_path, reproj_path=path, - resolution=resolution, + pixel_size=pixel_size, ) else: path = def_path @@ -679,7 +679,7 @@ def default_transform(self, **kwargs) -> (affine.Affine, int, int, CRS): """ default_band = self.get_default_band() def_path = self.get_band_paths( - [default_band], resolution=self.resolution, **kwargs + [default_band], pixel_size=self.pixel_size, **kwargs )[default_band] with rasterio.open(str(def_path)) as dst: return dst.transform, dst.width, dst.height, dst.crs diff --git a/eoreader/products/optical/vis1_product.py b/eoreader/products/optical/vis1_product.py index c3c364e5..ddf26bde 100644 --- a/eoreader/products/optical/vis1_product.py +++ b/eoreader/products/optical/vis1_product.py @@ -152,16 +152,16 @@ def _post_init(self, **kwargs) -> None: # Post init done by the super class super()._post_init(**kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ # Not Pansharpened images if self.band_combi == Vis1BandCombination.MS4: - return self._ms_res + self.pixel_size = self._ms_res # Pansharpened images else: - return self._pan_res + self.pixel_size = self._pan_res def _set_instrument(self) -> None: """ @@ -308,7 +308,7 @@ def footprint(self) -> gpd.GeoDataFrame: footprint_dezoom = 10 arr = rasters.read( self.get_default_band_path(), - resolution=self.resolution * footprint_dezoom, + resolution=self.pixel_size * footprint_dezoom, indexes=[1], ) diff --git a/eoreader/products/product.py b/eoreader/products/product.py index 92fed125..49d775f1 100644 --- a/eoreader/products/product.py +++ b/eoreader/products/product.py @@ -46,7 +46,7 @@ from rasterio.crs import CRS from rasterio.enums import Resampling from rasterio.vrt import WarpedVRT -from sertit import files, rasters, strings, xml +from sertit import files, logs, rasters, strings, xml from sertit.misc import ListEnum from eoreader import EOREADER_NAME, cache, utils @@ -201,6 +201,13 @@ def __init__( For SAR product, we use Ground Range resolution as we will automatically orthorectify the tiles. """ + self.pixel_size = None + """ + For SAR data, it is important to distinguish (square) pixel spacing from actual resolution. + (see `this ` for more information). + For optical data, those two terms have usually the same meaning (for a fully zoomed raster). + """ + self.condensed_name = None """ Condensed name, the filename with only useful data to keep the name unique @@ -255,8 +262,8 @@ def __init__( # Set product type, needs to be done after the post-initialization self._set_product_type() - # Set the resolution, needs to be done when knowing the product type - self.resolution = self._get_resolution() + # Set the pixel size, needs to be done when knowing the product type + self._set_pixel_size() self._map_bands() @@ -381,9 +388,9 @@ def _get_band_folder(self, writable: bool = False) -> Union[CloudPath, Path]: return band_folder @abstractmethod - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ raise NotImplementedError @@ -507,7 +514,7 @@ def get_date(self, as_date: bool = False) -> Union[str, dt.date]: def _construct_band_path( self, band: BandNames, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, writable: bool = False, **kwargs, @@ -517,22 +524,22 @@ def _construct_band_path( Args: band (BandNames): Wanted band - resolution (float): Band resolution in meters + pixel_size (float): Band pixel size in meters writable (bool): True if we want the band folder to be writeable kwargs: Additional arguments Returns: Union[CloudPath, Path]: Clean band path """ - # Manage resolution - if resolution is None: + # Manage pixel size + if pixel_size is None: if size is not None: - resolution = self._resolution_from_size(size) + pixel_size = self._pixel_size_from_img_size(size) else: - resolution = self.resolution + pixel_size = self.pixel_size # Convert to str - res_str = self._resolution_to_str(resolution) + res_str = self._pixel_size_to_str(pixel_size) return self._get_band_folder(writable).joinpath( f"{self.condensed_name}_{to_str(band)[0]}_{res_str.replace('.', '-')}.tif", @@ -647,7 +654,7 @@ def get_raw_band_paths(self, **kwargs) -> dict: @abstractmethod def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -666,7 +673,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Band resolution + pixel_size (float): Band pixel size (in meters) kwargs: Other arguments used to load bands Returns: @@ -792,7 +799,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -805,8 +812,8 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray @@ -817,7 +824,7 @@ def _read_band( def load( self, bands: Union[list, BandNames, str], - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -841,29 +848,32 @@ def load( >>> from eoreader.bands import * >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip" >>> prod = Reader().open(path) - >>> bands = prod.load([GREEN, NDVI], resolution=20) + >>> bands = prod.load([GREEN, NDVI], pixel_size=20) Args: bands (Union[list, BandNames, str]): Band list - resolution (float): Resolution of the band, in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Pixel size of the band, in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: {band_name, band xarray} """ + if not pixel_size and "resolution" in kwargs: + logs.deprecation_warning( + "`resolution` is deprecated in favor of `pixel_size` to avoid confusion." + ) + pixel_size = kwargs.pop("resolution") + # Check if all bands are valid bands = to_band(bands) for band in bands: assert self.has_band(band), f"{self.name} has not a {to_str(band)[0]} band." - if not resolution and not size: - resolution = self.resolution - # Load bands (only once ! and convert the bands to be loaded to correct format) unique_bands = list(set(to_band(bands))) - band_dict = self._load(unique_bands, resolution, size, **kwargs) + band_dict = self._load(unique_bands, pixel_size, size, **kwargs) # Manage the case of arrays of different size -> collocate arrays if needed band_dict = self._collocate_bands(band_dict) @@ -884,7 +894,7 @@ def load( def _load( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -893,8 +903,8 @@ def _load( Args: bands (list): Band list - resolution (float): Resolution of the band, in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Pixel size of the band, in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: @@ -910,12 +920,9 @@ def _load( if is_index(band): if self._has_index(band): if band in indices.EOREADER_ALIASES: - from warnings import warn - - warn( + logs.deprecation_warning( "Aliases of Awesome Spectral Indices won't be available in future versions of EOReader. " - f"Please use {indices.EOREADER_ALIASES[band]} instead of {band}", - category=DeprecationWarning, + f"Please use {indices.EOREADER_ALIASES[band]} instead of {band}" ) index_list.append(band) else: @@ -971,7 +978,7 @@ def _load( if unique_bands: LOGGER.debug(f"Loading bands {to_str(unique_bands)}") loaded_bands = self._load_bands( - unique_bands, resolution=resolution, size=size, **kwargs + unique_bands, pixel_size=pixel_size, size=size, **kwargs ) # Add bands @@ -984,7 +991,7 @@ def _load( self._load_spectral_indices( index_list, loaded_bands, - resolution=resolution, + pixel_size=pixel_size, size=size, **kwargs, ) @@ -994,7 +1001,7 @@ def _load( if dem_list: LOGGER.debug(f"Loading DEM bands {to_str(dem_list)}") bands_dict.update( - self._load_dem(dem_list, resolution=resolution, size=size, **kwargs) + self._load_dem(dem_list, pixel_size=pixel_size, size=size, **kwargs) ) # Add Clouds @@ -1002,7 +1009,7 @@ def _load( LOGGER.debug(f"Loading Cloud bands {to_str(clouds_list)}") bands_dict.update( self._load_clouds( - clouds_list, resolution=resolution, size=size, **kwargs + clouds_list, pixel_size=pixel_size, size=size, **kwargs ) ) @@ -1011,7 +1018,7 @@ def _load( def _load_clouds( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -1020,8 +1027,8 @@ def _load_clouds( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -1032,17 +1039,17 @@ def _load_clouds( def _load_bands( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -1053,7 +1060,7 @@ def _load_spectral_indices( self, index_list: list, loaded_bands: dict, - resolution: Union[float, tuple] = None, + pixel_size: Union[float, tuple] = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: @@ -1062,8 +1069,8 @@ def _load_spectral_indices( Args: bands (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Additional arguments Returns: dict: Dictionary {band_name, band_xarray} @@ -1071,7 +1078,7 @@ def _load_spectral_indices( band_dict = {} for idx in index_list: idx_path = self._construct_band_path( - idx, resolution, size, writable=False, **kwargs + idx, pixel_size, size, writable=False, **kwargs ) if idx_path.is_file(): band_dict[idx] = utils.read(idx_path) @@ -1081,7 +1088,7 @@ def _load_spectral_indices( # Write on disk idx_path = self._construct_band_path( - idx, resolution, size, writable=True, **kwargs + idx, pixel_size, size, writable=True, **kwargs ) utils.write(idx_arr, idx_path) band_dict[idx] = idx_arr @@ -1091,17 +1098,17 @@ def _load_spectral_indices( def _load_dem( self, band_list: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: band_list (list): List of the wanted bands - resolution (int): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (int): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -1114,27 +1121,27 @@ def _load_dem( if band == DEM: path = self._warp_dem( kwargs.get(DEM_KW, dem_path), - resolution=resolution, + pixel_size=pixel_size, size=size, **kwargs, ) elif band == SLOPE: path = self._compute_slope( kwargs.get(SLOPE_KW, dem_path), - resolution=resolution, + pixel_size=pixel_size, size=size, ) elif band == HILLSHADE: path = self._compute_hillshade( kwargs.get(HILLSHADE_KW, dem_path), - resolution=resolution, + pixel_size=pixel_size, size=size, ) else: raise InvalidTypeError(f"Unknown DEM band: {band}") dem_bands[band] = utils.read( - path, resolution=resolution, size=size + path, pixel_size=pixel_size, size=size ).astype(np.float32) return dem_bands @@ -1200,7 +1207,7 @@ def has_bands(self, bands: Union[list, BandNames, str]) -> bool: - DEM band - cloud band - See :code:`has_bands` for a code example. + See :code:`has_band` for a code example. Args: bands (Union[list, BandNames, str]): EOReader bands (optical, SAR, clouds, DEM) @@ -1397,7 +1404,7 @@ def stac(self) -> StacItem: def _warp_dem( self, dem_path: str = "", - resolution: Union[float, tuple] = None, + pixel_size: Union[float, tuple] = None, size: Union[list, tuple] = None, resampling: Resampling = Resampling.bilinear, **kwargs, @@ -1411,14 +1418,14 @@ def _warp_dem( >>> from eoreader.bands import * >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip" >>> prod = Reader().open(path) - >>> prod.warp_dem(resolution=20) # In meters + >>> prod.warp_dem(pixel_size=20) # In meters '/path/to/20200824T110631_S2_T30TTK_L1C_150432_DEM.tif' Args: dem_path (str): DEM path, using EUDEM/MERIT DEM if none - resolution (Union[float, tuple]): Resolution in meters. If not specified, use the product resolution. + pixel_size (Union[float, tuple]): Pixel size in meters. If not specified, use the product pixel size. resampling (Resampling): Resampling method - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: @@ -1450,8 +1457,8 @@ def _warp_dem( LOGGER.debug("Using DEM: %s", dem_path) def_tr, def_w, def_h, def_crs = self.default_transform(**kwargs) with rasterio.open(str(dem_path)) as dem_ds: - # Get adjusted transform and shape (with new resolution) - if size is not None and resolution is None: + # Get adjusted transform and shape (with new pixel_size) + if size is not None and pixel_size is None: try: # Get destination transform @@ -1466,14 +1473,14 @@ def _warp_dem( except (TypeError, KeyError): raise ValueError( - f"Size should exist (as resolution is None)" + f"Size should exist (as pixel_size is None)" f" and castable to a list: {size}" ) else: - # Refine resolution - if resolution is None: - resolution = self.resolution + # Refine pixel_size + if pixel_size is None: + pixel_size = self.pixel_size bounds = transform.array_bounds(def_h, def_w, def_tr) dst_tr, out_w, out_h = warp.calculate_default_transform( @@ -1482,7 +1489,7 @@ def _warp_dem( def_w, def_h, *bounds, - resolution=resolution, + resolution=pixel_size, ) vrt_options = { @@ -1506,7 +1513,7 @@ def _warp_dem( def _compute_hillshade( self, dem_path: str = "", - resolution: Union[float, tuple] = None, + pixel_size: Union[float, tuple] = None, size: Union[list, tuple] = None, resampling: Resampling = Resampling.bilinear, ) -> Union[Path, CloudPath]: @@ -1515,8 +1522,8 @@ def _compute_hillshade( Args: dem_path (str): DEM path, using EUDEM/MERIT DEM if none - resolution (Union[float, tuple]): Resolution in meters. If not specified, use the product resolution. - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[float, tuple]): Pixel size in meters. If not specified, use the product pixel size. + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. resampling (Resampling): Resampling method Returns: @@ -1528,7 +1535,7 @@ def _compute_hillshade( def _compute_slope( self, dem_path: str = "", - resolution: Union[float, tuple] = None, + pixel_size: Union[float, tuple] = None, size: Union[list, tuple] = None, resampling: Resampling = Resampling.bilinear, ) -> Union[Path, CloudPath]: @@ -1537,8 +1544,8 @@ def _compute_slope( Args: dem_path (str): DEM path, using EUDEM/MERIT DEM if none - resolution (Union[float, tuple]): Resolution in meters. If not specified, use the product resolution. - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[float, tuple]): Pixel size in meters. If not specified, use the product pixel size. + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. resampling (Resampling): Resampling method Returns: @@ -1546,7 +1553,7 @@ def _compute_slope( """ # Warp DEM - warped_dem_path = self._warp_dem(dem_path, resolution, size, resampling) + warped_dem_path = self._warp_dem(dem_path, pixel_size, size, resampling) # Get slope path slope_name = f"{self.condensed_name}_SLOPE_{files.get_filename(dem_path)}.tif" @@ -1601,7 +1608,7 @@ def _collocate_bands(bands: dict, master_xds: xr.DataArray = None) -> dict: def stack( self, bands: list, - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, stack_path: Union[str, CloudPath, Path] = None, save_as_int: bool = False, @@ -1616,12 +1623,12 @@ def stack( >>> from eoreader.bands import * >>> path = r"S2A_MSIL1C_20200824T110631_N0209_R137_T30TTK_20200824T150432.SAFE.zip" >>> prod = Reader().open(path) - >>> stack = prod.stack([NDVI, MNDWI, GREEN], resolution=20) # In meters + >>> stack = prod.stack([NDVI, MNDWI, GREEN], pixel_size=20) # In meters Args: bands (list): Bands and index combination - resolution (float): Stack resolution. . If not specified, use the product resolution. - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Stack pixel size. . If not specified, use the product pixel size. + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. stack_path (Union[str, CloudPath, Path]): Stack path save_as_int (bool): Convert stack to uint16 to save disk space (and therefore multiply the values by 10.000) **kwargs: Other arguments passed to :code:`load` or :code:`rioxarray.to_raster()` (such as :code:`compress`) @@ -1629,10 +1636,18 @@ def stack( Returns: xr.DataArray: Stack as a DataArray """ + # Manage already existing stack on disk + if stack_path: + stack_path = AnyPath(stack_path) + if stack_path.is_file(): + return utils.read(stack_path, resolution=pixel_size, size=size) + else: + os.makedirs(str(stack_path.parent), exist_ok=True) + bands = to_band(bands) # Create the analysis stack - band_dict = self.load(bands, resolution=resolution, size=size, **kwargs) + band_dict = self.load(bands, pixel_size=pixel_size, size=size, **kwargs) # Stack bands if save_as_int: @@ -1645,12 +1660,8 @@ def stack( stack = self._update_attrs(stack, bands, **kwargs) # Write on disk - LOGGER.debug("Saving stack") if stack_path: - stack_path = AnyPath(stack_path) - if not stack_path.parent.exists(): - os.makedirs(str(stack_path.parent), exist_ok=True) - + LOGGER.debug("Saving stack") utils.write(stack, stack_path, dtype=dtype, nodata=nodata, **kwargs) return stack @@ -1771,15 +1782,15 @@ def default_transform(self, **kwargs) -> (Affine, int, int, CRS): with rasterio.open(str(self.get_default_band_path(**kwargs))) as dst: return dst.transform, dst.width, dst.height, dst.crs - def _resolution_from_size(self, size: Union[list, tuple] = None) -> tuple: + def _pixel_size_from_img_size(self, size: Union[list, tuple] = None) -> tuple: """ - Compute the corresponding resolution to a given size (positive resolution) + Compute the corresponding pixel size to a given image size (positive resolution) Args: size (Union[list, tuple]): Size Returns: - tuple: Resolution as a tuple (x, y) + tuple: Pixel size as a tuple (x, y) """ def_tr, def_w, def_h, def_crs = self.default_transform() bounds = transform.array_bounds(def_h, def_w, def_tr) @@ -1792,7 +1803,7 @@ def _resolution_from_size(self, size: Union[list, tuple] = None) -> tuple: def_w, def_h, *bounds, - resolution=self.resolution, + resolution=self.pixel_size, ) res_x = abs(utm_tr.a * utm_w / size[0]) res_y = abs(utm_tr.e * utm_h / size[1]) @@ -1801,7 +1812,7 @@ def _resolution_from_size(self, size: Union[list, tuple] = None) -> tuple: res_x = abs(def_tr.a * def_w / size[0]) res_y = abs(def_tr.e * def_h / size[1]) - # Round resolution to the closest meter (under 1 meter, allow centimetric resolution) + # Round pixel_size to the closest meter (under 1 meter, allow centimetric pixel_size) if res_x < 1.0: res_x = np.round(res_x, 1) else: @@ -1841,32 +1852,32 @@ def clear(self): for obj in objects: obj.cache_clear() - def _resolution_to_str(self, resolution: Union[float, tuple, list] = None): + def _pixel_size_to_str(self, pixel_size: Union[float, tuple, list] = None): """ - Convert a resolution to a normalized string + Convert a pixel_size to a normalized string Args: - resolution (Union[float, tuple, list]): Resolution + pixel_size (Union[float, tuple, list]): Pixel size Returns: - str: Resolution as a string + str: Pixel size as a string """ def _res_to_str(res): return f"{abs(res):.2f}m".replace(".", "-") - if resolution: - if isinstance(resolution, (tuple, list)): - res_x = _res_to_str(resolution[0]) - res_y = _res_to_str(resolution[1]) + if pixel_size: + if isinstance(pixel_size, (tuple, list)): + res_x = _res_to_str(pixel_size[0]) + res_y = _res_to_str(pixel_size[1]) if res_x == res_y: res_str = res_x else: res_str = f"{res_x}_{res_y}" else: - res_str = _res_to_str(resolution) + res_str = _res_to_str(pixel_size) else: - res_str = _res_to_str(self.resolution) + res_str = _res_to_str(self.pixel_size) return res_str @@ -1892,6 +1903,7 @@ def _to_repr(self) -> list: f"\tconstellation: {self.constellation if isinstance(self.constellation, str) else self.constellation.value}", f"\tsensor type: {self.sensor_type if isinstance(self.sensor_type, str) else self.sensor_type.value}", f"\tproduct type: {self.product_type if isinstance(self.product_type, str) else self.product_type.value}", + f"\tdefault pixel size: {self.pixel_size}", f"\tdefault resolution: {self.resolution}", f"\tacquisition datetime: {self.get_datetime(as_datetime=True).isoformat()}", f"\tband mapping:\n{band_repr}", diff --git a/eoreader/products/sar/capella_product.py b/eoreader/products/sar/capella_product.py index 12502995..2d79bf64 100644 --- a/eoreader/products/sar/capella_product.py +++ b/eoreader/products/sar/capella_product.py @@ -158,21 +158,21 @@ def __init__( # Initialization from the super class super().__init__(product_path, archive_path, output_path, remove_tmp, **kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) See here `here `_ """ if self.sensor_mode == CapellaSensorMode.SM: - def_res = 0.35 + def_pixel_size = 0.35 elif self.sensor_mode == CapellaSensorMode.SP: - def_res = 0.6 + def_pixel_size = 0.6 elif self.sensor_mode == CapellaSensorMode.SS: - def_res = 0.8 + def_pixel_size = 0.8 else: raise InvalidProductError(f"Unknown sensor mode: {self.sensor_mode}") - return def_res + self.pixel_size = def_pixel_size def _set_instrument(self) -> None: """ diff --git a/eoreader/products/sar/cosmo_product.py b/eoreader/products/sar/cosmo_product.py index 944ae5ad..52f7ae8f 100644 --- a/eoreader/products/sar/cosmo_product.py +++ b/eoreader/products/sar/cosmo_product.py @@ -21,7 +21,6 @@ import logging import os import tempfile -from abc import abstractmethod from datetime import datetime from enum import unique from io import BytesIO @@ -478,13 +477,13 @@ def get_orbit_direction(self) -> OrbitDirection: return od - def _pre_process_sar(self, band, resolution: float = None, **kwargs) -> str: + def _pre_process_sar(self, band, pixel_size: float = None, **kwargs) -> str: """ Pre-process SAR data (geocoding...) Args: band (sbn): Band to preprocess - resolution (float): Resolution + pixel_size (float): Pixl size kwargs: Additional arguments Returns: @@ -492,7 +491,7 @@ def _pre_process_sar(self, band, resolution: float = None, **kwargs) -> str: """ with h5netcdf.File(self._img_path, phony_dims="access") as raw_h5: if self.sar_prod_type == SarProductType.GDRG or self.nof_swaths == 1: - return super()._pre_process_sar(band, resolution, **kwargs) + return super()._pre_process_sar(band, pixel_size, **kwargs) else: LOGGER.warning( "Currently, SNAP doesn't handle multiswath Cosmo-SkyMed products. This is a workaround. See https://github.com/sertit/eoreader/issues/78" @@ -548,7 +547,7 @@ def _pre_process_sar(self, band, resolution: float = None, **kwargs) -> str: pp_swath_path.append( super()._pre_process_sar( band, - resolution, + pixel_size, prod_path=prod_path, suffix=group, **kwargs, @@ -594,24 +593,3 @@ def _pre_process_sar(self, band, resolution: float = None, **kwargs) -> str: ) return pp_path - - @abstractmethod - def _set_sensor_mode(self) -> None: - """ - Set SAR sensor mode - """ - raise NotImplementedError - - @abstractmethod - def _get_resolution(self) -> float: - """ - Get product default resolution (in meters) - """ - raise NotImplementedError - - @abstractmethod - def _set_instrument(self) -> None: - """ - Set product type - """ - raise NotImplementedError diff --git a/eoreader/products/sar/csg_product.py b/eoreader/products/sar/csg_product.py index c6349756..e31ca544 100644 --- a/eoreader/products/sar/csg_product.py +++ b/eoreader/products/sar/csg_product.py @@ -106,33 +106,33 @@ class CsgProduct(CosmoProduct): `here `_. """ - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) See here `_ for more information (tables 20). Taking the :code:`CSK legacy` values """ if self.sensor_mode == CsgSensorMode.S2A: - def_res = 0.4 + def_pixel_size = 0.4 elif self.sensor_mode == CsgSensorMode.S2B: - def_res = 0.63 + def_pixel_size = 0.63 elif self.sensor_mode == CsgSensorMode.S2C: - def_res = 0.8 + def_pixel_size = 0.8 elif self.sensor_mode == CsgSensorMode.PP: - def_res = 12.0 + def_pixel_size = 12.0 elif self.sensor_mode == CsgSensorMode.SC1: - def_res = 20.0 + def_pixel_size = 20.0 elif self.sensor_mode == CsgSensorMode.SC2: - def_res = 40.0 + def_pixel_size = 40.0 elif self.sensor_mode in [CsgSensorMode.SM, CsgSensorMode.QP]: - def_res = 3.0 + def_pixel_size = 3.0 else: - # Complex data has an empty field and its resolution is not known - def_res = -1.0 + # Complex data has an empty field and its pixel size is not known + def_pixel_size = -1.0 - return def_res + self.pixel_size = def_pixel_size def _set_instrument(self) -> None: """ @@ -164,7 +164,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -172,29 +172,29 @@ def _read_band( Read band from disk. .. WARNING:: - CSG SCS Products do not have a default resolution + CSG SCS Products do not have a default pixel size Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray """ - # In case of SCS data that doesn't have any resolution in the mtd - if self.resolution < 0.0: + # In case of SCS data that doesn't have any pixel_size in the mtd + if self.pixel_size < 0.0: with rasterio.open(path) as ds: - self.resolution = ds.res[0] + self.pixel_size = ds.res[0] try: - if resolution < 0.0: - resolution = self.resolution + if pixel_size < 0.0: + pixel_size = self.pixel_size except TypeError: pass return super()._read_band( - path=path, band=band, resolution=resolution, size=size, **kwargs + path=path, band=band, pixel_size=pixel_size, size=size, **kwargs ) diff --git a/eoreader/products/sar/csk_product.py b/eoreader/products/sar/csk_product.py index e9283a3b..1c036b08 100644 --- a/eoreader/products/sar/csk_product.py +++ b/eoreader/products/sar/csk_product.py @@ -66,30 +66,30 @@ class CskProduct(CosmoProduct): >>> prod = Reader().open(path) """ - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) See here `here `_ for more information (p. 30) """ if self.sensor_mode == CskSensorMode.HI: if self.product_type == CosmoProductType.SCS: - def_res = 3.0 + def_pixel_size = 3.0 else: - def_res = 5.0 + def_pixel_size = 5.0 elif self.sensor_mode == CskSensorMode.PP: - def_res = 20.0 + def_pixel_size = 20.0 elif self.sensor_mode == CskSensorMode.WR: - def_res = 30.0 + def_pixel_size = 30.0 elif self.sensor_mode == CskSensorMode.HR: - def_res = 100.0 + def_pixel_size = 100.0 elif self.sensor_mode == CskSensorMode.S2: - def_res = 1.0 + def_pixel_size = 1.0 else: raise InvalidProductError(f"Unknown sensor mode: {self.sensor_mode}") - return def_res + self.pixel_size = def_pixel_size def _set_sensor_mode(self) -> None: """Get sensor mode""" diff --git a/eoreader/products/sar/iceye_product.py b/eoreader/products/sar/iceye_product.py index 89417067..092093d5 100644 --- a/eoreader/products/sar/iceye_product.py +++ b/eoreader/products/sar/iceye_product.py @@ -98,22 +98,22 @@ def __init__( # Initialization from the super class super().__init__(product_path, archive_path, output_path, remove_tmp, **kwargs) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) See here `_ for more information (table B.3). """ if self.sensor_mode == IceyeSensorMode.SM: - def_res = 3.0 + def_pixel_size = 3.0 elif self.sensor_mode == IceyeSensorMode.SL: - def_res = 1.0 + def_pixel_size = 1.0 elif self.sensor_mode == IceyeSensorMode.SC: - def_res = 15.0 + def_pixel_size = 15.0 else: raise InvalidProductError(f"Unknown sensor mode: {self.sensor_mode}") - return def_res + self.pixel_size = def_pixel_size def _set_instrument(self) -> None: """ diff --git a/eoreader/products/sar/rcm_product.py b/eoreader/products/sar/rcm_product.py index 2da4c9a5..adceaae4 100644 --- a/eoreader/products/sar/rcm_product.py +++ b/eoreader/products/sar/rcm_product.py @@ -119,31 +119,31 @@ class RcmProduct(SarProduct): You can use directly the .zip file """ - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) See here `_ for more information (Beam Modes) """ if self.sensor_mode == RcmSensorMode.THREE_M: - def_res = 3.0 + def_pixel_size = 3.0 elif self.sensor_mode == RcmSensorMode.FIVE_M: - def_res = 5.0 + def_pixel_size = 5.0 elif self.sensor_mode == RcmSensorMode.QP: - def_res = 9.0 + def_pixel_size = 9.0 elif self.sensor_mode == RcmSensorMode.SIXTEEN_M: - def_res = 16.0 + def_pixel_size = 16.0 elif self.sensor_mode == RcmSensorMode.THIRTY_M: - def_res = 30.0 + def_pixel_size = 30.0 elif self.sensor_mode == RcmSensorMode.FIFTY_M: - def_res = 50.0 + def_pixel_size = 50.0 elif self.sensor_mode in [RcmSensorMode.HUNDRED_M, RcmSensorMode.SCLN]: - def_res = 100.0 + def_pixel_size = 100.0 else: raise InvalidProductError(f"Unknown sensor mode: {self.sensor_mode}") - return def_res + self.pixel_size = def_pixel_size def _pre_init(self, **kwargs) -> None: """ diff --git a/eoreader/products/sar/rs2_product.py b/eoreader/products/sar/rs2_product.py index b01ce5af..374001bb 100644 --- a/eoreader/products/sar/rs2_product.py +++ b/eoreader/products/sar/rs2_product.py @@ -77,7 +77,7 @@ class Rs2ProductType(ListEnum): @unique class Rs2SensorMode(ListEnum): """ - Get product default resolution (in meters) + Set product default pixel size (in meters) See here `_ for more information (Beam Modes) @@ -156,25 +156,25 @@ class Rs2Product(SarProduct): You can use directly the .zip file """ - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) """ # ------------------------------------------------------------- # Selective Single or Dual Polarization # Transmit H and/or V, receive H and/or V # F = "Fine", WF = "Wide Fine" if self.sensor_mode in [Rs2SensorMode.F, Rs2SensorMode.WF]: - def_res = 8.0 + def_pixel_size = 8.0 # S = "Standard", W = "Wide" elif self.sensor_mode in [Rs2SensorMode.S, Rs2SensorMode.W]: - def_res = 25.0 + def_pixel_size = 25.0 # SCN = "ScanSAR Narrow" elif self.sensor_mode == Rs2SensorMode.SCN: - def_res = 50.0 + def_pixel_size = 50.0 # SCW = "ScanSAR Wide" elif self.sensor_mode == Rs2SensorMode.SCW: - def_res = 100.0 + def_pixel_size = 100.0 # ------------------------------------------------------------- # Polarimetric @@ -182,20 +182,20 @@ def _get_resolution(self) -> float: # receive H and V on any pulse # FQ = "Fine Quad-Pol", WFQ = "Wide Fine Quad-Pol" elif self.sensor_mode in [Rs2SensorMode.FQ, Rs2SensorMode.WFQ]: - def_res = 12.0 + def_pixel_size = 12.0 # SQ = "Standard Quad-Pol", "Wide Standard Quad-Pol" elif self.sensor_mode in [Rs2SensorMode.SQ, Rs2SensorMode.WSQ]: - def_res = 25.0 + def_pixel_size = 25.0 # ------------------------------------------------------------- # Single Polarization HH # Transmit H, receive H # EH = "Extended High" elif self.sensor_mode == Rs2SensorMode.EH: - def_res = 25.0 + def_pixel_size = 25.0 # EL = "Extended Low" elif self.sensor_mode == Rs2SensorMode.EL: - def_res = 60.0 + def_pixel_size = 60.0 # ------------------------------------------------------------- # Selective Single Polarization @@ -203,28 +203,28 @@ def _get_resolution(self) -> float: # EH = "Extended High" # SLA = "Spotlight" elif self.sensor_mode == Rs2SensorMode.SLA: - def_res = 1.0 + def_pixel_size = 1.0 # U = "Ultra-Fine", WU = "Wide Ultra-Fine" elif self.sensor_mode in [Rs2SensorMode.U, Rs2SensorMode.WU]: - def_res = 3.0 + def_pixel_size = 3.0 # XF = "Extra-Fine" elif self.sensor_mode == Rs2SensorMode.XF: - def_res = 5.0 + def_pixel_size = 5.0 # MF = "Multi-Look Fine", WMF = "Wide Multi-Look Fine" elif self.sensor_mode in [Rs2SensorMode.MF, Rs2SensorMode.WMF]: - def_res = 8.0 + def_pixel_size = 8.0 # ------------------------------------------------------------- # Ocean surveillance and detection of vessels elif self.sensor_mode == Rs2SensorMode.OSVN: - def_res = 50.0 + def_pixel_size = 50.0 elif self.sensor_mode == Rs2SensorMode.DVWF: - def_res = 35.0 + def_pixel_size = 35.0 else: raise InvalidProductError(f"Unknown sensor mode: {self.sensor_mode}") - return def_res + self.pixel_size = def_pixel_size def _set_instrument(self) -> None: """ diff --git a/eoreader/products/sar/s1_product.py b/eoreader/products/sar/s1_product.py index 35161700..7162ad8e 100644 --- a/eoreader/products/sar/s1_product.py +++ b/eoreader/products/sar/s1_product.py @@ -109,9 +109,9 @@ class S1Product(SarProduct): You can use directly the .zip file """ - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) See here `_ for more information @@ -146,11 +146,11 @@ def _get_resolution(self) -> float: } try: - def_res = default_res[self.sensor_mode][res_class] + def_pixel_size = default_res[self.sensor_mode][res_class] except KeyError: raise InvalidProductError(f"Unknown sensor mode: {self.sensor_mode}") - return def_res + self.pixel_size = def_pixel_size def _set_instrument(self) -> None: """ diff --git a/eoreader/products/sar/saocom_product.py b/eoreader/products/sar/saocom_product.py index 68450034..398c2d4d 100644 --- a/eoreader/products/sar/saocom_product.py +++ b/eoreader/products/sar/saocom_product.py @@ -124,9 +124,9 @@ class SaocomPolarization(ListEnum): class SaocomProduct(SarProduct): """Class for SAOCOM-1 Products""" - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) See here `_ for more information (Beam Modes) @@ -144,22 +144,22 @@ def _get_resolution(self) -> float: if not polarization: raise InvalidProductError("polMode not found in metadata!") - def_res = None - # For complex data, set regular ground range resolution provided by the constructor + def_pixel_size = None + # For complex data, set regular ground range pixel_size and resolution provided by the constructor if self.sensor_mode == SaocomSensorMode.SM: - def_res = 10.0 + def_pixel_size = 10.0 elif self.sensor_mode == SaocomSensorMode.TN: if polarization == SaocomPolarization.QP: - def_res = 50.0 + def_pixel_size = 50.0 else: - def_res = 30.0 + def_pixel_size = 30.0 elif self.sensor_mode == SaocomSensorMode.TW: if polarization == SaocomPolarization.QP: - def_res = 100.0 + def_pixel_size = 100.0 else: - def_res = 50.0 + def_pixel_size = 50.0 - return def_res + self.pixel_size = def_pixel_size def _set_instrument(self) -> None: """ diff --git a/eoreader/products/sar/sar_product.py b/eoreader/products/sar/sar_product.py index 54b1ed97..bfeb4484 100644 --- a/eoreader/products/sar/sar_product.py +++ b/eoreader/products/sar/sar_product.py @@ -38,7 +38,13 @@ from eoreader import EOREADER_NAME, cache, utils from eoreader.bands import BandNames, SarBand, SarBandMap from eoreader.bands import SarBandNames as sab -from eoreader.env_vars import DEM_PATH, DSPK_GRAPH, PP_GRAPH, SAR_DEF_RES, SNAP_DEM_NAME +from eoreader.env_vars import ( + DEM_PATH, + DSPK_GRAPH, + PP_GRAPH, + SAR_DEF_PIXEL_SIZE, + SNAP_DEM_NAME, +) from eoreader.exceptions import InvalidProductError, InvalidTypeError from eoreader.keywords import SAR_INTERP_NA from eoreader.products.product import Product, SensorType @@ -191,6 +197,9 @@ def __init__( # Initialization from the super class super().__init__(product_path, archive_path, output_path, remove_tmp, **kwargs) + # ??? + self.pixel_spacing = self.pixel_size / 2.0 + def _map_bands(self) -> None: """ Map bands @@ -200,7 +209,7 @@ def _map_bands(self) -> None: band_name: SarBand( eoreader_name=band_name, name=band_name.name, - gsd=self.resolution, + gsd=self.pixel_size, id=band_name.value, asset_role=INTENSITY, ) @@ -384,7 +393,7 @@ def _set_sensor_mode(self) -> None: raise NotImplementedError def get_band_paths( - self, band_list: list, resolution: float = None, **kwargs + self, band_list: list, pixel_size: float = None, **kwargs ) -> dict: """ Return the paths of required bands. @@ -405,7 +414,7 @@ def get_band_paths( Args: band_list (list): List of the wanted bands - resolution (float): Band resolution + pixel_size (float): Band pixel size kwargs: Other arguments used to load bands Returns: @@ -437,13 +446,13 @@ def get_band_paths( exact_name=True, ) except FileNotFoundError: - self._pre_process_sar(speckle_band, resolution, **kwargs) + self._pre_process_sar(speckle_band, pixel_size, **kwargs) # Despeckle the noisy band band_paths[band] = self._despeckle_sar(speckle_band, **kwargs) else: band_paths[band] = self._pre_process_sar( - band, resolution, **kwargs + band, pixel_size, **kwargs ) return band_paths @@ -561,7 +570,7 @@ def _read_band( self, path: Union[CloudPath, Path], band: BandNames = None, - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[list, tuple] = None, **kwargs, ) -> xr.DataArray: @@ -574,8 +583,8 @@ def _read_band( Args: path (Union[CloudPath, Path]): Band path band (BandNames): Band to read - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: xr.DataArray: Band xarray @@ -583,7 +592,7 @@ def _read_band( """ return utils.read( path, - resolution=resolution, + pixel_size=pixel_size, size=size, resampling=Resampling.bilinear, **kwargs, @@ -592,17 +601,17 @@ def _read_band( def _load_bands( self, bands: Union[list, BandNames], - resolution: float = None, + pixel_size: float = None, size: Union[list, tuple] = None, **kwargs, ) -> dict: """ - Load bands as numpy arrays with the same resolution (and same metadata). + Load bands as numpy arrays with the same pixel size (and same metadata). Args: bands (list, BandNames): List of the wanted bands - resolution (float): Band resolution in meters - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (float): Band pixel size in meters + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. kwargs: Other arguments used to load bands Returns: dict: Dictionary {band_name, band_xarray} @@ -615,27 +624,27 @@ def _load_bands( if not isinstance(bands, list): bands = [bands] - if resolution is None and size is not None: - resolution = self._resolution_from_size(size) - band_paths = self.get_band_paths(bands, resolution, **kwargs) + if pixel_size is None and size is not None: + pixel_size = self._pixel_size_from_img_size(size) + band_paths = self.get_band_paths(bands, pixel_size, **kwargs) # Open bands and get array (resampled if needed) band_arrays = {} for band_name, band_path in band_paths.items(): # Read SAR band band_arrays[band_name] = self._read_band( - band_path, resolution=resolution, size=size, **kwargs + band_path, pixel_size=pixel_size, size=size, **kwargs ) return band_arrays - def _pre_process_sar(self, band: sab, resolution: float = None, **kwargs) -> str: + def _pre_process_sar(self, band: sab, pixel_size: float = None, **kwargs) -> str: """ Pre-process SAR data (geocoding...) Args: band (sbn): Band to preprocess - resolution (float): Resolution + pixel_size (float): Pixel size kwargs: Additional arguments Returns: @@ -719,9 +728,13 @@ def _pre_process_sar(self, band: sab, resolution: float = None, **kwargs) -> str # Command line if not os.path.isfile(pp_dim): - # Resolution - def_res = float(os.environ.get(SAR_DEF_RES, self.resolution)) - res_m = resolution if resolution else def_res + # pixel_size (use SNAP default pixel size for terrain correction) + def_pixel_size = float(os.environ.get(SAR_DEF_PIXEL_SIZE, 0)) + res_m = ( + pixel_size + if (pixel_size and pixel_size != self.pixel_size) + else def_pixel_size + ) res_deg = ( res_m / 10.0 * 8.983152841195215e-5 ) # Approx, shouldn't be used @@ -913,7 +926,7 @@ def interp_na(array, dim): def _compute_hillshade( self, dem_path: str = "", - resolution: Union[float, tuple] = None, + pixel_size: Union[float, tuple] = None, size: Union[list, tuple] = None, resampling: Resampling = Resampling.bilinear, ) -> Union[Path, CloudPath]: @@ -922,9 +935,9 @@ def _compute_hillshade( Args: dem_path (str): DEM path, using EUDEM/MERIT DEM if none - resolution (Union[float, tuple]): Resolution in meters. If not specified, use the product resolution. + pixel_size (Union[float, tuple]): Pixel size in meters. If not specified, use the product pixel size. resampling (Resampling): Resampling method - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. Returns: Union[Path, CloudPath]: Hillshade mask path """ diff --git a/eoreader/products/sar/tsx_product.py b/eoreader/products/sar/tsx_product.py index 83acd52f..b39b27c1 100644 --- a/eoreader/products/sar/tsx_product.py +++ b/eoreader/products/sar/tsx_product.py @@ -168,9 +168,9 @@ def __init__( if self.product_type != TsxProductType.SSC: self._geometric_res = getattr(TsxGeometricResolution, self.split_name[3]) - def _get_resolution(self) -> float: + def _set_pixel_size(self) -> None: """ - Get product default resolution (in meters) + Set product default pixel size (in meters) See here `_ for more information (Beam Modes) @@ -192,25 +192,25 @@ def _get_resolution(self) -> float: "acquisitionInfo or polarisationMode not found in metadata!" ) - def_res = None + def_pixel_size = None if self.sensor_mode == TsxSensorMode.HS: if polarization == TsxPolarization.S: - def_res = 1.1 + def_pixel_size = 1.1 elif polarization == TsxPolarization.D: - def_res = 2.2 + def_pixel_size = 2.2 elif self.sensor_mode == TsxSensorMode.SL: if polarization == TsxPolarization.S: - def_res = 1.7 + def_pixel_size = 1.7 elif polarization == TsxPolarization.D: - def_res = 3.4 + def_pixel_size = 3.4 elif self.sensor_mode == TsxSensorMode.ST: if polarization == TsxPolarization.S: - def_res = 0.24 + def_pixel_size = 0.24 elif self.sensor_mode == TsxSensorMode.SM: if polarization == TsxPolarization.S: - def_res = 3.3 + def_pixel_size = 3.3 elif polarization == TsxPolarization.D: - def_res = 6.6 + def_pixel_size = 6.6 elif self.sensor_mode == TsxSensorMode.SC: # Read metadata try: @@ -223,15 +223,15 @@ def _get_resolution(self) -> float: ) # Four beams if nof_beams == 4: - def_res = 18.5 + def_pixel_size = 18.5 elif nof_beams == 6: # Six beams - def_res = 40.0 + def_pixel_size = 40.0 else: raise InvalidProductError(f"Unknown sensor mode: {self.sensor_mode}") - return def_res + self.pixel_size = def_pixel_size def _set_instrument(self) -> None: """ diff --git a/eoreader/reader.py b/eoreader/reader.py index 541dc682..f49a88cc 100644 --- a/eoreader/reader.py +++ b/eoreader/reader.py @@ -424,6 +424,7 @@ def open( constellation: Sentinel-2 sensor type: Optical product type: MSIL1C + default pixel size: 10.0 default resolution: 10.0 acquisition datetime: 2021-05-17T10:36:19 band mapping: diff --git a/eoreader/stac/stac_item.py b/eoreader/stac/stac_item.py index 39b835b3..209cf4d3 100644 --- a/eoreader/stac/stac_item.py +++ b/eoreader/stac/stac_item.py @@ -91,7 +91,7 @@ def __init__(self, prod, **kwargs): ] # Common mtd - self.gsd = self._prod.resolution + self.gsd = self._prod.pixel_size self.title = self._prod.condensed_name self.constellation = self._prod.constellation.value.lower() self.created = datetime.utcnow() diff --git a/eoreader/utils.py b/eoreader/utils.py index 481a2c17..cade4f21 100644 --- a/eoreader/utils.py +++ b/eoreader/utils.py @@ -23,7 +23,6 @@ from pathlib import Path from typing import Callable, Union -import geopandas as gpd import numpy as np import pandas as pd import xarray as xr @@ -33,7 +32,7 @@ from rasterio.enums import Resampling from rasterio.errors import NotGeoreferencedWarning from rasterio.rpc import RPC -from sertit import rasters +from sertit import rasters, vectors from eoreader import EOREADER_NAME from eoreader.bands import is_index, is_sat_band, to_str @@ -126,7 +125,7 @@ def use_dask(): def read( path: Union[str, CloudPath, Path], - resolution: Union[tuple, list, float] = None, + pixel_size: Union[tuple, list, float] = None, size: Union[tuple, list] = None, resampling: Resampling = Resampling.nearest, masked: bool = True, @@ -148,8 +147,8 @@ def read( Args: path (Union[str, CloudPath, Path]): Path to the raster - resolution (Union[tuple, list, float]): Resolution of the wanted band, in dataset resolution unit (X, Y) - size (Union[tuple, list]): Size of the array (width, height). Not used if resolution is provided. + pixel_size (Union[tuple, list, float]): Size of the pixels of the wanted band, in dataset unit (X, Y) + size (Union[tuple, list]): Size of the array (width, height). Not used if pixel_size is provided. resampling (Resampling): Resampling method masked (bool): Get a masked array indexes (Union[int, list]): Indexes to load. Load the whole array if None. @@ -176,7 +175,7 @@ def read( warnings.simplefilter("ignore", category=NotGeoreferencedWarning) return rasters.read( path, - resolution=resolution, + resolution=pixel_size, resampling=resampling, masked=masked, indexes=indexes, @@ -329,44 +328,6 @@ def to_list(pd_table, field) -> list: raise KeyError(f"Invalid RPC file, missing key: {msg}") -def simplify_footprint( - footprint: gpd.GeoDataFrame, resolution: float, max_nof_vertices: int = 50 -) -> gpd.GeoDataFrame: - """ - Simplify footprint - - Args: - footprint (gpd.GeoDataFrame): Footprint to be simplified - resolution (float): Corresponding resolution - max_nof_vertices (int): Maximum number of vertices of the wanted footprint - - Returns: - gpd.GeoDataFrame: Simplified footprint - """ - # Number of pixels of tolerance - tolerance = [1, 2, 4, 8, 16, 32, 64] - - # Process only if given footprint is too complex (too many vertices) - def simplify_geom(value): - nof_vertices = len(value.exterior.coords) - if nof_vertices > max_nof_vertices: - for tol in tolerance: - # Simplify footprint - value = value.simplify( - tolerance=tol * resolution, preserve_topology=True - ) - - # Check if OK - nof_vertices = len(value.exterior.coords) - if nof_vertices <= max_nof_vertices: - break - return value - - footprint.geometry = footprint.geometry.apply(simplify_geom) - - return footprint - - def simplify(footprint_fct: Callable): """ Simplify footprint decorator @@ -382,7 +343,7 @@ def simplify(footprint_fct: Callable): def simplify_wrapper(self): """Simplify footprint wrapper""" footprint = footprint_fct(self) - return simplify_footprint(footprint, self.resolution) + return vectors.simplify_footprint(footprint, self.pixel_size) return simplify_wrapper diff --git a/requirements.txt b/requirements.txt index 0ac54383..b71adde6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ spyndex>=0.3.0 pystac[validation] # SERTIT libs -sertit[full]>=1.24.0 +sertit[full]>=1.25.0 # Optimizations dask[complete]>=2021.10.0 diff --git a/setup.py b/setup.py index c6fa9e4a..07664be9 100644 --- a/setup.py +++ b/setup.py @@ -31,10 +31,10 @@ "scipy", "rasterio>=1.3.0", "xarray>=0.18.0", - "rioxarray>=0.4.0", + "rioxarray>=0.10.0", "geopandas>=0.11.0", - "sertit[full]>=1.22.0", - "spyndex>=0.2.0", + "sertit[full]>=1.25.0", + "spyndex>=0.3.0", "pyresample", "zarr", "rtree",