diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..485dee64b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/webknossos/Changelog.md b/webknossos/Changelog.md new file mode 100644 index 000000000..660809a0f --- /dev/null +++ b/webknossos/Changelog.md @@ -0,0 +1,55 @@ +# Change Log + +All notable changes to the webknossos python library are documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/) `MAJOR.MINOR.PATCH`. +For upgrade instructions, please check the respective *Breaking Changes* sections. + +## Unreleased +[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.13...HEAD) + +### Breaking Changes + +- Breaking changes were introduced for geometry classes in [#421](https://github.com/scalableminds/webknossos-libs/pull/421): + - `BoundingBox` + - is now immutable, use convenience methods, e.g. `bb.with_topleft((0,0,0))` + - properties topleft and size are now Vec3Int instead of np.array, they are each immutable as well + - all `to_`-conversions return a copy, some were renamed: + - `to_array` → `to_list` + - `as_np` → `to_np` + - `as_wkw` → `to_wkw_dict` + - `from_wkw` → `from_wkw_dict` + - `as_config` → `to_config_dict` + - `as_checkpoint_name` → `to_checkpoint_name` + - `as_tuple6` → `to_tuple6` + - `as_csv` → `to_csv` + - `as_named_tuple` → `to_named_tuple` + - `as_slices` → `to_slices` + - `copy` → (gone, immutable) + + - `Mag` + - is now immutable + - `mag.mag` is now `mag._mag` (considered private, use to_list instead if you really need it as list) + - all `to_`-conversions return a copy, some were renamed: + - `to_array` → `to_list` + - `scale_by` → (gone, immutable) + - `divide_by` → (gone, immutable) + - `as_np` → `to_np` + +### Added + + - An immutable Vec3Int class was introduced that holds three integers and provides a number of convenience methods and accessors. [#421](https://github.com/scalableminds/webknossos-libs/pull/421) + +### Changed + +- `BoundingBox` and `Mag` are now immutable attr classes containing `Vec3Int` values. See breaking changes above. + +### Fixed + +- + +## [0.8.13](https://github.com/scalableminds/webknossos-cuber/releases/tag/v0.8.13) - 2021-09-22 +[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.12...v0.8.13) + +This is the latest release at the time of creating this changelog. diff --git a/webknossos/poetry.lock b/webknossos/poetry.lock index d22ed2a81..e35f6e115 100644 --- a/webknossos/poetry.lock +++ b/webknossos/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "anyio" -version = "3.3.0" +version = "3.3.1" description = "High level compatibility layer for multiple asynchronous event loop implementations" category = "main" optional = false @@ -26,7 +26,7 @@ python-versions = "*" [[package]] name = "astroid" -version = "2.7.2" +version = "2.8.0" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false @@ -35,7 +35,7 @@ python-versions = "~=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" typed-ast = {version = ">=1.4.0,<1.5", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} -typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} wrapt = ">=1.11,<1.13" [[package]] @@ -157,11 +157,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "cloudpickle" -version = "1.6.0" +version = "2.0.0" description = "Extended pickling support for Python objects" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [[package]] name = "cluster-tools" @@ -206,7 +206,7 @@ six = "*" [[package]] name = "executing" -version = "0.8.0" +version = "0.8.1" description = "Get the currently executing AST node of a frame, and other information" category = "dev" optional = false @@ -222,7 +222,7 @@ python-versions = ">=3.6" [[package]] name = "httpcore" -version = "0.13.6" +version = "0.13.7" description = "A minimal low-level HTTP client." category = "main" optional = false @@ -471,7 +471,7 @@ python-versions = "*" [[package]] name = "networkx" -version = "2.6.2" +version = "2.6.3" description = "Python package for creating and manipulating graphs and networks" category = "main" optional = false @@ -573,7 +573,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "pillow" -version = "8.3.1" +version = "8.3.2" description = "Python Imaging Library (Fork)" category = "main" optional = false @@ -581,7 +581,7 @@ python-versions = ">=3.6" [[package]] name = "platformdirs" -version = "2.2.0" +version = "2.3.0" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false @@ -593,17 +593,18 @@ test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock [[package]] name = "pluggy" -version = "0.13.1" +version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "py" @@ -654,19 +655,20 @@ python-versions = ">=3.5" [[package]] name = "pylint" -version = "2.10.2" +version = "2.11.1" description = "python code static checker" category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] -astroid = ">=2.7.2,<2.8" +astroid = ">=2.8.0,<2.9" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" platformdirs = ">=2.2.0" toml = ">=0.7.1" +typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [[package]] name = "pyparsing" @@ -686,7 +688,7 @@ python-versions = ">=3.6" [[package]] name = "pytest" -version = "6.2.4" +version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -699,7 +701,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<1.0.0a1" +pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" @@ -749,7 +751,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "regex" -version = "2021.8.27" +version = "2021.8.28" description = "Alternative regular expression module, to replace re." category = "dev" optional = false @@ -771,7 +773,7 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "10.9.0" +version = "10.10.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "main" optional = false @@ -893,7 +895,7 @@ python-versions = "*" [[package]] name = "typing-extensions" -version = "3.10.0.0" +version = "3.10.0.2" description = "Backported and Experimental Type Hints for Python 3.5+" category = "main" optional = false @@ -938,16 +940,16 @@ content-hash = "d218dae44f0344d28ce803b209eba66ee2bceb2ef95ddbde2483e46ae3911d3d [metadata.files] anyio = [ - {file = "anyio-3.3.0-py3-none-any.whl", hash = "sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0"}, - {file = "anyio-3.3.0.tar.gz", hash = "sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374"}, + {file = "anyio-3.3.1-py3-none-any.whl", hash = "sha256:d7c604dd491eca70e19c78664d685d5e4337612d574419d503e76f5d7d1590bd"}, + {file = "anyio-3.3.1.tar.gz", hash = "sha256:85913b4e2fec030e8c72a8f9f98092eeb9e25847a6e00d567751b77e34f856fe"}, ] appdirs = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] astroid = [ - {file = "astroid-2.7.2-py3-none-any.whl", hash = "sha256:ecc50f9b3803ebf8ea19aa2c6df5622d8a5c31456a53c741d3be044d96ff0948"}, - {file = "astroid-2.7.2.tar.gz", hash = "sha256:b6c2d75cd7c2982d09e7d41d70213e863b3ba34d3bd4014e08f167cee966e99e"}, + {file = "astroid-2.8.0-py3-none-any.whl", hash = "sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471"}, + {file = "astroid-2.8.0.tar.gz", hash = "sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708"}, ] asttokens = [ {file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"}, @@ -1032,8 +1034,8 @@ click = [ {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, ] cloudpickle = [ - {file = "cloudpickle-1.6.0-py3-none-any.whl", hash = "sha256:3a32d0eb0bc6f4d0c57fbc4f3e3780f7a81e6fee0fa935072884d58ae8e1cc7c"}, - {file = "cloudpickle-1.6.0.tar.gz", hash = "sha256:9bc994f9e9447593bd0a45371f0e7ac7333710fcf64a4eb9834bf149f4ef2f32"}, + {file = "cloudpickle-2.0.0-py3-none-any.whl", hash = "sha256:6b2df9741d06f43839a3275c4e6632f7df6487a1f181f5f46a052d3c917c3d11"}, + {file = "cloudpickle-2.0.0.tar.gz", hash = "sha256:5cd02f3b417a783ba84a4ec3e290ff7929009fe51f6405423cfccfadd43ba4a4"}, ] cluster-tools = [ {file = "cluster_tools-1.60.tar.gz", hash = "sha256:732ad5b6dc25e33fc664edc02b68beb76c66418c2dbf2b0695c988f6df09a0ba"}, @@ -1051,16 +1053,16 @@ cycler = [ {file = "cycler-0.10.0.tar.gz", hash = "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"}, ] executing = [ - {file = "executing-0.8.0-py2.py3-none-any.whl", hash = "sha256:7ef8637519b0c01b69c4a1785a0d13dcc2cf9f0d078804268887edad2b4f26d1"}, - {file = "executing-0.8.0.tar.gz", hash = "sha256:77ed91874d321338865ea2bf7337636a5de8e9a7f8323527d29c4fad2a1b48f7"}, + {file = "executing-0.8.1-py2.py3-none-any.whl", hash = "sha256:cfaa61564eff6e7a3afda98ea0ba7c646171d9e325203a159bf29a3c2438c129"}, + {file = "executing-0.8.1.tar.gz", hash = "sha256:f3dd49578371a633bd1ff5ca0af4afad65e6d549a06eb35ef20df198eced58ef"}, ] h11 = [ {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, ] httpcore = [ - {file = "httpcore-0.13.6-py3-none-any.whl", hash = "sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff"}, - {file = "httpcore-0.13.6.tar.gz", hash = "sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e"}, + {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, + {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, ] httpx = [ {file = "httpx-0.18.2-py3-none-any.whl", hash = "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c"}, @@ -1272,8 +1274,8 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] networkx = [ - {file = "networkx-2.6.2-py3-none-any.whl", hash = "sha256:5fcb7004be69e8fbdf07dcb502efa5c77cadcaad6982164134eeb9721f826c2e"}, - {file = "networkx-2.6.2.tar.gz", hash = "sha256:2306f1950ce772c5a59a57f5486d59bb9cab98497c45fc49cbc45ac0dec119bb"}, + {file = "networkx-2.6.3-py3-none-any.whl", hash = "sha256:80b6b89c77d1dfb64a4c7854981b60aeea6360ac02c6d4e4913319e0a313abef"}, + {file = "networkx-2.6.3.tar.gz", hash = "sha256:c0946ed31d71f1b732b5aaa6da5a0388a345019af232ce2f49c766e2d6795c51"}, ] numpy = [ {file = "numpy-1.21.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38e8648f9449a549a7dfe8d8755a5979b45b3538520d1e735637ef28e8c2dc50"}, @@ -1328,53 +1330,67 @@ pathspec = [ {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, ] pillow = [ - {file = "Pillow-8.3.1-1-cp36-cp36m-win_amd64.whl", hash = "sha256:fd7eef578f5b2200d066db1b50c4aa66410786201669fb76d5238b007918fb24"}, - {file = "Pillow-8.3.1-1-cp37-cp37m-win_amd64.whl", hash = "sha256:75e09042a3b39e0ea61ce37e941221313d51a9c26b8e54e12b3ececccb71718a"}, - {file = "Pillow-8.3.1-1-cp38-cp38-win_amd64.whl", hash = "sha256:c0e0550a404c69aab1e04ae89cca3e2a042b56ab043f7f729d984bf73ed2a093"}, - {file = "Pillow-8.3.1-1-cp39-cp39-win_amd64.whl", hash = "sha256:479ab11cbd69612acefa8286481f65c5dece2002ffaa4f9db62682379ca3bb77"}, - {file = "Pillow-8.3.1-1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f156d6ecfc747ee111c167f8faf5f4953761b5e66e91a4e6767e548d0f80129c"}, - {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, - {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, - {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, - {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fc214a6b75d2e0ea7745488da7da3c381f41790812988c7a92345978414fad37"}, - {file = "Pillow-8.3.1-cp36-cp36m-win32.whl", hash = "sha256:a17ca41f45cf78c2216ebfab03add7cc350c305c38ff34ef4eef66b7d76c5229"}, - {file = "Pillow-8.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:67b3666b544b953a2777cb3f5a922e991be73ab32635666ee72e05876b8a92de"}, - {file = "Pillow-8.3.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ff04c373477723430dce2e9d024c708a047d44cf17166bf16e604b379bf0ca14"}, - {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9364c81b252d8348e9cc0cb63e856b8f7c1b340caba6ee7a7a65c968312f7dab"}, - {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2f381932dca2cf775811a008aa3027671ace723b7a38838045b1aee8669fdcf"}, - {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d0da39795049a9afcaadec532e7b669b5ebbb2a9134576ebcc15dd5bdae33cc0"}, - {file = "Pillow-8.3.1-cp37-cp37m-win32.whl", hash = "sha256:2b6dfa068a8b6137da34a4936f5a816aba0ecc967af2feeb32c4393ddd671cba"}, - {file = "Pillow-8.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a4eef1ff2d62676deabf076f963eda4da34b51bc0517c70239fafed1d5b51500"}, - {file = "Pillow-8.3.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:660a87085925c61a0dcc80efb967512ac34dbb256ff7dd2b9b4ee8dbdab58cf4"}, - {file = "Pillow-8.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:15a2808e269a1cf2131930183dcc0419bc77bb73eb54285dde2706ac9939fa8e"}, - {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:969cc558cca859cadf24f890fc009e1bce7d7d0386ba7c0478641a60199adf79"}, - {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ee77c14a0299d0541d26f3d8500bb57e081233e3fa915fa35abd02c51fa7fae"}, - {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c11003197f908878164f0e6da15fce22373ac3fc320cda8c9d16e6bba105b844"}, - {file = "Pillow-8.3.1-cp38-cp38-win32.whl", hash = "sha256:3f08bd8d785204149b5b33e3b5f0ebbfe2190ea58d1a051c578e29e39bfd2367"}, - {file = "Pillow-8.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:70af7d222df0ff81a2da601fab42decb009dc721545ed78549cb96e3a1c5f0c8"}, - {file = "Pillow-8.3.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:37730f6e68bdc6a3f02d2079c34c532330d206429f3cee651aab6b66839a9f0e"}, - {file = "Pillow-8.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bc3c7ef940eeb200ca65bd83005eb3aae8083d47e8fcbf5f0943baa50726856"}, - {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c35d09db702f4185ba22bb33ef1751ad49c266534339a5cebeb5159d364f6f82"}, - {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b2efa07f69dc395d95bb9ef3299f4ca29bcb2157dc615bae0b42c3c20668ffc"}, - {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc866706d56bd3a7dbf8bac8660c6f6462f2f2b8a49add2ba617bc0c54473d83"}, - {file = "Pillow-8.3.1-cp39-cp39-win32.whl", hash = "sha256:9a211b663cf2314edbdb4cf897beeb5c9ee3810d1d53f0e423f06d6ebbf9cd5d"}, - {file = "Pillow-8.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:c2a5ff58751670292b406b9f06e07ed1446a4b13ffced6b6cab75b857485cbc8"}, - {file = "Pillow-8.3.1-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c379425c2707078dfb6bfad2430728831d399dc95a7deeb92015eb4c92345eaf"}, - {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:114f816e4f73f9ec06997b2fde81a92cbf0777c9e8f462005550eed6bae57e63"}, - {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8960a8a9f4598974e4c2aeb1bff9bdd5db03ee65fd1fce8adf3223721aa2a636"}, - {file = "Pillow-8.3.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:147bd9e71fb9dcf08357b4d530b5167941e222a6fd21f869c7911bac40b9994d"}, - {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1fd5066cd343b5db88c048d971994e56b296868766e461b82fa4e22498f34d77"}, - {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4ebde71785f8bceb39dcd1e7f06bcc5d5c3cf48b9f69ab52636309387b097c8"}, - {file = "Pillow-8.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4"}, - {file = "Pillow-8.3.1.tar.gz", hash = "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:c691b26283c3a31594683217d746f1dad59a7ae1d4cfc24626d7a064a11197d4"}, + {file = "Pillow-8.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f514c2717012859ccb349c97862568fdc0479aad85b0270d6b5a6509dbc142e2"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be25cb93442c6d2f8702c599b51184bd3ccd83adebd08886b682173e09ef0c3f"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d675a876b295afa114ca8bf42d7f86b5fb1298e1b6bb9a24405a3f6c8338811c"}, + {file = "Pillow-8.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59697568a0455764a094585b2551fd76bfd6b959c9f92d4bdec9d0e14616303a"}, + {file = "Pillow-8.3.2-cp310-cp310-win32.whl", hash = "sha256:2d5e9dc0bf1b5d9048a94c48d0813b6c96fccfa4ccf276d9c36308840f40c228"}, + {file = "Pillow-8.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:11c27e74bab423eb3c9232d97553111cc0be81b74b47165f07ebfdd29d825875"}, + {file = "Pillow-8.3.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:11eb7f98165d56042545c9e6db3ce394ed8b45089a67124298f0473b29cb60b2"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f23b2d3079522fdf3c09de6517f625f7a964f916c956527bed805ac043799b8"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19ec4cfe4b961edc249b0e04b5618666c23a83bc35842dea2bfd5dfa0157f81b"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5a31c07cea5edbaeb4bdba6f2b87db7d3dc0f446f379d907e51cc70ea375629"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15ccb81a6ffc57ea0137f9f3ac2737ffa1d11f786244d719639df17476d399a7"}, + {file = "Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8f284dc1695caf71a74f24993b7c7473d77bc760be45f776a2c2f4e04c170550"}, + {file = "Pillow-8.3.2-cp36-cp36m-win32.whl", hash = "sha256:4abc247b31a98f29e5224f2d31ef15f86a71f79c7f4d2ac345a5d551d6393073"}, + {file = "Pillow-8.3.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a048dad5ed6ad1fad338c02c609b862dfaa921fcd065d747194a6805f91f2196"}, + {file = "Pillow-8.3.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:06d1adaa284696785375fa80a6a8eb309be722cf4ef8949518beb34487a3df71"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd24054aaf21e70a51e2a2a5ed1183560d3a69e6f9594a4bfe360a46f94eba83"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a330bf7014ee034046db43ccbb05c766aa9e70b8d6c5260bfc38d73103b0ba"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13654b521fb98abdecec105ea3fb5ba863d1548c9b58831dd5105bb3873569f1"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1bd983c565f92779be456ece2479840ec39d386007cd4ae83382646293d681b"}, + {file = "Pillow-8.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4326ea1e2722f3dc00ed77c36d3b5354b8fb7399fb59230249ea6d59cbed90da"}, + {file = "Pillow-8.3.2-cp37-cp37m-win32.whl", hash = "sha256:085a90a99404b859a4b6c3daa42afde17cb3ad3115e44a75f0d7b4a32f06a6c9"}, + {file = "Pillow-8.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:18a07a683805d32826c09acfce44a90bf474e6a66ce482b1c7fcd3757d588df3"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4e59e99fd680e2b8b11bbd463f3c9450ab799305d5f2bafb74fefba6ac058616"}, + {file = "Pillow-8.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d89a2e9219a526401015153c0e9dd48319ea6ab9fe3b066a20aa9aee23d9fd3"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fd98c8294f57636084f4b076b75f86c57b2a63a8410c0cd172bc93695ee979"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b11c9d310a3522b0fd3c35667914271f570576a0e387701f370eb39d45f08a4"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0412516dcc9de9b0a1e0ae25a280015809de8270f134cc2c1e32c4eeb397cf30"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bcb04ff12e79b28be6c9988f275e7ab69f01cc2ba319fb3114f87817bb7c74b6"}, + {file = "Pillow-8.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b9911ec70731711c3b6ebcde26caea620cbdd9dcb73c67b0730c8817f24711b"}, + {file = "Pillow-8.3.2-cp38-cp38-win32.whl", hash = "sha256:ce2e5e04bb86da6187f96d7bab3f93a7877830981b37f0287dd6479e27a10341"}, + {file = "Pillow-8.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:35d27687f027ad25a8d0ef45dd5208ef044c588003cdcedf05afb00dbc5c2deb"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:04835e68ef12904bc3e1fd002b33eea0779320d4346082bd5b24bec12ad9c3e9"}, + {file = "Pillow-8.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10e00f7336780ca7d3653cf3ac26f068fa11b5a96894ea29a64d3dc4b810d630"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cde7a4d3687f21cffdf5bb171172070bb95e02af448c4c8b2f223d783214056"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c3ff00110835bdda2b1e2b07f4a2548a39744bb7de5946dc8e95517c4fb2ca6"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d409030bf3bd05fa66fb5fdedc39c521b397f61ad04309c90444e893d05f7d"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bff50ba9891be0a004ef48828e012babaaf7da204d81ab9be37480b9020a82b"}, + {file = "Pillow-8.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7dbfbc0020aa1d9bc1b0b8bcf255a7d73f4ad0336f8fd2533fcc54a4ccfb9441"}, + {file = "Pillow-8.3.2-cp39-cp39-win32.whl", hash = "sha256:963ebdc5365d748185fdb06daf2ac758116deecb2277ec5ae98139f93844bc09"}, + {file = "Pillow-8.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:cc9d0dec711c914ed500f1d0d3822868760954dce98dfb0b7382a854aee55d19"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2c661542c6f71dfd9dc82d9d29a8386287e82813b0375b3a02983feac69ef864"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:548794f99ff52a73a156771a0402f5e1c35285bd981046a502d7e4793e8facaa"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b68f565a4175e12e68ca900af8910e8fe48aaa48fd3ca853494f384e11c8bcd"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:838eb85de6d9307c19c655c726f8d13b8b646f144ca6b3771fa62b711ebf7624"}, + {file = "Pillow-8.3.2-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:feb5db446e96bfecfec078b943cc07744cc759893cef045aa8b8b6d6aaa8274e"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:fc0db32f7223b094964e71729c0361f93db43664dd1ec86d3df217853cedda87"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fd4fd83aa912d7b89b4b4a1580d30e2a4242f3936882a3f433586e5ab97ed0d5"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d0c8ebbfd439c37624db98f3877d9ed12c137cadd99dde2d2eae0dab0bbfc355"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6cb3dd7f23b044b0737317f892d399f9e2f0b3a02b22b2c692851fb8120d82c6"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a66566f8a22561fc1a88dc87606c69b84fa9ce724f99522cf922c801ec68f5c1"}, + {file = "Pillow-8.3.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce651ca46d0202c302a535d3047c55a0131a720cf554a578fc1b8a2aff0e7d96"}, + {file = "Pillow-8.3.2.tar.gz", hash = "sha256:dde3f3ed8d00c72631bc19cbfff8ad3b6215062a5eed402381ad365f82f0c18c"}, ] platformdirs = [ - {file = "platformdirs-2.2.0-py3-none-any.whl", hash = "sha256:4666d822218db6a262bdfdc9c39d21f23b4cfdb08af331a81e92751daf6c866c"}, - {file = "platformdirs-2.2.0.tar.gz", hash = "sha256:632daad3ab546bd8e6af0537d09805cec458dce201bccfe23012df73332e181e"}, + {file = "platformdirs-2.3.0-py3-none-any.whl", hash = "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648"}, + {file = "platformdirs-2.3.0.tar.gz", hash = "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f"}, ] pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, @@ -1417,8 +1433,8 @@ pygments = [ {file = "Pygments-2.10.0.tar.gz", hash = "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"}, ] pylint = [ - {file = "pylint-2.10.2-py3-none-any.whl", hash = "sha256:e178e96b6ba171f8ef51fbce9ca30931e6acbea4a155074d80cc081596c9e852"}, - {file = "pylint-2.10.2.tar.gz", hash = "sha256:6758cce3ddbab60c52b57dcc07f0c5d779e5daf0cf50f6faacbef1d3ea62d2a1"}, + {file = "pylint-2.11.1-py3-none-any.whl", hash = "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126"}, + {file = "pylint-2.11.1.tar.gz", hash = "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, @@ -1448,8 +1464,8 @@ pyrsistent = [ {file = "pyrsistent-0.18.0.tar.gz", hash = "sha256:773c781216f8c2900b42a7b638d5b517bb134ae1acbebe4d1e8f1f41ea60eb4b"}, ] pytest = [ - {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, - {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, @@ -1523,55 +1539,55 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] regex = [ - {file = "regex-2021.8.27-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:507861cf3d97a86fbe26ea6cc04660ae028b9e4080b8290e28b99547b4e15d89"}, - {file = "regex-2021.8.27-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:105122fa63da98d8456d5026bc6ac5a1399fd82fa6bad22c6ea641b1572c9142"}, - {file = "regex-2021.8.27-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83946ca9278b304728b637bc8d8200ab1663a79de85e47724594917aeed0e892"}, - {file = "regex-2021.8.27-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee318974a1fdacba1701bc9e552e9015788d6345416364af6fa987424ff8df53"}, - {file = "regex-2021.8.27-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dde0ac721c7c5bfa5f9fc285e811274dec3c392f2c1225f7d07ca98a8187ca84"}, - {file = "regex-2021.8.27-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:862b6164e9a38b5c495be2c2854e75fd8af12c5be4c61dc9b42d255980d7e907"}, - {file = "regex-2021.8.27-cp310-cp310-win32.whl", hash = "sha256:7684016b73938ca12d160d2907d141f06b7597bd17d854e32bb7588be01afa1d"}, - {file = "regex-2021.8.27-cp310-cp310-win_amd64.whl", hash = "sha256:a5f3bc727fea58f21d99c22e6d4fca652dc11dbc2a1e7cfc4838cd53b2e3691f"}, - {file = "regex-2021.8.27-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:db888d4fb33a2fd54b57ac55d5015e51fa849f0d8592bd799b4e47f83bd04e00"}, - {file = "regex-2021.8.27-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92eb03f47427fea452ff6956d11f5d5a3f22a048c90a0f34fa223e6badab6c85"}, - {file = "regex-2021.8.27-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7406dd2e44c7cfb4680c0a45a03264381802c67890cf506c147288f04c67177d"}, - {file = "regex-2021.8.27-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7db58ad61f3f6ea393aaf124d774ee0c58806320bc85c06dc9480f5c7219c250"}, - {file = "regex-2021.8.27-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd45b4542134de63e7b9dd653e0a2d7d47ffed9615e3637c27ca5f6b78ea68bb"}, - {file = "regex-2021.8.27-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e02dad60e3e8442eefd28095e99b2ac98f2b8667167493ac6a2f3aadb5d84a17"}, - {file = "regex-2021.8.27-cp36-cp36m-win32.whl", hash = "sha256:de0d06ccbc06af5bf93bddec10f4f80275c5d74ea6d28b456931f3955f58bc8c"}, - {file = "regex-2021.8.27-cp36-cp36m-win_amd64.whl", hash = "sha256:2a0a5e323cf86760784ce2b91d8ab5ea09d0865d6ef4da0151e03d15d097b24e"}, - {file = "regex-2021.8.27-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6530b7b9505123cdea40a2301225183ca65f389bc6129f0c225b9b41680268d8"}, - {file = "regex-2021.8.27-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f3e36086d6631ceaf468503f96a3be0d247caef0660c9452fb1b0c055783851"}, - {file = "regex-2021.8.27-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ddb4f9ce6bb388ecc97b4b3eb37e786f05d7d5815e8822e0d87a3dbd7100649"}, - {file = "regex-2021.8.27-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2de1429e4eeab799c168a4f6e6eecdf30fcaa389bba4039cc8a065d6b7aad647"}, - {file = "regex-2021.8.27-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f03fc0a25122cdcbf39136510d4ea7627f732206892db522adf510bc03b8c67"}, - {file = "regex-2021.8.27-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:503c1ba0920a46a1844363725215ef44d59fcac2bd2c03ae3c59aa9d08d29bd6"}, - {file = "regex-2021.8.27-cp37-cp37m-win32.whl", hash = "sha256:24d68499a27b2d93831fde4a9b84ea5b19e0ab141425fbc9ab1e5b4dad179df7"}, - {file = "regex-2021.8.27-cp37-cp37m-win_amd64.whl", hash = "sha256:6729914dd73483cd1c8aaace3ac082436fc98b0072743ac136eaea0b3811d42f"}, - {file = "regex-2021.8.27-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d9cbe0c755ab8b6f583169c0783f7278fc6b195e423b09c5a8da6f858025e96"}, - {file = "regex-2021.8.27-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2404336fd16788ea757d4218a2580de60adb052d9888031e765320be8884309"}, - {file = "regex-2021.8.27-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:208851a2f8dd31e468f0b5aa6c94433975bd67a107a4e7da3bdda947c9f85e25"}, - {file = "regex-2021.8.27-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3ee8ad16a35c45a5bab098e39020ecb6fec3b0e700a9d88983d35cbabcee79c8"}, - {file = "regex-2021.8.27-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56ae6e3cf0506ec0c40b466e31f41ee7a7149a2b505ae0ee50edd9043b423d27"}, - {file = "regex-2021.8.27-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2778c6cb379d804e429cc8e627392909e60db5152b42c695c37ae5757aae50ae"}, - {file = "regex-2021.8.27-cp38-cp38-win32.whl", hash = "sha256:e960fe211496333b2f7e36badf4c22a919d740386681f79139ee346b403d1ca1"}, - {file = "regex-2021.8.27-cp38-cp38-win_amd64.whl", hash = "sha256:116c277774f84266044e889501fe79cfd293a8b4336b7a5e89b9f20f1e5a9f21"}, - {file = "regex-2021.8.27-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32753eda8d413ce4f208cfe01dd61171a78068a6f5d5f38ccd751e00585cdf1d"}, - {file = "regex-2021.8.27-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84057cfae5676f456b03970eb78b7e182fddc80c2daafd83465a3d6ca9ff8dbf"}, - {file = "regex-2021.8.27-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6180dbf5945b27e9420e1b58c3cacfc79ad5278bdad3ea35109f5680fbe16d1"}, - {file = "regex-2021.8.27-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b158f673ae6a6523f13704f70aa7e4ce875f91e379bece4362c89db18db189d5"}, - {file = "regex-2021.8.27-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19acdb8831a4e3b03b23369db43178d8fee1f17b99c83af6cd907886f76bd9d4"}, - {file = "regex-2021.8.27-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:12eaf0bbe568bd62e6cade7937e0bf01a2a4cef49a82f4fd204401e78409e158"}, - {file = "regex-2021.8.27-cp39-cp39-win32.whl", hash = "sha256:1401cfa4320691cbd91191ec678735c727dee674d0997b0902a5a38ad482faf5"}, - {file = "regex-2021.8.27-cp39-cp39-win_amd64.whl", hash = "sha256:0696eb934dee723e3292056a2c046ddb1e4dd3887685783a9f4af638e85dee76"}, - {file = "regex-2021.8.27.tar.gz", hash = "sha256:e9700c52749cb3e90c98efd72b730c97b7e4962992fca5fbcaf1363be8e3b849"}, + {file = "regex-2021.8.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a"}, + {file = "regex-2021.8.28-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308"}, + {file = "regex-2021.8.28-cp310-cp310-win32.whl", hash = "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed"}, + {file = "regex-2021.8.28-cp310-cp310-win_amd64.whl", hash = "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8"}, + {file = "regex-2021.8.28-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1"}, + {file = "regex-2021.8.28-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f"}, + {file = "regex-2021.8.28-cp36-cp36m-win32.whl", hash = "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354"}, + {file = "regex-2021.8.28-cp36-cp36m-win_amd64.whl", hash = "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645"}, + {file = "regex-2021.8.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759"}, + {file = "regex-2021.8.28-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906"}, + {file = "regex-2021.8.28-cp37-cp37m-win32.whl", hash = "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a"}, + {file = "regex-2021.8.28-cp37-cp37m-win_amd64.whl", hash = "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc"}, + {file = "regex-2021.8.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b"}, + {file = "regex-2021.8.28-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e"}, + {file = "regex-2021.8.28-cp38-cp38-win32.whl", hash = "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d"}, + {file = "regex-2021.8.28-cp38-cp38-win_amd64.whl", hash = "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2"}, + {file = "regex-2021.8.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8"}, + {file = "regex-2021.8.28-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed"}, + {file = "regex-2021.8.28-cp39-cp39-win32.whl", hash = "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374"}, + {file = "regex-2021.8.28-cp39-cp39-win_amd64.whl", hash = "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73"}, + {file = "regex-2021.8.28.tar.gz", hash = "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1"}, ] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] rich = [ - {file = "rich-10.9.0-py3-none-any.whl", hash = "sha256:2c84d9b3459c16bf413fe0f9644c7ae1791971e0bb944dfae56e7c7634b187ab"}, - {file = "rich-10.9.0.tar.gz", hash = "sha256:ba285f1c519519490034284e6a9d2e6e3f16dc7690f2de3d9140737d81304d22"}, + {file = "rich-10.10.0-py3-none-any.whl", hash = "sha256:0b8cbcb0b8d476a7f002feaed9f35e51615f673c6c291d76ddf0c555574fd3c7"}, + {file = "rich-10.10.0.tar.gz", hash = "sha256:bacf58b25fea6b920446fe4e7abdc6c7664c4530c4098e7a1bc79b16b8551dfa"}, ] scikit-image = [ {file = "scikit-image-0.16.2.tar.gz", hash = "sha256:dd7fbd32da74d4e9967dc15845f731f16e7966cee61f5dc0e12e2abb1305068c"}, @@ -1672,9 +1688,9 @@ types-python-dateutil = [ {file = "types_python_dateutil-0.1.6-py3-none-any.whl", hash = "sha256:5b6241ea9fca2d8878cc152017d9524da62a7a856b98e31006e68b02aab47442"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, - {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, - {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, + {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, + {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, + {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, ] wkw = [ {file = "wkw-1.1.11-py3-none-manylinux1_x86_64.whl", hash = "sha256:302a37bbfa4d531a715d2ca9b8f5ac4e8ea21b09ceb7708af386070ec8474b31"}, diff --git a/webknossos/pyproject.toml b/webknossos/pyproject.toml index 4dab66778..2ab64cbaa 100644 --- a/webknossos/pyproject.toml +++ b/webknossos/pyproject.toml @@ -18,10 +18,10 @@ networkx = "^2.6.2" numpy = "^1.15.0" # see https://numpy.org/neps/nep-0029-deprecation_policy.html#support-table python-dateutil = "^2.8.0" python-dotenv = "^0.19.0" -rich = "^10.9.0" scikit-image = "^0.16.0" scipy = "^1.4.0" wkw = "1.1.11" +rich = "^10.9.0" [tool.poetry.dev-dependencies] # autoflake diff --git a/webknossos/tests/test_bounding_box.py b/webknossos/tests/test_bounding_box.py index 31bbdf9c3..5da962622 100644 --- a/webknossos/tests/test_bounding_box.py +++ b/webknossos/tests/test_bounding_box.py @@ -46,3 +46,16 @@ def test_in_mag() -> None: assert BoundingBox((2, 2, 2), (10, 10, 10)).in_mag(Mag(2)) == BoundingBox( topleft=(1, 1, 1), size=(5, 5, 5) ) + + +def test_with_bounds() -> None: + + assert BoundingBox((1, 2, 3), (5, 5, 5)).with_bounds_x(0, 10) == BoundingBox( + (0, 2, 3), (10, 5, 5) + ) + assert BoundingBox((1, 2, 3), (5, 5, 5)).with_bounds_y( + new_topleft_y=0 + ) == BoundingBox((1, 0, 3), (5, 5, 5)) + assert BoundingBox((1, 2, 3), (5, 5, 5)).with_bounds_z( + new_size_z=10 + ) == BoundingBox((1, 2, 3), (5, 5, 10)) diff --git a/webknossos/tests/test_dataset.py b/webknossos/tests/test_dataset.py index d1dc718b5..6067e37ad 100644 --- a/webknossos/tests/test_dataset.py +++ b/webknossos/tests/test_dataset.py @@ -209,6 +209,7 @@ def test_modify_existing_dataset() -> None: ) ds2 = Dataset(TESTOUTPUT_DIR / "simple_wk_dataset") + ds2.add_layer( "segmentation", LayerCategories.SEGMENTATION_TYPE, @@ -761,9 +762,9 @@ def test_changing_layer_bounding_box() -> None: original_data = mag.read(size=bbox_size) assert original_data.shape == (3, 24, 24, 24) - old_bbox = layer.bounding_box - old_bbox.size = np.array([12, 12, 10]) - layer.bounding_box = old_bbox # decrease bounding box + layer.bounding_box = layer.bounding_box.with_size( + [12, 12, 10] + ) # decrease bounding box bbox_size = ds.get_layer("color").bounding_box.size assert tuple(bbox_size) == (12, 12, 10) @@ -771,8 +772,9 @@ def test_changing_layer_bounding_box() -> None: assert less_data.shape == (3, 12, 12, 10) assert np.array_equal(original_data[:, :12, :12, :10], less_data) - old_bbox.size = np.array([36, 48, 60]) - layer.bounding_box = old_bbox # increase the bounding box + layer.bounding_box = layer.bounding_box.with_size( + [36, 48, 60] + ) # increase the bounding box bbox_size = ds.get_layer("color").bounding_box.size assert tuple(bbox_size) == (36, 48, 60) diff --git a/webknossos/tests/test_mag.py b/webknossos/tests/test_mag.py index ff1c9dd6e..344679456 100644 --- a/webknossos/tests/test_mag.py +++ b/webknossos/tests/test_mag.py @@ -5,14 +5,14 @@ def test_mag_constructor() -> None: mag = Mag(16) - assert mag.to_array() == [16, 16, 16] + assert mag.to_list() == [16, 16, 16] mag = Mag("256") - assert mag.to_array() == [256, 256, 256] + assert mag.to_list() == [256, 256, 256] mag = Mag("16-2-4") - assert mag.to_array() == [16, 2, 4] + assert mag.to_list() == [16, 2, 4] mag1 = Mag("16-2-4") mag2 = Mag("8-2-4") @@ -20,6 +20,6 @@ def test_mag_constructor() -> None: assert mag1 > mag2 assert mag1.to_layer_name() == "16-2-4" - assert np.all(mag1.as_np() == np.array([16, 2, 4])) + assert np.all(mag1.to_np() == np.array([16, 2, 4])) assert mag1 == Mag(mag1) - assert mag1 == Mag(mag1.as_np()) + assert mag1 == Mag(mag1.to_np()) diff --git a/webknossos/tests/test_vec3_int.py b/webknossos/tests/test_vec3_int.py new file mode 100644 index 000000000..862b2a2de --- /dev/null +++ b/webknossos/tests/test_vec3_int.py @@ -0,0 +1,95 @@ +import numpy as np + +from webknossos.geometry import Mag, Vec3Int + + +def test_with() -> None: + + assert Vec3Int(1, 2, 3).with_x(5) == Vec3Int(5, 2, 3) + assert Vec3Int(1, 2, 3).with_y(5) == Vec3Int(1, 5, 3) + assert Vec3Int(1, 2, 3).with_z(5) == Vec3Int(1, 2, 5) + + +def test_import() -> None: + + assert Vec3Int(1, 2, 3) == Vec3Int(1, 2, 3) + assert Vec3Int((1, 2, 3)) == Vec3Int(1, 2, 3) + assert Vec3Int([1, 2, 3]) == Vec3Int(1, 2, 3) + assert Vec3Int(i for i in [1, 2, 3]) == Vec3Int(1, 2, 3) + assert Vec3Int(np.array([1, 2, 3])) == Vec3Int(1, 2, 3) + assert Vec3Int(Mag(4)) == Vec3Int(4, 4, 4) + + +def test_export() -> None: + + assert Vec3Int(1, 2, 3).x == 1 + assert Vec3Int(1, 2, 3).y == 2 + assert Vec3Int(1, 2, 3).z == 3 + assert Vec3Int(1, 2, 3)[0] == 1 + assert Vec3Int(1, 2, 3)[1] == 2 + assert Vec3Int(1, 2, 3)[2] == 3 + assert np.array_equal(Vec3Int(1, 2, 3).to_np(), np.array([1, 2, 3])) + assert Vec3Int(1, 2, 3).to_list() == [1, 2, 3] + assert Vec3Int(1, 2, 3).to_tuple() == (1, 2, 3) + + +def test_operator_arithmetic() -> None: + + # other is Vec3Int + assert Vec3Int(1, 2, 3) + Vec3Int(4, 5, 6) == Vec3Int(5, 7, 9) + assert Vec3Int(1, 2, 3) + Vec3Int(0, 0, 0) == Vec3Int(1, 2, 3) + assert Vec3Int(1, 2, 3) - Vec3Int(4, 5, 6) == Vec3Int(-3, -3, -3) + assert Vec3Int(1, 2, 3) * Vec3Int(4, 5, 6) == Vec3Int(4, 10, 18) + assert Vec3Int(4, 5, 6) // Vec3Int(1, 2, 3) == Vec3Int(4, 2, 2) + assert Vec3Int(4, 5, 6) % Vec3Int(1, 2, 3) == Vec3Int(0, 1, 0) + + # other is scalar int + assert Vec3Int(1, 2, 3) * 3 == Vec3Int(3, 6, 9) + assert Vec3Int(1, 2, 3) + 3 == Vec3Int(4, 5, 6) + assert Vec3Int(1, 2, 3) - 3 == Vec3Int(-2, -1, 0) + assert Vec3Int(4, 5, 6) // 2 == Vec3Int(2, 2, 3) + assert Vec3Int(4, 5, 6) % 3 == Vec3Int(1, 2, 0) + + # other is Vec3IntLike (e.g. tuple) + assert Vec3Int(1, 2, 3) + (4, 5, 6) == Vec3Int(5, 7, 9) + + # be wary of the tuple “+” operation: + assert (1, 2, 3) + Vec3Int(4, 5, 6) == (1, 2, 3, 4, 5, 6) + + assert -Vec3Int(1, 2, 3) == Vec3Int(-1, -2, -3) + + +def test_method_arithmetic() -> None: + + assert Vec3Int(4, 5, 6).ceildiv(Vec3Int(1, 2, 3)) == Vec3Int(4, 3, 2) + assert Vec3Int(4, 5, 6).ceildiv((1, 2, 3)) == Vec3Int(4, 3, 2) + assert Vec3Int(4, 5, 6).ceildiv(2) == Vec3Int(2, 3, 3) + + assert Vec3Int(1, 2, 6).pairmax(Vec3Int(4, 5, 3)) == Vec3Int(4, 5, 6) + assert Vec3Int(1, 2, 6).pairmin(Vec3Int(4, 5, 3)) == Vec3Int(1, 2, 3) + + +def test_repr() -> None: + + assert str(Vec3Int(1, 2, 3)) == "Vec3Int(1,2,3)" + + +def test_prod() -> None: + + assert Vec3Int(1, 2, 3).prod() == 6 + + +def test_contains() -> None: + + assert Vec3Int(1, 2, 3).contains(1) + assert not Vec3Int(1, 2, 3).contains(4) + + +def test_custom_initialization() -> None: + + assert Vec3Int.zeros() == Vec3Int(0, 0, 0) + assert Vec3Int.ones() == Vec3Int(1, 1, 1) + assert Vec3Int.full(4) == Vec3Int(4, 4, 4) + + assert Vec3Int.ones() - Vec3Int.ones() == Vec3Int.zeros() + assert Vec3Int.full(4) == Vec3Int.ones() * 4 diff --git a/webknossos/webknossos/dataset/dataset.py b/webknossos/webknossos/dataset/dataset.py index c9ed9714e..7390e4c76 100644 --- a/webknossos/webknossos/dataset/dataset.py +++ b/webknossos/webknossos/dataset/dataset.py @@ -9,10 +9,11 @@ from shutil import rmtree from typing import Any, Dict, Optional, Tuple, Union, cast +import attr import numpy as np import wkw -from webknossos.geometry import BoundingBox +from webknossos.geometry import BoundingBox, Vec3Int from webknossos.utils import get_executor_for_args from .layer import ( @@ -32,7 +33,6 @@ _extract_num_channels, _properties_floating_type_to_python_type, dataset_converter, - layer_properties_converter, ) from .view import View @@ -238,8 +238,8 @@ def add_layer( segmentation_layer_properties: SegmentationLayerProperties = ( SegmentationLayerProperties( - **layer_properties_converter.unstructure( - layer_properties + **( + attr.asdict(layer_properties, recurse=False) ), # use all attributes from LayerProperties largest_segment_id=kwargs["largest_segment_id"], ) @@ -517,12 +517,9 @@ def copy_dataset( # The bounding box needs to be updated manually because chunked views do not have a reference to the dataset itself # The base view of a MagDataset always starts at (0, 0, 0) - target_mag._global_offset = (0, 0, 0) - target_mag._size = cast( - Tuple[int, int, int], - tuple( - bbox.align_with_mag(mag, ceil=True).in_mag(mag).bottomright - ), + target_mag._global_offset = Vec3Int(0, 0, 0) + target_mag._size = ( + bbox.align_with_mag(mag, ceil=True).in_mag(mag).bottomright ) target_mag.layer.bounding_box = bbox diff --git a/webknossos/webknossos/dataset/downsampling_utils.py b/webknossos/webknossos/dataset/downsampling_utils.py index 3eb7dfe4d..b33b74645 100644 --- a/webknossos/webknossos/dataset/downsampling_utils.py +++ b/webknossos/webknossos/dataset/downsampling_utils.py @@ -2,13 +2,13 @@ import math from enum import Enum from itertools import product -from typing import Callable, List, Optional, Tuple, Union, cast +from typing import Callable, List, Optional, Tuple, cast import numpy as np from scipy.ndimage import zoom from wkw import wkw -from webknossos.geometry import Mag +from webknossos.geometry import Mag, Vec3Int, Vec3IntLike from webknossos.utils import time_start, time_stop from .view import View @@ -33,9 +33,6 @@ class InterpolationModes(Enum): DEFAULT_EDGE_LEN = 256 -Vec3 = Union[Tuple[int, int, int], np.ndarray] - - def determine_buffer_edge_len(dataset: wkw.Dataset) -> int: return min(DEFAULT_EDGE_LEN, dataset.header.file_len * dataset.header.block_len) @@ -43,16 +40,16 @@ def determine_buffer_edge_len(dataset: wkw.Dataset) -> int: def calculate_mags_to_downsample( from_mag: Mag, max_mag: Mag, scale: Optional[Tuple[float, float, float]] ) -> List[Mag]: - assert np.all(from_mag.as_np() <= max_mag.as_np()) + assert np.all(from_mag.to_np() <= max_mag.to_np()) mags = [] current_mag = from_mag while current_mag < max_mag: if scale is None: # In case the sampling mode is CONSTANT_Z or ISOTROPIC: - current_mag = Mag(np.minimum(current_mag.as_np() * 2, max_mag.as_np())) + current_mag = Mag(np.minimum(current_mag.to_np() * 2, max_mag.to_np())) else: # In case the sampling mode is ANISOTROPIC: - current_size = current_mag.as_np() * np.array(scale) + current_size = current_mag.to_np() * np.array(scale) min_value = np.min(current_size) min_value_bitmask = np.array(current_size == min_value) factor = min_value_bitmask + 1 @@ -68,11 +65,11 @@ def calculate_mags_to_downsample( # The smaller the ratio between the smallest dimension and the largest dimension, the better. if all_scaled_ratio < min_scaled_ratio: # Multiply all dimensions with "2" - current_mag = Mag(np.minimum(current_mag.as_np() * 2, max_mag.as_np())) + current_mag = Mag(np.minimum(current_mag.to_np() * 2, max_mag.to_np())) else: # Multiply only the minimal dimension by "2". current_mag = Mag( - np.minimum(current_mag.as_np() * factor, max_mag.as_np()) + np.minimum(current_mag.to_np() * factor, max_mag.to_np()) ) mags += [current_mag] @@ -88,7 +85,8 @@ def calculate_mags_to_upsample( ] + [min_mag] -def calculate_default_max_mag(dataset_size: Vec3) -> Mag: +def calculate_default_max_mag(dataset_size: Vec3IntLike) -> Mag: + dataset_size = Vec3Int(dataset_size) # The lowest mag should have a size of ~ 100vx**2 per slice max_x_y = max(dataset_size[0], dataset_size[1]) # highest power of 2 larger (or equal) than max_x_y divided by 100 @@ -234,7 +232,7 @@ def downsample_unpadded_data( logging.info( f"Downsampling buffer of size {buffer.shape} to mag {target_mag.to_layer_name()}" ) - target_mag_np = np.array(target_mag.to_array()) + target_mag_np = np.array(target_mag.to_list()) current_dimension_size = np.array(buffer.shape[1:]) padding_size_for_downsampling = ( target_mag_np - (current_dimension_size % target_mag_np) % target_mag_np @@ -243,12 +241,12 @@ def downsample_unpadded_data( buffer = np.pad( buffer, pad_width=[(0, 0)] + padding_size_for_downsampling, mode="constant" ) - dimension_decrease = np.array([1] + target_mag.to_array()) + dimension_decrease = np.array([1] + target_mag.to_list()) downsampled_buffer_shape = np.array(buffer.shape) // dimension_decrease downsampled_buffer = np.empty(dtype=buffer.dtype, shape=downsampled_buffer_shape) for channel in range(buffer.shape[0]): downsampled_buffer[channel] = downsample_cube( - buffer[channel], target_mag.to_array(), interpolation_mode + buffer[channel], target_mag.to_list(), interpolation_mode ) return downsampled_buffer diff --git a/webknossos/webknossos/dataset/layer.py b/webknossos/webknossos/dataset/layer.py index d0ea46a82..0a2bf3089 100644 --- a/webknossos/webknossos/dataset/layer.py +++ b/webknossos/webknossos/dataset/layer.py @@ -11,7 +11,7 @@ from os.path import join from pathlib import Path from shutil import rmtree -from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union, cast +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union import numpy as np from wkw import wkw @@ -458,23 +458,20 @@ def _assert_mag_does_not_exist_yet( @property def bounding_box(self) -> BoundingBox: - return self._properties.bounding_box.copy() + return self._properties.bounding_box @bounding_box.setter def bounding_box(self, bbox: BoundingBox) -> None: """ Updates the offset and size of the bounding box of this layer in the properties. """ - self._properties.bounding_box = bbox.copy() + self._properties.bounding_box = bbox for mag, mag_view in self.mags.items(): - mag_view._size = cast( - Tuple[int, int, int], - tuple( - self._properties.bounding_box.align_with_mag(mag, ceil=True) - .in_mag(mag) - .bottomright - ), + mag_view._size = ( + self._properties.bounding_box.align_with_mag(mag, ceil=True) + .in_mag(mag) + .bottomright ) self.dataset._export_as_json() @@ -534,8 +531,8 @@ def downsample( elif sampling_mode == SamplingModes.ISOTROPIC: scale = None elif sampling_mode == SamplingModes.CONSTANT_Z: - max_mag_with_fixed_z = max_mag.to_array() - max_mag_with_fixed_z[2] = from_mag.to_array()[2] + max_mag_with_fixed_z = max_mag.to_list() + max_mag_with_fixed_z[2] = from_mag.to_list()[2] max_mag = Mag(max_mag_with_fixed_z) scale = None else: @@ -545,7 +542,7 @@ def downsample( mags_to_downsample = calculate_mags_to_downsample(from_mag, max_mag, scale) - if len(set([max(m.to_array()) for m in mags_to_downsample])) != len( + if len(set([max(m.to_list()) for m in mags_to_downsample])) != len( mags_to_downsample ): msg = ( @@ -605,7 +602,7 @@ def downsample_mag( prev_mag_view = self.mags[from_mag] mag_factors = [ - t // s for (t, s) in zip(target_mag.to_array(), from_mag.to_array()) + t // s for (t, s) in zip(target_mag.to_list(), from_mag.to_list()) ] # initialize the new mag @@ -684,11 +681,11 @@ def downsample_mag_list( ), f"Failed to downsample data. The from_mag ({from_mag}) does not exist." # The lambda function is important because 'sorted(target_mags)' would only sort by the maximum element per mag - target_mags = sorted(target_mags, key=lambda m: m.to_array()) + target_mags = sorted(target_mags, key=lambda m: m.to_list()) for i in range(len(target_mags) - 1): assert np.less_equal( - target_mags[i].as_np(), target_mags[i + 1].as_np() + target_mags[i].to_np(), target_mags[i + 1].to_np() ).all(), ( f"Downsampling failed: cannot downsample {target_mags[i].to_layer_name()} to {target_mags[i + 1].to_layer_name()}. " f"Check 'target_mags' ({', '.join([str(mag) for mag in target_mags])}): each pair of adjacent Mags results in a downsampling step." @@ -735,8 +732,8 @@ def upsample( elif sampling_mode == SamplingModes.ISOTROPIC: scale = None elif sampling_mode == SamplingModes.CONSTANT_Z: - min_mag_with_fixed_z = min_mag.to_array() - min_mag_with_fixed_z[2] = from_mag.to_array()[2] + min_mag_with_fixed_z = min_mag.to_list() + min_mag_with_fixed_z[2] = from_mag.to_list()[2] min_mag = Mag(min_mag_with_fixed_z) scale = self.dataset.scale else: @@ -755,7 +752,7 @@ def upsample( prev_mag_view = self.mags[prev_mag] mag_factors = [ - t / s for (t, s) in zip(target_mag.to_array(), prev_mag.to_array()) + t / s for (t, s) in zip(target_mag.to_list(), prev_mag.to_list()) ] # initialize the new mag diff --git a/webknossos/webknossos/dataset/mag_view.py b/webknossos/webknossos/dataset/mag_view.py index 9cfb250a8..76abe5ab7 100644 --- a/webknossos/webknossos/dataset/mag_view.py +++ b/webknossos/webknossos/dataset/mag_view.py @@ -4,13 +4,13 @@ from argparse import Namespace from os.path import join from pathlib import Path -from typing import TYPE_CHECKING, Generator, List, Tuple, Union, cast +from typing import TYPE_CHECKING, Generator, List, Optional, Tuple, Union, cast from uuid import uuid4 import numpy as np from wkw import wkw -from webknossos.geometry import BoundingBox, Mag +from webknossos.geometry import BoundingBox, Mag, Vec3Int, Vec3IntLike from webknossos.utils import get_executor_for_args, wait_and_ensure_success from .compress_utils import compress_file_job @@ -23,8 +23,6 @@ from .view import View -Vec3 = Union[Tuple[int, int, int], np.ndarray] - def _find_mag_path_on_disk(dataset_path: Path, layer_name: str, mag_name: str) -> Path: mag = Mag(mag_name) @@ -39,7 +37,7 @@ def _find_mag_path_on_disk(dataset_path: Path, layer_name: str, mag_name: str) - def _convert_mag1_offset( mag1_offset: Union[List, np.ndarray], target_mag: Mag ) -> np.ndarray: - return np.array(mag1_offset) // target_mag.as_np() # floor div + return np.array(mag1_offset) // target_mag.to_np() # floor div class MagView(View): @@ -109,7 +107,7 @@ def _properties(self) -> MagViewProperties: return next( mag_property for mag_property in self.layer._properties.wkw_resolutions - if Mag(mag_property.resolution).to_array() == self.mag.to_array() + if mag_property.resolution == self.mag ) @property @@ -120,7 +118,7 @@ def name(self) -> str: def mag(self) -> Mag: return self._mag - def write(self, data: np.ndarray, offset: Vec3 = (0, 0, 0)) -> None: + def write(self, data: np.ndarray, offset: Vec3IntLike = Vec3Int(0, 0, 0)) -> None: """ Writes the `data` at the specified `offset` to disk (like `webknossos.dataset.view.View.write()`). @@ -130,14 +128,16 @@ def write(self, data: np.ndarray, offset: Vec3 = (0, 0, 0)) -> None: Note that writing compressed data which is not aligned with the blocks on disk may result in diminished performance, as full blocks will automatically be read to pad the write actions. """ + offset = Vec3Int(offset) + self._assert_valid_num_channels(data.shape) super().write(data, offset) - current_offset_in_mag1 = self.layer.bounding_box.topleft - current_size_in_mag1 = self.layer.bounding_box.size + current_offset_in_mag1 = self.layer.bounding_box.topleft.to_np() + current_size_in_mag1 = self.layer.bounding_box.size.to_np() - mag_np = self.mag.as_np() + mag_np = self.mag.to_np() - offset_in_mag1 = np.array(offset) * mag_np + offset_in_mag1 = offset.to_np() * mag_np # The (-1, -1, -1) is for backwards compatibility because we used '(-1, -1, -1)' to indicate that there is no data written yet. new_offset_in_mag1 = ( @@ -154,10 +154,8 @@ def write(self, data: np.ndarray, offset: Vec3 = (0, 0, 0)) -> None: ).max(axis=0) total_size_in_mag1 = max_end_offset_in_mag1 - np.array(new_offset_in_mag1) - self._size = cast( - Tuple[int, int, int], - tuple(_convert_mag1_offset(max_end_offset_in_mag1, self.mag)), - ) # The base view of a MagDataset always starts at (0, 0, 0) + # The base view of a MagDataset always starts at (0, 0, 0) + self._size = Vec3Int(_convert_mag1_offset(max_end_offset_in_mag1, self.mag)) self.layer.bounding_box = BoundingBox( new_offset_in_mag1, @@ -166,8 +164,8 @@ def write(self, data: np.ndarray, offset: Vec3 = (0, 0, 0)) -> None: def get_view( self, - offset: Vec3 = None, - size: Vec3 = None, + offset: Optional[Vec3IntLike] = None, + size: Optional[Vec3IntLike] = None, read_only: bool = None, ) -> View: """ @@ -207,25 +205,18 @@ def get_view( bb = self.layer.bounding_box # The (-1, -1, -1) is for backwards compatibility because we used '(-1, -1, -1)' to indicate that there is no data written yet. - if tuple(bb.topleft) == (-1, -1, -1): - bb.topleft = np.array((0, 0, 0)) + if bb.topleft == Vec3Int(-1, -1, -1): + bb = bb.with_topleft((0, 0, 0)) bb = bb.align_with_mag(self.mag, ceil=True).in_mag(self.mag) - view_offset = cast( - Tuple[int, int, int], - tuple(offset if offset is not None else tuple(bb.topleft)), - ) - - if size is None: - size = cast( - Tuple[int, int, int], tuple(bb.bottomright - np.array(view_offset)) - ) + offset = Vec3Int(offset) if offset is not None else bb.topleft + size = Vec3Int(size) if size is not None else bb.bottomright - offset - assert bb.contains_bbox(BoundingBox(view_offset, size)) or read_only + assert bb.contains_bbox(BoundingBox(offset, size)) or read_only return super().get_view( - view_offset, - cast(Tuple[int, int, int], tuple(size)), + offset, + size, read_only, ) diff --git a/webknossos/webknossos/dataset/properties.py b/webknossos/webknossos/dataset/properties.py index 807fbeb65..877da186d 100644 --- a/webknossos/webknossos/dataset/properties.py +++ b/webknossos/webknossos/dataset/properties.py @@ -98,7 +98,7 @@ class LayerViewConfiguration: @attr.define class MagViewProperties: - resolution: Union[int, Mag] + resolution: Mag cube_length: int @@ -133,13 +133,13 @@ class DatasetProperties: dataset_converter = cattr.Converter() # register (un-)structure hooks for non-attr-classes -bbox_to_wkw: Callable[[BoundingBox], dict] = lambda o: o.as_wkw() +bbox_to_wkw: Callable[[BoundingBox], dict] = lambda o: o.to_wkw_dict() dataset_converter.register_unstructure_hook(BoundingBox, bbox_to_wkw) dataset_converter.register_structure_hook( - BoundingBox, lambda d, _: BoundingBox.from_wkw(d) + BoundingBox, lambda d, _: BoundingBox.from_wkw_dict(d) ) -mag_to_array: Callable[[Mag], List[int]] = lambda o: o.to_array() +mag_to_array: Callable[[Mag], List[int]] = lambda o: o.to_list() dataset_converter.register_unstructure_hook(Mag, mag_to_array) dataset_converter.register_structure_hook(Mag, lambda d, _: Mag(d)) @@ -199,22 +199,3 @@ def disambiguate_layer_properties(obj: dict, _: Any) -> LayerProperties: dataset_converter.register_structure_hook( Union[SegmentationLayerProperties, LayerProperties], disambiguate_layer_properties ) - - -def disambiguate_mag(obj: dict, _: Any) -> Mag: - # This function is necessary because cattrs does not support unions of non-attrs objects out of the box - return Mag(obj) - - -dataset_converter.register_structure_hook(Union[int, Mag], disambiguate_mag) - -# Separate converter to unstructure LayerProperties -# This is used to initialize SegmentationLayerProperties from LayerProperties -# The important difference to the dataset_converter is that the names of the attributes stay the same while defaults are also omitted. -layer_properties_converter = cattr.Converter() -layer_properties_converter.register_unstructure_hook( # use register_unstructure_hook_func - LayerProperties, - make_dict_unstructure_fn( - LayerProperties, layer_properties_converter, omit_if_default=True - ), -) diff --git a/webknossos/webknossos/dataset/view.py b/webknossos/webknossos/dataset/view.py index 20c3a1f4f..5cbf365a3 100644 --- a/webknossos/webknossos/dataset/view.py +++ b/webknossos/webknossos/dataset/view.py @@ -2,18 +2,16 @@ import warnings from pathlib import Path from types import TracebackType -from typing import Callable, Optional, Tuple, Type, Union, cast +from typing import Callable, Optional, Tuple, Type, Union import cluster_tools import numpy as np from cluster_tools.schedulers.cluster_executor import ClusterExecutor from wkw import Dataset, wkw -from webknossos.geometry import BoundingBox +from webknossos.geometry import BoundingBox, Vec3Int, Vec3IntLike from webknossos.utils import wait_and_ensure_success -Vec3 = Union[Tuple[int, int, int], np.ndarray] - class View: """ @@ -27,8 +25,8 @@ def __init__( self, path_to_mag_view: Path, header: wkw.Header, - size: Tuple[int, int, int], - global_offset: Tuple[int, int, int] = (0, 0, 0), + size: Vec3IntLike, + global_offset: Vec3IntLike, is_bounded: bool = True, read_only: bool = False, mag_view_bbox_at_creation: Optional[BoundingBox] = None, @@ -39,8 +37,8 @@ def __init__( self._dataset: Optional[Dataset] = None self._path = path_to_mag_view self._header: wkw.Header = header - self._size: Tuple[int, int, int] = size - self._global_offset: Tuple[int, int, int] = global_offset + self._size: Vec3Int = Vec3Int(size) + self._global_offset: Vec3Int = Vec3Int(global_offset) self._is_bounded = is_bounded self._read_only = read_only self._is_opened = False @@ -60,11 +58,11 @@ def header(self) -> wkw.Header: return self._header @property - def size(self) -> Tuple[int, int, int]: + def size(self) -> Vec3Int: return self._size @property - def global_offset(self) -> Tuple[int, int, int]: + def global_offset(self) -> Vec3Int: return self._global_offset @property @@ -105,7 +103,7 @@ def close(self) -> None: def write( self, data: np.ndarray, - offset: Vec3 = (0, 0, 0), + offset: Vec3IntLike = Vec3Int(0, 0, 0), ) -> None: """ Writes the `data` at the specified `offset` to disk. @@ -116,9 +114,11 @@ def write( """ assert not self.read_only, "Cannot write data to an read_only View" + offset = Vec3Int(offset) + was_opened = self._is_opened # assert the size of the parameter data is not in conflict with the attribute self.size - data_dims = cast(Tuple[int, int, int], data.shape[-3:]) + data_dims = Vec3Int(data.shape[-3:]) _assert_positive_dimensions(offset, data_dims) self._assert_bounds(offset, data_dims) @@ -126,10 +126,7 @@ def write( data = data[0] # remove channel dimension # calculate the absolute offset - absolute_offset = cast( - Tuple[int, int, int], - tuple(sum(x) for x in zip(self.global_offset, offset)), - ) + absolute_offset = self.global_offset + offset if self._is_compressed(): absolute_offset, data = self._handle_compressed_write(absolute_offset, data) @@ -138,15 +135,15 @@ def write( self.open() assert self._dataset is not None # because the View was opened - self._dataset.write(absolute_offset, data) + self._dataset.write(absolute_offset.to_np(), data) if not was_opened: self.close() def read( self, - offset: Vec3 = (0, 0, 0), - size: Vec3 = None, + offset: Vec3IntLike = Vec3Int(0, 0, 0), + size: Optional[Vec3IntLike] = None, ) -> np.ndarray: """ The user can specify the `offset` and the `size` of the requested data. @@ -178,18 +175,17 @@ def read( ``` """ - size = self.size if size is None else size + offset = Vec3Int(offset) + size = self.size if size is None else Vec3Int(size) # assert the parameter size is not in conflict with the attribute self.size _assert_positive_dimensions(offset, size) self._assert_bounds(offset, size) # calculate the absolute offset - absolute_offset = tuple(sum(x) for x in zip(self.global_offset, offset)) + absolute_offset = self.global_offset + offset - return self._read_without_checks( - cast(Tuple[int, int, int], absolute_offset), size - ) + return self._read_without_checks(absolute_offset, size) def read_bbox(self, bounding_box: Optional[BoundingBox] = None) -> np.ndarray: """ @@ -203,15 +199,15 @@ def read_bbox(self, bounding_box: Optional[BoundingBox] = None) -> np.ndarray: def _read_without_checks( self, - absolute_offset: Vec3, - size: Vec3, + absolute_offset: Vec3Int, + size: Vec3Int, ) -> np.ndarray: was_opened = self._is_opened if not was_opened: self.open() assert self._dataset is not None # because the View was opened - data = self._dataset.read(absolute_offset, size) + data = self._dataset.read(absolute_offset.to_np(), size.to_np()) if not was_opened: self.close() @@ -220,8 +216,8 @@ def _read_without_checks( def get_view( self, - offset: Tuple[int, int, int] = None, - size: Tuple[int, int, int] = None, + offset: Vec3IntLike = Vec3Int(0, 0, 0), + size: Optional[Vec3IntLike] = None, read_only: bool = None, ) -> "View": """ @@ -256,17 +252,12 @@ def get_view( read_only or read_only == self.read_only ), "Failed to get subview. The calling view is read_only. Therefore, the subview also has to be read_only." - if offset is None: - offset = (0, 0, 0) - - if size is None: - size = self.size + offset = Vec3Int(offset) + size = self.size if size is None else Vec3Int(size) _assert_positive_dimensions(offset, size) self._assert_bounds(offset, size, not read_only) - view_offset = cast( - Tuple[int, int, int], tuple(self.global_offset + np.array(offset)) - ) + view_offset = self.global_offset + offset return View( self._path, self.header, @@ -279,8 +270,8 @@ def get_view( def _assert_bounds( self, - offset: Vec3, - size: Vec3, + offset: Vec3Int, + size: Vec3Int, strict: bool = None, ) -> None: if strict is None: @@ -295,7 +286,7 @@ def _assert_bounds( def for_each_chunk( self, work_on_chunk: Callable[[Tuple["View", int]], None], - chunk_size: Tuple[int, int, int], + chunk_size: Vec3IntLike, executor: Optional[ Union[ClusterExecutor, cluster_tools.WrappedProcessPoolExecutor] ] = None, @@ -330,6 +321,8 @@ def some_work(args: Tuple[View, int], some_parameter: int) -> None: ``` """ + chunk_size = Vec3Int(chunk_size) + _check_chunk_size(chunk_size) # This "view" object assures that the operation cannot exceed the bounding box of the properties. # `View.get_view()` returns a `View` of the same size as the current object (because of the default parameters). @@ -343,13 +336,10 @@ def some_work(args: Tuple[View, int], some_parameter: int) -> None: chunk_size, list(chunk_size) ) ): - relative_offset = cast( - Tuple[int, int, int], - tuple(np.array(chunk.topleft) - np.array(view.global_offset)), - ) + relative_offset = chunk.topleft - view.global_offset chunk_view = view.get_view( offset=relative_offset, - size=cast(Tuple[int, int, int], tuple(chunk.size)), + size=chunk.size, ) job_args.append((chunk_view, i)) @@ -364,8 +354,8 @@ def for_zipped_chunks( self, work_on_chunk: Callable[[Tuple["View", "View", int]], None], target_view: "View", - source_chunk_size: Vec3, - target_chunk_size: Vec3, + source_chunk_size: Vec3IntLike, + target_chunk_size: Vec3IntLike, executor: Optional[ Union[ClusterExecutor, cluster_tools.WrappedProcessPoolExecutor] ] = None, @@ -389,6 +379,8 @@ def for_zipped_chunks( - source_chunk_size: (2048, 2048, 2048) - target_chunk_size: (1024, 1024, 1024) // this must be a multiple of the file size on disk to avoid concurrent writes """ + source_chunk_size = Vec3Int(source_chunk_size) + target_chunk_size = Vec3Int(target_chunk_size) _check_chunk_size(source_chunk_size) _check_chunk_size(target_chunk_size) @@ -396,19 +388,19 @@ def for_zipped_chunks( source_view = self.get_view() target_view = target_view.get_view() - source_offset = np.array(source_view.global_offset) - target_offset = np.array(target_view.global_offset) - source_chunk_size_np = np.array(source_chunk_size) - target_chunk_size_np = np.array(target_chunk_size) + source_offset = source_view.global_offset + target_offset = target_view.global_offset + source_chunk_size_np = source_chunk_size.to_np() + target_chunk_size_np = target_chunk_size.to_np() - assert np.all( - np.array(source_view.size) + assert not source_view.size.contains( + 0 ), "Calling 'for_zipped_chunks' failed because the size of the source view contains a 0." - assert np.all( - np.array(target_view.size) + assert not target_view.size.contains( + 0 ), "Calling 'for_zipped_chunks' failed because the size of the target view contains a 0." assert np.array_equal( - np.array(source_view.size) / np.array(target_view.size), + source_view.size.to_np() / target_view.size.to_np(), source_chunk_size_np / target_chunk_size_np, ), f"Calling 'for_zipped_chunks' failed because the ratio of the view sizes (source size = {source_view.size}, target size = {target_view.size}) must be equal to the ratio of the chunk sizes (source_chunk_size = {source_chunk_size}, source_chunk_size = {target_chunk_size}))" @@ -419,27 +411,27 @@ def for_zipped_chunks( job_args = [] source_chunks = BoundingBox(source_offset, source_view.size).chunk( - source_chunk_size_np, list(source_chunk_size_np) + source_chunk_size, source_chunk_size.to_list() ) target_chunks = BoundingBox(target_offset, target_view.size).chunk( - target_chunk_size, list(target_chunk_size) + target_chunk_size, target_chunk_size.to_list() ) for i, (source_chunk, target_chunk) in enumerate( zip(source_chunks, target_chunks) ): # source chunk - relative_source_offset = np.array(source_chunk.topleft) - source_offset + relative_source_offset = source_chunk.topleft - source_offset source_chunk_view = source_view.get_view( - offset=cast(Tuple[int, int, int], tuple(relative_source_offset)), - size=cast(Tuple[int, int, int], tuple(source_chunk.size)), + offset=relative_source_offset, + size=source_chunk.size, read_only=True, ) # target chunk - relative_target_offset = np.array(target_chunk.topleft) - target_offset + relative_target_offset = target_chunk.topleft - target_offset target_chunk_view = target_view.get_view( - size=cast(Tuple[int, int, int], tuple(target_chunk.size)), - offset=cast(Tuple[int, int, int], tuple(relative_target_offset)), + size=target_chunk.size, + offset=relative_target_offset, ) job_args.append((source_chunk_view, target_chunk_view, i)) @@ -458,8 +450,8 @@ def _is_compressed(self) -> bool: ) def _handle_compressed_write( - self, absolute_offset: Tuple[int, int, int], data: np.ndarray - ) -> Tuple[Tuple[int, int, int], np.ndarray]: + self, absolute_offset: Vec3Int, data: np.ndarray + ) -> Tuple[Vec3Int, np.ndarray]: # calculate aligned bounding box file_bb = np.full(3, self.header.file_len * self.header.block_len) absolute_offset_np = np.array(absolute_offset) @@ -503,7 +495,9 @@ def _handle_compressed_write( "Warning: write() was called on a compressed mag without block alignment. Performance will be degraded as the data has to be padded first.", RuntimeWarning, ) - aligned_data = self._read_without_checks(aligned_offset, aligned_shape) + aligned_data = self._read_without_checks( + Vec3Int(aligned_offset), Vec3Int(aligned_shape) + ) index_slice = ( slice(None, None), @@ -516,7 +510,7 @@ def _handle_compressed_write( ) # overwrite the specified data aligned_data[tuple(index_slice)] = data - return cast(Tuple[int, int, int], tuple(aligned_offset)), aligned_data + return Vec3Int(aligned_offset), aligned_data else: return absolute_offset, data @@ -549,7 +543,7 @@ def _mag_view_bounding_box_at_creation(self) -> BoundingBox: return self._mag_view_bbox_at_creation -def _assert_positive_dimensions(offset: Vec3, size: Vec3) -> None: +def _assert_positive_dimensions(offset: Vec3Int, size: Vec3Int) -> None: if any(x < 0 for x in offset): raise AssertionError( f"The offset ({offset}) contains a negative value. All dimensions must be larger or equal to '0'." @@ -560,7 +554,7 @@ def _assert_positive_dimensions(offset: Vec3, size: Vec3) -> None: ) -def _check_chunk_size(chunk_size: Vec3) -> None: +def _check_chunk_size(chunk_size: Vec3Int) -> None: assert chunk_size is not None if 0 in chunk_size: diff --git a/webknossos/webknossos/geometry/__init__.py b/webknossos/webknossos/geometry/__init__.py index 1baf7afe1..9652a9b61 100644 --- a/webknossos/webknossos/geometry/__init__.py +++ b/webknossos/webknossos/geometry/__init__.py @@ -1,2 +1,3 @@ from .bounding_box import BoundingBox from .mag import Mag +from .vec3_int import Vec3Int, Vec3IntLike diff --git a/webknossos/webknossos/geometry/bounding_box.py b/webknossos/webknossos/geometry/bounding_box.py index d2230cfbd..185a28a28 100644 --- a/webknossos/webknossos/geometry/bounding_box.py +++ b/webknossos/webknossos/geometry/bounding_box.py @@ -12,11 +12,11 @@ cast, ) +import attr import numpy as np from .mag import Mag - -Shape3D = Union[List[int], Tuple[int, int, int], np.ndarray] +from .vec3_int import Vec3Int, Vec3IntLike class BoundingBoxNamedTuple(NamedTuple): @@ -24,25 +24,71 @@ class BoundingBoxNamedTuple(NamedTuple): size: Tuple[int, int, int] +@attr.frozen class BoundingBox: - def __init__(self, topleft: Shape3D, size: Shape3D): - - self.topleft = np.array(topleft, dtype=int) - self.size = np.array(size, dtype=int) + topleft: Vec3Int = attr.ib(converter=Vec3Int) + size: Vec3Int = attr.ib(converter=Vec3Int) @property - def bottomright(self) -> np.ndarray: + def bottomright(self) -> Vec3Int: return self.topleft + self.size + def with_topleft(self, new_topleft: Vec3IntLike) -> "BoundingBox": + + return BoundingBox(new_topleft, self.size) + + def with_size(self, new_size: Vec3IntLike) -> "BoundingBox": + + return BoundingBox(self.topleft, new_size) + + def with_bounds_x( + self, new_topleft_x: Optional[int] = None, new_size_x: Optional[int] = None + ) -> "BoundingBox": + """Returns a copy of the bounding box with topleft.x optionally replaced and size.x optionally replaced.""" + + new_topleft = ( + self.topleft.with_x(new_topleft_x) + if new_topleft_x is not None + else self.topleft + ) + new_size = self.size.with_x(new_size_x) if new_size_x is not None else self.size + return BoundingBox(new_topleft, new_size) + + def with_bounds_y( + self, new_topleft_y: Optional[int] = None, new_size_y: Optional[int] = None + ) -> "BoundingBox": + """Returns a copy of the bounding box with topleft.y optionally replaced and size.y optionally replaced.""" + + new_topleft = ( + self.topleft.with_y(new_topleft_y) + if new_topleft_y is not None + else self.topleft + ) + new_size = self.size.with_y(new_size_y) if new_size_y is not None else self.size + return BoundingBox(new_topleft, new_size) + + def with_bounds_z( + self, new_topleft_z: Optional[int] = None, new_size_z: Optional[int] = None + ) -> "BoundingBox": + """Returns a copy of the bounding box with topleft.z optionally replaced and size.z optionally replaced.""" + + new_topleft = ( + self.topleft.with_z(new_topleft_z) + if new_topleft_z is not None + else self.topleft + ) + new_size = self.size.with_z(new_size_z) if new_size_z is not None else self.size + return BoundingBox(new_topleft, new_size) + @staticmethod - def from_wkw(bbox: Dict) -> "BoundingBox": + def from_wkw_dict(bbox: Dict) -> "BoundingBox": return BoundingBox( bbox["topLeft"], [bbox["width"], bbox["height"], bbox["depth"]] ) @staticmethod - def from_config(bbox: Dict) -> "BoundingBox": + def from_config_dict(bbox: Dict) -> "BoundingBox": return BoundingBox(bbox["topleft"], bbox["size"]) @staticmethod @@ -50,13 +96,14 @@ def from_tuple6(tuple6: Tuple[int, int, int, int, int, int]) -> "BoundingBox": return BoundingBox(tuple6[0:3], tuple6[3:6]) @staticmethod - def from_tuple2(tuple2: Tuple[Shape3D, Shape3D]) -> "BoundingBox": + def from_tuple2(tuple2: Tuple[Vec3IntLike, Vec3IntLike]) -> "BoundingBox": return BoundingBox(tuple2[0], tuple2[1]) @staticmethod - def from_points(points: Iterable[Shape3D]) -> "BoundingBox": + def from_points(points: Iterable[Vec3IntLike]) -> "BoundingBox": + """Returns a bounding box exactly containing all points.""" - all_points = np.array(points) + all_points = np.array([Vec3Int(point).to_list() for point in points]) topleft = all_points.min(axis=0) bottomright = all_points.max(axis=0) @@ -78,9 +125,9 @@ def from_checkpoint_name(checkpoint_name: str) -> "BoundingBox": match is not None ), f"Could not extract bounding box from {checkpoint_name}" bbox_tuple = tuple(int(value) for value in match.group().split("_")) - topleft = cast(Tuple[int, int, int], bbox_tuple[:3]) - size = cast(Tuple[int, int, int], bbox_tuple[3:6]) - return BoundingBox.from_tuple2((topleft, size)) + return BoundingBox.from_tuple6( + cast(Tuple[int, int, int, int, int, int], bbox_tuple) + ) @staticmethod def from_csv(csv_bbox: str) -> "BoundingBox": @@ -101,7 +148,9 @@ def from_auto( else: return BoundingBox.from_csv(obj) elif isinstance(obj, dict): - return BoundingBox.from_wkw(obj) + if "size" in obj: + return BoundingBox.from_config_dict(obj) + return BoundingBox.from_wkw_dict(obj) elif isinstance(obj, BoundingBoxNamedTuple): return BoundingBox.from_named_tuple(obj) elif isinstance(obj, list) or isinstance(obj, tuple): @@ -112,26 +161,26 @@ def from_auto( raise Exception("Unknown bounding box format.") - def as_wkw(self) -> dict: + def to_wkw_dict(self) -> dict: ( # pylint: disable=unbalanced-tuple-unpacking width, height, depth, - ) = self.size.tolist() + ) = self.size.to_list() return { - "topLeft": self.topleft.tolist(), + "topLeft": self.topleft.to_list(), "width": width, "height": height, "depth": depth, } - def as_config(self) -> dict: + def to_config_dict(self) -> dict: - return {"topleft": self.topleft.tolist(), "size": self.size.tolist()} + return {"topleft": self.topleft.to_list(), "size": self.size.to_list()} - def as_checkpoint_name(self) -> str: + def to_checkpoint_name(self) -> str: x, y, z = self.topleft width, height, depth = self.size @@ -139,15 +188,15 @@ def as_checkpoint_name(self) -> str: x=x, y=y, z=z, width=width, height=height, depth=depth ) - def as_tuple6(self) -> Tuple[int, int, int, int, int, int]: + def to_tuple6(self) -> Tuple[int, int, int, int, int, int]: - return tuple(self.topleft.tolist() + self.size.tolist()) # type: ignore + return tuple(self.topleft.to_list() + self.size.to_list()) # type: ignore - def as_csv(self) -> str: + def to_csv(self) -> str: - return ",".join(map(str, self.as_tuple6())) + return ",".join(map(str, self.to_tuple6())) - def as_named_tuple(self) -> BoundingBoxNamedTuple: + def to_named_tuple(self) -> BoundingBoxNamedTuple: return BoundingBoxNamedTuple( topleft=cast(Tuple[int, int, int], tuple(self.topleft)), size=cast(Tuple[int, int, int], tuple(self.size)), @@ -165,21 +214,19 @@ def __str__(self) -> str: def __eq__(self, other: object) -> bool: if isinstance(other, BoundingBox): - return np.array_equal(self.topleft, other.topleft) and np.array_equal( - self.size, other.size - ) + return self.topleft == other.topleft and self.size == other.size else: raise NotImplementedError() def padded_with_margins( - self, margins_left: Shape3D, margins_right: Optional[Shape3D] = None + self, margins_left: Vec3IntLike, margins_right: Optional[Vec3IntLike] = None ) -> "BoundingBox": if margins_right is None: margins_right = margins_left - margins_left = np.array(margins_left) - margins_right = np.array(margins_right) + margins_left = Vec3Int(margins_left) + margins_right = Vec3Int(margins_right) return BoundingBox( topleft=self.topleft - margins_left, @@ -191,8 +238,8 @@ def intersected_with( ) -> "BoundingBox": """If dont_assert is set to False, this method may return empty bounding boxes (size == (0, 0, 0))""" - topleft = np.maximum(self.topleft, other.topleft) - bottomright = np.minimum(self.bottomright, other.bottomright) + topleft = np.maximum(self.topleft.to_np(), other.topleft.to_np()) + bottomright = np.minimum(self.bottomright.to_np(), other.bottomright.to_np()) size = np.maximum(bottomright - topleft, (0, 0, 0)) intersection = BoundingBox(topleft, size) @@ -214,22 +261,22 @@ def extended_by(self, other: "BoundingBox") -> "BoundingBox": def is_empty(self) -> bool: - return not all(self.size > 0) + return not all(self.size.to_np() > 0) def in_mag(self, mag: Mag) -> "BoundingBox": - np_mag = np.array(mag.to_array()) + np_mag = np.array(mag.to_list()) assert ( - np.count_nonzero(self.topleft % np_mag) == 0 + np.count_nonzero(self.topleft.to_np() % np_mag) == 0 ), f"topleft {self.topleft} is not aligned with the mag {mag}. Use BoundingBox.align_with_mag()." assert ( - np.count_nonzero(self.bottomright % np_mag) == 0 + np.count_nonzero(self.bottomright.to_np() % np_mag) == 0 ), f"bottomright {self.bottomright} is not aligned with the mag {mag}. Use BoundingBox.align_with_mag()." return BoundingBox( - topleft=(self.topleft // np_mag).astype(int), - size=(self.size // np_mag).astype(int), + topleft=(self.topleft // np_mag), + size=(self.size // np_mag), ) def align_with_mag(self, mag: Mag, ceil: bool = False) -> "BoundingBox": @@ -238,9 +285,12 @@ def align_with_mag(self, mag: Mag, ceil: bool = False) -> "BoundingBox": :argument ceil: If true, the bounding box is enlarged when necessary. If false, it's shrinked when necessary. """ - np_mag = np.array(mag.to_array()) + np_mag = np.array(mag.to_list()) - align = lambda point, round_fn: round_fn(point / np_mag).astype(int) * np_mag + align = ( + lambda point, round_fn: round_fn(point.to_np() / np_mag).astype(int) + * np_mag + ) if ceil: topleft = align(self.topleft, np.floor) @@ -250,9 +300,9 @@ def align_with_mag(self, mag: Mag, ceil: bool = False) -> "BoundingBox": bottomright = align(self.bottomright, np.floor) return BoundingBox(topleft, bottomright - topleft) - def contains(self, coord: Shape3D) -> bool: + def contains(self, coord: Vec3IntLike) -> bool: - coord = np.array(coord) + coord = Vec3Int(coord).to_np() return cast( bool, @@ -263,7 +313,9 @@ def contains_bbox(self, inner_bbox: "BoundingBox") -> bool: return inner_bbox.intersected_with(self, dont_assert=True) == inner_bbox def chunk( - self, chunk_size: Shape3D, chunk_border_alignments: Optional[List[int]] = None + self, + chunk_size: Vec3IntLike, + chunk_border_alignments: Optional[List[int]] = None, ) -> Generator["BoundingBox", None, None]: """Decompose the bounding box into smaller chunks of size `chunk_size`. @@ -272,8 +324,8 @@ def chunk( *between two chunks* will be divisible by that value. """ - start = self.topleft.copy() - chunk_size = np.array(chunk_size) + start = self.topleft.to_np() + chunk_size = Vec3Int(chunk_size).to_np() start_adjust = np.array([0, 0, 0]) if chunk_border_alignments is not None: @@ -307,22 +359,18 @@ def volume(self) -> int: def slice_array(self, array: np.ndarray) -> np.ndarray: return array[ - self.topleft[0] : self.bottomright[0], - self.topleft[1] : self.bottomright[1], - self.topleft[2] : self.bottomright[2], + self.topleft.x : self.bottomright.x, + self.topleft.y : self.bottomright.y, + self.topleft.z : self.bottomright.z, ] - def as_slices(self) -> Tuple[slice, slice, slice]: + def to_slices(self) -> Tuple[slice, slice, slice]: return np.index_exp[ - self.topleft[0] : self.bottomright[0], - self.topleft[1] : self.bottomright[1], - self.topleft[2] : self.bottomright[2], + self.topleft.x : self.bottomright.x, + self.topleft.z : self.bottomright.y, + self.topleft.z : self.bottomright.z, ] - def copy(self) -> "BoundingBox": - - return BoundingBox(self.topleft.copy(), self.size.copy()) - - def offset(self, vector: Tuple[int, int, int]) -> "BoundingBox": + def offset(self, vector: Vec3IntLike) -> "BoundingBox": - return BoundingBox(self.topleft + np.array(vector), self.size.copy()) + return BoundingBox(self.topleft + Vec3Int(vector), self.size) diff --git a/webknossos/webknossos/geometry/mag.py b/webknossos/webknossos/geometry/mag.py index 498aae92f..1c7c3ea50 100644 --- a/webknossos/webknossos/geometry/mag.py +++ b/webknossos/webknossos/geometry/mag.py @@ -1,87 +1,115 @@ import re from functools import total_ordering from math import log2 -from typing import Any, List +from typing import Any, Iterator, List, Optional, Tuple, cast +import attr import numpy as np +from .vec3_int import Vec3Int, Vec3IntLike + + +def _import_mag(mag_like: Any) -> Vec3Int: + as_vec3_int: Optional[Vec3Int] = None + + if isinstance(mag_like, Mag): + as_vec3_int = mag_like.to_vec3_int() + elif isinstance(mag_like, int): + as_vec3_int = Vec3Int(mag_like, mag_like, mag_like) + elif isinstance(mag_like, Vec3Int): + as_vec3_int = mag_like + elif isinstance(mag_like, list) or isinstance(mag_like, tuple): + as_vec3_int = Vec3Int(cast(Vec3IntLike, mag_like)) + elif isinstance(mag_like, str): + if re.match(r"^\d+$", mag_like) is not None: + as_vec3_int = Vec3Int(int(mag_like), int(mag_like), int(mag_like)) + elif re.match(r"^\d+-\d+-\d+$", mag_like) is not None: + as_vec3_int = Vec3Int([int(m) for m in mag_like.split("-")]) + elif isinstance(mag_like, np.ndarray): + as_vec3_int = Vec3Int(mag_like) + + if as_vec3_int is None: + raise ValueError( + "Mag must be int or a vector3 of ints or a string shaped like e.g. 2-2-1" + ) + for m in as_vec3_int: + assert ( + log2(m) % 1 == 0 + ), f"Mag components must be power of 2, got {m} in {as_vec3_int}." + + return as_vec3_int + @total_ordering -class Mag(object): - def __init__(self, mag: Any): - self.mag: List[int] = [] - - if isinstance(mag, int): - self.mag = [mag] * 3 - elif isinstance(mag, list): - self.mag = mag - elif isinstance(mag, tuple): - self.mag = [mag_d for mag_d in mag] - elif isinstance(mag, str): - if re.match(r"^\d+$", mag) is not None: - self.mag = [int(mag)] * 3 - elif re.match(r"^\d+-\d+-\d+$", mag) is not None: - self.mag = [int(m) for m in mag.split("-")] - elif isinstance(mag, Mag): - self.mag = mag.mag - elif isinstance(mag, np.ndarray): - assert mag.shape == (3,) - self.mag = list(mag) - - if self.mag is None or len(self.mag) != 3: - raise ValueError( - "magnification must be int or a vector3 of ints or a string shaped like e.g. 2-2-1" - ) - - for m in self.mag: - assert log2(m) % 1 == 0, "magnification needs to be power of 2." +@attr.frozen(order=False) +class Mag: + _mag: Vec3Int = attr.ib(converter=_import_mag) + + @property + def x(self) -> int: + return self._mag.x + + @property + def y(self) -> int: + return self._mag.y + + @property + def z(self) -> int: + return self._mag.z + + @property + def max_dim(self) -> int: + return max(self._mag) def __lt__(self, other: Any) -> bool: - return max(self.mag) < (max(Mag(other).to_array())) + return self.max_dim < Mag(other).max_dim def __le__(self, other: Any) -> bool: - return max(self.mag) <= (max(Mag(other).to_array())) + return self.max_dim <= Mag(other).max_dim def __eq__(self, other: Any) -> bool: - return all(m1 == m2 for m1, m2 in zip(self.mag, Mag(other).mag)) + return self.to_vec3_int() == Mag(other).to_vec3_int() def __str__(self) -> str: return self.to_layer_name() - def __expr__(self) -> str: + def __repr__(self) -> str: return f"Mag({self.to_layer_name()})" def to_layer_name(self) -> str: - x, y, z = self.mag + x, y, z = self._mag if x == y and y == z: return str(x) else: return self.to_long_layer_name() def to_long_layer_name(self) -> str: - x, y, z = self.mag + x, y, z = self._mag return "{}-{}-{}".format(x, y, z) - def to_array(self) -> List[int]: - return self.mag + def to_list(self) -> List[int]: + return self._mag.to_list() - def scaled_by(self, factor: int) -> "Mag": - return Mag([mag * factor for mag in self.mag]) + def to_np(self) -> np.ndarray: + return self._mag.to_np() - def scale_by(self, factor: int) -> None: - self.mag = [mag * factor for mag in self.mag] + def to_vec3_int(self) -> Vec3Int: + return self._mag - def divided(self, coord: List[int]) -> List[int]: - return [c // m for c, m in zip(coord, self.mag)] + def to_tuple(self) -> Tuple[int, int, int]: + return self._mag.to_tuple() - def divide_by(self, d: int) -> None: - self.mag = [mag // d for mag in self.mag] + def scaled_by(self, factor: int) -> "Mag": + return Mag(self._mag * factor) - def divided_by(self, d: int) -> "Mag": - return Mag([mag // d for mag in self.mag]) + def __mul__(self, factor: int) -> "Mag": + return Mag(self._mag * factor) - def as_np(self) -> np.ndarray: - return np.array(self.mag) + def __floordiv__(self, d: int) -> "Mag": + return Mag(self._mag // d) def __hash__(self) -> int: - return hash(tuple(self.mag)) + return hash(self._mag) + + def __iter__(self) -> Iterator[int]: + return iter(self._mag) diff --git a/webknossos/webknossos/geometry/vec3_int.py b/webknossos/webknossos/geometry/vec3_int.py new file mode 100644 index 000000000..4153ff718 --- /dev/null +++ b/webknossos/webknossos/geometry/vec3_int.py @@ -0,0 +1,134 @@ +from operator import add, floordiv, mod, mul, sub +from typing import Any, Callable, Iterable, List, Optional, Tuple, Union, cast + +import numpy as np + + +class Vec3Int(tuple): + def __new__( + cls, + vec: Union[int, "Vec3IntLike"], + y: Optional[int] = None, + z: Optional[int] = None, + ) -> "Vec3Int": + if isinstance(vec, Vec3Int): + return vec + + as_tuple: Optional[Tuple[int, int, int]] = None + value_error = "Vector components must be three integers or a Vec3IntLike object" + + if isinstance(vec, int): + assert y is not None and z is not None, value_error + assert isinstance(y, int) and isinstance(z, int), value_error + as_tuple = vec, y, z + else: + assert y is None and z is None, value_error + if isinstance(vec, np.ndarray): + assert vec.shape == ( + 3, + ), f"Numpy array for Vec3Int must have shape (3,), got {vec.shape}." + if isinstance(vec, Iterable): + as_tuple = cast(Tuple[int, int, int], tuple(int(item) for item in vec)) + assert len(as_tuple) == 3, value_error + assert as_tuple is not None and len(as_tuple) == 3, value_error + + return super().__new__(cls, cast(Iterable, as_tuple)) + + @property + def x(self) -> int: + return self[0] + + @property + def y(self) -> int: + return self[1] + + @property + def z(self) -> int: + return self[2] + + def with_x(self, new_x: int) -> "Vec3Int": + return Vec3Int(new_x, self.y, self.z) + + def with_y(self, new_y: int) -> "Vec3Int": + return Vec3Int(self.x, new_y, self.z) + + def with_z(self, new_z: int) -> "Vec3Int": + return Vec3Int(self.x, self.y, new_z) + + def to_np(self) -> np.ndarray: + return np.array((self.x, self.y, self.z)) + + def to_list(self) -> List[int]: + return [self.x, self.y, self.z] + + def to_tuple(self) -> Tuple[int, int, int]: + return self.x, self.y, self.z + + def contains(self, needle: int) -> bool: + return self.x == needle or self.y == needle or self.z == needle + + def _element_wise( + self, other: Union[int, "Vec3IntLike"], fn: Callable[[int, Any], int] + ) -> "Vec3Int": + if isinstance(other, int): + other_imported = Vec3Int(other, other, other) + else: + other_imported = Vec3Int(other) + return Vec3Int( + ( + fn(self.x, other_imported.x), + fn(self.y, other_imported.y), + fn(self.z, other_imported.z), + ) + ) + + # note: (arguments incompatible with superclass, do not add Vec3Int to plain tuple! Hence the type:ignore) + def __add__(self, other: Union[int, "Vec3IntLike"]) -> "Vec3Int": # type: ignore[override] + return self._element_wise(other, add) + + def __sub__(self, other: Union[int, "Vec3IntLike"]) -> "Vec3Int": + return self._element_wise(other, sub) + + def __mul__(self, other: Union[int, "Vec3IntLike"]) -> "Vec3Int": + return self._element_wise(other, mul) + + def __floordiv__(self, other: Union[int, "Vec3IntLike"]) -> "Vec3Int": + return self._element_wise(other, floordiv) + + def __mod__(self, other: Union[int, "Vec3IntLike"]) -> "Vec3Int": + return self._element_wise(other, mod) + + def __neg__(self) -> "Vec3Int": + return Vec3Int(-self.x, -self.y, -self.z) + + def ceildiv(self, other: Union[int, "Vec3IntLike"]) -> "Vec3Int": + return (self + other - 1) // other + + def pairmax(self, other: Union[int, "Vec3IntLike"]) -> "Vec3Int": + return self._element_wise(other, max) + + def pairmin(self, other: Union[int, "Vec3IntLike"]) -> "Vec3Int": + return self._element_wise(other, min) + + def prod(self) -> int: + return self.x * self.y * self.z + + def __repr__(self) -> str: + return f"Vec3Int({self.x},{self.y},{self.z})" + + @classmethod + def zeros(cls) -> "Vec3Int": + return cls(0, 0, 0) + + @classmethod + def ones(cls) -> "Vec3Int": + return cls(1, 1, 1) + + @classmethod + def full(cls, an_int: int) -> "Vec3Int": + return cls(an_int, an_int, an_int) + + +Vec3IntLike = Union[ + Vec3Int, Tuple[int, int, int], Tuple[int, ...], np.ndarray, List[int], Iterable[int] +] diff --git a/wkcuber/Changelog.md b/wkcuber/Changelog.md index 14bfc5224..a2862a46c 100644 --- a/wkcuber/Changelog.md +++ b/wkcuber/Changelog.md @@ -3,7 +3,7 @@ All notable changes to webknossos-cuber are documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MICRO`. +and this project adheres to [Semantic Versioning](http://semver.org/) `MAJOR.MINOR.PATCH`. For upgrade instructions, please check the respective *Breaking Changes* sections. ## Unreleased diff --git a/wkcuber/tests/test_export_wkw_as_tiff.py b/wkcuber/tests/test_export_wkw_as_tiff.py index 0b675125a..b2ab81587 100644 --- a/wkcuber/tests/test_export_wkw_as_tiff.py +++ b/wkcuber/tests/test_export_wkw_as_tiff.py @@ -17,7 +17,7 @@ def test_export_tiff_stack() -> None: destination_path = os.path.join("testoutput", DS_NAME + "_tiff") bbox = BoundingBox((100, 100, 10), (100, 500, 50)) - bbox_dict = bbox.as_config() + bbox_dict = bbox.to_config_dict() args_list = [ "--source_path", str(SOURCE_PATH), @@ -28,7 +28,7 @@ def test_export_tiff_stack() -> None: "--name", "test_export", "--bbox", - bbox.as_csv(), + bbox.to_csv(), "--mag", "1", ] diff --git a/wkcuber/wkcuber/cubing.py b/wkcuber/wkcuber/cubing.py index b3879f944..335c0dcd8 100644 --- a/wkcuber/wkcuber/cubing.py +++ b/wkcuber/wkcuber/cubing.py @@ -233,7 +233,7 @@ def cubing_job( buffer, target_mag, interpolation_mode ) - target_wkw.write([0, 0, z_batch[0] / target_mag.to_array()[2]], buffer) + target_wkw.write([0, 0, z_batch[0] / target_mag.to_list()[2]], buffer) logging.debug( "Cubing of z={}-{} took {:.8f}s".format( z_batch[0], z_batch[-1], time.time() - ref_time diff --git a/wkcuber/wkcuber/export_wkw_as_tiff.py b/wkcuber/wkcuber/export_wkw_as_tiff.py index 90ae5676c..8dd923be0 100644 --- a/wkcuber/wkcuber/export_wkw_as_tiff.py +++ b/wkcuber/wkcuber/export_wkw_as_tiff.py @@ -138,17 +138,17 @@ def export_tiff_slice( tiff_bbox = tiff_bbox.copy() number_of_slices = ( - min(tiff_bbox["size"][2] - batch_number * batch_size, batch_size) // mag.mag[2] + min(tiff_bbox["size"][2] - batch_number * batch_size, batch_size) // mag.z ) tiff_bbox["size"] = ( - tiff_bbox["size"][0] // mag.mag[0], - tiff_bbox["size"][1] // mag.mag[1], + tiff_bbox["size"][0] // mag.x, + tiff_bbox["size"][1] // mag.y, number_of_slices, ) tiff_bbox["topleft"] = ( - tiff_bbox["topleft"][0] // mag.mag[0], - tiff_bbox["topleft"][1] // mag.mag[1], - (tiff_bbox["topleft"][2] + batch_number * batch_size) // mag.mag[2], + tiff_bbox["topleft"][0] // mag.x, + tiff_bbox["topleft"][1] // mag.y, + (tiff_bbox["topleft"][2] + batch_number * batch_size) // mag.z, ) with wkw.Dataset.open(str(dataset_path)) as dataset: diff --git a/wkcuber/wkcuber/metadata.py b/wkcuber/wkcuber/metadata.py index ceaf99de0..2db57ef0f 100644 --- a/wkcuber/wkcuber/metadata.py +++ b/wkcuber/wkcuber/metadata.py @@ -264,7 +264,7 @@ def list_cubes(layer_path: str) -> Iterable[Tuple[int, int, int]]: } -def detect_resolutions(dataset_path: Path, layer: str) -> Generator: +def detect_resolutions(dataset_path: Path, layer: str) -> Generator[Mag, None, None]: for mag in listdir(path.join(dataset_path, layer)): try: yield Mag(mag) @@ -304,7 +304,7 @@ def detect_standard_layer( resolutions = [ { - "resolution": mag.to_array(), + "resolution": mag.to_list(), "cubeLength": detect_cubeLength(dataset_path, layer_name, mag), } for mag in mags diff --git a/wkcuber/wkcuber/utils.py b/wkcuber/wkcuber/utils.py index 90ed9a55a..f85193adf 100644 --- a/wkcuber/wkcuber/utils.py +++ b/wkcuber/wkcuber/utils.py @@ -420,7 +420,7 @@ def ceil_div_np(numerator: np.ndarray, denominator: np.ndarray) -> np.ndarray: def convert_mag1_offset( mag1_offset: Union[List, np.ndarray], target_mag: Mag ) -> np.ndarray: - return np.array(mag1_offset) // target_mag.as_np() # floor div + return np.array(mag1_offset) // target_mag.to_np() # floor div def get_executor_args(global_args: argparse.Namespace) -> argparse.Namespace: