diff --git a/docs/source/userguide/step_by_step/refinement.rst b/docs/source/userguide/step_by_step/refinement.rst index 618e732..e388c7c 100644 --- a/docs/source/userguide/step_by_step/refinement.rst +++ b/docs/source/userguide/step_by_step/refinement.rst @@ -7,21 +7,57 @@ Theoretical basics ------------------ The purpose of this step is to refine the disparity identified in the previous step. -The available refinement method is : +The available refinement methods are: + * **Interpolation**: -* **Interpolation** : It consists on 3 differents steps: + It consists on 3 different steps: + * First, the cost_volumes is reshaped to obtain the 2D (disp_row, disp_col) costs map for each pixel, so we will obtain (row * col) 2D cost maps. + * The cost map of each pixel is interpolated using scipy to obtain a continuous function. + * Then, the interpolated functions are minimized using scipy to obtain the refined disparities. - * First, the cost_volumes is reshaped to obtain the 2D (disp_row, disp_col) costs map for each pixel, so we will obtain (row * col) 2D cost maps. - * The cost map of each pixel is interpolated using scipy to obtain a continuous function. - * Then, the interpolated functions are minimized using scipy to obtain the refined disparities. + * **Dichotomy**: + + It’s an iterative process that will, at each iteration: + * compute the half way positions between each best candidate in the cost volume and its nearest neighbours. + * compute the similarity coefficients at those positions using the given filter method. + * find the new best candidate from those computed coefficients. Configuration and parameters ---------------------------- -+---------------------+-------------------+--------+---------------+---------------------+----------+ -| Name | Description | Type | Default value | Available value | Required | -+=====================+===================+========+===============+=====================+==========+ -| *refinement_method* | Refinement method | string | |"interpolation" | No | -+---------------------+-------------------+--------+---------------+---------------------+----------+ + +.. list-table:: Configuration and parameters + :header-rows: 1 + + * - Name + - Description + - Type + - Default value + - Available value + - Required + * - *refinemement_method* + - Refinement method + - string + - + - | "interpolation", + | "dichotomy", + - Yes + * - *iterations* + - Number of iterations + - integer + - + - | 1 to 9 + | *if above, will be bound to 9* + | **Only available if "dichotomy" method** + - Yes + * - *filter* + - Name of the filter to use + - str + - + - | "sinc", + | "bicubic", + | "spline", + | **Only available if "dichotomy" method** + - Yes **Example** diff --git a/notebooks/introduction_and_basic_usage.ipynb b/notebooks/introduction_and_basic_usage.ipynb index 3e08312..2348545 100644 --- a/notebooks/introduction_and_basic_usage.ipynb +++ b/notebooks/introduction_and_basic_usage.ipynb @@ -131,11 +131,11 @@ "source": [ "### Available implementations for each step\n", "\n", - "| Step | Algorithms implemented |\n", - "|:--------------------------|:-----------------------|\n", - "| Matching cost computation | SAD / SSD / ZNNC |\n", - "| Disparity computation | Winner-Takes-All |\n", - "| Subpixel disparity refinement | Interpolation |" + "| Step | Algorithms implemented |\n", + "|:------------------------------|:--------------------------|\n", + "| Matching cost computation | SAD / SSD / ZNNC |\n", + "| Disparity computation | Winner-Takes-All |\n", + "| Subpixel disparity refinement | Interpolation / Dichotomy |" ] }, { diff --git a/notebooks/usage_dichotomy.ipynb b/notebooks/usage_dichotomy.ipynb new file mode 100644 index 0000000..140a094 --- /dev/null +++ b/notebooks/usage_dichotomy.ipynb @@ -0,0 +1,317 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0df35f6b-af85-4f3f-85e0-1a98db89addd", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "markdown", + "id": "497ca01c-8e3a-4afb-b241-1e13187985db", + "metadata": {}, + "source": [ + "# Pandora2D : a coregistration framework" + ] + }, + { + "cell_type": "markdown", + "id": "6a9c8638-a464-48ba-b897-3f1ae9ac71d9", + "metadata": {}, + "source": [ + "# Usage of step and ROI (Region of Interest) parameters in the configuration file" + ] + }, + { + "cell_type": "markdown", + "id": "923e2107-5b8d-4876-bf94-3af1cd42375c", + "metadata": {}, + "source": [ + "#### Imports and external functions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3de893ac-1b5b-4637-9329-375d6b2472b3", + "metadata": {}, + "outputs": [], + "source": [ + "import io\n", + "from pprint import pprint\n", + "from pathlib import Path\n", + "from IPython.display import Image, display" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abfdcda2-d42c-4007-8a40-204a22a05ceb", + "metadata": {}, + "outputs": [], + "source": [ + "def plot_state_machine(pandora2d_machine):\n", + " \"\"\"\n", + " Show the schemes of step of Pandora2D Machine\n", + " \"\"\"\n", + " stream = io.BytesIO()\n", + " try:\n", + " pandora2d_machine.get_graph().draw(stream, prog='dot', format='png')\n", + " display(Image(stream.getvalue()))\n", + " except:\n", + " print(\"It is not possible to show the graphic of the state machine. To solve it, please install graphviz on your system (apt-get install graphviz if operating in Linux) and install python package with pip insall graphviz\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6cb630f-15b2-4237-9bd2-0fa74f6490a0", + "metadata": {}, + "outputs": [], + "source": [ + "from snippets.utils import *" + ] + }, + { + "cell_type": "markdown", + "id": "f5b88d48-fdf1-4ca4-a78f-a47ce5831fb2", + "metadata": {}, + "source": [ + "# Pandora2D execution options with state machine" + ] + }, + { + "cell_type": "markdown", + "id": "08d1d2d5-1b3d-4cfa-81cf-03f186086a1b", + "metadata": {}, + "source": [ + "#### Imports of pandora2d" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df1514c6-d5d5-440f-9f9d-f4bfb43a372e", + "metadata": {}, + "outputs": [], + "source": [ + "# Load pandora2d imports\n", + "from pandora2d import run \n", + "from pandora2d.state_machine import Pandora2DMachine\n", + "from pandora2d.check_configuration import check_conf\n", + "from pandora2d.img_tools import get_roi_processing, create_datasets_from_inputs" + ] + }, + { + "cell_type": "markdown", + "id": "bc3ba980-bdcc-41ba-afe5-c88af42c8be6", + "metadata": {}, + "source": [ + "#### Load and visualize input data " + ] + }, + { + "cell_type": "markdown", + "id": "c4fc2c4e-cf4d-4857-9386-484d1a534c9f", + "metadata": {}, + "source": [ + "Provide image path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3aeede83-86ab-4236-999e-58a4978b452d", + "metadata": {}, + "outputs": [], + "source": [ + "# Paths to left and right images\n", + "img_left_path = \"data/left.tif\"\n", + "img_right_path = \"data/right.tif\"" + ] + }, + { + "cell_type": "markdown", + "id": "7c97e607-4d39-49ea-bea2-70b6c97574db", + "metadata": {}, + "source": [ + "Provide output directory to write results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86242921-7443-4370-a050-f1d486f3ef57", + "metadata": {}, + "outputs": [], + "source": [ + "output_dir = Path.cwd() / \"output\"\n", + "# If necessary, create output dir\n", + "output_dir.mkdir(exist_ok=True,parents=True)" + ] + }, + { + "cell_type": "markdown", + "id": "96605ac2-36ff-40a5-8b47-9e489a30d9e2", + "metadata": {}, + "source": [ + "Convert input data to dataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf6b2360-9477-4295-a49a-c3272995b9fd", + "metadata": {}, + "outputs": [], + "source": [ + "input_config = {\n", + " \"left\": {\"img\": img_left_path, \"nodata\": np.nan},\n", + " \"right\": {\"img\": img_right_path, \"nodata\": np.nan},\n", + " \"col_disparity\": [-2, 2],\n", + " \"row_disparity\": [-2, 2],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "83dce3f4-88a5-49f5-90f2-dae03b71319e", + "metadata": {}, + "outputs": [], + "source": [ + "img_left, img_right = create_datasets_from_inputs(input_config=input_config)" + ] + }, + { + "cell_type": "markdown", + "id": "b46ad540-b4bc-45ed-bd55-18e6d378a34a", + "metadata": {}, + "source": [ + "Visualize input data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbef0f0c-e477-4d04-a456-1f09b9a494c6", + "metadata": {}, + "outputs": [], + "source": [ + "fig = plt.figure(figsize=(10,10))\n", + "ax0 = fig.add_subplot(1,2,1)\n", + "ax0.imshow(img_left[\"im\"].data)\n", + "plt.title(\"Left image\")\n", + "ax1 = fig.add_subplot(1,2,2)\n", + "ax1.imshow(img_right[\"im\"].data)\n", + "plt.title(\"Right image\")" + ] + }, + { + "cell_type": "markdown", + "id": "1d5d5e87-9d0c-4fd8-b963-78d4a6f33ca9", + "metadata": {}, + "source": [ + "#### Instantiate the machine" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a23e39f0-4419-4e87-8598-40d7967d9a4b", + "metadata": {}, + "outputs": [], + "source": [ + "pandora2d_machine = Pandora2DMachine()" + ] + }, + { + "cell_type": "markdown", + "id": "57cf45cb-0797-4b0a-9097-08e2a4beb950", + "metadata": {}, + "source": [ + "#### Define pipeline configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e85ecdd4-ed19-49dc-bdc5-0cb62f6a9d19", + "metadata": {}, + "outputs": [], + "source": [ + "user_cfg = {\n", + " \"input\": {\n", + " \"left\": {\n", + " \"img\": img_left_path,\n", + " \"nodata\": \"NaN\",\n", + " },\n", + " \"right\": {\n", + " \"img\": img_right_path,\n", + " \"nodata\": \"NaN\",\n", + " },\n", + " \"col_disparity\": [-3, 3],\n", + " \"row_disparity\": [-3, 3],\n", + " },\n", + " \"pipeline\":{\n", + " \"matching_cost\" : {\n", + " \"matching_cost_method\": \"zncc\",\n", + " \"window_size\": 7,\n", + " },\n", + " \"disparity\": {\n", + " \"disparity_method\": \"wta\",\n", + " \"invalid_disparity\": -9999\n", + " },\n", + " \"refinement\" : {\n", + " \"refinement_method\" : \"dichotomy\",\n", + " \"number_of_iterations\": 2,\n", + " \"filter\": \"sinc\",\n", + " }\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "cc9ab648-3d35-4b7f-b43b-05f98c484e57", + "metadata": {}, + "source": [ + "#### Check the user configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8990415-3e46-4bdf-a103-58674a70efda", + "metadata": {}, + "outputs": [], + "source": [ + "cfg = check_conf(user_cfg, pandora2d_machine)\n", + "pprint(cfg)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pandora2d/refinement/__init__.py b/pandora2d/refinement/__init__.py index 0ed2a09..21a5a84 100644 --- a/pandora2d/refinement/__init__.py +++ b/pandora2d/refinement/__init__.py @@ -24,4 +24,5 @@ """ from . import interpolation +from . import dichotomy from .refinement import AbstractRefinement diff --git a/pandora2d/refinement/dichotomy.py b/pandora2d/refinement/dichotomy.py new file mode 100644 index 0000000..992c547 --- /dev/null +++ b/pandora2d/refinement/dichotomy.py @@ -0,0 +1,74 @@ +# Copyright (c) 2024 Centre National d'Etudes Spatiales (CNES). +# +# This file is part of PANDORA2D +# +# https://github.com/CNES/Pandora2D +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Module for Dichotomy refinement method. +""" +import logging +from typing import Dict + +import xarray as xr + +from json_checker import And +from . import refinement + + +@refinement.AbstractRefinement.register_subclass("dichotomy") +class Dichotomy(refinement.AbstractRefinement): + """Subpixel refinement method by dichotomy.""" + + NB_MAX_ITER = 9 + schema = { + "refinement_method": And(str, lambda x: x in ["dichotomy"]), + "iterations": And(int, lambda it: it > 0), + "filter": And(str, lambda x: x in ["sinc", "bicubic", "spline"]), + } + + @classmethod + def check_conf(cls, cfg: Dict) -> Dict: + """ + Check the refinement method configuration. + + Will change `number_of_iterations` value by `Dichotomy.NB_MAX_ITER` if above `Dichotomy.NB_MAX_ITER`. + + :param cfg: user_config for refinement method + :type cfg: dict + :return: cfg: global configuration + :rtype: cfg: dict + """ + cfg = super().check_conf(cfg) + if cfg["iterations"] > cls.NB_MAX_ITER: + logging.warning( + "number_of_iterations %s is above maximum iteration. Maximum value of %s will be used instead.", + cfg["iterations"], + cls.NB_MAX_ITER, + ) + cfg["iterations"] = cls.NB_MAX_ITER + return cfg + + def refinement_method(self, cost_volumes: xr.Dataset, pixel_maps: xr.Dataset) -> None: + """ + Return the subpixel disparity maps + + :param cost_volumes: cost_volumes 4D row, col, disp_col, disp_row + :type cost_volumes: xarray.dataset + :param pixel_maps: pixels disparity maps + :type pixel_maps: xarray.dataset + :return: the refined disparity maps + :rtype: Tuple[np.ndarray, np.ndarray] + """ + logging.warning("refinement_method of Dichotomy not yet implemented") diff --git a/pandora2d/refinement/interpolation.py b/pandora2d/refinement/interpolation.py index ad4ae96..c6efcc7 100644 --- a/pandora2d/refinement/interpolation.py +++ b/pandora2d/refinement/interpolation.py @@ -24,8 +24,8 @@ """ import multiprocessing -from typing import Dict, Tuple -from json_checker import And, Checker +from typing import Tuple +from json_checker import And from pandora.margins.descriptors import UniformMargins from scipy.interpolate import interp2d @@ -47,34 +47,9 @@ class Interpolation(refinement.AbstractRefinement): # The minimum number of data points required along the interpolation axis is (k+1)**2, # with k=1 for linear, k=3 for cubic and k=5 for quintic interpolation. margins = UniformMargins(3) # cubic kernel - - def __init__(self, cfg: Dict) -> None: - """ - :param cfg: optional configuration, {} - :type cfg: dict - :return: None - """ - self.cfg = self.check_conf(cfg) - - @staticmethod - def check_conf(cfg: Dict) -> Dict: - """ - Check the refinement configuration - - :param cfg: user_config for refinement - :type cfg: dict - :return: cfg: global configuration - :rtype: cfg: dict - """ - - schema = { - "refinement_method": And(str, lambda x: x in ["interpolation"]), - } - - checker = Checker(schema) - checker.validate(cfg) - - return cfg + schema = { + "refinement_method": And(str, lambda x: x in ["interpolation"]), + } @staticmethod def wrapper_interp2d(params: np.ndarray, func: interp2d) -> np.ndarray: diff --git a/pandora2d/refinement/refinement.py b/pandora2d/refinement/refinement.py index b441520..e86f020 100644 --- a/pandora2d/refinement/refinement.py +++ b/pandora2d/refinement/refinement.py @@ -26,6 +26,7 @@ import logging from typing import Dict, Tuple from abc import abstractmethod, ABCMeta +from json_checker import Checker import xarray as xr import numpy as np @@ -45,6 +46,8 @@ class AbstractRefinement: cfg = None margins = NullMargins() + schema: Dict # This will raise an AttributeError if not override in subclasses + # If we don't make cfg optional, we got this error when we use subprocesses in refinement_method : # AbstractRefinement.__new__() missing 1 required positional argument: 'cfg' def __new__(cls, cfg: dict | None = None): @@ -101,6 +104,29 @@ def decorator(subclass): return decorator + def __init__(self, cfg: Dict) -> None: + """ + :param cfg: optional configuration, {} + :type cfg: dict + :return: None + """ + self.cfg = self.check_conf(cfg) + + @classmethod + def check_conf(cls, cfg: Dict) -> Dict: + """ + Check the refinement method configuration. + + :param cfg: user_config for refinement method + :type cfg: dict + :return: cfg: global configuration + :rtype: cfg: dict + """ + checker = Checker(cls.schema) + checker.validate(cfg) + + return cfg + @abstractmethod def refinement_method(self, cost_volumes: xr.Dataset, pixel_maps: xr.Dataset) -> Tuple[np.ndarray, np.ndarray]: """ diff --git a/tests/test_dichotomy.py b/tests/test_dichotomy.py new file mode 100644 index 0000000..cb91a03 --- /dev/null +++ b/tests/test_dichotomy.py @@ -0,0 +1,124 @@ +# Copyright (c) 2024 Centre National d'Etudes Spatiales (CNES). +# +# This file is part of PANDORA2D +# +# https://github.com/CNES/Pandora2D +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Test the refinement.dichotomy module. +""" +import pytest +import json_checker + +from pytest_mock import MockerFixture + +from pandora2d import refinement + +# Make pylint happy with fixtures: +# pylint: disable=redefined-outer-name + + +@pytest.fixture() +def config(): + """Basic configuration expected to be good.""" + return {"refinement_method": "dichotomy", "iterations": 2, "filter": "sinc"} + + +def test_factory(config): + """With `refinement_method` equals to `dichotomy`, we should get a Dichotomy object.""" + dichotomy_instance = refinement.AbstractRefinement(config) # type: ignore[abstract] + + assert isinstance(dichotomy_instance, refinement.dichotomy.Dichotomy) + assert isinstance(dichotomy_instance, refinement.AbstractRefinement) + + +class TestCheckConf: + """Test the check_conf method.""" + + def test_method_field(self, config): + """An exception should be raised if `refinement_method` is not `dichotomy`.""" + config["refinement_method"] = "invalid_method" + + with pytest.raises(json_checker.core.exceptions.DictCheckerError) as err: + refinement.dichotomy.Dichotomy(config) + assert "invalid_method" in err.value.args[0] + + def test_iterations_below_minimum(self, config): + """An exception should be raised.""" + config["iterations"] = 0 + + with pytest.raises(json_checker.core.exceptions.DictCheckerError) as err: + refinement.dichotomy.Dichotomy(config) + assert "Not valid data" in err.value.args[0] + assert "iterations" in err.value.args[0] + + def test_iterations_above_maximum(self, config, caplog): + """Test that when user set an iteration value above defined maximum, + we replace it by this maximum and log a warning. + """ + config["iterations"] = 10 + + dichotomy_instance = refinement.dichotomy.Dichotomy(config) + + assert dichotomy_instance.cfg["iterations"] == 9 + assert ( + "number_of_iterations 10 is above maximum iteration. Maximum value of 9 will be used instead." + in caplog.messages + ) + + @pytest.mark.parametrize("iterations", [1, 9]) + def test_iterations_in_allowed_range(self, config, iterations): + """It should not fail.""" + config["iterations"] = iterations + + dichotomy_instance = refinement.dichotomy.Dichotomy(config) + + assert dichotomy_instance.cfg["iterations"] == iterations + + @pytest.mark.parametrize("filter_name", ["sinc", "bicubic", "spline"]) + def test_valid_filter_names(self, config, filter_name): + """Test accepted filter names.""" + config["filter"] = filter_name + + dichotomy_instance = refinement.dichotomy.Dichotomy(config) + + assert dichotomy_instance.cfg["filter"] == filter_name + + @pytest.mark.parametrize("missing", ["refinement_method", "iterations", "filter"]) + def test_fails_on_missing_keys(self, config, missing): + """Should raise an error when a mandatory key is missing.""" + del config[missing] + + with pytest.raises(json_checker.core.exceptions.MissKeyCheckerError) as err: + refinement.dichotomy.Dichotomy(config) + assert f"Missing keys in current response: {missing}" in err.value.args[0] + + def test_fails_on_unexpected_key(self, config): + """Should raise an error when an unexpected key is given.""" + config["unexpected_key"] = "unexpected_value" + + with pytest.raises(json_checker.core.exceptions.MissKeyCheckerError) as err: + refinement.dichotomy.Dichotomy(config) + assert "Missing keys in expected schema: unexpected_key" in err.value.args[0] + + +def test_refinement_method(config, caplog, mocker: MockerFixture): + """Not yet implemented.""" + + dichotomy_instance = refinement.dichotomy.Dichotomy(config) + + # We can pass anything as it is not yet implemented + dichotomy_instance.refinement_method(mocker.ANY, mocker.ANY) + + assert "refinement_method of Dichotomy not yet implemented" in caplog.messages