diff --git a/data_samples/images/maricopa.zip b/data_samples/images/maricopa.zip index ffabbbf..1611d3b 100644 Binary files a/data_samples/images/maricopa.zip and b/data_samples/images/maricopa.zip differ diff --git a/data_samples/json_conf_files/a_variable_disparity_pipeline.json b/data_samples/json_conf_files/a_variable_disparity_pipeline.json new file mode 100644 index 0000000..0d828ab --- /dev/null +++ b/data_samples/json_conf_files/a_variable_disparity_pipeline.json @@ -0,0 +1,30 @@ +{ + "input": { + "left": { + "img": "./maricopa/left.tif", + "nodata": -9999 + }, + "right": { + "img": "./maricopa/right.tif", + "nodata": -9999 + }, + "col_disparity": {"init": "./maricopa/init_col_disparity_grid.tif", "range": 5}, + "row_disparity": {"init": "./maricopa/init_row_disparity_grid.tif", "range": 5} + }, + "pipeline": { + "matching_cost": { + "matching_cost_method": "zncc", + "window_size": 5 + }, + "disparity": { + "disparity_method": "wta", + "invalid_disparity": "NaN" + }, + "refinement": { + "refinement_method": "dichotomy", + "iterations": 2, + "filter": {"method": "bicubic"} + } + } + } + \ No newline at end of file diff --git a/docs/source/exploring_the_field/initial_disparity.rst b/docs/source/exploring_the_field/initial_disparity.rst index cbac738..6e0d59e 100644 --- a/docs/source/exploring_the_field/initial_disparity.rst +++ b/docs/source/exploring_the_field/initial_disparity.rst @@ -22,6 +22,13 @@ In the configuration file, the user is required to enter disparity range for row } } +.. note:: + The "init" key can be either: + - an integer if the initial disparity is common to every point in the image. + - a path to a disparity grid if each point has its own initial disparity value. + + (see :ref:`inputs`) + The min and max disparity would then be equal to (example for columns): .. code:: python diff --git a/docs/source/userguide/input.rst b/docs/source/userguide/input.rst index 57f993b..a47f578 100644 --- a/docs/source/userguide/input.rst +++ b/docs/source/userguide/input.rst @@ -82,8 +82,8 @@ Image (left and right) and disparity (col_disparity and row_disparity) propertie - Default value - Required * - *init* - - Initial point - - int + - Initial point or path to initial grid + - int or str - - Yes * - *range* @@ -92,6 +92,12 @@ Image (left and right) and disparity (col_disparity and row_disparity) propertie - - Yes +.. note:: + The initial disparity can be either: + - constant for each point in the image, in which case *init* dictionary key is an integer + - variable, in which case *init* is a string which returns the path to a grid containing + an integer initial value for each point in the image. + .. warning:: With sad/ssd matching_cost_method in the pipeline (see :ref:`Sequencing`) , `nodata` only accepts `int` type. @@ -101,7 +107,10 @@ Image (left and right) and disparity (col_disparity and row_disparity) propertie - Value not equal to 0 for invalid pixel -**Example** +Examples +******** + +**Input with constant initial disparity** .. code:: json :name: Input example @@ -128,3 +137,29 @@ Image (left and right) and disparity (col_disparity and row_disparity) propertie } } +**Input with variable initial disparity** + +.. code:: json + :name: Input example with disparity grid + + { + "input": + { + "left": { + "img": "./data/left.tif", + "nodata": -9999, + "mask": "./data/mask_left.tif" + }, + "right": { + "img": "/data/right.tif", + "nodata": -9999 + }, + "col_disparity": {"init": "./data/col_disparity_grid.tif", "range": 3}, + "row_disparity": {"init": "./data/row_disparity_grid.tif", "range": 3} + } + , + "pipeline" : + { + // pipeline content + } + } diff --git a/pandora2d/check_configuration.py b/pandora2d/check_configuration.py index 72d2e99..26843b5 100644 --- a/pandora2d/check_configuration.py +++ b/pandora2d/check_configuration.py @@ -30,8 +30,9 @@ import numpy as np import xarray as xr from json_checker import And, Checker, Or +from rasterio.io import DatasetReader -from pandora.img_tools import get_metadata +from pandora.img_tools import get_metadata, rasterio_open from pandora.check_configuration import ( check_dataset, check_images, @@ -102,13 +103,99 @@ def check_input_section(user_cfg: Dict[str, dict], estimation_config: dict = Non if estimation_config is None: left_image_metadata = get_metadata(cfg["input"]["left"]["img"]) - check_disparity_ranges_are_inside_image( - left_image_metadata, cfg["input"]["row_disparity"], cfg["input"]["col_disparity"] - ) + check_disparity(left_image_metadata, cfg["input"]) return cfg +def check_disparity(image_metadata: xr.Dataset, input_cfg: Dict) -> None: + """ + All checks on disparity + + :param image_metadata: only metadata on the left image + :type image_metadata: xr.Dataset + :param input_cfg: input configuration + :type input_cfg: Dict + + """ + + # Check that disparities are dictionaries or grids + if not (isinstance(input_cfg["row_disparity"], dict) and isinstance(input_cfg["col_disparity"], dict)): + raise AttributeError("The disparities in rows and columns must be given as 2 dictionaries.") + + if isinstance(input_cfg["row_disparity"]["init"], str) and isinstance(input_cfg["col_disparity"]["init"], str): + + # Read disparity grids + disparity_row_reader = rasterio_open(input_cfg["row_disparity"]["init"]) + disparity_col_reader = rasterio_open(input_cfg["col_disparity"]["init"]) + + # Check disparity grids size and number of bands + check_disparity_grids(image_metadata, disparity_row_reader) + check_disparity_grids(image_metadata, disparity_col_reader) + + # Get correct disparity dictionaries from init disparity grids to give as input of + # the check_disparity_ranges_are_inside_image method + row_disp_dict = get_dictionary_from_init_grid(disparity_row_reader, input_cfg["row_disparity"]["range"]) + col_disp_dict = get_dictionary_from_init_grid(disparity_col_reader, input_cfg["col_disparity"]["range"]) + + elif isinstance(input_cfg["row_disparity"]["init"], int) and isinstance(input_cfg["col_disparity"]["init"], int): + row_disp_dict = input_cfg["row_disparity"] + col_disp_dict = input_cfg["col_disparity"] + + else: + raise ValueError("Initial columns and row disparity values must be two strings or two integers") + + # Check that disparity ranges are not totally out of the image + check_disparity_ranges_are_inside_image(image_metadata, row_disp_dict, col_disp_dict) + + +def check_disparity_grids(image_metadata: xr.Dataset, disparity_reader: DatasetReader) -> None: + """ + Check that disparity grids contains two bands and are + the same size as the input image + + :param image_metadata: + :type image_metadata: xr.Dataset + :param disparity_reader: disparity grids + :type disparity_reader: rasterio.io.DatasetReader + """ + + # Check that disparity grids are 1-channel grids + if disparity_reader.count != 1: + raise AttributeError("Initial disparity grid must be a 1-channel grid") + + # Check that disparity grids are the same size as the input image + if (disparity_reader.height, disparity_reader.width) != ( + image_metadata.sizes["row"], + image_metadata.sizes["col"], + ): + raise AttributeError("Initial disparity grids and image must have the same size") + + +def get_dictionary_from_init_grid(disparity_reader: DatasetReader, disp_range: int) -> Dict: + """ + Get correct dictionaries to give as input of check_disparity_ranges_are_inside_image method + from initial disparity grids. + + :param disparity_reader: initial disparity grid + :type disparity_reader: rasterio.io.DatasetReader + :param disp_range: range of exploration + :type disp_range: int + :return: a disparity dictionary to give to check_disparity_ranges_are_inside_image() method + :rtype: Dict + """ + + init_disp_grid = disparity_reader.read(1) + + # Get dictionary with integer init value corresponding to the maximum absolute value of init_disp_grid + disp_dict = { + "init": np.max(np.abs(init_disp_grid)), + "range": disp_range, + } + + return disp_dict + + def check_disparity_ranges_are_inside_image( image_metadata: xr.Dataset, row_disparity: Dict, col_disparity: Dict ) -> None: @@ -283,8 +370,8 @@ def get_roi_config(user_cfg: Dict[str, dict]) -> Dict[str, dict]: "nodata": Or(int, lambda input: np.isnan(input), lambda input: np.isinf(input)), "mask": And(Or(str, lambda input: input is None), rasterio_can_open), }, - "col_disparity": {"init": int, "range": And(int, lambda x: x >= 0)}, - "row_disparity": {"init": int, "range": And(int, lambda x: x >= 0)}, + "col_disparity": {"init": Or(int, rasterio_can_open), "range": And(int, lambda x: x >= 0)}, + "row_disparity": {"init": Or(int, rasterio_can_open), "range": And(int, lambda x: x >= 0)}, } default_short_configuration_input = { diff --git a/pandora2d/common.py b/pandora2d/common.py index 7faa6eb..3183896 100644 --- a/pandora2d/common.py +++ b/pandora2d/common.py @@ -222,14 +222,28 @@ def set_out_of_row_disparity_range_to_other_value( # WARNING: if one day we switch disp_row with disp_col index should be -2 ndisp_row = data[data_var_name].shape[-1] - for disp_row in range(ndisp_row): - masking = np.nonzero( - np.logical_or( - data.coords["disp_row"].data[disp_row] < min_disp_grid, - data.coords["disp_row"].data[disp_row] > max_disp_grid, + # We want to put special value on points that are not in the global disparity range (row_disparity_source) + if data_var_name == "cost_volumes": + for disp_row in range(ndisp_row): + masking = np.nonzero( + np.logical_or( + (data.coords["disp_row"].data[disp_row] < min_disp_grid) + & (data.coords["disp_row"].data[disp_row] >= data.attrs["row_disparity_source"][0]), + (data.coords["disp_row"].data[disp_row] > max_disp_grid) + & (data.coords["disp_row"].data[disp_row] <= data.attrs["row_disparity_source"][1]), + ) ) - ) - data[data_var_name].data[masking[0], masking[1], :, disp_row] = value + data[data_var_name].data[masking[0], masking[1], :, disp_row] = value + + else: + for disp_row in range(ndisp_row): + masking = np.nonzero( + np.logical_or( + data.coords["disp_row"].data[disp_row] < min_disp_grid, + data.coords["disp_row"].data[disp_row] > max_disp_grid, + ) + ) + data[data_var_name].data[masking[0], masking[1], :, disp_row] = value def set_out_of_col_disparity_range_to_other_value( @@ -259,11 +273,25 @@ def set_out_of_col_disparity_range_to_other_value( # WARNING: if one day we switch disp_row with disp_col index should be -1 ndisp_col = data[data_var_name].shape[-2] - for disp_col in range(ndisp_col): - masking = np.nonzero( - np.logical_or( - data.coords["disp_col"].data[disp_col] < min_disp_grid, - data.coords["disp_col"].data[disp_col] > max_disp_grid, + # We want to put special value on points that are not in the global disparity range (col_disparity_source) + if data_var_name == "cost_volumes": + for disp_col in range(ndisp_col): + masking = np.nonzero( + np.logical_or( + (data.coords["disp_col"].data[disp_col] < min_disp_grid) + & (data.coords["disp_col"].data[disp_col] >= data.attrs["col_disparity_source"][0]), + (data.coords["disp_col"].data[disp_col] > max_disp_grid) + & (data.coords["disp_col"].data[disp_col] <= data.attrs["col_disparity_source"][1]), + ) + ) + data[data_var_name].data[masking[0], masking[1], disp_col, :] = value + + else: + for disp_col in range(ndisp_col): + masking = np.nonzero( + np.logical_or( + data.coords["disp_col"].data[disp_col] < min_disp_grid, + data.coords["disp_col"].data[disp_col] > max_disp_grid, + ) ) - ) - data[data_var_name].data[masking[0], masking[1], disp_col, :] = value + data[data_var_name].data[masking[0], masking[1], disp_col, :] = value diff --git a/pandora2d/img_tools.py b/pandora2d/img_tools.py index 0d3d7df..eb48c78 100644 --- a/pandora2d/img_tools.py +++ b/pandora2d/img_tools.py @@ -34,9 +34,10 @@ from xarray import Coordinate as Coordinates import copy -from typing import List, Dict, Union, NamedTuple, Any - +from typing import List, Dict, Union, NamedTuple, Any, Tuple from math import floor +from numpy.typing import NDArray + import xarray as xr import numpy as np from scipy.ndimage import shift, zoom @@ -85,10 +86,10 @@ def create_datasets_from_inputs(input_config: Dict, roi: Dict = None, estimation return Datasets( pandora_img_tools.create_dataset_from_inputs(input_config["left"], roi).pipe( - add_left_disparity_grid, input_config + add_disparity_grid, input_config["col_disparity"], input_config["row_disparity"] ), pandora_img_tools.create_dataset_from_inputs(input_config["right"], roi).pipe( - add_right_disparity_grid, input_config + add_disparity_grid, input_config["col_disparity"], input_config["row_disparity"], True ), ) @@ -125,82 +126,120 @@ def check_disparity_presence(input_config): def check_disparity_types(disparity: Any) -> None: """ - Check that disparity is a Sequence of length 2. + Check that disparity a dictionary with keys "init" and range" + where "init" is either: + + - a integer + - a path to a grid with integer values + :param disparity: disparity to check :type disparity: Any :raises SystemExit: if it does not meet requirements """ - if disparity is None or not isinstance(disparity, Dict) or ("init" and "range") not in disparity: - raise ValueError("Disparity should be a dictionnary with keys : init and range", disparity) - if not isinstance(disparity["init"], int): - raise ValueError("Disparity init should be an integer") + + # Check disparity type + if disparity is None or not isinstance(disparity, Dict): + raise ValueError("Disparity should be a dictionary") + + # Check that dictionary keys are correct + if not set(disparity.keys()) == {"init", "range"}: + raise ValueError("Disparity dictionary should contains keys : init and range", disparity) + # Check that init is an integer or a path to a grid + if not isinstance(disparity["init"], (int, str)): + raise ValueError("Disparity init should be an integer or a path to a grid") + + # Check that range value is a postive integer if disparity["range"] < 0 or not isinstance(disparity["range"], int): raise ValueError("Disparity range should be an integer greater or equal to 0") -def add_left_disparity_grid(dataset: xr.Dataset, configuration: Dict) -> xr.Dataset: +def add_disparity_grid(dataset: xr.Dataset, col_disparity: Dict, row_disparity: Dict, right=False): """ - Add left disparity to dataset. + Add disparity to dataset - :param dataset: dataset to add disparity grid to + :param dataset: xarray dataset :type dataset: xr.Dataset - :param configuration: configuration with information about disparity - :type configuration: Dict + :param col_disparity: Disparity interval for columns + :type col_disparity: Dict + :param row_disparity: Disparity interval for rows + :type row_disparity: Dict + :param right: indicates whether the disparity grid is added to the right dataset + :type right: bool + :return: dataset : updated dataset :rtype: xr.Dataset """ - col_disparity = configuration["col_disparity"] - row_disparity = configuration["row_disparity"] - return add_disparity_grid(dataset, col_disparity, row_disparity) + # Creates min and max disparity grids + col_disp_min_max, col_disp_interval = get_min_max_disp_from_dicts(dataset, col_disparity, right) + row_disp_min_max, row_disp_interval = get_min_max_disp_from_dicts(dataset, row_disparity, right) + + # Add disparity grids to dataset + for key, disparity_data, source in zip( + ["col_disparity", "row_disparity"], [col_disp_min_max, row_disp_min_max], [col_disp_interval, row_disp_interval] + ): + dataset[key] = xr.DataArray( + disparity_data, + dims=["band_disp", "row", "col"], + coords={"band_disp": ["min", "max"]}, + ) + + dataset.attrs[f"{key}_source"] = source + return dataset -def add_right_disparity_grid(dataset: xr.Dataset, configuration: Dict) -> xr.Dataset: +def get_min_max_disp_from_dicts(dataset: xr.Dataset, disparity: Dict, right: bool = False) -> Tuple[NDArray, List]: """ - Add right disparity to dataset. + Transforms input disparity dicts with constant init into min/max disparity grids - :param dataset: dataset to add disparity grid to + :param dataset: xarray dataset :type dataset: xr.Dataset - :param configuration: configuration with information about disparity - :type configuration: Dict - :return: dataset : updated dataset - :rtype: xr.Dataset + :param disparity: input disparity + :type disparity: Dict + :param right: indicates whether the disparity grid is added to the right dataset + :type right: bool + :return: 3D numpy array containing min/max disparity grids and list with disparity source + :rtype: Tuple[NDArray, List] """ - col_disparity = configuration["col_disparity"].copy() - row_disparity = configuration["row_disparity"].copy() - col_disparity["init"] = -col_disparity["init"] - row_disparity["init"] = -row_disparity["init"] + # Creates min and max disparity grids if initial disparity is constant (int) + if isinstance(disparity["init"], int): - return add_disparity_grid(dataset, col_disparity, row_disparity) + shape = (dataset.sizes["row"], dataset.sizes["col"]) + disp_interval = [ + disparity["init"] * pow(-1, right) - disparity["range"], + disparity["init"] * pow(-1, right) + disparity["range"], + ] -def add_disparity_grid(dataset: xr.Dataset, col_disparity: Dict, row_disparity: Dict) -> xr.Dataset: - """ - Add disparity to dataset + disp_min_max = np.array([np.full(shape, disparity) for disparity in disp_interval]) - :param dataset: xarray dataset - :type dataset: xr.Dataset - :param col_disparity: Disparity interval for columns - :type col_disparity: Dict - :param row_disparity: Disparity interval for rows - :type row_disparity: Dict + # Creates min and max disparity grids if initial disparities are variable (grid) + elif isinstance(disparity["init"], str): - :return: dataset : updated dataset - :rtype: xr.Dataset - """ - col_disp_min_max = [col_disparity["init"] - col_disparity["range"], col_disparity["init"] + col_disparity["range"]] - row_disp_min_max = [row_disparity["init"] - row_disparity["range"], row_disparity["init"] + row_disparity["range"]] - shape = (dataset.sizes["row"], dataset.sizes["col"]) - for key, disparity_interval in zip(["col_disparity", "row_disparity"], [col_disp_min_max, row_disp_min_max]): - dataset[key] = xr.DataArray( - np.array([np.full(shape, disparity) for disparity in disparity_interval]), - dims=["band_disp", "row", "col"], - coords={"band_disp": ["min", "max"]}, + # Get dataset coordinates to select correct zone of disparity grids if we are using a ROI + rows = dataset.row.data + cols = dataset.col.data + + # Get disparity data + disp_data = pandora_img_tools.rasterio_open(disparity["init"]).read()[ + :, rows[0] : rows[-1] + 1, cols[0] : cols[-1] + 1 + ] + + # Use disparity data to creates min/max grids + disp_min_max = np.squeeze( + np.array( + [ + disp_data * pow(-1, right) - disparity["range"], + disp_data * pow(-1, right) + disparity["range"], + ] + ) ) - dataset.attrs[f"{key}_source"] = disparity_interval - return dataset + + disp_interval = [np.min(disp_min_max[0, ::]), np.max(disp_min_max[1, ::])] + + return disp_min_max, disp_interval def shift_disp_row_img(img_right: xr.Dataset, dec_row: int) -> xr.Dataset: @@ -262,12 +301,26 @@ def get_roi_processing(roi: dict, col_disparity: Dict, row_disparity: Dict) -> d """ new_roi = copy.deepcopy(roi) - new_roi["margins"] = ( - max(abs(col_disparity["init"] - col_disparity["range"]), roi["margins"][0]), - max(abs(row_disparity["init"] - row_disparity["range"]), roi["margins"][1]), - max(abs(col_disparity["init"] + col_disparity["range"]), roi["margins"][2]), - max(abs(row_disparity["init"] + row_disparity["range"]), roi["margins"][3]), - ) + if isinstance(col_disparity["init"], str) and isinstance(row_disparity["init"], str): + + # Read disparity grids + disparity_row_init = pandora_img_tools.rasterio_open(row_disparity["init"]).read() + disparity_col_init = pandora_img_tools.rasterio_open(col_disparity["init"]).read() + + new_roi["margins"] = ( + int(max(abs(np.min(disparity_col_init - col_disparity["range"])), roi["margins"][0])), + int(max(abs(np.min(disparity_row_init - row_disparity["range"])), roi["margins"][1])), + int(max(abs(np.max(disparity_col_init + col_disparity["range"])), roi["margins"][2])), + int(max(abs(np.max(disparity_row_init + row_disparity["range"])), roi["margins"][3])), + ) + + else: + new_roi["margins"] = ( + max(abs(col_disparity["init"] - col_disparity["range"]), roi["margins"][0]), + max(abs(row_disparity["init"] - row_disparity["range"]), roi["margins"][1]), + max(abs(col_disparity["init"] + col_disparity["range"]), roi["margins"][2]), + max(abs(row_disparity["init"] + row_disparity["range"]), roi["margins"][3]), + ) # Update user ROI with new margins. roi["margins"] = new_roi["margins"] diff --git a/pandora2d/matching_cost/matching_cost.py b/pandora2d/matching_cost/matching_cost.py index 744507f..6476bb2 100644 --- a/pandora2d/matching_cost/matching_cost.py +++ b/pandora2d/matching_cost/matching_cost.py @@ -38,7 +38,10 @@ from pandora2d import img_tools import pandora2d.schema as cst_schema -from pandora2d.common import set_out_of_row_disparity_range_to_other_value +from pandora2d.common import ( + set_out_of_row_disparity_range_to_other_value, + set_out_of_col_disparity_range_to_other_value, +) class MatchingCost: @@ -403,6 +406,20 @@ def compute_cost_volumes( # Delete ROI_margins attributes which we used to calculate the row coordinates in the cost_volumes del cost_volumes.attrs["ROI_margins_for_cv"] - set_out_of_row_disparity_range_to_other_value(cost_volumes, grid_min_row, grid_max_row, np.nan, "cost_volumes") + + set_out_of_row_disparity_range_to_other_value( + cost_volumes, + img_left["row_disparity"].sel(band_disp="min").data, + img_left["row_disparity"].sel(band_disp="max").data, + np.nan, + "cost_volumes", + ) + set_out_of_col_disparity_range_to_other_value( + cost_volumes, + img_left["col_disparity"].sel(band_disp="min").data, + img_left["col_disparity"].sel(band_disp="max").data, + np.nan, + "cost_volumes", + ) return cost_volumes diff --git a/tests/conftest.py b/tests/conftest.py index d42e273..6e50edc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,8 +20,6 @@ Module with global test fixtures. """ -import json - # pylint: disable=redefined-outer-name import pathlib import re @@ -35,8 +33,6 @@ import pandora2d -import pandora2d - def pytest_collection_modifyitems(config, items): """ @@ -240,3 +236,97 @@ def run(configuration, output_dir="output"): return tmp_path return run + + +@pytest.fixture() +def constant_initial_disparity(): + """ + Create a correct disparity dictionary + with constant initial disparity + """ + return {"init": 1, "range": 3} + + +@pytest.fixture() +def second_constant_initial_disparity(): + """ + Create a correct disparity dictionary + with constant initial disparity + """ + return {"init": 0, "range": 2} + + +@pytest.fixture() +def make_input_cfg(left_img_path, right_img_path, request): + """Get input configuration with given disparities""" + + input_cfg = { + "left": { + "img": left_img_path, + "nodata": -9999, + }, + "right": {"img": right_img_path, "nodata": -9999}, + "col_disparity": request.getfixturevalue(request.param["col_disparity"]), + "row_disparity": request.getfixturevalue(request.param["row_disparity"]), + } + + return input_cfg + + +@pytest.fixture +def left_img_shape(left_img_path): + """ + Get shape of left image stored in left_img_path fixture + """ + + with rasterio.open(left_img_path) as src: + width = src.width + height = src.height + + return (height, width) + + +@pytest.fixture +def create_disparity_grid_fixture(tmp_path): + """ + Creates initial disparity grid and save it in tmp. + """ + + def create_disparity_grid(data, disp_range, suffix_path, band=False, disp_type=rasterio.dtypes.int64): + + if not band: + disparity_grid = xr.DataArray(data, dims=["row", "col"]) + else: + disparity_grid = xr.DataArray(data, dims=["row", "col", "band"]) + + path = tmp_path / suffix_path + + write_data_array(data_array=disparity_grid, filename=str(path), dtype=disp_type) + + return {"init": str(path), "range": disp_range} + + return create_disparity_grid + + +@pytest.fixture +def correct_grid(left_img_shape, create_disparity_grid_fixture): + """Create a correct initial disparity grid and save it in tmp""" + + height, width = left_img_shape + + # Array of size (height, width) with alternating rows of 2, 0 and 3 + init_band = np.tile([[2], [0], [3]], (height // 3 + 1, width))[:height, :] + + return create_disparity_grid_fixture(init_band, 5, "disparity.tif") + + +@pytest.fixture +def second_correct_grid(left_img_shape, create_disparity_grid_fixture): + """Create a correct initial disparity grid and save it in tmp""" + + height, width = left_img_shape + + # Array of size (height, width) with alternating columns of 5, -21 and -1 + init_band = np.tile([[5, -21, -1]], (height, width // 3 + 1))[:, :width] + + return create_disparity_grid_fixture(init_band, 5, "second_disparity.tif") diff --git a/tests/functional_tests/conftest.py b/tests/functional_tests/conftest.py index fb9dbc0..3ec0992 100644 --- a/tests/functional_tests/conftest.py +++ b/tests/functional_tests/conftest.py @@ -40,3 +40,14 @@ def correct_pipeline_with_optical_flow(): "refinement": {"refinement_method": "optical_flow"}, } } + + +@pytest.fixture() +def correct_pipeline_with_dichotomy(): + return { + "pipeline": { + "matching_cost": {"matching_cost_method": "zncc", "window_size": 5}, + "disparity": {"disparity_method": "wta", "invalid_disparity": -99}, + "refinement": {"refinement_method": "dichotomy", "iterations": 2, "filter": {"method": "bicubic"}}, + } + } diff --git a/tests/functional_tests/test_pipelines.py b/tests/functional_tests/test_pipelines.py index 2ff98b8..b8bf988 100644 --- a/tests/functional_tests/test_pipelines.py +++ b/tests/functional_tests/test_pipelines.py @@ -28,6 +28,9 @@ import pytest +import numpy as np +import rasterio + def remove_extra_keys(extended: dict, reference: dict): """ @@ -233,3 +236,68 @@ def test_configuration_with_mask(run_pipeline, input_cfg, correct_pipeline_witho report = json.load(report_file) assert report["statistics"]["disparity"].keys() == {"row", "col"} + + +@pytest.mark.parametrize( + ["make_input_cfg", "pipeline"], + [ + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "second_correct_grid"}, + "correct_pipeline_without_refinement", + id="Pipeline with disparity grids", + ), + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "second_correct_grid"}, + "correct_pipeline_with_dichotomy", + id="Pipeline with disparity grids and dichotomy", + ), + ], + indirect=["make_input_cfg"], +) +def test_disparity_grids(run_pipeline, make_input_cfg, pipeline, request): + """ + Description: Test pipeline with disparity grids + """ + + configuration = { + "input": make_input_cfg, + "ROI": {"col": {"first": 210, "last": 240}, "row": {"first": 210, "last": 240}}, + **request.getfixturevalue(pipeline), + } + configuration["pipeline"]["disparity"]["invalid_disparity"] = np.nan + + run_dir = run_pipeline(configuration) + + with rasterio.open(run_dir / "output" / "row_disparity.tif") as src: + row_map = src.read(1) + with rasterio.open(run_dir / "output" / "columns_disparity.tif") as src: + col_map = src.read(1) + + non_nan_row_map = ~np.isnan(row_map) + non_nan_col_map = ~np.isnan(col_map) + + # Minimal and maximal disparities corresponding to correct_grid_path fixture + min_max_disp_row = np.array( + [ + np.tile([[-3], [-5], [-2]], (375 // 3 + 1, 450))[210:241, 210:241], + np.tile([[7], [5], [8]], (375 // 3 + 1, 450))[210:241, 210:241], + ] + ) + + # Minimal and maximal disparities corresponding to second_correct_grid_path fixture + min_max_disp_col = np.array( + [ + np.tile([[0, -26, -6]], (375, 450 // 3 + 1))[210:241, 210:241], + np.tile([[10, -16, 4]], (375, 450 // 3 + 1))[210:241, 210:241], + ] + ) + + # Checks that the resulting disparities are well within the ranges created from the input disparity grids + assert np.all( + (row_map[non_nan_row_map] >= min_max_disp_row[0, ::][non_nan_row_map]) + & (row_map[non_nan_row_map] <= min_max_disp_row[1, ::][non_nan_row_map]) + ) + assert np.all( + (col_map[non_nan_col_map] >= min_max_disp_col[0, ::][non_nan_col_map]) + & (col_map[non_nan_col_map] <= min_max_disp_col[1, ::][non_nan_col_map]) + ) diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 6cbde52..18f39d8 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -27,7 +27,7 @@ from skimage.io import imsave from pandora2d import Pandora2DMachine -from pandora2d.img_tools import add_left_disparity_grid +from pandora2d.img_tools import add_disparity_grid @pytest.fixture() @@ -182,9 +182,7 @@ def left_stereo_object(): "transform": Affine(1.0, 0.0, 0.0, 0.0, 1.0, 0.0), } - return left.pipe( - add_left_disparity_grid, {"col_disparity": {"init": 1, "range": 1}, "row_disparity": {"init": -1, "range": 1}} - ) + return left.pipe(add_disparity_grid, {"init": 1, "range": 1}, {"init": -1, "range": 1}) @pytest.fixture() @@ -256,8 +254,74 @@ def stereo_object_with_args(): return ( left_arg.pipe( - add_left_disparity_grid, - {"col_disparity": {"init": 1, "range": 1}, "row_disparity": {"init": -1, "range": 1}}, + add_disparity_grid, + {"init": 1, "range": 1}, + {"init": -1, "range": 1}, ), right_arg, ) + + +@pytest.fixture +def incorrect_disp_dict(): + """Create an incorrect disparity dictionary""" + return {"init": -460, "range": 3} + + +@pytest.fixture +def out_of_image_grid(left_img_shape, create_disparity_grid_fixture): + """ + Create an initial disparity grid with a point that has its disparity interval outside the image + and save it in tmp + """ + + height, width = left_img_shape + + init_band = np.random.randint(-3, 4, size=(height, width)) + init_band[0, 0] = -455 + + return create_disparity_grid_fixture(init_band, 2, "out_of_image_disparity.tif") + + +@pytest.fixture +def negative_exploration_grid(left_img_shape, create_disparity_grid_fixture): + """ + Create an initial disparity grid with a point that has a negative exploration value + and save it in tmp + """ + + height, width = left_img_shape + + init_band = np.random.randint(-3, 4, size=(height, width)) + + return create_disparity_grid_fixture(init_band, -2, "negative_exploration_disparity.tif") + + +@pytest.fixture +def two_bands_grid(left_img_shape, create_disparity_grid_fixture): + """ + Create an initial disparity grid with two bands and save it in tmp + """ + + height, width = left_img_shape + + first_band = np.random.randint(-3, 4, size=(height, width)) + second_band = np.random.randint(0, 6, size=(height, width)) + + data = np.stack([first_band, second_band], axis=-1) + + return create_disparity_grid_fixture(data, 2, "two_bands_disparity.tif", True) + + +@pytest.fixture +def wrong_size_grid(left_img_shape, create_disparity_grid_fixture): + """ + Create an initial disparity grid of wrong size + and save it in tmp + """ + + height, width = left_img_shape + + init_band = np.random.randint(-3, 4, size=(height - 2, width + 4)) + + return create_disparity_grid_fixture(init_band, 3, "wrong_size_disparity.tif") diff --git a/tests/unit_tests/test_check_configuration.py b/tests/unit_tests/test_check_configuration.py index e2c5b6f..f634977 100644 --- a/tests/unit_tests/test_check_configuration.py +++ b/tests/unit_tests/test_check_configuration.py @@ -33,7 +33,7 @@ from json_checker import DictCheckerError, MissKeyCheckerError from skimage.io import imsave - +from pandora.img_tools import get_metadata from pandora2d.img_tools import create_datasets_from_inputs, add_disparity_grid from pandora2d import check_configuration @@ -582,6 +582,93 @@ def test_disparity_partially_out(self, pandora2d_machine, configuration): check_configuration.check_conf(configuration, pandora2d_machine) +class TestCheckDisparity: + """ + Test check_disparity method + """ + + @pytest.mark.parametrize( + ["make_input_cfg"], + [ + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "second_correct_grid"}, + id="Correct disparity with variable initial value", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "second_constant_initial_disparity"}, + id="Correct disparity with constant initial value", + ), + ], + indirect=["make_input_cfg"], + ) + def test_passes_check_disparity(self, left_img_path, make_input_cfg): + """ + Test check_disparity method with correct input disparities + """ + + image_metadata = get_metadata(left_img_path) + + check_configuration.check_disparity(image_metadata, make_input_cfg) + + @pytest.mark.parametrize( + ["make_input_cfg", "error_type", "error_message"], + [ + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "left_img_shape"}, + AttributeError, + "The disparities in rows and columns must be given as 2 dictionaries", + id="Col disparity is not a dictionary", + ), + pytest.param( + {"row_disparity": "left_img_shape", "col_disparity": "correct_grid"}, + AttributeError, + "The disparities in rows and columns must be given as 2 dictionaries", + id="Row disparity is not a dictionary", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "correct_grid"}, + ValueError, + "Initial columns and row disparity values must be two strings or two integers", + id="Initial value is different for columns and rows disparity", + ), + pytest.param( + {"row_disparity": "out_of_image_grid", "col_disparity": "second_correct_grid"}, + ValueError, + "Row disparity range out of image", + id="Row disparity grid out of image for one point", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "incorrect_disp_dict"}, + ValueError, + "Column disparity range out of image", + id="Column disparity dict out of image for one point", + ), + pytest.param( + {"row_disparity": "two_bands_grid", "col_disparity": "correct_grid"}, + AttributeError, + "Initial disparity grid must be a 1-channel grid", + id="Row disparity grid has two band", + ), + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "wrong_size_grid"}, + AttributeError, + "Initial disparity grids and image must have the same size", + id="Column disparity grid size is different from image size", + ), + ], + indirect=["make_input_cfg"], + ) + def test_fails_check_disparity(self, left_img_path, make_input_cfg, error_type, error_message): + """ + Test check_disparity method with incorrect input disparities + """ + + image_metadata = get_metadata(left_img_path) + + with pytest.raises(error_type, match=error_message): + check_configuration.check_disparity(image_metadata, make_input_cfg) + + @pytest.mark.parametrize( "extra_section_name", [ diff --git a/tests/unit_tests/test_common.py b/tests/unit_tests/test_common.py index d9849f9..4b2ced8 100644 --- a/tests/unit_tests/test_common.py +++ b/tests/unit_tests/test_common.py @@ -406,6 +406,8 @@ def dataset(self, range_row, range_col, disp_range_col, disp_range_row, data_var "disp_row": disp_range_row, }, ) + + xarray.attrs = {"col_disparity_source": [2, 8], "row_disparity_source": [-5, 0]} min_disp_grid = np.full((xarray.sizes["row"], xarray.sizes["col"]), xarray.coords[disp_coords].data[0]) max_disp_grid = np.full((xarray.sizes["row"], xarray.sizes["col"]), xarray.coords[disp_coords].data[-1]) return xarray, min_disp_grid, max_disp_grid diff --git a/tests/unit_tests/test_disparity.py b/tests/unit_tests/test_disparity.py index 6f6afd2..33b9b10 100644 --- a/tests/unit_tests/test_disparity.py +++ b/tests/unit_tests/test_disparity.py @@ -34,7 +34,7 @@ from pandora.margins import Margins from pandora2d import matching_cost, disparity -from pandora2d.img_tools import add_left_disparity_grid +from pandora2d.img_tools import add_disparity_grid class TestCheckConf: @@ -207,17 +207,23 @@ def disparity_matcher(): @pytest.fixture() def img_left(default_attributs, data_left, disparity_cfg): + """ + Creates left image fixture + """ left = xr.Dataset( {"im": (["row", "col"], data_left)}, coords={"row": np.arange(data_left.shape[0]), "col": np.arange(data_left.shape[1])}, ) left.attrs = default_attributs - left.pipe(add_left_disparity_grid, disparity_cfg) + left.pipe(add_disparity_grid, disparity_cfg["col_disparity"], disparity_cfg["row_disparity"]) return left @pytest.fixture() def img_right(default_attributs, data_right): + """ + Creates right image fixture + """ right = xr.Dataset( {"im": (["row", "col"], data_right)}, coords={"row": np.arange(data_right.shape[0]), "col": np.arange(data_right.shape[1])}, diff --git a/tests/unit_tests/test_img_tools/test_create_datasets_from_input.py b/tests/unit_tests/test_img_tools/test_create_datasets_from_input.py index d0eb4cc..6e758ed 100644 --- a/tests/unit_tests/test_img_tools/test_create_datasets_from_input.py +++ b/tests/unit_tests/test_img_tools/test_create_datasets_from_input.py @@ -33,51 +33,58 @@ from pandora2d import img_tools -def _make_input_section(left_img_path, right_img_path): - """This is not a fixture because we want to use it with different scopes.""" - return { - "left": { - "img": left_img_path, - "nodata": -9999, - }, - "right": { - "img": right_img_path, - "nodata": -9999, - }, - "col_disparity": {"init": 0, "range": 2}, - "row_disparity": {"init": 1, "range": 3}, - } - - -@pytest.fixture() -def input_section(left_img_path, right_img_path): - return _make_input_section(left_img_path, right_img_path) - - class TestReturnedValue: """Test expected properties of returned value of create_datasets_from_inputs.""" @pytest.fixture() - def result(self, left_img_path, right_img_path): - return img_tools.create_datasets_from_inputs(_make_input_section(left_img_path, right_img_path)) + def result(self, make_input_cfg): + return img_tools.create_datasets_from_inputs(make_input_cfg) - def test_use_function_from_pandora(self, mocker, input_section): + @pytest.mark.parametrize( + ["make_input_cfg"], + [ + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "correct_grid"}, + id="Correct disparity grids", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "second_constant_initial_disparity"}, + id="Correct disparity dictionaries", + ), + ], + indirect=["make_input_cfg"], + ) + def test_use_function_from_pandora(self, mocker, make_input_cfg): """Test we use `create_dataset_from_inputs` from pandora. We assume this function is well tested in Pandora and that we just need to test that we use it. """ pandora_function = mocker.patch.object(img_tools.pandora_img_tools, "create_dataset_from_inputs") - img_tools.create_datasets_from_inputs(input_section) + img_tools.create_datasets_from_inputs(make_input_cfg) pandora_function.assert_has_calls( [ - mocker.call(input_section["left"], None), - mocker.call(input_section["right"], None), + mocker.call(make_input_cfg["left"], None), + mocker.call(make_input_cfg["right"], None), ], any_order=True, ) + @pytest.mark.parametrize( + ["make_input_cfg"], + [ + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "correct_grid"}, + id="Correct disparity grids", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "second_constant_initial_disparity"}, + id="Correct disparity dictionaries", + ), + ], + indirect=["make_input_cfg"], + ) def test_returns_left_and_right_datasets(self, result, left_img_path, right_img_path): """Test left and right datasets are returned as namedtuple.""" assert len(result) == 2 @@ -91,33 +98,318 @@ def test_returns_left_and_right_datasets(self, result, left_img_path, right_img_ pandora.img_tools.rasterio_open(right_img_path).read(1, out_dtype=np.float32), ) + @pytest.mark.parametrize( + ["make_input_cfg"], + [ + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "correct_grid"}, + id="Correct disparity grids", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "second_constant_initial_disparity"}, + id="Correct disparity dictionaries", + ), + ], + indirect=["make_input_cfg"], + ) def test_disp_band_coordinates(self, result): """Test disp_band coordinates is present.""" np.testing.assert_equal(result.left.coords["band_disp"].data, ["min", "max"]) - def test_disparity_source(self, result): + @pytest.mark.parametrize( + [ + "make_input_cfg", + "left_col_disparity_source", + "left_row_disparity_source", + "right_col_disparity_source", + "right_row_disparity_source", + ], + [ + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "second_correct_grid"}, + [-26, 10], + [-5, 8], + [-10, 26], + [-8, 5], + id="Correct disparity grids", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "second_constant_initial_disparity"}, + [-2, 2], + [-2, 4], + [-2, 2], + [-4, 2], + id="Correct disparity dictionaries", + ), + ], + indirect=["make_input_cfg"], + ) + def test_disparity_source( + self, + result, + left_col_disparity_source, + left_row_disparity_source, + right_col_disparity_source, + right_row_disparity_source, + ): """Test.""" - assert result.left.attrs["col_disparity_source"] == [-2, 2] - assert result.left.attrs["row_disparity_source"] == [-2, 4] - assert result.right.attrs["col_disparity_source"] == [-2, 2] - assert result.right.attrs["row_disparity_source"] == [-4, 2] + assert result.left.attrs["col_disparity_source"] == left_col_disparity_source + assert result.left.attrs["row_disparity_source"] == left_row_disparity_source + assert result.right.attrs["col_disparity_source"] == right_col_disparity_source + assert result.right.attrs["row_disparity_source"] == right_row_disparity_source - def test_resulting_disparity_grids(self, result): + @pytest.mark.parametrize( + [ + "make_input_cfg", + "expected_left_col_disparity", + "expected_left_row_disparity", + "expected_right_col_disparity", + "expected_right_row_disparity", + ], + [ + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "second_correct_grid"}, + # Array of size 2x375x450 with alternating cols of 0, -26 and -6 + # for array[0,::] and alternating cols of 10, -16 and 4 for array[1,::] + np.array( + [ + np.tile([[0, -26, -6]], (375, 450 // 3 + 1))[:, :450], + np.tile([[10, -16, 4]], (375, 450 // 3 + 1))[:, :450], + ] + ), + # Array of size 2x375x450 with alternating rows of -3, -5 and -2 + # for array[0,::] and alternating rows of 7, 5 and 8 for array[1,::] + np.array( + [ + np.tile([[-3], [-5], [-2]], (375 // 3 + 1, 450))[:375, :], + np.tile([[7], [5], [8]], (375 // 3 + 1, 450))[:375, :], + ] + ), + # Array of size 2x375x450 with alternating cols of -10, 16 and -4 + # for array[0,::] and alternating cols of 0, 26 and 6 for array[1,::] + np.array( + [ + np.tile([[-10, 16, -4]], (375, 450 // 3 + 1))[:, :450], + np.tile([[0, 26, 6]], (375, 450 // 3 + 1))[:, :450], + ] + ), + # Array of size 2x375x450 with alternating rows of -7, -5 and -8 + # for array[0,::] and alternating rows of 3, 5 and 2 for array[1,::] + np.array( + [ + np.tile([[-7], [-5], [-8]], (375 // 3 + 1, 450))[:375, :], + np.tile([[3], [5], [2]], (375 // 3 + 1, 450))[:375, :], + ] + ), + id="Correct disparity grids", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "second_constant_initial_disparity"}, + np.array([np.full((375, 450), -2), np.full((375, 450), 2)]), + np.array([np.full((375, 450), -2), np.full((375, 450), 4)]), + np.array([np.full((375, 450), -2), np.full((375, 450), 2)]), + np.array([np.full((375, 450), -4), np.full((375, 450), 2)]), + id="Correct disparity dictionaries", + ), + ], + indirect=["make_input_cfg"], + ) + def test_resulting_disparity_grids( + self, + result, + expected_left_col_disparity, + expected_left_row_disparity, + expected_right_col_disparity, + expected_right_row_disparity, + ): """ - Test the method create_dataset_from_inputs with the disparity + Test the method create_dataset_from_inputs with dictionary and grid disparity """ - expected_left_col_disparity = np.array([np.full((375, 450), -2), np.full((375, 450), 2)]) - expected_left_row_disparity = np.array([np.full((375, 450), -2), np.full((375, 450), 4)]) - expected_right_col_disparity = np.array([np.full((375, 450), -2), np.full((375, 450), 2)]) - expected_right_row_disparity = np.array([np.full((375, 450), -4), np.full((375, 450), 2)]) np.testing.assert_array_equal(result.left["col_disparity"], expected_left_col_disparity) np.testing.assert_array_equal(result.left["row_disparity"], expected_left_row_disparity) np.testing.assert_array_equal(result.right["col_disparity"], expected_right_col_disparity) np.testing.assert_array_equal(result.right["row_disparity"], expected_right_row_disparity) + @pytest.mark.parametrize( + [ + "make_input_cfg", + "expected_left_col_disparity", + "expected_left_row_disparity", + "expected_right_col_disparity", + "expected_right_row_disparity", + "roi", + ], + [ + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "second_correct_grid"}, + # Array of size 2x96x97 with alternating cols of 0, -26 and -6 + # for array[0,::] and alternating cols of 10, -16 and 4 for array[1,::] + np.array( + [ + np.tile([[0, -26, -6]], (375, 450 // 3 + 1))[7:103, 8:105], + np.tile([[10, -16, 4]], (375, 450 // 3 + 1))[7:103, 8:105], + ] + ), + # Array of size 2x96x97 with alternating rows of -3, -5 and -2 + # for array[0,::] and alternating rows of 7, 5 and 8 for array[1,::] + np.array( + [ + np.tile([[-3], [-5], [-2]], (375 // 3 + 1, 450))[7:103, 8:105], + np.tile([[7], [5], [8]], (375 // 3 + 1, 450))[7:103, 8:105], + ] + ), + # Array of size 2x96x97 with alternating cols of -10, 16 and -4 + # for array[0,::] and alternating cols of 0, 26 and 6 for array[1,::] + np.array( + [ + np.tile([[-10, 16, -4]], (375, 450 // 3 + 1))[7:103, 8:105], + np.tile([[0, 26, 6]], (375, 450 // 3 + 1))[7:103, 8:105], + ] + ), + # Array of size 2x96x97 with alternating rows of -7, -5 and -8 + # for array[0,::] and alternating rows of 3, 5 and 2 for array[1,::] + np.array( + [ + np.tile([[-7], [-5], [-8]], (375 // 3 + 1, 450))[7:103, 8:105], + np.tile([[3], [5], [2]], (375 // 3 + 1, 450))[7:103, 8:105], + ] + ), + # ROI + {"col": {"first": 10, "last": 100}, "row": {"first": 10, "last": 100}, "margins": (2, 3, 4, 2)}, + id="Disparity grids with centered ROI", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "second_constant_initial_disparity"}, + np.array([np.full((96, 97), -2), np.full((96, 97), 2)]), + np.array([np.full((96, 97), -2), np.full((96, 97), 4)]), + np.array([np.full((96, 97), -2), np.full((96, 97), 2)]), + np.array([np.full((96, 97), -4), np.full((96, 97), 2)]), + {"col": {"first": 10, "last": 100}, "row": {"first": 10, "last": 100}, "margins": (2, 3, 4, 2)}, + id="Disparity dictionaries with centered ROI", + ), + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "second_correct_grid"}, + # Array of size 2x96x102 with alternating cols of 0, -26 and -6 + # for array[0,::] and alternating cols of 10, -16 and 4 for array[1,::] + np.array( + [ + np.tile([[0, -26, -6]], (375, 450 // 3 + 1))[7:103, 348:450], + np.tile([[10, -16, 4]], (375, 450 // 3 + 1))[7:103, 348:450], + ] + ), + # Array of size 2x96x102 with alternating rows of -3, -5 and -2 + # for array[0,::] and alternating rows of 7, 5 and 8 for array[1,::] + np.array( + [ + np.tile([[-3], [-5], [-2]], (375 // 3 + 1, 450))[7:103, 348:450], + np.tile([[7], [5], [8]], (375 // 3 + 1, 450))[7:103, 348:450], + ] + ), + # Array of size 2x96x102 with alternating cols of -10, 16 and -4 + # for array[0,::] and alternating cols of 0, 26 and 6 for array[1,::] + np.array( + [ + np.tile([[-10, 16, -4]], (375, 450 // 3 + 1))[7:103, 348:450], + np.tile([[0, 26, 6]], (375, 450 // 3 + 1))[7:103, 348:450], + ] + ), + # Array of size 2x96x102 with alternating rows of -7, -5 and -8 + # for array[0,::] and alternating rows of 3, 5 and 2 for array[1,::] + np.array( + [ + np.tile([[-7], [-5], [-8]], (375 // 3 + 1, 450))[7:103, 348:450], + np.tile([[3], [5], [2]], (375 // 3 + 1, 450))[7:103, 348:450], + ] + ), + # ROI + {"col": {"first": 350, "last": 460}, "row": {"first": 10, "last": 100}, "margins": (2, 3, 4, 2)}, + id="Disparity grids with right overlapping ROI", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "second_constant_initial_disparity"}, + np.array([np.full((96, 102), -2), np.full((96, 102), 2)]), + np.array([np.full((96, 102), -2), np.full((96, 102), 4)]), + np.array([np.full((96, 102), -2), np.full((96, 102), 2)]), + np.array([np.full((96, 102), -4), np.full((96, 102), 2)]), + {"col": {"first": 350, "last": 460}, "row": {"first": 10, "last": 100}, "margins": (2, 3, 4, 2)}, + id="Disparity dictionaries with right overlapping ROI", + ), + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "second_correct_grid"}, + # Array of size 2x103x97 with alternating cols of 0, -26 and -6 + # for array[0,::] and alternating cols of 10, -16 and 4 for array[1,::] + np.array( + [ + np.tile([[0, -26, -6]], (375, 450 // 3 + 1))[0:103, 8:105], + np.tile([[10, -16, 4]], (375, 450 // 3 + 1))[0:103, 8:105], + ] + ), + # Array of size 2x103x97 with alternating rows of -3, -5 and -2 + # for array[0,::] and alternating rows of 7, 5 and 8 for array[1,::] + np.array( + [ + np.tile([[-3], [-5], [-2]], (375 // 3 + 1, 450))[0:103, 8:105], + np.tile([[7], [5], [8]], (375 // 3 + 1, 450))[0:103, 8:105], + ] + ), + # Array of size 2x103x97 with alternating cols of -10, 16 and -4 + # for array[0,::] and alternating cols of 0, 26 and 6 for array[1,::] + np.array( + [ + np.tile([[-10, 16, -4]], (375, 450 // 3 + 1))[0:103, 8:105], + np.tile([[0, 26, 6]], (375, 450 // 3 + 1))[0:103, 8:105], + ] + ), + # Array of size 2x103x97 with alternating rows of -7, -5 and -8 + # for array[0,::] and alternating rows of 3, 5 and 2 for array[1,::] + np.array( + [ + np.tile([[-7], [-5], [-8]], (375 // 3 + 1, 450))[0:103, 8:105], + np.tile([[3], [5], [2]], (375 // 3 + 1, 450))[0:103, 8:105], + ] + ), + # ROI + {"col": {"first": 10, "last": 100}, "row": {"first": 0, "last": 100}, "margins": (2, 3, 4, 2)}, + id="Disparity grids with top overlapping ROI", + ), + pytest.param( + {"row_disparity": "constant_initial_disparity", "col_disparity": "second_constant_initial_disparity"}, + np.array([np.full((103, 97), -2), np.full((103, 97), 2)]), + np.array([np.full((103, 97), -2), np.full((103, 97), 4)]), + np.array([np.full((103, 97), -2), np.full((103, 97), 2)]), + np.array([np.full((103, 97), -4), np.full((103, 97), 2)]), + {"col": {"first": 10, "last": 100}, "row": {"first": 0, "last": 100}, "margins": (2, 3, 4, 2)}, + id="Disparity dictionaries with top overlapping ROI", + ), + ], + indirect=["make_input_cfg"], + ) + def test_resulting_disparity_grids_with_roi( + self, + make_input_cfg, + expected_left_col_disparity, + expected_left_row_disparity, + expected_right_col_disparity, + expected_right_row_disparity, + roi, + ): + """ + Test the method create_dataset_from_inputs with dictionary and grid disparity with ROI + + """ + + make_input_cfg["ROI"] = roi + + datasets = img_tools.create_datasets_from_inputs(make_input_cfg, roi=roi) + + np.testing.assert_array_equal(datasets.left["col_disparity"], expected_left_col_disparity) + np.testing.assert_array_equal(datasets.left["row_disparity"], expected_left_row_disparity) + np.testing.assert_array_equal(datasets.right["col_disparity"], expected_right_col_disparity) + np.testing.assert_array_equal(datasets.right["row_disparity"], expected_right_row_disparity) + class TestDisparityChecking: """Test checks done on disparities.""" @@ -132,7 +424,7 @@ class TestDisparityChecking: ), ], ) - def test_fails_when_disparity_is_missing(self, input_section, missing, message): + def test_fails_when_disparity_is_missing(self, correct_input_cfg, missing, message): """ Description : Test when disparity is not provided. Data : @@ -141,30 +433,47 @@ def test_fails_when_disparity_is_missing(self, input_section, missing, message): Requirement : EX_CONF_08 """ for key in missing: - del input_section[key] + del correct_input_cfg["input"][key] with pytest.raises(KeyError) as exc_info: - img_tools.create_datasets_from_inputs(input_section) + img_tools.create_datasets_from_inputs(correct_input_cfg["input"]) assert exc_info.value.args[0] == message - @pytest.mark.parametrize("disparity", [None, 1, 3.14, "grid_path", [-2, 2]]) + @pytest.mark.parametrize("disparity", [None, 1, 3.14, [-2, 2]]) + @pytest.mark.parametrize("disparity_key", ["col_disparity", "row_disparity"]) + def test_fails_when_disparities_have_wrong_type(self, correct_input_cfg, disparity_key, disparity): + """ + Description : Test if disparities are not dictionaries or grid in the input section + Data : + - Left image : cones/monoband/left.png + - Right image : cones/monoband/right.png + Requirement : EX_CONF_08 + """ + correct_input_cfg["input"][disparity_key] = disparity + + with pytest.raises(ValueError) as exc_info: + img_tools.create_datasets_from_inputs(correct_input_cfg["input"]) + assert exc_info.value.args[0] == "Disparity should be a dictionary" + + @pytest.mark.parametrize("disparity", [{"wrong_init": 2, "range": 2}]) @pytest.mark.parametrize("disparity_key", ["col_disparity", "row_disparity"]) - def test_fails_when_disparities_are_not_dictionaries(self, input_section, disparity_key, disparity): + def test_fails_when_dict_has_wrong_keys(self, correct_input_cfg, disparity_key, disparity): """ - Description : Test if disparities are dictionaries in the input section + Description : Test dict with wrong keys Data : - Left image : cones/monoband/left.png - Right image : cones/monoband/right.png Requirement : EX_CONF_08 """ - input_section[disparity_key] = disparity + correct_input_cfg["input"][disparity_key] = disparity with pytest.raises(ValueError) as exc_info: - img_tools.create_datasets_from_inputs(input_section) - assert exc_info.value.args[0] == "Disparity should be a dictionnary with keys : init and range" + img_tools.create_datasets_from_inputs(correct_input_cfg["input"]) + + assert exc_info.value.args[0] == "Disparity dictionary should contains keys : init and range" @pytest.mark.parametrize("disparity", [{"init": 2.0, "range": 2}]) @pytest.mark.parametrize("disparity_key", ["col_disparity", "row_disparity"]) - def test_fails_when_init_is_a_float(self, input_section, disparity_key, disparity): + def test_fails_when_init_is_a_float(self, correct_input_cfg, disparity_key, disparity): """ Description : Test if init is a float Data : @@ -172,16 +481,16 @@ def test_fails_when_init_is_a_float(self, input_section, disparity_key, disparit - Right image : cones/monoband/right.png Requirement : EX_CONF_08 """ - input_section[disparity_key] = disparity + correct_input_cfg["input"][disparity_key] = disparity with pytest.raises(ValueError) as exc_info: - img_tools.create_datasets_from_inputs(input_section) + img_tools.create_datasets_from_inputs(correct_input_cfg["input"]) - assert exc_info.value.args[0] == "Disparity init should be an integer" + assert exc_info.value.args[0] == "Disparity init should be an integer or a path to a grid" @pytest.mark.parametrize("disparity", [{"init": 2, "range": -2}]) @pytest.mark.parametrize("disparity_key", ["col_disparity", "row_disparity"]) - def test_fails_when_range_is_lt_0(self, input_section, disparity_key, disparity): + def test_fails_when_range_is_lt_0(self, correct_input_cfg, disparity_key, disparity): """ Description : Test if range is lower than 0 Data : @@ -189,16 +498,16 @@ def test_fails_when_range_is_lt_0(self, input_section, disparity_key, disparity) - Right image : cones/monoband/right.png Requirement : EX_CONF_08 """ - input_section[disparity_key] = disparity + correct_input_cfg["input"][disparity_key] = disparity with pytest.raises(ValueError) as exc_info: - img_tools.create_datasets_from_inputs(input_section) + img_tools.create_datasets_from_inputs(correct_input_cfg["input"]) assert exc_info.value.args[0] == "Disparity range should be an integer greater or equal to 0" @pytest.mark.parametrize("disparity", [None, np.nan, np.inf, float("nan"), float("inf")]) @pytest.mark.parametrize("disparity_key", ["col_disparity", "row_disparity"]) - def test_fails_with_bad_disparity_values(self, input_section, disparity_key, disparity): + def test_fails_with_bad_disparity_values(self, correct_input_cfg, disparity_key, disparity): """ Description : Test if the disparity is a dictionary Data : @@ -206,23 +515,50 @@ def test_fails_with_bad_disparity_values(self, input_section, disparity_key, dis - Right image : cones/monoband/right.png Requirement : EX_CONF_08 """ - input_section[disparity_key] = disparity + correct_input_cfg["input"][disparity_key] = disparity with pytest.raises(ValueError) as exc_info: - img_tools.create_datasets_from_inputs(input_section) - assert exc_info.value.args[0] == "Disparity should be a dictionnary with keys : init and range" + img_tools.create_datasets_from_inputs(correct_input_cfg["input"]) + assert exc_info.value.args[0] == "Disparity should be a dictionary" + + @pytest.mark.parametrize( + ["make_input_cfg"], + [ + pytest.param( + {"row_disparity": "negative_exploration_grid", "col_disparity": "correct_grid"}, + id="Negative exploration grid for row disparity", + ), + pytest.param( + {"row_disparity": "correct_grid", "col_disparity": "negative_exploration_grid"}, + id="Negative exploration grid for col disparity", + ), + ], + indirect=["make_input_cfg"], + ) + def test_fails_when_range_band_is_lt_0(self, make_input_cfg): + """ + Description : Test if range band contains values lower than 0 + Data : + - Left image : cones/monoband/left.png + - Right image : cones/monoband/right.png + """ + + with pytest.raises(ValueError) as exc_info: + img_tools.create_datasets_from_inputs(make_input_cfg) + + assert exc_info.value.args[0] == "Disparity range should be an integer greater or equal to 0" - def test_create_dataset_from_inputs_with_estimation_step(self, input_section): + def test_create_dataset_from_inputs_with_estimation_step(self, correct_input_cfg): """ test dataset_from_inputs with an estimation step and no disparity range """ - configuration_with_estimation = {"input": input_section} + configuration_with_estimation = correct_input_cfg del configuration_with_estimation["input"]["row_disparity"] del configuration_with_estimation["input"]["col_disparity"] configuration_with_estimation["pipeline"] = {"estimation": {"estimation_method": "phase_cross_correlation"}} result = img_tools.create_datasets_from_inputs( - input_section, estimation_cfg=configuration_with_estimation["pipeline"].get("estimation") + correct_input_cfg["input"], estimation_cfg=configuration_with_estimation["pipeline"].get("estimation") ) assert result.left.attrs["col_disparity_source"] == [-9999, -9999] diff --git a/tests/unit_tests/test_img_tools/test_get_roi_processing.py b/tests/unit_tests/test_img_tools/test_get_roi_processing.py index 4dc0bd1..c3e2d95 100644 --- a/tests/unit_tests/test_img_tools/test_get_roi_processing.py +++ b/tests/unit_tests/test_img_tools/test_get_roi_processing.py @@ -25,6 +25,7 @@ # pylint: disable=redefined-outer-name import pytest +import numpy as np from pandora2d import img_tools @@ -78,3 +79,67 @@ def test_roi_with_negative_and_positive_disparities(default_roi, col_disparity, assert test_roi_column["margins"] == expected assert test_roi_column == default_roi + + +@pytest.fixture +def positive_grid(left_img_shape, create_disparity_grid_fixture): + """Create a positive disparity grid and save it in tmp""" + + height, width = left_img_shape + + # Array of size (height, width) with alternating rows of 6 and 8 + init_band = np.tile([[6], [8]], (height // 2 + 1, width))[:height, :] + + return create_disparity_grid_fixture(init_band, 2, "postive_disparity.tif") + + +@pytest.fixture +def negative_grid(left_img_shape, create_disparity_grid_fixture): + """Create a negative disparity grid and save it in tmp""" + + height, width = left_img_shape + + # Array of size (height, width) with alternating rows of -5 and -7 + init_band = np.tile([[-5], [-7]], (height // 2 + 1, width))[:height, :] + + return create_disparity_grid_fixture(init_band, 2, "negative_disparity.tif") + + +@pytest.fixture +def lower_than_margins_grid(left_img_shape, create_disparity_grid_fixture): + """ + Create a disparity grid with disparity lower than default_roi margins + and save it in tmp + """ + + height, width = left_img_shape + + init_band = np.full((height, width), 0) + + return create_disparity_grid_fixture(init_band, 1, "lower_than_margins_disparity.tif") + + +@pytest.mark.parametrize( + ["col_disparity", "row_disparity", "expected"], + [ + pytest.param("second_correct_grid", "correct_grid", (26, 5, 10, 8), id="Negative and positive disparities"), + pytest.param("negative_grid", "positive_grid", (9, 4, 3, 10), id="Negative disparities for columns"), + pytest.param("positive_grid", "negative_grid", (4, 9, 10, 3), id="Negative disparities for rows"), + pytest.param( + "lower_than_margins_grid", + "lower_than_margins_grid", + (2, 2, 2, 2), + id="Margins greater than disparities", + ), + ], +) +def test_roi_with_negative_and_positive_disparities_grids(default_roi, col_disparity, row_disparity, expected, request): + """ + Test the get_roi_processing method with grid disparities + """ + test_roi_column = img_tools.get_roi_processing( + default_roi, request.getfixturevalue(col_disparity), request.getfixturevalue(row_disparity) + ) + + assert test_roi_column["margins"] == expected + assert test_roi_column == default_roi diff --git a/tests/unit_tests/test_refinement/test_optical_flow.py b/tests/unit_tests/test_refinement/test_optical_flow.py index 52de9f1..3d4a70a 100644 --- a/tests/unit_tests/test_refinement/test_optical_flow.py +++ b/tests/unit_tests/test_refinement/test_optical_flow.py @@ -656,8 +656,8 @@ def image( "valid_pixels": 0, "no_data_mask": 1, "crs": None, - "col_disparity_source": "grid_col", - "row_disparity_source": "grid_row", + "col_disparity_source": [np.min(col_min_disparity), np.max(col_max_disparity)], + "row_disparity_source": [np.min(row_min_disparity), np.max(col_max_disparity)], }, )