From 827e10c83dedcc35bb1ffa4bc72204c7b390bc8d Mon Sep 17 00:00:00 2001 From: Matt McCormick Date: Tue, 22 Aug 2023 10:26:01 -0400 Subject: [PATCH] feat(compare-images): add compare_images.py, compare_images_async.py to dispatch --- .../itkwasm_compare_images/__init__.py | 2 + .../itkwasm_compare_images/compare_images.py | 48 +++++++++ .../compare_images_async.py | 48 +++++++++ .../itkwasm-compare-images/pyproject.toml | 15 ++- .../itkwasm-compare-images/test/__init__.py | 0 .../itkwasm-compare-images/test/fixtures.py | 31 ++++++ .../test/test_compare_double_images.py | 30 ++++++ .../test/test_compare_images.py | 84 ++++++++++++++++ .../test/test_compare_images_async.py | 99 +++++++++++++++++++ 9 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/compare_images.py create mode 100644 packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/compare_images_async.py create mode 100644 packages/compare-images/python/itkwasm-compare-images/test/__init__.py create mode 100644 packages/compare-images/python/itkwasm-compare-images/test/fixtures.py create mode 100644 packages/compare-images/python/itkwasm-compare-images/test/test_compare_double_images.py create mode 100644 packages/compare-images/python/itkwasm-compare-images/test/test_compare_images.py create mode 100644 packages/compare-images/python/itkwasm-compare-images/test/test_compare_images_async.py diff --git a/packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/__init__.py b/packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/__init__.py index 21b625c10..35215d764 100644 --- a/packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/__init__.py +++ b/packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/__init__.py @@ -2,6 +2,8 @@ from .compare_double_images_async import compare_double_images_async from .compare_double_images import compare_double_images +from .compare_images_async import compare_images_async +from .compare_images import compare_images from .vector_magnitude_async import vector_magnitude_async from .vector_magnitude import vector_magnitude diff --git a/packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/compare_images.py b/packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/compare_images.py new file mode 100644 index 000000000..fb3ca999d --- /dev/null +++ b/packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/compare_images.py @@ -0,0 +1,48 @@ +import os +from typing import Dict, Tuple, Optional, List, Any + +from itkwasm import ( + environment_dispatch, + Image, +) + +def compare_images( + test_image: Image, + baseline_images: List[Image] = [], + difference_threshold: float = 0, + radius_tolerance: int = 0, + number_of_pixels_tolerance: int = 0, + ignore_boundary_pixels: bool = False, +) -> Tuple[Any, Image, Image]: + """Compare images with a tolerance for regression testing. + + :param test_image: The input test image + :type test_image: Image + + :param baseline_images: Baseline images compare against + :type baseline_images: Image + + :param difference_threshold: Intensity difference for pixels to be considered different. + :type difference_threshold: float + + :param radius_tolerance: Radius of the neighborhood around a pixel to search for similar intensity values. + :type radius_tolerance: int + + :param number_of_pixels_tolerance: Number of pixels that can be different before the test fails. + :type number_of_pixels_tolerance: int + + :param ignore_boundary_pixels: Ignore boundary pixels. Useful when resampling may have introduced difference pixel values along the image edge. + :type ignore_boundary_pixels: bool + + :return: Metrics for the baseline with the fewest number of pixels outside the tolerances. + :rtype: Any + + :return: Absolute difference image + :rtype: Image + + :return: Unsigned char, 2D difference image for rendering + :rtype: Image + """ + func = environment_dispatch("itkwasm_compare_images", "compare_images") + output = func(test_image, baseline_images=baseline_images, difference_threshold=difference_threshold, radius_tolerance=radius_tolerance, number_of_pixels_tolerance=number_of_pixels_tolerance, ignore_boundary_pixels=ignore_boundary_pixels) + return output diff --git a/packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/compare_images_async.py b/packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/compare_images_async.py new file mode 100644 index 000000000..cac25b5e1 --- /dev/null +++ b/packages/compare-images/python/itkwasm-compare-images/itkwasm_compare_images/compare_images_async.py @@ -0,0 +1,48 @@ +import os +from typing import Dict, Tuple, Optional, List, Any + +from itkwasm import ( + environment_dispatch, + Image, +) + +async def compare_images_async( + test_image: Image, + baseline_images: List[Image] = [], + difference_threshold: float = 0, + radius_tolerance: int = 0, + number_of_pixels_tolerance: int = 0, + ignore_boundary_pixels: bool = False, +) -> Tuple[Any, Image, Image]: + """Compare images with a tolerance for regression testing. + + :param test_image: The input test image + :type test_image: Image + + :param baseline_images: Baseline images compare against + :type baseline_images: Image + + :param difference_threshold: Intensity difference for pixels to be considered different. + :type difference_threshold: float + + :param radius_tolerance: Radius of the neighborhood around a pixel to search for similar intensity values. + :type radius_tolerance: int + + :param number_of_pixels_tolerance: Number of pixels that can be different before the test fails. + :type number_of_pixels_tolerance: int + + :param ignore_boundary_pixels: Ignore boundary pixels. Useful when resampling may have introduced difference pixel values along the image edge. + :type ignore_boundary_pixels: bool + + :return: Metrics for the baseline with the fewest number of pixels outside the tolerances. + :rtype: Any + + :return: Absolute difference image + :rtype: Image + + :return: Unsigned char, 2D difference image for rendering + :rtype: Image + """ + func = environment_dispatch("itkwasm_compare_images", "compare_images_async") + output = await func(test_image, baseline_images=baseline_images, difference_threshold=difference_threshold, radius_tolerance=radius_tolerance, number_of_pixels_tolerance=number_of_pixels_tolerance, ignore_boundary_pixels=ignore_boundary_pixels) + return output diff --git a/packages/compare-images/python/itkwasm-compare-images/pyproject.toml b/packages/compare-images/python/itkwasm-compare-images/pyproject.toml index 402ccc289..9033de171 100644 --- a/packages/compare-images/python/itkwasm-compare-images/pyproject.toml +++ b/packages/compare-images/python/itkwasm-compare-images/pyproject.toml @@ -45,6 +45,9 @@ path = "itkwasm_compare_images/_version.py" [tool.hatch.envs.default] dependencies = [ "pytest", + "pytest-pyodide", + "itk-webassemblyinterface >= 1.0.b127", + "itkwasm >= 1.0.b130", ] [project.urls] @@ -52,8 +55,16 @@ Home = "https://github.com/InsightSoftwareConsortium/itk-wasm" Source = "https://github.com/InsightSoftwareConsortium/itk-wasm" [tool.hatch.envs.default.scripts] -test = "pytest" - +test = [ + "hatch build -t wheel", + "pytest -s --dist-dir=./dist --rt=chrome", +] +download-pyodide = [ + "curl -L https://github.com/pyodide/pyodide/releases/download/0.23.1/pyodide-0.23.1.tar.bz2 -o pyodide.tar.bz2", + "tar xjf pyodide.tar.bz2", + "rm -rf dist pyodide.tar.bz2", + "mv pyodide dist", +] [tool.hatch.build] exclude = [ diff --git a/packages/compare-images/python/itkwasm-compare-images/test/__init__.py b/packages/compare-images/python/itkwasm-compare-images/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/compare-images/python/itkwasm-compare-images/test/fixtures.py b/packages/compare-images/python/itkwasm-compare-images/test/fixtures.py new file mode 100644 index 000000000..deef3d21f --- /dev/null +++ b/packages/compare-images/python/itkwasm-compare-images/test/fixtures.py @@ -0,0 +1,31 @@ +import pytest +import sys +import pickle + +if sys.version_info < (3,10): + pytest.skip("Skipping pyodide tests on older Python", allow_module_level=True) + +from itkwasm_compare_images import __version__ as test_package_version + +@pytest.fixture +def package_wheel(): + return f"itkwasm_compare_images-{test_package_version}-py3-none-any.whl" + +@pytest.fixture +def input_data(): + from pathlib import Path + input_base_path = Path('..', '..', 'test', 'data') + test_files = [ + Path('input') / 'cake_easy.iwi.cbor', + Path('input') / 'cake_hard.iwi.cbor', + Path('input') / 'cake_easy.png', + Path('input') / 'cake_hard.png', + Path('input') / 'apple.jpg', + Path('input') / 'orange.jpg', + ] + data = {} + for f in test_files: + path = str(input_base_path / f) + '.pickle' + with open(path, 'rb') as fp: + data[str(f.name)] = pickle.load(fp) + return data diff --git a/packages/compare-images/python/itkwasm-compare-images/test/test_compare_double_images.py b/packages/compare-images/python/itkwasm-compare-images/test/test_compare_double_images.py new file mode 100644 index 000000000..3a90d9a49 --- /dev/null +++ b/packages/compare-images/python/itkwasm-compare-images/test/test_compare_double_images.py @@ -0,0 +1,30 @@ +from pathlib import Path +import itk +from itkwasm import Image + +def test_compare_double_images(): + from itkwasm_compare_images_wasi import compare_double_images + + test_image_file = 'cake_easy.iwi.cbor' + test_image_path = Path('..', '..', 'test', 'data', 'input', test_image_file) + test_image = itk.imread(test_image_path) + test_dict = itk.dict_from_image(test_image) + test_image = Image(**test_dict) + + baseline_image_file = 'cake_hard.iwi.cbor' + baseline_image_path = Path('..', '..', 'test', 'data', 'input', baseline_image_file) + baseline_image = itk.imread(baseline_image_path) + baseline_dict = itk.dict_from_image(baseline_image) + baseline_image = Image(**baseline_dict) + + metrics, difference_image, difference_image_rendering = compare_double_images(test_image, baseline_images=[baseline_image]) + + assert metrics['almostEqual'] == False + assert metrics['numberOfPixelsWithDifferences'] == 9915 + assert metrics['minimumDifference'] == 1.0 + assert metrics['maximumDifference'] == 107.0 + assert metrics['totalDifference'] == 337334.0 + assert metrics['meanDifference'] == 34.02259203227433 + + assert difference_image.imageType.componentType == 'float64' + assert difference_image_rendering.imageType.componentType == 'uint8' diff --git a/packages/compare-images/python/itkwasm-compare-images/test/test_compare_images.py b/packages/compare-images/python/itkwasm-compare-images/test/test_compare_images.py new file mode 100644 index 000000000..ad4663ac7 --- /dev/null +++ b/packages/compare-images/python/itkwasm-compare-images/test/test_compare_images.py @@ -0,0 +1,84 @@ +from pathlib import Path +import itk +from itkwasm import Image + +def test_compare_double_images(): + from itkwasm_compare_images import compare_images + + test_image_file = 'cake_easy.iwi.cbor' + test_image_path = Path('..', '..', 'test', 'data', 'input', test_image_file) + test_image = itk.imread(test_image_path) + test_dict = itk.dict_from_image(test_image) + test_image = Image(**test_dict) + + baseline_image_file = 'cake_hard.iwi.cbor' + baseline_image_path = Path('..', '..', 'test', 'data', 'input', baseline_image_file) + baseline_image = itk.imread(baseline_image_path) + baseline_dict = itk.dict_from_image(baseline_image) + baseline_image = Image(**baseline_dict) + + metrics, difference_image, difference_image_rendering = compare_images(test_image, baseline_images=[baseline_image]) + + assert metrics['almostEqual'] == False + assert metrics['numberOfPixelsWithDifferences'] == 9915 + assert metrics['minimumDifference'] == 1.0 + assert metrics['maximumDifference'] == 107.0 + assert metrics['totalDifference'] == 337334.0 + assert metrics['meanDifference'] == 34.02259203227433 + + assert difference_image.imageType.componentType == 'float64' + assert difference_image_rendering.imageType.componentType == 'uint8' + +def test_compare_uint8_images(): + from itkwasm_compare_images import compare_images + + test_image_file = 'cake_easy.png' + test_image_path = Path('..', '..', 'test', 'data', 'input', test_image_file) + test_image = itk.imread(test_image_path) + test_dict = itk.dict_from_image(test_image) + test_image = Image(**test_dict) + + baseline_image_file = 'cake_hard.png' + baseline_image_path = Path('..', '..', 'test', 'data', 'input', baseline_image_file) + baseline_image = itk.imread(baseline_image_path) + baseline_dict = itk.dict_from_image(baseline_image) + baseline_image = Image(**baseline_dict) + + metrics, difference_image, difference_image_rendering = compare_images(test_image, baseline_images=[baseline_image]) + + assert metrics['almostEqual'] == False + assert metrics['numberOfPixelsWithDifferences'] == 9915 + assert metrics['minimumDifference'] == 1.0 + assert metrics['maximumDifference'] == 107.0 + assert metrics['totalDifference'] == 337334.0 + assert metrics['meanDifference'] == 34.02259203227433 + + assert difference_image.imageType.componentType == 'float64' + assert difference_image_rendering.imageType.componentType == 'uint8' + +def test_compare_rgb_images(): + from itkwasm_compare_images import compare_images + + test_image_file = 'apple.jpg' + test_image_path = Path('..', '..', 'test', 'data', 'input', test_image_file) + test_image = itk.imread(test_image_path) + test_dict = itk.dict_from_image(test_image) + test_image = Image(**test_dict) + + baseline_image_file = 'orange.jpg' + baseline_image_path = Path('..', '..', 'test', 'data', 'input', baseline_image_file) + baseline_image = itk.imread(baseline_image_path) + baseline_dict = itk.dict_from_image(baseline_image) + baseline_image = Image(**baseline_dict) + + metrics, difference_image, difference_image_rendering = compare_images(test_image, baseline_images=[baseline_image]) + + assert metrics['almostEqual'] == False + assert metrics['numberOfPixelsWithDifferences'] == 26477 + assert metrics['minimumDifference'] == 0.002273026683894841 + assert metrics['maximumDifference'] == 312.2511648746159 + assert metrics['totalDifference'] == 3121656.100202402 + assert metrics['meanDifference'] == 117.90067228924735 + + assert difference_image.imageType.componentType == 'float64' + assert difference_image_rendering.imageType.componentType == 'uint8' diff --git a/packages/compare-images/python/itkwasm-compare-images/test/test_compare_images_async.py b/packages/compare-images/python/itkwasm-compare-images/test/test_compare_images_async.py new file mode 100644 index 000000000..28256b2e4 --- /dev/null +++ b/packages/compare-images/python/itkwasm-compare-images/test/test_compare_images_async.py @@ -0,0 +1,99 @@ +import sys + +if sys.version_info < (3,10): + pytest.skip("Skipping pyodide tests on older Python", allow_module_level=True) + +from pytest_pyodide import run_in_pyodide +from .fixtures import package_wheel, input_data + +@run_in_pyodide(packages=['micropip', 'numpy']) +async def test_compare_double_images_async(selenium, package_wheel, input_data): + import micropip + await micropip.install(package_wheel, 'numpy', 'itkwasm') + + from itkwasm_compare_images import compare_double_images_async + import numpy as np + from itkwasm import Image + from itkwasm.pyodide import to_js as itkwasm_to_js + + test_image_file = 'cake_easy.iwi.cbor' + test_image = Image(**input_data[test_image_file]) + + baseline_image_file = 'cake_hard.iwi.cbor' + baseline_image = Image(**input_data[baseline_image_file]) + + metrics, difference_image, difference_image_rendering = await compare_double_images_async(test_image, baseline_images=[baseline_image]) + + assert metrics['almostEqual'] == False + assert metrics['numberOfPixelsWithDifferences'] == 9915 + assert metrics['minimumDifference'] == 1.0 + assert metrics['maximumDifference'] == 107.0 + assert metrics['totalDifference'] == 337334.0 + assert metrics['meanDifference'] == 34.02259203227433 + + assert difference_image.imageType.componentType == 'float64' + assert difference_image_rendering.imageType.componentType == 'uint8' + +@run_in_pyodide(packages=['micropip', 'numpy']) +async def test_compare_images_async(selenium, package_wheel, input_data): + import micropip + await micropip.install(package_wheel, 'numpy', 'itkwasm') + + from itkwasm_compare_images import compare_images_async + import numpy as np + from itkwasm import Image + from itkwasm.pyodide import to_js as itkwasm_to_js + + test_image_file = 'cake_easy.iwi.cbor' + test_image = Image(**input_data[test_image_file]) + + baseline_image_file = 'cake_hard.iwi.cbor' + baseline_image = Image(**input_data[baseline_image_file]) + + metrics, difference_image, difference_image_rendering = await compare_images_async(test_image, baseline_images=[baseline_image]) + + assert metrics['almostEqual'] == False + assert metrics['numberOfPixelsWithDifferences'] == 9915 + assert metrics['minimumDifference'] == 1.0 + assert metrics['maximumDifference'] == 107.0 + assert metrics['totalDifference'] == 337334.0 + assert metrics['meanDifference'] == 34.02259203227433 + + assert difference_image.imageType.componentType == 'float64' + assert difference_image_rendering.imageType.componentType == 'uint8' + + test_image_file = 'cake_easy.png' + test_image = Image(**input_data[test_image_file]) + + baseline_image_file = 'cake_hard.png' + baseline_image = Image(**input_data[baseline_image_file]) + + metrics, difference_image, difference_image_rendering = await compare_images_async(test_image, baseline_images=[baseline_image]) + + assert metrics['almostEqual'] == False + assert metrics['numberOfPixelsWithDifferences'] == 9915 + assert metrics['minimumDifference'] == 1.0 + assert metrics['maximumDifference'] == 107.0 + assert metrics['totalDifference'] == 337334.0 + assert metrics['meanDifference'] == 34.02259203227433 + + assert difference_image.imageType.componentType == 'float64' + assert difference_image_rendering.imageType.componentType == 'uint8' + + test_image_file = 'apple.jpg' + test_image = Image(**input_data[test_image_file]) + + baseline_image_file = 'orange.jpg' + baseline_image = Image(**input_data[baseline_image_file]) + + metrics, difference_image, difference_image_rendering = await compare_images_async(test_image, baseline_images=[baseline_image]) + + assert metrics['almostEqual'] == False + assert metrics['numberOfPixelsWithDifferences'] == 26477 + assert metrics['minimumDifference'] == 0.002273026683894841 + assert metrics['maximumDifference'] == 312.2511648746159 + assert metrics['totalDifference'] == 3121656.100202402 + assert metrics['meanDifference'] == 117.90067228924735 + + assert difference_image.imageType.componentType == 'float64' + assert difference_image_rendering.imageType.componentType == 'uint8'