diff --git a/.flake8 b/.flake8 index ce555863..c443919c 100644 --- a/.flake8 +++ b/.flake8 @@ -4,7 +4,8 @@ # F401: unused imports in __init__.py-s # I251: allow absolute imports in upper files # B028: !r is not supported for python<3.8 -ignore = W503,E203,B028 +# W604: backticks in str-s are ok +ignore = W503,E203,B028,W604 per-file-ignores = __init__.py:F401 tests/*:I251 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 766d566d..dc8f8d0f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,10 +11,10 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: fregante/setup-git-user@v1 + - uses: actions/checkout@v4 + - uses: fregante/setup-git-user@v2 - run: git fetch origin gh-pages --depth=1 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ee32c90..3a0e1017 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,9 +12,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.9 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 @@ -41,10 +41,10 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 - name: Install cibuildwheel - run: python -m pip install cibuildwheel==2.10.0 + run: python -m pip install cibuildwheel==2.17.0 - name: Install gcc for mac if: matrix.os == 'macOS-11' run: | @@ -78,7 +78,7 @@ jobs: env: CIBW_ENVIRONMENT_MACOS: > PATH="/usr/local/opt/llvm/bin:$PATH" LDFLAGS="-L/usr/local/opt/llvm/lib" CPPFLAGS="-I/usr/local/opt/llvm/include" - CIBW_BUILD: cp36-* cp37-* cp38-* cp39-* cp310-* cp311-* + CIBW_BUILD: cp36-* cp37-* cp38-* cp39-* cp310-* cp311-* cp312-* CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<2.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' - uses: actions/upload-artifact@v3 with: @@ -88,11 +88,13 @@ jobs: needs: [ check_version ] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.9 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 - name: Build - run: python setup.py sdist + run: | + pip install build + python -m build --sdist - uses: actions/upload-artifact@v3 with: path: dist/*.tar.gz diff --git a/.github/workflows/test_build.yml b/.github/workflows/test_build.yml index 587a129e..b0498967 100644 --- a/.github/workflows/test_build.yml +++ b/.github/workflows/test_build.yml @@ -15,10 +15,12 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.9' - name: Install cibuildwheel - run: python -m pip install cibuildwheel==2.10.0 + run: python -m pip install cibuildwheel==2.17.0 - name: Install gcc for mac if: matrix.os == 'macOS-11' run: | @@ -52,6 +54,6 @@ jobs: env: CIBW_ENVIRONMENT_MACOS: > PATH="/usr/local/opt/llvm/bin:$PATH" LDFLAGS="-L/usr/local/opt/llvm/lib" CPPFLAGS="-I/usr/local/opt/llvm/include" - CIBW_BUILD: cp39-* cp36-* + CIBW_BUILD: cp39-* cp36-* cp312-* CIBW_SKIP: "*musllinux* *manylinux_x86_64" CIBW_BEFORE_BUILD_LINUX: 'if [ $(python -c "import sys; print(sys.version_info[1])") -ge 9 ]; then python -m pip install "numpy<2.0.0" --config-settings=setup-args="-Dallow-noblas=true"; fi' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 513995f8..322a975d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,12 +10,12 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: [ '3.6', '3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: [ '3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -28,7 +28,8 @@ jobs: if [ "$GITHUB_BASE_REF" = "master" ] && [ "$MATCH" != "" ]; then exit 1; fi - name: Build the package run: | - python setup.py sdist + pip install build + python -m build --sdist - name: Install run: | @@ -73,7 +74,9 @@ jobs: if: ${{ always() }} - name: Upload coverage results - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: fail_ci_if_error: true files: reports/coverage-${{ matrix.python-version }}.xml diff --git a/README.md b/README.md index 9f6c21d6..56af5a83 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ y = zoom(x, 2, axis=[0, 1]) # without the need to compute the scale factor z = zoom_to_shape(x, (4, 120, 67)) ``` -Works faster only for `ndim<=4, dtype=float32 or float64 (and bool-int16-32-64 if order == 0), output=None, order=0 or 1, mode='constant', grid_mode=False` +Works faster only for `ndim<=4, dtype=float32 or float64 (and bool-int16-32-64-uint8-16-32 if order == 0), output=None, order=0 or 1, mode='constant', grid_mode=False` ### Fast 1d linear interpolation ```python diff --git a/imops/__version__.py b/imops/__version__.py index 37e9d6dd..8843c51c 100644 --- a/imops/__version__.py +++ b/imops/__version__.py @@ -1 +1 @@ -__version__ = '0.8.6' +__version__ = '0.8.7' diff --git a/imops/crop.py b/imops/crop.py index 2476305e..90cb338f 100644 --- a/imops/crop.py +++ b/imops/crop.py @@ -3,7 +3,7 @@ from .backend import BackendLike from .numeric import _NUMERIC_DEFAULT_NUM_THREADS from .pad import pad -from .utils import AxesLike, AxesParams, broadcast_axis, fill_by_indices +from .utils import AxesLike, AxesParams, assert_subdtype, broadcast_axis, fill_by_indices def crop_to_shape(x: np.ndarray, shape: AxesLike, axis: AxesLike = None, ratio: AxesParams = 0.5) -> np.ndarray: @@ -35,6 +35,9 @@ def crop_to_shape(x: np.ndarray, shape: AxesLike, axis: AxesLike = None, ratio: >>> cropped = crop_to_shape(x, [3, 4, 5]) # fail due to bigger resulting shape """ x = np.asarray(x) + shape = np.asarray(shape) + assert_subdtype(shape.dtype, np.integer, 'shape') + axis, shape, ratio = broadcast_axis(axis, x.ndim, shape, ratio) old_shape, new_shape = np.array(x.shape), np.array(fill_by_indices(x.shape, shape, axis)) @@ -89,6 +92,9 @@ def crop_to_box( >>> cropped = crop_to_box(x, np.array([[0], [5]]), axis=0, padding_values=0) # pad with 0-s to shape [5, 3, 4] """ x = np.asarray(x) + box = np.asarray(box) + assert_subdtype(box.dtype, np.integer, 'box') + start, stop = box axis, start, stop = broadcast_axis(axis, x.ndim, start, stop) diff --git a/imops/morphology.py b/imops/morphology.py index 249adc55..9f19d758 100644 --- a/imops/morphology.py +++ b/imops/morphology.py @@ -80,13 +80,13 @@ def wrapped( raise ValueError('Input image and footprint number of dimensions must be the same.') if not image.any(): - warn(f'{op_name} is applied to the fully False mask (mask.any() == False).', stacklevel=3) + warn(f'{op_name} is applied to the fully False mask (mask.any() == False).', stacklevel=3) # noqa output.fill(False) return output if image.all(): - warn(f'{op_name} is applied to the fully True mask (mask.all() == True).', stacklevel=3) + warn(f'{op_name} is applied to the fully True mask (mask.all() == True).', stacklevel=3) # noqa output.fill(True) return output diff --git a/imops/src/_zoom.pyx b/imops/src/_zoom.pyx index 44b4d078..9aabbc81 100644 --- a/imops/src/_zoom.pyx +++ b/imops/src/_zoom.pyx @@ -18,6 +18,8 @@ from libc.math cimport floor, sqrt ctypedef cython.floating FLOAT ctypedef fused NUM: np.uint8_t + np.uint16_t + np.uint32_t short int long long diff --git a/imops/utils.py b/imops/utils.py index 4521ae7e..92d8d5c9 100644 --- a/imops/utils.py +++ b/imops/utils.py @@ -196,3 +196,8 @@ def check_len(*args) -> None: lengths = list(map(len, args)) if any(length != lengths[0] for length in lengths): raise ValueError(f'Arguments of equal length are required: {", ".join(map(str, lengths))}') + + +def assert_subdtype(dtype, ref_dtype, name): + if not np.issubdtype(dtype, ref_dtype): + raise ValueError(f'`{name}` must be of {ref_dtype.__name__} dtype, got {dtype}') diff --git a/imops/zoom.py b/imops/zoom.py index 455966a2..6d1868ff 100644 --- a/imops/zoom.py +++ b/imops/zoom.py @@ -81,7 +81,7 @@ def zoom( """ Rescale `x` according to `scale_factor` along the `axis`. - Uses a fast parallelizable implementation for fp32 / fp64 (and bool-int16-32-64 if order == 0) inputs, + Uses a fast parallelizable implementation for fp32-fp64 and bool-int16-32-64-uint8-16-32 if order == 0 inputs, ndim <= 4 and order = 0 or 1. Parameters @@ -136,7 +136,7 @@ def zoom_to_shape( """ Rescale `x` to match `shape` along the `axis`. - Uses a fast parallelizable implementation for fp32 / fp64 (and bool-int16-32-64 if order == 0) inputs, + Uses a fast parallelizable implementation for fp32-fp64 and bool-int16-32-64-uint8-16-32 if order == 0 inputs, ndim <= 4 and order = 0 or 1. Parameters @@ -198,7 +198,7 @@ def _zoom( backend: BackendLike = None, ) -> np.ndarray: """ - Faster parallelizable version of `scipy.ndimage.zoom` for fp32 / fp64 (and bool-int16-32-64 if order == 0) inputs. + Faster parallelizable version of `scipy.ndimage.zoom` for fp32-fp64 and bool-int16-32-64-uint8-16-32 if order == 0 Works faster only for ndim <= 4. Shares interface with `scipy.ndimage.zoom` except for @@ -228,7 +228,8 @@ def _zoom( or ( dtype not in (np.float32, np.float64) if order == 1 - else dtype not in (bool, np.float32, np.float64, np.int16, np.int32, np.int64) + else dtype + not in (bool, np.float32, np.float64, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32) ) or ndim > 4 or output is not None @@ -236,8 +237,8 @@ def _zoom( or grid_mode ): warn( - 'Fast zoom is only supported for ndim<=4, dtype=fp32 or fp64 (and bool-int16-32-64 if order == 0), ' - "output=None, order=0 or 1, mode='constant', grid_mode=False. Falling back to scipy's implementation.", + 'Fast zoom is only supported for ndim<=4, dtype=fp32-fp64 and bool-int16-32-64-uint8-16-32 if order == 0, ' + "output=None, order=0 or 1 , mode='constant', grid_mode=False. Falling back to scipy's implementation.", stacklevel=3, ) return scipy_zoom( diff --git a/pyproject.toml b/pyproject.toml index 2ee62668..4ca5df98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ] [options] diff --git a/setup.py b/setup.py index d20d0f10..f255b459 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', ] with open(root / 'requirements.txt', encoding='utf-8') as file: diff --git a/tests/test_crop.py b/tests/test_crop.py index 8d9e569f..3be805b0 100644 --- a/tests/test_crop.py +++ b/tests/test_crop.py @@ -52,7 +52,21 @@ def test_raises(): @seeded_by(SEED) def test_crop_to_shape(): x = np.random.rand(3, 10, 10) - shape = 3, 4, 8 + shape = (3, 4, 8) assert crop_to_shape(x, shape).shape == shape with pytest.raises(ValueError): crop_to_shape(x, (3, 15, 10)) + + +def test_crop_to_float_shape(): + x = np.random.rand(3, 10, 10) + float_shape = (1.337, 3.1415, 2.7182) + with pytest.raises(ValueError): + crop_to_shape(x, float_shape) + + +def test_crop_to_float_box(): + x = np.random.rand(3, 10, 10) + float_box = [[0, 1], [4, 4.5], [3.1, 9]] + with pytest.raises(ValueError): + crop_to_box(x, float_box) diff --git a/tests/test_pad.py b/tests/test_pad.py index 664db814..19fa3a0a 100644 --- a/tests/test_pad.py +++ b/tests/test_pad.py @@ -140,6 +140,9 @@ def test_restore_crop_invalid_box(): with pytest.raises(ValueError): restore_crop(x, np.array([[0, 0, 0], [1, 1, 1]]), [4, 4, 4]) + with pytest.raises(ValueError): + restore_crop(x, np.array([[0.5, 0.5, 0.5], [1.5, 1.5, 1.5]]), [4.5, 4.5, 4.5]) + def test_pad_to_divisible(): x = np.zeros((4, 8, 12)) diff --git a/tests/test_zoom.py b/tests/test_zoom.py index 88e9d6eb..353bcd40 100644 --- a/tests/test_zoom.py +++ b/tests/test_zoom.py @@ -39,7 +39,7 @@ def order(request): return request.param -@pytest.fixture(params=[np.float32, np.float64, bool, np.int16, np.int32, np.int64]) +@pytest.fixture(params=[np.float32, np.float64, bool, np.int16, np.int32, np.int64, np.uint8, np.uint16, np.uint32]) def dtype(request): return request.param