diff --git a/knoten/csm.py b/knoten/csm.py index d951b1fa270511b5fdee051be0c01c30ad1218b6..f80753052a4b4d9c8ff55b02efd29a3e3102b348 100644 --- a/knoten/csm.py +++ b/knoten/csm.py @@ -157,13 +157,12 @@ def _(dem, image_pt, camera, max_its = 20, tolerance = 0.001): intersection = generate_ground_point(0.0, image_pt, camera) iterations = 0 semi_major, semi_minor = get_radii(camera) - + source_proj = f'+proj=cart +a={semi_major} +b={semi_minor}' dest_proj = f'+proj=lonlat +a={semi_major} +b={semi_minor}' transformer = utils.create_transformer(source_proj, dest_proj) - while iterations != max_its: - lon, lat, alt = transformer.transform(intersection.x, + lon, lat, _ = transformer.transform(intersection.x, intersection.y, intersection.z, errcheck=True) @@ -174,9 +173,7 @@ def _(dem, image_pt, camera, max_its = 20, tolerance = 0.001): raise ValueError(f'No DEM height at {lat}, {lon}') next_intersection = camera.imageToGround(image_pt, float(height)) - dist = max(abs(intersection.x - next_intersection.x), - abs(intersection.y - next_intersection.y), - abs(intersection.z - next_intersection.z)) + dist = _compute_intersection_distance(intersection, next_intersection) intersection = next_intersection iterations += 1 @@ -184,6 +181,30 @@ def _(dem, image_pt, camera, max_its = 20, tolerance = 0.001): break return intersection +def _compute_intersection_distance(intersection, next_intersection): + """ + Private func that takes two csmapi Ecef objects or other objects with + x,y,z properties and computes the distance between them. This is the + maximum distance in 3D space. + + Parameters + ---------- + intersection : object + Any object with x,y, and z properties that are numeric + + next_intersection : object + Any object with x,y, and z properties that are numeric + + Returns + ------- + dist : float + The maximum distance between intersection and next_intersection + in one of the three planes (x,y,z) + """ + return max(abs(intersection.x - next_intersection.x), + abs(intersection.y - next_intersection.y), + abs(intersection.z - next_intersection.z)) + def generate_boundary(isize, npoints=10): ''' Generates a bounding box given a camera model diff --git a/tests/test_csm.py b/tests/test_csm.py new file mode 100644 index 0000000000000000000000000000000000000000..600e84a80b2ba65f783fd73ea7a0d135ca8db54f --- /dev/null +++ b/tests/test_csm.py @@ -0,0 +1,76 @@ +from unittest import mock +import pytest + +from plio.io.io_gdal import GeoDataset + +import csmapi +from knoten import csm + +@pytest.fixture +def mock_dem(): + mock_dem = mock.MagicMock(spec_set=GeoDataset) + #mock_dem.read_array.return_value = 100 + #mock_dem.latlon_to_pixel.return_value = (0.5,0.5) + return mock_dem + +@pytest.fixture +def mock_sensor(): + mock_sensor = mock.MagicMock(spec=csmapi.RasterGM) + return mock_sensor + +@pytest.fixture +def pt(): + return csmapi.ImageCoord(0.0, 0.0) + +def test_generate_ground_point_with_float(mock_sensor): + csm.generate_ground_point(0, (0.5, 0.5), mock_sensor) + # The internal conversion from tuple to csmapi.ImageCoord means + # assert_called_once_with fails due to different addresses of + # different objects. + mock_sensor.imageToGround.assert_called_once() + +def test_generate_ground_point_with_imagecoord(mock_sensor, pt): + height = 0.0 + csm.generate_ground_point(height, pt, mock_sensor) + mock_sensor.imageToGround.assert_called_once_with(pt, height) + +@mock.patch.object(csm, 'get_radii', return_value=(10,10)) +@mock.patch('pyproj.transformer.Transformer.transform', return_value=(0,0,0)) +@mock.patch.object(csm, '_compute_intersection_distance', return_value=0) +def test_generate_ground_point_with_dtm(mock_dem, mock_sensor, pt): + # Passing the mock_dem fixture fails for some reason. The + # isinstance(obj, GeoDataset) check fails, causing the singldispath + # to never dispatch to the func under test. + mock_dem = mock.MagicMock(spec_set=GeoDataset) + mock_dem.no_data_value = 10 + mock_dem.read_array.return_value = [[100]] + mock_dem.latlon_to_pixel.return_value = (0.5,0.5) + csm.generate_ground_point(mock_dem, pt, mock_sensor) + # This call is mocked so that the intitial intersection and + # one iteration should occur. Therefore, the call count + # should always be 2. + assert mock_sensor.imageToGround.call_count == 2 + +from collections import namedtuple + +@mock.patch.object(csm, 'get_radii', return_value=(10,10)) +@mock.patch('pyproj.transformer.Transformer.transform', return_value=(0,0,0)) +def test_generate_ground_point_with_dtm_ndv(mock_dem, mock_sensor, pt): + # Passing the mock_dem fixture fails for some reason. The + # isinstance(obj, GeoDataset) check fails, causing the singldispath + # to never dispatch to the func under test. + mock_dem = mock.MagicMock(spec_set=GeoDataset) + + # If the no data value equals the height, this should raise a value error + mock_dem.no_data_value = 100 + mock_dem.read_array.return_value = [[100]] + mock_dem.latlon_to_pixel.return_value = (0.5,0.5) + with pytest.raises(ValueError): + csm.generate_ground_point(mock_dem, pt, mock_sensor) + +def test__compute_intersection_distance(): + Point = namedtuple("Point", 'x, y, z') + pt1 = Point(0,0,0) + pt2 = Point(1,1,1) + dist = csm._compute_intersection_distance(pt1, pt2) + assert dist == 1 \ No newline at end of file