From 4d5dac1d47dc5a8b0009edbc56d56d43cb761b15 Mon Sep 17 00:00:00 2001 From: Adeel Hassan Date: Thu, 10 Oct 2024 13:23:14 -0400 Subject: [PATCH 1/2] disallow non-finite Box coords --- rastervision_core/rastervision/core/box.py | 4 ++++ tests/core/test_box.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/rastervision_core/rastervision/core/box.py b/rastervision_core/rastervision/core/box.py index 164459fa7..3521a5716 100644 --- a/rastervision_core/rastervision/core/box.py +++ b/rastervision_core/rastervision/core/box.py @@ -40,6 +40,10 @@ def __init__(self, ymin: int, xmin: int, ymax: int, xmax: int): ymax: maximum y value xmax: maximum x value """ + if not all(math.isfinite(v) for v in (ymin, xmin, ymax, xmax)): + raise ValueError( + f'Invalid Box coordinates: {(ymin, xmin, ymax, xmax)}.') + self.ymin = ymin self.xmin = xmin self.ymax = ymax diff --git a/tests/core/test_box.py b/tests/core/test_box.py index b3b55d023..e13c24207 100644 --- a/tests/core/test_box.py +++ b/tests/core/test_box.py @@ -443,6 +443,11 @@ def test_in(self): with self.assertRaises(NotImplementedError): _ = '' in Box(0, 0, 1, 1) + def test_error_on_nonfinite_inputs(self): + self.assertRaises(ValueError, lambda: Box(np.inf, 0, 0, 0)) + self.assertRaises(ValueError, lambda: Box(-np.inf, 0, 0, 0)) + self.assertRaises(ValueError, lambda: Box(np.nan, 0, 0, 0)) + if __name__ == '__main__': unittest.main() From 59e65e24d15daa3e8d0e8298a36a75ac570eb070 Mon Sep 17 00:00:00 2001 From: Adeel Hassan Date: Thu, 10 Oct 2024 13:26:22 -0400 Subject: [PATCH 2/2] handle non-deterministic pyproj failure For some transformations, pyproj attempts to download transformation grids from the internet for improved accuracy when Transformer.transform() is called. If it fails to connect to the internet, it silently returns (inf, inf) and silently modifies its behavior to not access the internet on subsequent calls, causing them to succeed (though possibly with a loss of accuracy). See https://github.com/pyproj4/pyproj/issues/705 for details. This workaround forces an error to be raised by setting errcheck=True and ignoring the first error. --- .../rasterio_crs_transformer.py | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/rastervision_core/rastervision/core/data/crs_transformer/rasterio_crs_transformer.py b/rastervision_core/rastervision/core/data/crs_transformer/rasterio_crs_transformer.py index 14263b8dc..303549d30 100644 --- a/rastervision_core/rastervision/core/data/crs_transformer/rasterio_crs_transformer.py +++ b/rastervision_core/rastervision/core/data/crs_transformer/rasterio_crs_transformer.py @@ -1,6 +1,8 @@ from typing import TYPE_CHECKING, Any -from pyproj import Transformer +import logging +from pyproj import Transformer +from pyproj.exceptions import ProjError import numpy as np import rasterio as rio from rasterio.transform import (rowcol, xy) @@ -12,6 +14,8 @@ if TYPE_CHECKING: from typing import Self +log = logging.getLogger(__name__) + class RasterioCRSTransformer(CRSTransformer): """Transformer for a RasterioRasterSource.""" @@ -61,7 +65,7 @@ def __repr__(self) -> str: out = f"""{cls_name}( image_crs="{image_crs_str}", map_crs="{map_crs_str}", - round_pixels="{self.round_pixels}", + round_pixels={self.round_pixels}, transform={transform_str}) """ return out @@ -78,7 +82,29 @@ def _map_to_pixel( Returns: (x, y) tuple in pixel coordinates """ - image_point = self.map2image(*map_point) + # For some transformations, pyproj attempts to download transformation + # grids from the internet for improved accuracy when + # Transformer.transform() is called. If it fails to connect to the + # internet, it silently returns (inf, inf) and silently modifies its + # behavior to not access the internet on subsequent calls, causing + # them to succeed (though possibly with a loss of accuracy). See + # https://github.com/pyproj4/pyproj/issues/705 for details. + # + # The below workaround forces an error to be raised by setting + # errcheck=True and ignoring the first error. + try: + image_point = self.map2image(*map_point, errcheck=True) + except ProjError as e: + log.debug(f'pyproj: {e}') + if 'network' in str(e).lower(): + log.warning( + 'pyproj tried and failed to connect to the internet to ' + 'download transformation grids for the transformation from\n' + f'{self.map_crs}\nto\n{self.image_crs}.\nSee ' + 'https://github.com/pyproj4/pyproj/issues/705 for details.' + ) + image_point = self.map2image(*map_point, errcheck=True) + x, y = image_point if self.round_pixels: row, col = rowcol(self.transform, x, y) @@ -103,7 +129,29 @@ def _pixel_to_map( col = col.astype(int) if isinstance(col, np.ndarray) else int(col) row = row.astype(int) if isinstance(row, np.ndarray) else int(row) image_point = xy(self.transform, row, col, offset='center') - map_point = self.image2map(*image_point) + + # For some transformations, pyproj attempts to download transformation + # grids from the internet for improved accuracy when + # Transformer.transform() is called. If it fails to connect to the + # internet, it silently returns (inf, inf) and silently modifies its + # behavior to not access the internet on subsequent calls, causing + # them to succeed (though possibly with a loss of accuracy). See + # https://github.com/pyproj4/pyproj/issues/705 for details. + # + # The below workaround forces an error to be raised by setting + # errcheck=True and ignoring the first error. + try: + map_point = self.image2map(*image_point, errcheck=True) + except ProjError as e: + log.debug(f'pyproj: {e}') + if 'network' in str(e).lower(): + log.warning( + 'pyproj tried and failed to connect to the internet to ' + 'download transformation grids for the transformation from' + f'\n{self.image_crs}\nto\n{self.map_crs}.\nSee ' + 'https://github.com/pyproj4/pyproj/issues/705 for details.' + ) + map_point = self.image2map(*image_point, errcheck=True) return map_point @classmethod