From f522635bd0522680f0f6bca3e133665a406042d3 Mon Sep 17 00:00:00 2001 From: Stijn Moreels <9039753+stijnmoreels@users.noreply.github.com> Date: Thu, 3 Feb 2022 13:32:15 +0100 Subject: [PATCH] test: add image conversion properties for improved implementation (#112) * test: add image conversion properties for improved implementation * pr-fix: install hypothesis in CI build * pr-sug: remove unnecessary imports --- arcus/ml/images/conversion.py | 24 +++++++----- build/ci-build.yml | 1 + tests/test_images_conversion.py | 69 ++++++++++++++++++++++++++------- 3 files changed, 70 insertions(+), 24 deletions(-) diff --git a/arcus/ml/images/conversion.py b/arcus/ml/images/conversion.py index b004441..76d4bc4 100644 --- a/arcus/ml/images/conversion.py +++ b/arcus/ml/images/conversion.py @@ -16,24 +16,30 @@ def to_blackwhite(image_list: np.array, threshold = 128, keep_3d_shape = False) Args: image_list (np.array): A numpy array that contains all selected images represented as np.array threshold (int): The threshold (between 0 and 255) that decides a pixel will be 0 or 255 (W/B) + keep_3d_shape (bool): The flag indicating whether to keep the images in shape (H,W,1) or not when encoutering 2D arrays Returns: np.array: A numpy array that contains all black & white images represented as np.array ''' + if threshold < 0 or threshold > 255: + raise ValueError("Requires a threshold for black/white conversion between 0-255 (inconclusive)") + + def to_gray_scale(img): + return np.dot(img, [0.3, 0.59, 0.11]) + def make_black_or_white(img): + return 1.0 * (img > threshold) + images = [] for img in image_list: - # Check if the image is scaled down by 255 or not - if(np.count_nonzero(img > 1)==0): + is_scaled_down_by_255 = np.count_nonzero(img > 1) == 0 + if is_scaled_down_by_255: img *= 255 - if(len(img.shape) == 3): - if(img.shape[2] == 3): - # Convert to grey scale - img = np.dot(img, [0.3, 0.59, 0.11]) + if(len(img.shape) == 3 and img.shape[2] == 3): + img = to_gray_scale(img) if (len(img.shape) == 2 and keep_3d_shape): img = np.expand_dims(img, -1) - # make all pixels < threshold black - img = 1.0 * (img > threshold) + img = make_black_or_white(img) images.append(img) - + return np.array(images) def prepare(image: np.array, image_size = None, diff --git a/build/ci-build.yml b/build/ci-build.yml index 1646f59..bb4ab05 100644 --- a/build/ci-build.yml +++ b/build/ci-build.yml @@ -79,6 +79,7 @@ stages: - script: | pip install pytest pip install pytest-cov + pip install hypothesis pip install lazydocs pytest tests --doctest-modules --junitxml=junit/test-results.xml --cov=. --cov-report=xml --cov-report=html displayName: 'Unit tests (pytest)' diff --git a/tests/test_images_conversion.py b/tests/test_images_conversion.py index 5809027..431c218 100644 --- a/tests/test_images_conversion.py +++ b/tests/test_images_conversion.py @@ -5,25 +5,66 @@ import arcus.ml.images.io as ami import arcus.ml.images.conversion as conv import logging -import numpy as np +import numpy as np +from hypothesis import given, strategies as st image_path = 'tests/resources/images/lungs' +def is_black_or_white(cell): + return cell == 0 or cell == 1 +def all_black_and_white(arr): + result = [] + for cell_z in arr: + for cell_y in cell_z: + if isinstance(cell_y, np.ndarray): + for cell_x in cell_y: + result.append(is_black_or_white(cell_x)) + else: + result.append(is_black_or_white(cell_y)) + return all(result) + +@st.composite +def images(draw, dimension = 3): + def create_array_rec(strategy, lengths, index = 0): + if index == len(lengths): + return strategy + length = lengths[index] + arr = st.lists(strategy, min_size=length, max_size=length).map(lambda x : np.array(x)) + return create_array_rec(arr, lengths, index + 1) + + num = st.integers(min_value=0, max_value=255) + amount = st.integers(min_value=1, max_value=10) + return draw(st.lists(amount, min_size=dimension, max_size=dimension).flatmap(lambda lengths : create_array_rec(num, lengths))) + +@given(images()) +def test_black_white_property(image_list): + image_converted = conv.to_blackwhite(image_list) + assert all_black_and_white(image_converted) + +@given(images(dimension=2)) +def test_black_white_2D_property(image_list): + image_converted = conv.to_blackwhite(image_list) + assert all_black_and_white(image_converted) + +def test_black_white_empty_stays_empty(): + image_converted = conv.to_blackwhite(np.array([])) + image_converted = [] + +@given(images(), st.integers().filter(lambda x : x < 0 or x > 255)) +def test_black_white_outofbounds_threshold(image_list, threshold): + with pytest.raises(ValueError, match=r".* threshold .*"): + conv.to_blackwhite(image_list, threshold) + def test_black_white_conversion(): image_list = ami.load_images(image_path) - ls = conv.to_blackwhite(image_list) - pixel_value = ls[0][0][0] - assert pixel_value == 0 or pixel_value == 1 - assert len(ls[0].shape)==2 + image_converted = conv.to_blackwhite(image_list) + assert all_black_and_white(image_converted) def test_black_white_conversion_double_arrays(): image_list = ami.load_images(image_path) - test_list = list() - for img in image_list: - test_list.append(img / 255) - ls = conv.to_blackwhite(np.array(test_list)) - pixel_value = ls[0][0][0] - assert pixel_value == 0 or pixel_value == 1 + test_list = image_list / 255 + image_converted = conv.to_blackwhite(np.array(test_list)) + assert all_black_and_white(image_converted) def test_crop_image(): image = ami.load_images(image_path, max_images=1, image_size=50)[0] @@ -32,10 +73,8 @@ def test_crop_image(): def test_black_white_conversion_3dshape(): image_list = ami.load_images(image_path) - ls = conv.to_blackwhite(image_list, keep_3d_shape=True) - pixel_value = ls[0][0][0] - assert pixel_value == 0 or pixel_value == 1 - assert len(ls[0].shape)==3 + image_converted = conv.to_blackwhite(image_list, keep_3d_shape=True) + assert all_black_and_white(image_converted) def test_black_white_conversion_2x3dshape(): lung = ami.load_images(image_path, convert_to_grey=True, image_size = 30, keep_3d_shape=True)[0]