diff --git a/.gitlab-ci/.gitlab-test.yml b/.gitlab-ci/.gitlab-test.yml index a444348..96e7502 100644 --- a/.gitlab-ci/.gitlab-test.yml +++ b/.gitlab-ci/.gitlab-test.yml @@ -6,9 +6,15 @@ # # 1) pybsm specific notebooks. # 2) Added the original example as a job +# 3) Poetry install for opencv-python-headless # ############################################################################### +.test-setup: + before_script: + - !reference [.shared-setup, before_script] + - poetry install --sync --only main,dev-testing --extras headless + notebooks: parallel: matrix: diff --git a/README.md b/README.md index 8fb5640..2329acf 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,30 @@ See [here for more installation documentation]( https://pybsm.readthedocs.io/en/latest/installation.html). +Installing using the previous commands will not install OpenCV. While pyBSM will still function, there are a handful of functions that require OpenCV. + +There are two options for installing pyBSM with OpenCV: graphics or headless. Graphics will install `opencv-python` as the OpenCV implementation while headless will install `opencv-python-headless` as the OpenCV implementation. + +To install pybsm with `opencv-python` +* using pip: +```bash +pip install pybsm[graphics] +``` +* using Poetry +```bash +poetry install --sync --with dev-linting,dev-testing,dev-docs --extras graphics +``` + +To install pybsm with `opencv-python-headless` +* using pip: +```bash +pip install pybsm[headless] +``` +* using Poetry +```bash +poetry install --sync --with dev-linting,dev-testing,dev-docs --extras headless +``` + ## Getting Started We provide a number of examples based on Jupyter notebooks in the diff --git a/docs/release_notes/pending_release.rst b/docs/release_notes/pending_release.rst index c35c728..de8fdc3 100644 --- a/docs/release_notes/pending_release.rst +++ b/docs/release_notes/pending_release.rst @@ -14,6 +14,14 @@ CI/CD * Updates to dependencies to support the new CI/CD. +* Changed `opencv-python` to an optional dependency. + +* Added `opencv-python-headless` as an optional dependency. + +* Added two extras (graphics and headless) for `opencv-python` and `opencv-python-headless` compatibility. + +* Changed CI to use headless extra. + Documentation * Added sphinx's autosummary template for recursively populating diff --git a/poetry.lock b/poetry.lock index 5b2dbb2..77d21e9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -2429,7 +2429,7 @@ files = [ name = "opencv-python" version = "4.10.0.84" description = "Wrapper package for OpenCV python bindings." -optional = false +optional = true python-versions = ">=3.6" files = [ {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, @@ -2452,6 +2452,33 @@ numpy = [ {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] +[[package]] +name = "opencv-python-headless" +version = "4.10.0.84" +description = "Wrapper package for OpenCV python bindings." +optional = true +python-versions = ">=3.6" +files = [ + {file = "opencv-python-headless-4.10.0.84.tar.gz", hash = "sha256:f2017c6101d7c2ef8d7bc3b414c37ff7f54d64413a1847d89970b6b7069b4e1a"}, + {file = "opencv_python_headless-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a4f4bcb07d8f8a7704d9c8564c224c8b064c63f430e95b61ac0bffaa374d330e"}, + {file = "opencv_python_headless-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:5ae454ebac0eb0a0b932e3406370aaf4212e6a3fdb5038cc86c7aea15a6851da"}, + {file = "opencv_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46071015ff9ab40fccd8a163da0ee14ce9846349f06c6c8c0f2870856ffa45db"}, + {file = "opencv_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:377d08a7e48a1405b5e84afcbe4798464ce7ee17081c1c23619c8b398ff18295"}, + {file = "opencv_python_headless-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:9092404b65458ed87ce932f613ffbb1106ed2c843577501e5768912360fc50ec"}, + {file = "opencv_python_headless-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:afcf28bd1209dd58810d33defb622b325d3cbe49dcd7a43a902982c33e5fad05"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.21.0", markers = "python_version <= \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\" and python_version >= \"3.8\""}, + {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""}, + {version = ">=1.17.3", markers = "(platform_system != \"Darwin\" and platform_system != \"Linux\") and python_version >= \"3.8\" and python_version < \"3.9\" or platform_system != \"Darwin\" and python_version >= \"3.8\" and python_version < \"3.9\" and platform_machine != \"aarch64\" or platform_machine != \"arm64\" and python_version >= \"3.8\" and python_version < \"3.9\" and platform_system != \"Linux\" or (platform_machine != \"arm64\" and platform_machine != \"aarch64\") and python_version >= \"3.8\" and python_version < \"3.9\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, + {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] + [[package]] name = "overrides" version = "7.7.0" @@ -4156,7 +4183,11 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] +[extras] +graphics = ["opencv-python"] +headless = ["opencv-python-headless"] + [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "bf920e0907626c7258ef0f517ee3514203e55b43593937a2f4746983a164d212" +content-hash = "a926865000f7582ba8b8bf2c415eaf98f2947178ca0fe54b8a06f7b5821962b1" diff --git a/pyproject.toml b/pyproject.toml index 773c2c3..05715aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,8 +47,13 @@ scipy = [ {version = "<1.11.1", python = "~3.8.1"}, # Can't satisfy CVE-2023-25399 because it is too restrictive {version = ">=1.10.0,<1.14", python = ">=3.9"}, # CVE-2023-25399 ] -opencv-python = ">=4.6" setuptools = ">=65.6.1" +opencv-python = {version = ">=4.6", optional = true} +opencv-python-headless = {version = ">=4.6", optional = true} + +[tool.poetry.extras] +graphics = ["opencv-python"] +headless = ["opencv-python-headless"] # :auto dev-linting: # Linting diff --git a/src/pybsm/otf/functional.py b/src/pybsm/otf/functional.py index 57cdce4..ec1e956 100755 --- a/src/pybsm/otf/functional.py +++ b/src/pybsm/otf/functional.py @@ -65,7 +65,12 @@ import warnings from typing import Callable, Tuple -import cv2 +try: + import cv2 + + is_usable = True +except ImportError: + is_usable = False # 3rd party imports import numpy as np @@ -671,11 +676,10 @@ def polychromatic_turbulence_OTF( # noqa: N802 # calculate the coherence diameter over the band r0_at_1um = coherence_diameter(1.0e-6, z_path, cn2) - r0_function = ( - lambda wav: r0_at_1um # noqa: E731 - * wav ** (6.0 / 5.0) - * (1e-6) ** (-6.0 / 5.0) - ) + + def r0_function(wav: float) -> float: + return r0_at_1um * wav ** (6.0 / 5.0) * (1e-6) ** (-6.0 / 5.0) # noqa: E731 + r0_band = weighted_by_wavelength(wavelengths, weights, r0_function) # calculate the turbulence OTF @@ -1077,6 +1081,10 @@ def otf_to_psf(otf: np.ndarray, df: float, dx_out: float) -> np.ndarray: if df or dx_out are 0 """ + if not is_usable: + raise ImportError( + "OpenCV not found. Please install 'pybsm[graphics]' or 'pybsm[headless]'." + ) # transform the psf psf = np.real(np.fft.fftshift(np.fft.ifft2(np.fft.fftshift(otf)))) @@ -1419,6 +1427,10 @@ def apply_otf_to_image( # this function. Therefore, we can calculate the instantaneous field of view # (iFOV) of the assumed real camera, which is # 2*arctan(ref_gsd/2/ref_range). + if not is_usable: + raise ImportError( + "OpenCV not found. Please install 'pybsm[graphics]' or 'pybsm[headless]'." + ) psf = otf_to_psf(otf, df, 2 * np.arctan(ref_gsd / 2 / ref_range)) # filter the image @@ -1581,6 +1593,10 @@ def resample_2D( # noqa: N802 if dx_in is 0 """ + if not is_usable: + raise ImportError( + "OpenCV not found. Please install 'pybsm[graphics]' or 'pybsm[headless]'." + ) new_x = int(np.round(img_in.shape[1] * dx_in / dx_out)) new_y = int(np.round(img_in.shape[0] * dx_in / dx_out)) img_out = cv2.resize(img_in, (new_x, new_y)) diff --git a/tests/otf/test_otf.py b/tests/otf/test_otf.py index ecb69e4..3e34e5b 100644 --- a/tests/otf/test_otf.py +++ b/tests/otf/test_otf.py @@ -1,13 +1,24 @@ +import unittest.mock as mock from typing import Callable, Dict, Tuple -import cv2 import numpy as np import pytest from pybsm import otf from pybsm.simulation import Scenario, Sensor +try: + import cv2 + is_usable = True +except ImportError: + is_usable = False + + +@pytest.mark.skipif( + not is_usable, + reason="OpenCV not found. Please install 'pybsm[graphics]' or `pybsm[headless]`.", +) class TestOTF: @pytest.mark.parametrize( ("lambda0", "z_path", "cn2"), @@ -1680,3 +1691,10 @@ def test_apply_otf_to_image( ) assert np.isclose(output[0], expected[0], atol=5e-20).all() assert np.isclose(output[1], expected[1], atol=5e-20).all() + + +@mock.patch("pybsm.otf.functional.is_usable", False) +def test_missing_deps() -> None: + """Test that an exception is raised when required dependencies are not installed.""" + with pytest.raises(ImportError, match=r"OpenCV not found"): + otf.resample_2D(np.ones((5, 5)), 1.0, 1.0)