Repository: rapidsai/cucim (language: python)

Issue:
Forward-merge branch-23.02 to branch-23.04
Forward-merge triggered by push to `branch-23.02` that creates a PR to keep `branch-23.04` up-to-date. If this PR is unable to be immediately merged due to conflicts, it will remain open for the team to manually merge.
[QST] conda compatibility with scikit-image 0.20.0
scikit-image recently updated to 0.20.0 on conda-forge, however cucim's dependencies explicitly prevent 0.20.0 or above from being installed. Is there any reason why this limitation is in place, especially when cucim's API is already using the 0.20.0 scikit-image API?
Improve rank filtering performance by removing use of footprint kernel when possible
In many cases such as rank filtering, if all elements of the kernel array are equal to one, then there is no reason to pay the overhead of allocating the footprint or checking its values during the filtering operation.

This MR is a small update to vendored CuPy ndimage code to avoid overhead of footprint creation and use when possible. 

Will update with benchmark results of acceleration vs. the kernel size. It is approximately a 2x improvement for small kernels (e.g. 3x3, 5x5, 3x3x3) approaching no acceleration for larger kernel sizes.
Add GitHub Actions Workflows.
This PR adds GitHub Actions workflows to `cucim`.

### Task list

Coverage required for this PR:
- [x] C++ build
- [x] ~C++ tests~ (There are no C++ tests.)
- [x] Python build
- [x] Python tests
  - Some tests have been marked as `xfail` on ARM following discussion with @jakirkham and @grlee77. Those will be addressed in follow-up work.
- [x] Style checks


Discussion / hints:



# [Codecov](https://codecov.io/gh/rapidsai/cucim/pull/471?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rapidsai) Report
> :exclamation: No coverage uploaded for pull request base (`branch-23.02@dbbeaf9`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rapidsai#section-missing-base-commit).
> Patch has no changes to coverable lines.

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff               @@
##             branch-23.02     #471   +/-   ##
===============================================
  Coverage                ?   92.94%           
===============================================
  Files                   ?      130           
  Lines                   ?     9773           
  Branches                ?        0           
===============================================
  Hits                    ?     9084           
  Misses                  ?      689           
  Partials                ?        0           
```



Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rapidsai). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rapidsai)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/rapidsai/cucim/pull/471?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rapidsai).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=rapidsai).

Here's a summary of the latest `pytest` failures:

- on `amd64`
  - it seems like all of the `amd64` tests only have a single failure ([1](https://github.com/rapidsai/cucim/actions/runs/3818400587/attempts/1#summary-10386516675),[2](https://github.com/rapidsai/cucim/actions/runs/3818400587/attempts/1#summary-10386516722),[3](https://github.com/rapidsai/cucim/actions/runs/3818400587/attempts/1#summary-10386516751)) that says `ImportError: libGL.so.1: cannot open shared object file: No such file or directory`. I can't seem to figure out where this `.so` is available in our Jenkins CI images, so I'm not sure how to fix it for GHAs.
- on `arm64`
  - the `arm64` test failures ([link](https://github.com/rapidsai/cucim/actions/runs/3818400587/attempts/1#summary-10386516778)) have the same `.so` error as `amd64`, plus a few others. Just a reminder, we don't test `arm64` in PRs on Jenkins, so these failures could be legit
I think the `libGL.so.1` errors are from attempting to run code that requires a display (or virtual GL output device?) on a headless machine without GL libraries.

On my WSL Ubuntu 22.04 system, I have `/usr/lib/x86_64-linux-gnu/libGL.so.1` which is provided by the apt package `libgl1`. I am not sure if that's sufficient here. We might be able to do something like export an environment variable to tell the package to run in "headless" mode. I see there are some channels like `fastai` offering a package named `opencv-python-headless`, and a PyPI package: https://pypi.org/project/opencv-python-headless/ but no `conda-forge` package labeled "headless."

Some related StackOverflow answers are here: https://stackoverflow.com/questions/55313610/importerror-libgl-so-1-cannot-open-shared-object-file-no-such-file-or-directo
I think we have a few potential courses of action:
- Install `libgl1` in Ubuntu images and `mesa-libGL` in CentOS/Rocky images. I am convinced that this is necessary to use the conda-forge packages because of [this comment](https://github.com/conda-forge/opencv-feedstock/issues/321#issuecomment-1165865099)
- Use the [`fastai::opencv-python-headless`](https://anaconda.org/fastai/opencv-python-headless) package and skip OpenCV tests on aarch64 (package not available for aarch64)
- Try the PyPI `opencv-python-headless` by adding it as a `pip` dependency in the conda environment -- not sure if this would work but it might be worth trying
- Skip these tests entirely (probably not a sufficient solution)
@ajschmidt8 I implemented the first proposed solution here: https://github.com/rapidsai/ci-imgs/pull/31

If you're satisfied with that, we can update the images and retry this PR.

edit: We decided against that approach and used a "headless" version of opencv from `pip` in 6045729. See https://github.com/rapidsai/ci-imgs/pull/31#issuecomment-1370343903
/merge
/merge

Fix patch:
diff --git a/.dockerignore b/.dockerignore
index 26120d7d9..26a3e77ec 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -4,4 +4,3 @@
 #build
 #install
 !temp/gds
-
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index a8a0eea99..bcbaeee36 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -20,7 +20,7 @@ A clear and concise description of what you expected to happen.
  - Environment location: [Bare-metal, Docker, Cloud(specify cloud provider)]
  - Method of cuCIM install: [conda, Docker, or from source]
    - If method of install is [Docker], provide `docker pull` & `docker run` commands used
- 
+
 
 **Additional context**
 Add any other context about the problem here.
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index c64bfc4a7..6c301e492 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -28,17 +28,17 @@ concurrency:
 jobs:
   cpp-build:
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.04
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
       date: ${{ inputs.date }}
       sha: ${{ inputs.sha }}
   docs-build:
-    if: ${{ startsWith(github.ref, 'refs/heads/branch-') }}
+    if: github.ref_type == 'branch' && github.event_name == 'push'
     needs: [python-build]
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.04
     with:
       build_type: branch
       node_type: "gpu-latest-1"
@@ -48,7 +48,7 @@ jobs:
   python-build:
     needs: [cpp-build]
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.04
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
@@ -57,7 +57,7 @@ jobs:
   upload-conda:
     needs: [cpp-build, python-build]
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/conda-upload-packages.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/conda-upload-packages.yaml@branch-23.04
     with:
       build_type: ${{ inputs.build_type || 'branch' }}
       branch: ${{ inputs.branch }}
diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml
index a0433495f..4bd1513e5 100644
--- a/.github/workflows/pr.yaml
+++ b/.github/workflows/pr.yaml
@@ -18,32 +18,32 @@ jobs:
       - conda-python-tests
       - docs-build
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/pr-builder.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/pr-builder.yaml@branch-23.04
   checks:
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/checks.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/checks.yaml@branch-23.04
   conda-cpp-build:
     needs: checks
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/conda-cpp-build.yaml@branch-23.04
     with:
       build_type: pull-request
   conda-python-build:
     needs: conda-cpp-build
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-build.yaml@branch-23.04
     with:
       build_type: pull-request
   conda-python-tests:
     needs: conda-python-build
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.04
     with:
       build_type: pull-request
   docs-build:
     needs: conda-python-build
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/custom-job.yaml@branch-23.04
     with:
       build_type: pull-request
       node_type: "gpu-latest-1"
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index f60388162..c8f84c353 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -4,4 +4,4 @@
       <option name="ENABLED" value="true" />
     </clangFormatSettings>
   </code_scheme>
-</component>
\ No newline at end of file
+</component>
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
index 79ee123c2..0f7bc519d 100644
--- a/.idea/codeStyles/codeStyleConfig.xml
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -2,4 +2,4 @@
   <state>
     <option name="USE_PER_PROJECT_SETTINGS" value="true" />
   </state>
-</component>
\ No newline at end of file
+</component>
diff --git a/.idea/cucim.iml b/.idea/cucim.iml
index 6d70257c7..08cda128a 100644
--- a/.idea/cucim.iml
+++ b/.idea/cucim.iml
@@ -1,2 +1,2 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<module classpath="CMake" type="CPP_MODULE" version="4" />
\ No newline at end of file
+<module classpath="CMake" type="CPP_MODULE" version="4" />
diff --git a/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h b/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h
index b0e223c0a..cf0461d4c 100644
--- a/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h
+++ b/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h
@@ -12,4 +12,4 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- */
\ No newline at end of file
+ */
diff --git a/.idea/fileTemplates/internal/CMakeLists.txt.cmake b/.idea/fileTemplates/internal/CMakeLists.txt.cmake
index d71d94dba..846356219 100644
--- a/.idea/fileTemplates/internal/CMakeLists.txt.cmake
+++ b/.idea/fileTemplates/internal/CMakeLists.txt.cmake
@@ -1,1 +1,1 @@
-#parse("NVIDIA_CMAKE_HEADER.cmake")
\ No newline at end of file
+#parse("NVIDIA_CMAKE_HEADER.cmake")
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 8822db8f1..2019083a1 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -4,4 +4,4 @@
   <component name="JavaScriptSettings">
     <option name="languageLevel" value="ES6" />
   </component>
-</project>
\ No newline at end of file
+</project>
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 94a25f7f4..5ace414d8 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -3,4 +3,4 @@
   <component name="VcsDirectoryMappings">
     <mapping directory="$PROJECT_DIR$" vcs="Git" />
   </component>
-</project>
\ No newline at end of file
+</project>
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5f2a03745..58656d15b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,16 +2,14 @@
 #   pre-commit install
 # To update the pre-commit hooks run:
 #   pre-commit install-hooks
-exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)'
+exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg|.*\.patch)(/|$)'
 repos:
-  # TODO: re-enable this after GitHub Actions migration, it currently makes a
-  # large number of changes that shouldn't go in the GitHub Actions PR.
-  #- repo: https://github.com/pre-commit/pre-commit-hooks
-  #  rev: v4.4.0
-  #  hooks:
-  #    - id: trailing-whitespace
-  #    - id: end-of-file-fixer
-  #    - id: debug-statements
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.4.0
+    hooks:
+      - id: trailing-whitespace
+      - id: end-of-file-fixer
+      - id: debug-statements
   - repo: https://github.com/PyCQA/isort
     rev: 5.12.0
     hooks:
diff --git a/3rdparty/LICENSE.CLI11 b/3rdparty/LICENSE.CLI11
index b2e9e76bc..17739d11c 100644
--- a/3rdparty/LICENSE.CLI11
+++ b/3rdparty/LICENSE.CLI11
@@ -22,4 +22,4 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.Catch2 b/3rdparty/LICENSE.Catch2
index 127a5bc39..36b7cd93c 100644
--- a/3rdparty/LICENSE.Catch2
+++ b/3rdparty/LICENSE.Catch2
@@ -20,4 +20,4 @@ FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-DEALINGS IN THE SOFTWARE.
\ No newline at end of file
+DEALINGS IN THE SOFTWARE.
diff --git a/3rdparty/LICENSE.aicsimageio b/3rdparty/LICENSE.aicsimageio
index 9d2714de3..4fe6ea85a 100644
--- a/3rdparty/LICENSE.aicsimageio
+++ b/3rdparty/LICENSE.aicsimageio
@@ -8,4 +8,4 @@ Redistribution and use in source and binary forms, with or without modification,
 
 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.benchmark b/3rdparty/LICENSE.benchmark
index 7a4a3ea24..d64569567 100644
--- a/3rdparty/LICENSE.benchmark
+++ b/3rdparty/LICENSE.benchmark
@@ -199,4 +199,4 @@
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
-   limitations under the License.
\ No newline at end of file
+   limitations under the License.
diff --git a/3rdparty/LICENSE.boost b/3rdparty/LICENSE.boost
index 127a5bc39..36b7cd93c 100644
--- a/3rdparty/LICENSE.boost
+++ b/3rdparty/LICENSE.boost
@@ -20,4 +20,4 @@ FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-DEALINGS IN THE SOFTWARE.
\ No newline at end of file
+DEALINGS IN THE SOFTWARE.
diff --git a/3rdparty/LICENSE.click b/3rdparty/LICENSE.click
index e63d78d30..d12a84918 100644
--- a/3rdparty/LICENSE.click
+++ b/3rdparty/LICENSE.click
@@ -25,4 +25,4 @@ TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.cupy b/3rdparty/LICENSE.cupy
index db8ef9d96..de1113bdd 100644
--- a/3rdparty/LICENSE.cupy
+++ b/3rdparty/LICENSE.cupy
@@ -17,4 +17,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
\ No newline at end of file
+THE SOFTWARE.
diff --git a/3rdparty/LICENSE.dask b/3rdparty/LICENSE.dask
index 720334e99..19079fc53 100644
--- a/3rdparty/LICENSE.dask
+++ b/3rdparty/LICENSE.dask
@@ -26,4 +26,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.dask-cuda b/3rdparty/LICENSE.dask-cuda
index 97b4c9dd9..d3a429420 100644
--- a/3rdparty/LICENSE.dask-cuda
+++ b/3rdparty/LICENSE.dask-cuda
@@ -198,4 +198,4 @@
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
-   limitations under the License.
\ No newline at end of file
+   limitations under the License.
diff --git a/3rdparty/LICENSE.dlpack b/3rdparty/LICENSE.dlpack
index 330d6516b..20a9c8a7b 100644
--- a/3rdparty/LICENSE.dlpack
+++ b/3rdparty/LICENSE.dlpack
@@ -198,4 +198,4 @@
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
-   limitations under the License.
\ No newline at end of file
+   limitations under the License.
diff --git a/3rdparty/LICENSE.dockcross b/3rdparty/LICENSE.dockcross
new file mode 100644
index 000000000..00affdc5f
--- /dev/null
+++ b/3rdparty/LICENSE.dockcross
@@ -0,0 +1,19 @@
+Copyright (c) 2015, 2016, 2017, 2018, 2021 Steeve Morin, Rob Burns, Matthew McCormick, Jean-Christophe-Fillion-Robin, Bensuperpc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/3rdparty/LICENSE.fmt b/3rdparty/LICENSE.fmt
index 8f9216805..f0ec3db4d 100644
--- a/3rdparty/LICENSE.fmt
+++ b/3rdparty/LICENSE.fmt
@@ -24,4 +24,4 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 As an exception, if, as a result of your compiling your source code, portions
 of this Software are embedded into a machine-executable object form of such
 source code, you may redistribute such embedded portions in such object form
-without including the above copyright and permission notices.
\ No newline at end of file
+without including the above copyright and permission notices.
diff --git a/3rdparty/LICENSE.folly b/3rdparty/LICENSE.folly
index 347b899b2..48bdb1282 100644
--- a/3rdparty/LICENSE.folly
+++ b/3rdparty/LICENSE.folly
@@ -197,4 +197,4 @@ Files in folly/external/farmhash licensed as follows
     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-    THE SOFTWARE.
\ No newline at end of file
+    THE SOFTWARE.
diff --git a/3rdparty/LICENSE.gputil b/3rdparty/LICENSE.gputil
index 4beb83afd..c099b9b5c 100644
--- a/3rdparty/LICENSE.gputil
+++ b/3rdparty/LICENSE.gputil
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/3rdparty/LICENSE.json b/3rdparty/LICENSE.json
index 548b989de..ae171d568 100644
--- a/3rdparty/LICENSE.json
+++ b/3rdparty/LICENSE.json
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/3rdparty/LICENSE.libcuckoo b/3rdparty/LICENSE.libcuckoo
index 9e29b39d8..9d8b367fc 100644
--- a/3rdparty/LICENSE.libcuckoo
+++ b/3rdparty/LICENSE.libcuckoo
@@ -15,4 +15,4 @@ limitations under the License.
 ---------------------------
 
 The third-party libraries have their own licenses, as detailed in their source
-files.
\ No newline at end of file
+files.
diff --git a/3rdparty/LICENSE.libdeflate b/3rdparty/LICENSE.libdeflate
index 784bb2131..1f1b81cd5 100644
--- a/3rdparty/LICENSE.libdeflate
+++ b/3rdparty/LICENSE.libdeflate
@@ -18,4 +18,4 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/3rdparty/LICENSE.libjpeg-turbo b/3rdparty/LICENSE.libjpeg-turbo
index 2ebba184e..99c9aadcc 100644
--- a/3rdparty/LICENSE.libjpeg-turbo
+++ b/3rdparty/LICENSE.libjpeg-turbo
@@ -129,4 +129,4 @@ libjpeg-turbo binary distributions under the IJG License.  However, the IJG
 License specifically refers to the Independent JPEG Group and does not extend
 attribution and endorsement protections to other entities.  Thus, it was
 desirable to choose a license that granted us the same protections for new code
-that were granted to the IJG for code derived from their software.
\ No newline at end of file
+that were granted to the IJG for code derived from their software.
diff --git a/3rdparty/LICENSE.libspng b/3rdparty/LICENSE.libspng
index 95edda67c..462e8c7f8 100644
--- a/3rdparty/LICENSE.libspng
+++ b/3rdparty/LICENSE.libspng
@@ -22,4 +22,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVERs
 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.libtiff b/3rdparty/LICENSE.libtiff
index 0c45c085e..dc255dec6 100644
--- a/3rdparty/LICENSE.libtiff
+++ b/3rdparty/LICENSE.libtiff
@@ -18,4 +18,4 @@ ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
 OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF
 LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
-OF THIS SOFTWARE.
\ No newline at end of file
+OF THIS SOFTWARE.
diff --git a/3rdparty/LICENSE.numpy b/3rdparty/LICENSE.numpy
index 6eddd9a2c..8ce645218 100644
--- a/3rdparty/LICENSE.numpy
+++ b/3rdparty/LICENSE.numpy
@@ -27,4 +27,4 @@ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.nvjpeg2000 b/3rdparty/LICENSE.nvjpeg2000
index 098bda266..1bfeab9ab 100644
--- a/3rdparty/LICENSE.nvjpeg2000
+++ b/3rdparty/LICENSE.nvjpeg2000
@@ -164,8 +164,3 @@ This supplement is an exhibit to the Agreement and is incorporated as an integra
 
 3. Licensing. If the distribution terms in this Agreement are not suitable for your organization, or for any questions regarding this Agreement, please contact NVIDIA at nvidia-compute-license-questions@nvidia.com.
  (v. August 27, 2020)
-
-
-
-
-
diff --git a/3rdparty/LICENSE.opencv-contrib-python b/3rdparty/LICENSE.opencv-contrib-python
index 7a4a3ea24..d64569567 100644
--- a/3rdparty/LICENSE.opencv-contrib-python
+++ b/3rdparty/LICENSE.opencv-contrib-python
@@ -199,4 +199,4 @@
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
-   limitations under the License.
\ No newline at end of file
+   limitations under the License.
diff --git a/3rdparty/LICENSE.openjpeg b/3rdparty/LICENSE.openjpeg
index 120e57eb9..a18c3fd3d 100644
--- a/3rdparty/LICENSE.openjpeg
+++ b/3rdparty/LICENSE.openjpeg
@@ -1,6 +1,6 @@
 /*
- * The copyright in this software is being made available under the 2-clauses 
- * BSD License, included below. This software may be subject to other third 
+ * The copyright in this software is being made available under the 2-clauses
+ * BSD License, included below. This software may be subject to other third
  * party and contributor rights, including patent rights, and no such rights
  * are granted under this license.
  *
@@ -11,7 +11,7 @@
  * Copyright (c) 2005, Herve Drolon, FreeImage Team
  * Copyright (c) 2002-2003, Yannick Verschueren
  * Copyright (c) 2001-2003, David Janssens
- * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France 
+ * Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France
  * Copyright (c) 2012, CS Systemes d'Information, France
  *
  * All rights reserved.
@@ -36,4 +36,4 @@
  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  * POSSIBILITY OF SUCH DAMAGE.
- */
\ No newline at end of file
+ */
diff --git a/3rdparty/LICENSE.openslide b/3rdparty/LICENSE.openslide
index 40c1a45fb..8cb71f4c9 100644
--- a/3rdparty/LICENSE.openslide
+++ b/3rdparty/LICENSE.openslide
@@ -15,4 +15,4 @@ version.  See the file lgpl-2.1.txt for the text of the license.
 OpenSlide is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-Lesser General Public License for more details.
\ No newline at end of file
+Lesser General Public License for more details.
diff --git a/3rdparty/LICENSE.psutil b/3rdparty/LICENSE.psutil
index 2caddc69c..0bf4a7fc0 100644
--- a/3rdparty/LICENSE.psutil
+++ b/3rdparty/LICENSE.psutil
@@ -26,4 +26,4 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.pugixml b/3rdparty/LICENSE.pugixml
index 8df4e0df7..2503b81eb 100644
--- a/3rdparty/LICENSE.pugixml
+++ b/3rdparty/LICENSE.pugixml
@@ -21,4 +21,4 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/3rdparty/LICENSE.pybind11 b/3rdparty/LICENSE.pybind11
index 596c20cde..e466b0dfd 100644
--- a/3rdparty/LICENSE.pybind11
+++ b/3rdparty/LICENSE.pybind11
@@ -26,4 +26,4 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of
-external contributions to this project including patches, pull requests, etc.
\ No newline at end of file
+external contributions to this project including patches, pull requests, etc.
diff --git a/3rdparty/LICENSE.pybind11_json b/3rdparty/LICENSE.pybind11_json
index 645936957..435ed08c1 100644
--- a/3rdparty/LICENSE.pybind11_json
+++ b/3rdparty/LICENSE.pybind11_json
@@ -26,4 +26,4 @@ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.pytorch b/3rdparty/LICENSE.pytorch
index 244b249f2..9cb8cbef5 100644
--- a/3rdparty/LICENSE.pytorch
+++ b/3rdparty/LICENSE.pytorch
@@ -70,4 +70,4 @@ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.rmm b/3rdparty/LICENSE.rmm
index f49a4e16e..261eeb9e9 100644
--- a/3rdparty/LICENSE.rmm
+++ b/3rdparty/LICENSE.rmm
@@ -198,4 +198,4 @@
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
-   limitations under the License.
\ No newline at end of file
+   limitations under the License.
diff --git a/3rdparty/LICENSE.scifio b/3rdparty/LICENSE.scifio
index 38b089cc9..7b6a642d7 100644
--- a/3rdparty/LICENSE.scifio
+++ b/3rdparty/LICENSE.scifio
@@ -21,4 +21,4 @@ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.scikit-image b/3rdparty/LICENSE.scikit-image
index 956f518e9..108806ddc 100644
--- a/3rdparty/LICENSE.scikit-image
+++ b/3rdparty/LICENSE.scikit-image
@@ -78,4 +78,4 @@ skimage/_shared/version_requirements.py:is_installed:
     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
     LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
     OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
+    WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/3rdparty/LICENSE.taskflow b/3rdparty/LICENSE.taskflow
index 33887bc43..1917f3681 100644
--- a/3rdparty/LICENSE.taskflow
+++ b/3rdparty/LICENSE.taskflow
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/3rdparty/LICENSE.tifffile b/3rdparty/LICENSE.tifffile
index baada32fb..007c06e24 100644
--- a/3rdparty/LICENSE.tifffile
+++ b/3rdparty/LICENSE.tifffile
@@ -27,4 +27,4 @@ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3c5c6e91e..0ab2e178c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -227,4 +227,4 @@ export(PACKAGE ${CUCIM_PACKAGE_NAME})
 #include(CPack)
 
 # Unset cached options needed
-unset(CUCIM_STATIC_GDS CACHE)
\ No newline at end of file
+unset(CUCIM_STATIC_GDS CACHE)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f89d1007e..0ccffe49d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -285,4 +285,4 @@ Once cuCIM is installed, you can test the module through `./run test` command.
 ./run test python unit skimage  # execute all python unit tests in `skimage` module
 ./run test python unit clara    # execute all python unit tests in `clara` module
 ./run test python performance   # execute all python performance tests
-```
\ No newline at end of file
+```
diff --git a/Dockerfile-cuda110 b/Dockerfile-cuda110
index fbf169cc2..725ddf2b6 100644
--- a/Dockerfile-cuda110
+++ b/Dockerfile-cuda110
@@ -13,7 +13,7 @@
 # limitations under the License.
 #
 
-FROM gigony/manylinux2014-x64:20201013-46b1bff
+FROM dockcross/manylinux2014-x64:20230215-2dd9a1c
 
 ENV DEFAULT_DOCKCROSS_IMAGE gigony/manylinux2014-x64:cuda110
 ENV PATH=/usr/local/cuda/bin/:$PATH
@@ -22,7 +22,7 @@ ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64/:/usr/local/cuda/nvvm/lib64:${LD_LIBRA
 
 RUN curl -LO https://developer.download.nvidia.com/compute/cuda/11.0.3/local_installers/cuda_11.0.3_450.51.06_linux.run && \
     chmod +x cuda_*.run && \
-    ./cuda_*.run --silent --no-opengl-libs --toolkit && \
+    ./cuda_*.run --silent --no-opengl-libs --toolkit --override && \
     rm -f cuda_*.run;
 
 RUN curl -LO https://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/libnvjpeg2k0-0.0.1.17-1.x86_64.rpm && \
diff --git a/Dockerfile-cuda111 b/Dockerfile-cuda111
index ff8a31881..428c154d6 100644
--- a/Dockerfile-cuda111
+++ b/Dockerfile-cuda111
@@ -13,7 +13,7 @@
 # limitations under the License.
 #
 
-FROM gigony/manylinux2014-x64:20201013-46b1bff
+FROM dockcross/manylinux2014-x64:20230215-2dd9a1c
 
 ENV DEFAULT_DOCKCROSS_IMAGE gigony/manylinux2014-x64:cuda111
 ENV PATH=/usr/local/cuda/bin/:$PATH
@@ -22,7 +22,7 @@ ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64/:/usr/local/cuda/nvvm/lib64:${LD_LIBRA
 
 RUN curl -LO https://developer.download.nvidia.com/compute/cuda/11.1.0/local_installers/cuda_11.1.0_455.23.05_linux.run && \
     chmod +x cuda_*.run && \
-    ./cuda_*.run --silent --no-opengl-libs --toolkit && \
+    ./cuda_*.run --silent --no-opengl-libs --toolkit --override && \
     rm -f cuda_*.run;
 
 RUN curl -LO https://developer.download.nvidia.com/compute/cuda/repos/rhel7/x86_64/libnvjpeg2k0-0.0.1.17-1.x86_64.rpm && \
diff --git a/LICENSE b/LICENSE
index aa19d8080..6361d294b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -200,4 +200,4 @@ Copyright (c) 2020-2021, NVIDIA CORPORATION.  All rights reserved.
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
-   limitations under the License.
\ No newline at end of file
+   limitations under the License.
diff --git a/LICENSE-3rdparty.md b/LICENSE-3rdparty.md
index 984d0e952..4c2a045b1 100644
--- a/LICENSE-3rdparty.md
+++ b/LICENSE-3rdparty.md
@@ -287,3 +287,9 @@ PBA+
   - https://github.com/orzzzjq/Parallel-Banding-Algorithm-plus/blob/master/LICENSE
 - Copyright: School of Computing, National University of Singapore
 - Usage: PBA+ is used to implement the Euclidean distance transform.
+
+dockcross
+- License: MIT License
+  - https://github.com/dockcross/dockcross/blob/master/LICENSE
+- Copyright: Steeve Morin, Rob Burns, Matthew McCormick, Jean-Christophe-Fillion-Robin, Bensuperpc
+- Usage: Building Python wheels for Linux
diff --git a/README.md b/README.md
index 9a92b8de5..b02dc3ac0 100644
--- a/README.md
+++ b/README.md
@@ -70,7 +70,7 @@ pip install scipy scikit-image cupy-cuda110
 
 ### Notebooks
 
-Please check out our [Welcome](notebooks/Welcome.ipynb) notebook ([NBViewer](https://nbviewer.jupyter.org/github/rapidsai/cucim/blob/branch-23.02/notebooks/Welcome.ipynb))
+Please check out our [Welcome](notebooks/Welcome.ipynb) notebook ([NBViewer](https://nbviewer.jupyter.org/github/rapidsai/cucim/blob/branch-23.04/notebooks/Welcome.ipynb))
 
 #### Downloading sample images
 
diff --git a/VERSION b/VERSION
index 7c8431de5..6fd949303 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-23.02.00
+23.04.00
diff --git a/benchmarks/primitives.cpp b/benchmarks/primitives.cpp
index cd47efea7..d3dd2da54 100644
--- a/benchmarks/primitives.cpp
+++ b/benchmarks/primitives.cpp
@@ -201,4 +201,4 @@ BENCHMARK_MAIN();
 // string_memcpy                   20.3 ns         20.3 ns     32102053
 // string_strcpy                   24.8 ns         24.8 ns     27352024
 // string_strdup                   32.4 ns         32.4 ns     21458177
-// ```
\ No newline at end of file
+// ```
diff --git a/benchmarks/skimage/_image_bench.py b/benchmarks/skimage/_image_bench.py
index 854fae290..8e6ea88fa 100644
--- a/benchmarks/skimage/_image_bench.py
+++ b/benchmarks/skimage/_image_bench.py
@@ -172,7 +172,7 @@ def run_benchmark(self, duration=3, verbose=True):
                 )
                 print("Number of Repetitions : ", rep_kwargs_gpu)
                 perf_gpu = repeat(self.func_gpu, self.args_gpu, kw_gpu, **rep_kwargs_gpu)
-                
+
                 df.at[index, "shape"] = f"{self.shape}"
                 # df.at[index,  "description"] = index
                 df.at[index, "function_name"] = self.function_name
diff --git a/benchmarks/skimage/bench_convolve.py b/benchmarks/skimage/bench_convolve.py
index 60007b8a3..5b2c9632d 100644
--- a/benchmarks/skimage/bench_convolve.py
+++ b/benchmarks/skimage/bench_convolve.py
@@ -136,4 +136,3 @@ def _compare_implementations_other(
                     t_elem = perf1.gpu_times * 1000.
                     t_shared = perf2.gpu_times * 1000.
                     print(f"{function.__name__} | {func_kwargs} | {shape} | {cp.dtype(dtype).name} | {mode} | {output_preallocated} | {t_elem.mean():0.3f} +/- {t_elem.std():0.3f}  | {t_shared.mean():0.3f} +/- {t_shared.std():0.3f} | {t_elem.mean() / t_shared.mean():0.3f}")
-
diff --git a/benchmarks/skimage/run-all.sh b/benchmarks/skimage/run-all.sh
index ab763b0d1..cd9729a5f 100644
--- a/benchmarks/skimage/run-all.sh
+++ b/benchmarks/skimage/run-all.sh
@@ -1,5 +1,5 @@
 for file in ./cu*py
 do
   echo $file
-  time python "$file" 
+  time python "$file"
 done
diff --git a/build.sh b/build.sh
index 4cd9df76a..2f96597d6 100755
--- a/build.sh
+++ b/build.sh
@@ -242,4 +242,4 @@ if [ -n "${SCRIPT_DIR}" ]; then
     main "$@"
 fi
 
-# CLARA_VERSION=0.7.1-2008.4 ./serverctl get_latest_version_of recipes clara_bootstrap 2> /dev/null
\ No newline at end of file
+# CLARA_VERSION=0.7.1-2008.4 ./serverctl get_latest_version_of recipes clara_bootstrap 2> /dev/null
diff --git a/ci/checks/style.sh b/ci/checks/style.sh
deleted file mode 100644
index a52ef4e71..000000000
--- a/ci/checks/style.sh
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/bin/bash
-# Copyright (c) 2020-2021, NVIDIA CORPORATION.
-######################
-# cuCIM Style Tester #
-######################
-
-# Ignore errors and set path
-set +e
-PATH=/opt/conda/bin:$PATH
-LC_ALL=C.UTF-8
-LANG=C.UTF-8
-
-# Activate common conda env
-. /opt/conda/etc/profile.d/conda.sh
-conda activate rapids
-
-# Switch to `python/`
-cd python/cucim
-
-# Run isort and get results/return code
-ISORT=`isort --check-only src`
-ISORT_RETVAL=$?
-
-# Run black and get results/return code
-#BLACK=`black --check .`
-#BLACK_RETVAL=$?
-
-# Run flake8 and get results/return code
-FLAKE=`flake8 .`
-FLAKE_RETVAL=$?
-
-# Run flake8-cython and get results/return code
-#FLAKE_CYTHON=`flake8 --config=.flake8.cython .`
-#FLAKE_CYTHON_RETVAL=$?
-
-# Output results if failure otherwise show pass
-if [ "$ISORT_RETVAL" != "0" ]; then
-  echo -e "\n\n>>>> FAILED: isort style check; begin output\n\n"
-  echo -e "$ISORT"
-  echo -e "\n\n>>>> FAILED: isort style check; end output\n\n"
-else
-  echo -e "\n\n>>>> PASSED: isort style check\n\n"
-fi
-
-#if [ "$BLACK_RETVAL" != "0" ]; then
-#  echo -e "\n\n>>>> FAILED: black style check; begin output\n\n"
-#  echo -e "$BLACK"
-#  echo -e "\n\n>>>> FAILED: black style check; end output\n\n"
-#else
-#  echo -e "\n\n>>>> PASSED: black style check\n\n"
-#fi
-
-if [ "$FLAKE_RETVAL" != "0" ]; then
-  echo -e "\n\n>>>> FAILED: flake8 style check; begin output\n\n"
-  echo -e "$FLAKE"
-  echo -e "\n\n>>>> FAILED: flake8 style check; end output\n\n"
-else
-  echo -e "\n\n>>>> PASSED: flake8 style check\n\n"
-fi
-
-if [ "$FLAKE_CYTHON_RETVAL" != "0" ]; then
-  echo -e "\n\n>>>> FAILED: flake8-cython style check; begin output\n\n"
-  echo -e "$FLAKE_CYTHON"
-  echo -e "\n\n>>>> FAILED: flake8-cython style check; end output\n\n"
-else
-  echo -e "\n\n>>>> PASSED: flake8-cython style check\n\n"
-fi
-
-#RETVALS=($ISORT_RETVAL $BLACK_RETVAL $FLAKE_RETVAL $FLAKE_CYTHON_RETVAL)
-RETVALS=($FLAKE_RETVAL $FLAKE_CYTHON_RETVAL)
-IFS=$'\n'
-RETVAL=`echo "${RETVALS[*]}" | sort -nr | head -n1`
-
-exit $RETVAL
-# exit 0 # don't force style checks yet
diff --git a/ci/cpu/build.sh b/ci/cpu/build.sh
deleted file mode 100755
index 7e995d6f5..000000000
--- a/ci/cpu/build.sh
+++ /dev/null
@@ -1,116 +0,0 @@
-#!/bin/bash
-# Copyright (c) 2020-2022, NVIDIA CORPORATION.
-######################################
-# ucx-py CPU conda build script for CI #
-######################################
-set -e
-
-# Set path and build parallel level
-export PATH=/opt/conda/bin:/usr/local/cuda/bin:$PATH
-export PARALLEL_LEVEL=${PARALLEL_LEVEL:-4}
-
-# Set home to the job's workspace
-export HOME=$WORKSPACE
-
-# Switch to project root; also root of repo checkout
-cd $WORKSPACE
-
-# Get latest tag and number of commits since tag
-export GIT_DESCRIBE_TAG=`git describe --abbrev=0 --tags`
-export GIT_DESCRIBE_NUMBER=`git rev-list ${GIT_DESCRIBE_TAG}..HEAD --count`
-
-# If nightly build, append current YYMMDD to version
-if [[ "$BUILD_MODE" = "branch" && "$SOURCE_BRANCH" = branch-* ]] ; then
-  export VERSION_SUFFIX=`date +%y%m%d`
-fi
-
-# Get CUDA and Python version
-export CUDA_VERSION=${CUDA_VERSION:-$(cat /usr/local/cuda/version.txt | egrep -o "[[:digit:]]+.[[:digit:]]+.[[:digit:]]+")}
-export CUDA_VER=${CUDA_VERSION%.*}
-export PYTHON_VER=${PYTHON:-$(python -c "import sys; print('.'.join(map(str, sys.version_info[:2])))")}
-echo "CUDA_VERSION: ${CUDA_VERSION}"
-echo "CUDA_VER    : ${CUDA_VER}"
-echo "PYTHON_VER  : ${PYTHON_VER}"
-
-# Setup 'gpuci_conda_retry' for build retries (results in 2 total attempts)
-export GPUCI_CONDA_RETRY_MAX=1
-export GPUCI_CONDA_RETRY_SLEEP=30
-
-# Workaround to keep Jenkins builds working
-# until we migrate fully to GitHub Actions
-export RAPIDS_CUDA_VERSION="${CUDA}"
-
-export CONDA_BLD_DIR="${WORKSPACE}/.conda-bld"
-
-################################################################################
-# SETUP - Check environment
-################################################################################
-
-gpuci_logger "Get env"
-env
-
-gpuci_logger "Activate conda env"
-. /opt/conda/etc/profile.d/conda.sh
-conda activate rapids
-
-gpuci_logger "Check versions"
-python --version
-$CC --version
-$CXX --version
-conda info
-conda config --show-sources
-conda list --show-channel-urls
-
-# FIX Added to deal with Anancoda SSL verification issues during conda builds
-conda config --set ssl_verify False
-
-# FIXME: Remove
-gpuci_mamba_retry install -c conda-forge boa
-
-################################################################################
-# BUILD - Conda package builds (conda deps: libcucim)
-################################################################################
-
-# We get some error message like below if we use 'rapids' conda environment:
-#    CMake Warning at build-release/_deps/deps-rmm-src/tests/CMakeLists.txt:52 (add_executable):
-#      Cannot generate a safe runtime search path for target LOGGER_PTDS_TEST
-#      because files in some directories may conflict with libraries in implicit
-#      directories:
-#        runtime library [libcudart.so.11.0] in /opt/conda/envs/rapids/conda-bld/libcucim_1616020264601/_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_pl/lib may be hidden by files in:
-#          /opt/conda/envs/rapids/lib
-
-if [ "$BUILD_LIBCUCIM" == 1 ]; then
-  gpuci_conda_retry mambabuild -c conda-forge -c rapidsai-nightly \
-    --dirty \
-    --no-remove-work-dir \
-    --no-build-id \
-    --croot ${CONDA_BLD_DIR} \
-    --use-local \
-    conda/recipes/libcucim
-    # Copy the conda working directory for Project Flash
-    mkdir -p ${CONDA_BLD_DIR}/libcucim/work
-    cp -r ${CONDA_BLD_DIR}/work/* ${CONDA_BLD_DIR}/libcucim/work
-fi
-
-if [ "$BUILD_CUCIM" == 1 ]; then
-  # Set libcucim conda build folder for CPU build
-  export LIBCUCIM_BLD_PATH=${WORKSPACE}/ci/artifacts/cucim/cpu/.conda-bld
-
-  gpuci_conda_retry mambabuild -c ${LIBCUCIM_BLD_PATH} -c conda-forge -c rapidsai-nightly \
-    --python=${PYTHON_VER} \
-    --dirty \
-    --no-remove-work-dir \
-    --croot ${CONDA_BLD_DIR} \
-    --use-local \
-    conda/recipes/cucim
-fi
-
-################################################################################
-# UPLOAD - Conda packages
-################################################################################
-
-logger "Upload conda pkgs"
-source ci/cpu/upload_anaconda.sh
-
-# gpuci_logger "Upload pypi pkg..."
-# source ci/cpu/upload-pypi.sh
diff --git a/ci/cpu/upload_anaconda.sh b/ci/cpu/upload_anaconda.sh
deleted file mode 100755
index cc199e8ce..000000000
--- a/ci/cpu/upload_anaconda.sh
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/bin/bash
-#
-# Adopted from https://github.com/tmcdonell/travis-scripts/blob/dfaac280ac2082cd6bcaba3217428347899f2975/update-accelerate-buildbot.sh
-
-set -e
-
-# Setup 'gpuci_retry' for upload retries (results in 4 total attempts)
-export GPUCI_RETRY_MAX=3
-export GPUCI_RETRY_SLEEP=30
-
-# Set default label options if they are not defined elsewhere
-export LABEL_OPTION=${LABEL_OPTION:-"--label main"}
-
-# Skip uploads unless BUILD_MODE == "branch"
-if [ ${BUILD_MODE} != "branch" ]; then
-  echo "Skipping upload"
-  return 0
-fi
-
-# Skip uploads if there is no upload key
-if [ -z "$MY_UPLOAD_KEY" ]; then
-  echo "No upload key"
-  return 0
-fi
-
-################################################################################
-# SETUP - Get conda file output locations
-################################################################################
-
-gpuci_logger "Get conda file output locations"
-export LIBCUCIM_FILE=`conda build --no-build-id --croot ${CONDA_BLD_DIR} conda/recipes/libcucim --output`
-export CUCIM_FILE=`conda build --croot ${CONDA_BLD_DIR} conda/recipes/cucim --python=$PYTHON --output`
-
-################################################################################
-# UPLOAD - Conda packages
-################################################################################
-
-gpuci_logger "Starting conda uploads"
-
-if [[ "$BUILD_LIBCUCIM" == "1" ]]; then
-    gpuci_logger "Upload libcuCIM"
-    gpuci_retry anaconda -t ${MY_UPLOAD_KEY} upload -u ${CONDA_USERNAME:-rapidsai} ${LABEL_OPTION} --skip-existing ${LIBCUCIM_FILE} --no-progress
-fi
-
-if [[ "$BUILD_CUCIM" == "1" ]]; then
-    gpuci_logger "Upload cuCIM"
-    gpuci_retry anaconda -t ${MY_UPLOAD_KEY} upload -u ${CONDA_USERNAME:-rapidsai} ${LABEL_OPTION} --skip-existing ${CUCIM_FILE} --no-progress
-fi
diff --git a/ci/docs/build.sh b/ci/docs/build.sh
index fa1a78193..ed70f8a7d 100644
--- a/ci/docs/build.sh
+++ b/ci/docs/build.sh
@@ -53,7 +53,7 @@ for PROJECT in ${PROJECTS[@]}; do
     if [ ! -d "api/$PROJECT/$BRANCH_VERSION" ]; then
         mkdir -p api/$PROJECT/$BRANCH_VERSION
     fi
-    rm -rf $DOCS_WORKSPACE/api/$PROJECT/$BRANCH_VERSION/*	
+    rm -rf $DOCS_WORKSPACE/api/$PROJECT/$BRANCH_VERSION/*
 done
 
 mv $PROJECT_WORKSPACE/docs/build/html/* $DOCS_WORKSPACE/api/cucim/$BRANCH_VERSION
diff --git a/ci/gpu/build.sh b/ci/gpu/build.sh
deleted file mode 100755
index ffcef97d4..000000000
--- a/ci/gpu/build.sh
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/bin/bash
-# Copyright (c) 2020-2022, NVIDIA CORPORATION.
-#########################################
-# ucx-py GPU build and test script for CI #
-#########################################
-set -e
-NUMARGS=$#
-ARGS=$*
-
-set -x
-
-# apt-get install libnuma libnuma-dev
-
-# Arg parsing function
-function hasArg {
-    (( ${NUMARGS} != 0 )) && (echo " ${ARGS} " | grep -q " $1 ")
-}
-
-# Set path and build parallel level
-export PATH=/opt/conda/bin:/usr/local/cuda/bin:$PATH
-export PARALLEL_LEVEL=${PARALLEL_LEVEL:-4}
-
-# Set home to the job's workspace
-export HOME=$WORKSPACE
-
-# Parse git describe
-cd $WORKSPACE
-export GIT_DESCRIBE_TAG=`git describe --abbrev=0 --tags`
-export MINOR_VERSION=`echo $GIT_DESCRIBE_TAG | grep -o -E '([0-9]+\.[0-9]+)'`
-echo "MINOR_VERSION: ${MINOR_VERSION}"
-unset GIT_DESCRIBE_TAG
-
-# Get CUDA and Python version
-export CUDA_VERSION=${CUDA_VERSION:-$(cat /usr/local/cuda/version.txt | egrep -o "[[:digit:]]+.[[:digit:]]+.[[:digit:]]+")}
-export CUDA_VER=${CUDA_VERSION%.*}
-export PYTHON_VER=${PYTHON:-$(python -c "import sys; print('.'.join(map(str, sys.version_info[:2])))")}
-echo "CUDA_VERSION: ${CUDA_VERSION}"
-echo "CUDA_VER    : ${CUDA_VER}"
-echo "PYTHON_VER  : ${PYTHON_VER}"
-
-# Workaround to keep Jenkins builds working
-# until we migrate fully to GitHub Actions
-export RAPIDS_CUDA_VERSION="${CUDA}"
-
-################################################################################
-# SETUP - Check environment
-################################################################################
-
-gpuci_logger "Check environment"
-env
-
-gpuci_logger "Check GPU usage"
-nvidia-smi
-
-gpuci_logger "Activate conda env"
-. /opt/conda/etc/profile.d/conda.sh
-conda activate rapids
-
-################################################################################
-# BUILD - Build cuCIM
-################################################################################
-
-# To use 'conda-forge'-based package installation and to use 'Project Flash' feature,
-# we fake conda build folder for libcucim to '$WORKSPACE/ci/artifacts/cucim/cpu/conda-bld/' which is
-# conda build folder for CPU build.
-# For GPU build, we fake conda build folder for cucim to '/opt/conda/envs/rapids/conda-bld'.
-LIBCUCIM_BLD_PATH=$WORKSPACE/ci/artifacts/cucim/cpu/.conda-bld
-CUCIM_BLD_PATH=/opt/conda/envs/rapids/conda-bld
-mkdir -p ${CUCIM_BLD_PATH}
-
-# TODO: Move boa install to gpuci/rapidsai
-gpuci_mamba_retry install -c conda-forge boa
-gpuci_conda_retry mambabuild -c ${LIBCUCIM_BLD_PATH} -c conda-forge -c rapidsai-nightly \
-    --dirty \
-    --no-remove-work-dir \
-    --croot ${CUCIM_BLD_PATH} \
-    conda/recipes/cucim
-
-################################################################################
-# TEST - Run py.tests for cuCIM
-################################################################################
-
-# Install cuCIM and its dependencies
-gpuci_logger "Installing cuCIM and its dependencies"
-gpuci_mamba_retry install -y -c ${LIBCUCIM_BLD_PATH} -c ${CUCIM_BLD_PATH} -c rapidsai-nightly \
-    libcucim \
-    cucim
-
-gpuci_logger "Testing cuCIM import"
-python -c 'import cucim'
-
-gpuci_logger "Check versions"
-python --version
-$CC --version
-$CXX --version
-conda info
-conda config --show-sources
-conda list --show-channel-urls
-
-if hasArg --skip-tests; then
-    gpuci_logger "Skipping Tests"
-else
-    gpuci_logger "Check GPU usage"
-    nvidia-smi
-
-    gpuci_logger "Check NICs"
-    awk 'END{print $1}' /etc/hosts
-    cat /etc/hosts
-
-    gpuci_logger "Python py.test for cuCIM"
-    ./run test python all
-fi
diff --git a/ci/local/README.md b/ci/local/README.md
deleted file mode 100644
index 26b1960ab..000000000
--- a/ci/local/README.md
+++ /dev/null
@@ -1,58 +0,0 @@
-## Purpose
-
-This script is designed for developer and contributor use. This tool mimics the actions of gpuCI on your local machine. This allows you to test and even debug your code inside a gpuCI base container before pushing your code as a GitHub commit.
-The script can be helpful in locally triaging and debugging RAPIDS continuous integration failures.
-
-## Requirements
-
-```
-nvidia-docker
-```
-
-## Usage
-
-```
-bash build.sh [-h] [-H] [-s] [-r <repo_dir>] [-i <image_name>]
-Build and test your local repository using a base gpuCI Docker image
-
-where:
-    -H   Show this help text
-    -r   Path to repository (defaults to working directory)
-    -i   Use Docker image (default is gpuci/rapidsai-base:cuda10.0-ubuntu16.04-gcc5-py3.6)
-    -s   Skip building and testing and start an interactive shell in a container of the Docker image
-```
-
-Example Usage:
-`bash build.sh -r ~/rapids/ucx-py -i gpuci/rapidsai-base:cuda10.1-ubuntu16.04-gcc5-py3.6`
-
-For a full list of available gpuCI docker images, visit our [DockerHub](https://hub.docker.com/r/gpuci/rapidsai-base/tags) page.
-
-Style Check:
-```bash
-$ bash ci/local/build.sh -r ~/rapids/ucx-py -s
-$ . /opt/conda/etc/profile.d/conda.sh
-$ conda activate rapids   #Activate gpuCI conda environment
-$ cd rapids
-$ flake8 python
-```
-
-## Information
-
-There are some caveats to be aware of when using this script, especially if you plan on developing from within the container itself.
-
-
-### Docker Image Build Repository
-
-The docker image will generate build artifacts in a folder on your machine located in the `root` directory of the repository you passed to the script. For the above example, the directory is named `~/rapids/ucx-py/build_rapidsai-base_cuda10.1-ubuntu16.04-gcc5-py3.6/`. Feel free to remove this directory after the script is finished.
-
-*Note*: The script *will not* override your local build repository. Your local environment stays in tact.
-
-
-### Where The User is Dumped
-
-The script will build your repository and run all tests. If any tests fail, it dumps the user into the docker container itself to allow you to debug from within the container. If all the tests pass as expected the container exits and is automatically removed. Remember to exit the container if tests fail and you do not wish to debug within the container itself.
-
-
-### Container File Structure
-
-Your repository will be located in the `/rapids/` folder of the container. This folder is volume mounted from the local machine. Any changes to the code in this repository are replicated onto the local machine. The `cpp/build` and `python/build` directories within your repository is on a separate mount to avoid conflicting with your local build artifacts.
diff --git a/ci/local/build.sh b/ci/local/build.sh
deleted file mode 100755
index 598969bcd..000000000
--- a/ci/local/build.sh
+++ /dev/null
@@ -1,146 +0,0 @@
-#!/bin/bash
-
-
-GIT_DESCRIBE_TAG=`git describe --tags`
-MINOR_VERSION=`echo $GIT_DESCRIBE_TAG | grep -o -E '([0-9]+\.[0-9]+)'`
-
-DOCKER_IMAGE="gpuci/rapidsai:${MINOR_VERSION}-cuda10.1-devel-ubuntu16.04-py3.7"
-REPO_PATH=${PWD}
-RAPIDS_DIR_IN_CONTAINER="/rapids"
-CPP_BUILD_DIR="cpp/build"
-PYTHON_BUILD_DIR="python/build"
-CONTAINER_SHELL_ONLY=0
-
-SHORTHELP="$(basename "$0") [-h] [-H] [-s] [-r <repo_dir>] [-i <image_name>]"
-LONGHELP="${SHORTHELP}
-Build and test your local repository using a base gpuCI Docker image
-
-where:
-    -H   Show this help text
-    -r   Path to repository (defaults to working directory)
-    -i   Use Docker image (default is ${DOCKER_IMAGE})
-    -s   Skip building and testing and start an interactive shell in a container of the Docker image
-"
-
-# Limit GPUs available to container based on CUDA_VISIBLE_DEVICES
-if [[ -z "${CUDA_VISIBLE_DEVICES}" ]]; then
-    NVIDIA_VISIBLE_DEVICES="all"
-else
-    NVIDIA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES}
-fi
-
-while getopts ":hHr:i:s" option; do
-    case ${option} in
-        r)
-            REPO_PATH=${OPTARG}
-            ;;
-        i)
-            DOCKER_IMAGE=${OPTARG}
-            ;;
-        s)
-            CONTAINER_SHELL_ONLY=1
-            ;;
-        h)
-            echo "${SHORTHELP}"
-            exit 0
-            ;;
-        H)
-            echo "${LONGHELP}"
-            exit 0
-            ;;
-        *)
-            echo "ERROR: Invalid flag"
-            echo "${SHORTHELP}"
-            exit 1
-            ;;
-    esac
-done
-
-REPO_PATH_IN_CONTAINER="${RAPIDS_DIR_IN_CONTAINER}/$(basename "${REPO_PATH}")"
-CPP_BUILD_DIR_IN_CONTAINER="${RAPIDS_DIR_IN_CONTAINER}/$(basename "${REPO_PATH}")/${CPP_BUILD_DIR}"
-PYTHON_BUILD_DIR_IN_CONTAINER="${RAPIDS_DIR_IN_CONTAINER}/$(basename "${REPO_PATH}")/${PYTHON_BUILD_DIR}"
-
-
-# BASE_CONTAINER_BUILD_DIR is named after the image name, allowing for
-# multiple image builds to coexist on the local filesystem. This will
-# be mapped to the typical BUILD_DIR inside of the container. Builds
-# running in the container generate build artifacts just as they would
-# in a bare-metal environment, and the host filesystem is able to
-# maintain the host build in BUILD_DIR as well.
-# FIXME: Fix the shellcheck complaints
-# shellcheck disable=SC2001,SC2005,SC2046
-BASE_CONTAINER_BUILD_DIR=${REPO_PATH}/build_$(echo $(basename "${DOCKER_IMAGE}")|sed -e 's/:/_/g')
-CPP_CONTAINER_BUILD_DIR=${BASE_CONTAINER_BUILD_DIR}/cpp
-PYTHON_CONTAINER_BUILD_DIR=${BASE_CONTAINER_BUILD_DIR}/python
-# Create build directories. This is to ensure correct owner for directories. If
-# directories don't exist there is side effect from docker volume mounting creating build
-# directories owned by root(volume mount point(s))
-mkdir -p "${REPO_PATH}/${CPP_BUILD_DIR}"
-mkdir -p "${REPO_PATH}/${PYTHON_BUILD_DIR}"
-
-BUILD_SCRIPT="#!/bin/bash
-set -e
-WORKSPACE=${REPO_PATH_IN_CONTAINER}
-PREBUILD_SCRIPT=${REPO_PATH_IN_CONTAINER}/ci/gpu/prebuild.sh
-BUILD_SCRIPT=${REPO_PATH_IN_CONTAINER}/ci/gpu/build.sh
-cd \${WORKSPACE}
-if [ -f \${PREBUILD_SCRIPT} ]; then
-    source \${PREBUILD_SCRIPT}
-fi
-yes | source \${BUILD_SCRIPT}
-"
-
-if (( CONTAINER_SHELL_ONLY == 0 )); then
-    COMMAND="${CPP_BUILD_DIR_IN_CONTAINER}/build.sh || bash"
-else
-    COMMAND="bash"
-fi
-
-# Create the build dir for the container to mount, generate the build script inside of it
-mkdir -p "${BASE_CONTAINER_BUILD_DIR}"
-mkdir -p "${CPP_CONTAINER_BUILD_DIR}"
-mkdir -p "${PYTHON_CONTAINER_BUILD_DIR}"
-echo "${BUILD_SCRIPT}" > "${CPP_CONTAINER_BUILD_DIR}/build.sh"
-chmod ugo+x "${CPP_CONTAINER_BUILD_DIR}/build.sh"
-
-# Mount passwd and group files to docker. This allows docker to resolve username and group
-# avoiding these nags:
-#   * groups: cannot find name for group ID ID
-#   * I have no name!@id:/$
-# For ldap user user information is not present in system /etc/passwd and /etc/group files.
-# Hence we generate dummy files for ldap users which docker uses to resolve username and group
-
-PASSWD_FILE="/etc/passwd"
-GROUP_FILE="/etc/group"
-
-USER_FOUND=$(grep -wc "$(whoami)" < "$PASSWD_FILE")
-if [ "$USER_FOUND" == 0 ]; then
-  echo "Local User not found, LDAP WAR for docker mounts activated. Creating dummy passwd and group"
-  echo "files to allow docker resolve username and group"
-  cp "$PASSWD_FILE" /tmp/passwd
-  PASSWD_FILE="/tmp/passwd"
-  cp "$GROUP_FILE" /tmp/group
-  GROUP_FILE="/tmp/group"
-  echo "$(whoami):x:$(id -u):$(id -g):$(whoami),,,:$HOME:$SHELL" >> "$PASSWD_FILE"
-  echo "$(whoami):x:$(id -g):" >> "$GROUP_FILE"
-fi
-
-# Run the generated build script in a container
-docker pull "${DOCKER_IMAGE}"
-
-DOCKER_MAJOR=$(docker -v|sed 's/[^[0-9]*\([0-9]*\).*/\1/')
-GPU_OPTS="--gpus device=${NVIDIA_VISIBLE_DEVICES}"
-if [ "$DOCKER_MAJOR" -lt 19 ]
-then
-    GPU_OPTS="--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES='${NVIDIA_VISIBLE_DEVICES}'"
-fi
-
-docker run --rm -it ${GPU_OPTS} \
-       -u "$(id -u)":"$(id -g)" \
-       -v "${REPO_PATH}":"${REPO_PATH_IN_CONTAINER}" \
-       -v "${CPP_CONTAINER_BUILD_DIR}":"${CPP_BUILD_DIR_IN_CONTAINER}" \
-       -v "${PYTHON_CONTAINER_BUILD_DIR}":"${PYTHON_BUILD_DIR_IN_CONTAINER}" \
-       -v "$PASSWD_FILE":/etc/passwd:ro \
-       -v "$GROUP_FILE":/etc/group:ro \
-       --cap-add=SYS_PTRACE \
-       "${DOCKER_IMAGE}" bash -c "${COMMAND}"
diff --git a/conda/environments/all_cuda-118_arch-x86_64.yaml b/conda/environments/all_cuda-118_arch-x86_64.yaml
index 5d46021d7..d804f55b0 100644
--- a/conda/environments/all_cuda-118_arch-x86_64.yaml
+++ b/conda/environments/all_cuda-118_arch-x86_64.yaml
@@ -13,7 +13,7 @@ dependencies:
 - cudatoolkit=11.8
 - cupy >=10,<12.0.0a0
 - cxx-compiler
-- gcc_linux-64=9.*
+- gcc_linux-64=11.*
 - imagecodecs>=2021.6.8
 - ipython
 - jbig
@@ -35,7 +35,7 @@ dependencies:
 - pytest>=6.2.4
 - python>=3.8,<3.11
 - recommonmark
-- scikit-image >=0.19.0,<0.20.0a0
+- scikit-image >=0.19.0,<0.21.0a0
 - scipy
 - sphinx<6
 - sysroot_linux-64==2.17
diff --git a/conda/recipes/cucim/conda_build_config.yaml b/conda/recipes/cucim/conda_build_config.yaml
index c049d21fd..ccc49851a 100644
--- a/conda/recipes/cucim/conda_build_config.yaml
+++ b/conda/recipes/cucim/conda_build_config.yaml
@@ -1,8 +1,8 @@
 c_compiler_version:
-  - 9
+  - 11
 
 cxx_compiler_version:
-  - 9
+  - 11
 
 sysroot_version:
   - "2.17"
diff --git a/conda/recipes/cucim/meta.yaml b/conda/recipes/cucim/meta.yaml
index 252e705f6..7772aaaf6 100644
--- a/conda/recipes/cucim/meta.yaml
+++ b/conda/recipes/cucim/meta.yaml
@@ -1,10 +1,11 @@
-# Copyright (c) 2021-2022, NVIDIA CORPORATION.
+# Copyright (c) 2021-2023, NVIDIA CORPORATION.
 
-{% set version = environ.get('GIT_DESCRIBE_TAG', '0.0.0.dev').lstrip('v') + environ.get('VERSION_SUFFIX', '') %}
+{% set version = environ.get('GIT_DESCRIBE_TAG', '0.0.0.dev').lstrip('v') %}
 {% set minor_version = version.split('.')[0] + '.' + version.split('.')[2] %}
 {% set py_version = environ['CONDA_PY'] %}
 {% set cuda_version = '.'.join(environ['RAPIDS_CUDA_VERSION'].split('.')[:2]) %}
 {% set cuda_major = cuda_version.split('.')[0] %}
+{% set date_string = environ['RAPIDS_DATE_STRING'] %}
 
 package:
   name: cucim
@@ -15,10 +16,11 @@ source:
 
 build:
   number: {{ GIT_DESCRIBE_NUMBER }}
-  string: cuda_{{ cuda_major }}_py{{ py_version }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }}
+  string: cuda_{{ cuda_major }}_py{{ py_version }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }}
   script_env:
     - AWS_ACCESS_KEY_ID
     - AWS_SECRET_ACCESS_KEY
+    - AWS_SESSION_TOKEN
     - CMAKE_C_COMPILER_LAUNCHER
     - CMAKE_CUDA_COMPILER_LAUNCHER
     - CMAKE_CXX_COMPILER_LAUNCHER
@@ -29,6 +31,7 @@ build:
     - SCCACHE_REGION
     - SCCACHE_S3_KEY_PREFIX=cucim-aarch64 # [aarch64]
     - SCCACHE_S3_KEY_PREFIX=cucim-linux64 # [linux64]
+    - SCCACHE_S3_USE_SSL
 
 requirements:
   build:
@@ -45,7 +48,7 @@ requirements:
     - libcucim ={{ version }}
     - numpy 1.21
     - python
-    - scikit-image >=0.19.0,<0.20.0a0
+    - scikit-image >=0.19.0,<0.21.0a0
     - scipy
   run:
     - {{ pin_compatible('cudatoolkit', max_pin='x', min_pin='x') }}
@@ -55,15 +58,14 @@ requirements:
     - libcucim ={{ version }}
     # - openslide # skipping here but benchmark binary would need openslide library
     - python
-    - scikit-image >=0.19.0,<0.20.0a0
+    - scikit-image >=0.19.0,<0.21.0a0
     - scipy
 
-# TODO: Remove the linux64 tags on tests after disabling gpuCI / Jenkins
-tests:                                 # [linux64]
-  requirements:                        # [linux64]
-    - cudatoolkit ={{ cuda_version }}  # [linux64]
-  imports:                             # [linux64]
-    - cucim                            # [linux64]
+tests:
+  requirements:
+    - cudatoolkit ={{ cuda_version }}
+  imports:
+    - cucim
 
 about:
   home: https://developer.nvidia.com/multidimensional-image-processing
diff --git a/conda/recipes/libcucim/conda_build_config.yaml b/conda/recipes/libcucim/conda_build_config.yaml
index c049d21fd..ccc49851a 100644
--- a/conda/recipes/libcucim/conda_build_config.yaml
+++ b/conda/recipes/libcucim/conda_build_config.yaml
@@ -1,8 +1,8 @@
 c_compiler_version:
-  - 9
+  - 11
 
 cxx_compiler_version:
-  - 9
+  - 11
 
 sysroot_version:
   - "2.17"
diff --git a/conda/recipes/libcucim/meta.yaml b/conda/recipes/libcucim/meta.yaml
index 92a33b5b1..7543fafb2 100644
--- a/conda/recipes/libcucim/meta.yaml
+++ b/conda/recipes/libcucim/meta.yaml
@@ -1,9 +1,10 @@
-# Copyright (c) 2021-2022, NVIDIA CORPORATION.
+# Copyright (c) 2021-2023, NVIDIA CORPORATION.
 
-{% set version = environ.get('GIT_DESCRIBE_TAG', '0.0.0.dev').lstrip('v') + environ.get('VERSION_SUFFIX', '') %}
+{% set version = environ.get('GIT_DESCRIBE_TAG', '0.0.0.dev').lstrip('v') %}
 {% set minor_version = version.split('.')[0] + '.' + version.split('.')[1] %}
 {% set cuda_version = '.'.join(environ['RAPIDS_CUDA_VERSION'].split('.')[:2]) %}
 {% set cuda_major = cuda_version.split('.')[0] %}
+{% set date_string = environ['RAPIDS_DATE_STRING'] %}
 
 package:
   name: libcucim
@@ -14,10 +15,11 @@ source:
 
 build:
   number: {{ GIT_DESCRIBE_NUMBER }}
-  string: cuda{{ cuda_major }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }}
+  string: cuda{{ cuda_major }}_{{ date_string }}_{{ GIT_DESCRIBE_HASH }}_{{ GIT_DESCRIBE_NUMBER }}
   script_env:
     - AWS_ACCESS_KEY_ID
     - AWS_SECRET_ACCESS_KEY
+    - AWS_SESSION_TOKEN
     - CMAKE_C_COMPILER_LAUNCHER
     - CMAKE_CUDA_COMPILER_LAUNCHER
     - CMAKE_CXX_COMPILER_LAUNCHER
@@ -28,6 +30,7 @@ build:
     - SCCACHE_REGION
     - SCCACHE_S3_KEY_PREFIX=libcucim-aarch64 # [aarch64]
     - SCCACHE_S3_KEY_PREFIX=libcucim-linux64 # [linux64]
+    - SCCACHE_S3_USE_SSL
 
 requirements:
   build:
diff --git a/cpp/cmake/cucim-config.cmake.in b/cpp/cmake/cucim-config.cmake.in
index 42face690..e924ad2ad 100644
--- a/cpp/cmake/cucim-config.cmake.in
+++ b/cpp/cmake/cucim-config.cmake.in
@@ -23,4 +23,4 @@ find_dependency(CUDAToolkit)
 
 if(NOT TARGET cucim::cucim)
     include(${CMAKE_CURRENT_LIST_DIR}/cucim-targets.cmake)
-endif()
\ No newline at end of file
+endif()
diff --git a/cpp/cmake/deps/abseil.cmake b/cpp/cmake/deps/abseil.cmake
index d82f50334..72b27f2a2 100644
--- a/cpp/cmake/deps/abseil.cmake
+++ b/cpp/cmake/deps/abseil.cmake
@@ -41,4 +41,4 @@ if (NOT TARGET deps::abseil)
     target_link_libraries(deps::abseil INTERFACE absl::strings)
     set(deps-abseil_SOURCE_DIR ${deps-abseil_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-abseil_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/cmake/deps/boost.cmake b/cpp/cmake/deps/boost.cmake
index 558740e1e..3fb6db1e9 100644
--- a/cpp/cmake/deps/boost.cmake
+++ b/cpp/cmake/deps/boost.cmake
@@ -72,4 +72,4 @@ if (NOT TARGET deps::boost)
 
     set(deps-boost_SOURCE_DIR ${deps-boost_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-boost_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/cmake/deps/catch2.cmake b/cpp/cmake/deps/catch2.cmake
index 56d22a3c3..f5e9ca616 100644
--- a/cpp/cmake/deps/catch2.cmake
+++ b/cpp/cmake/deps/catch2.cmake
@@ -37,4 +37,4 @@ if (NOT TARGET deps::catch2)
     target_link_libraries(deps::catch2 INTERFACE Catch2::Catch2)
     set(deps-catch2_SOURCE_DIR ${deps-catch2_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-catch2_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/cmake/deps/cli11.cmake b/cpp/cmake/deps/cli11.cmake
index 343e69e18..185913c8e 100644
--- a/cpp/cmake/deps/cli11.cmake
+++ b/cpp/cmake/deps/cli11.cmake
@@ -38,4 +38,4 @@ endif ()
 # Note that library had a failure with nvcc compiler and gcc 9.x headers
 #  ...c++/9/tuple(553): error: pack "_UElements" does not have the same number of elements as "_Elements"
 #            __and_<is_nothrow_assignable<_Elements&, _UElements>...>::value;
-# Not using nvcc for main code that uses cli11 solved the issue.
\ No newline at end of file
+# Not using nvcc for main code that uses cli11 solved the issue.
diff --git a/cpp/cmake/deps/fmt.cmake b/cpp/cmake/deps/fmt.cmake
index 59e9c1fce..ef2797e18 100644
--- a/cpp/cmake/deps/fmt.cmake
+++ b/cpp/cmake/deps/fmt.cmake
@@ -40,4 +40,4 @@ if (NOT TARGET deps::fmt)
     target_link_libraries(deps::fmt INTERFACE fmt::fmt-header-only)
     set(deps-fmt_SOURCE_DIR ${deps-fmt_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-fmt_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/cmake/deps/json.cmake b/cpp/cmake/deps/json.cmake
index 4f716f120..c3f258ee1 100644
--- a/cpp/cmake/deps/json.cmake
+++ b/cpp/cmake/deps/json.cmake
@@ -37,4 +37,4 @@ if (NOT TARGET deps::json)
     target_link_libraries(deps::json INTERFACE nlohmann_json::nlohmann_json)
     set(deps-json_SOURCE_DIR ${deps-json_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-json_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/cmake/deps/libcuckoo.cmake b/cpp/cmake/deps/libcuckoo.cmake
index 78e53252a..3089730bc 100644
--- a/cpp/cmake/deps/libcuckoo.cmake
+++ b/cpp/cmake/deps/libcuckoo.cmake
@@ -62,4 +62,4 @@ if (NOT TARGET deps::libcuckoo)
     target_link_libraries(deps::libcuckoo INTERFACE libcuckoo)
     set(deps-libcuckoo_SOURCE_DIR ${deps-libcuckoo_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-libcuckoo_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/cmake/deps/pybind11.cmake b/cpp/cmake/deps/pybind11.cmake
index b7dca11f7..1fbba6c9d 100644
--- a/cpp/cmake/deps/pybind11.cmake
+++ b/cpp/cmake/deps/pybind11.cmake
@@ -17,7 +17,7 @@ if (NOT TARGET deps::pybind11)
     FetchContent_Declare(
             deps-pybind11
             GIT_REPOSITORY https://github.com/pybind/pybind11.git
-            GIT_TAG v2.6.2
+            GIT_TAG v2.10.3
             GIT_SHALLOW TRUE
     )
     FetchContent_GetProperties(deps-pybind11)
diff --git a/cpp/include/cucim/cpp20/find_if.h b/cpp/include/cucim/cpp20/find_if.h
index c046ef003..d5fae120c 100644
--- a/cpp/include/cucim/cpp20/find_if.h
+++ b/cpp/include/cucim/cpp20/find_if.h
@@ -42,4 +42,4 @@ constexpr InputIt find_if(InputIt first, InputIt last, UnaryPredicate p)
 }
 #endif
 
-}
\ No newline at end of file
+}
diff --git a/cpp/plugins/cucim.kit.cumed/.gitignore b/cpp/plugins/cucim.kit.cumed/.gitignore
index f593ea4f4..84a73e644 100644
--- a/cpp/plugins/cucim.kit.cumed/.gitignore
+++ b/cpp/plugins/cucim.kit.cumed/.gitignore
@@ -1,3 +1,2 @@
 cmake-build*
 install
-
diff --git a/cpp/plugins/cucim.kit.cumed/VERSION b/cpp/plugins/cucim.kit.cumed/VERSION
index 7c8431de5..6fd949303 100644
--- a/cpp/plugins/cucim.kit.cumed/VERSION
+++ b/cpp/plugins/cucim.kit.cumed/VERSION
@@ -1,1 +1,1 @@
-23.02.00
+23.04.00
diff --git a/cpp/plugins/cucim.kit.cuslide/.gitignore b/cpp/plugins/cucim.kit.cuslide/.gitignore
index f593ea4f4..84a73e644 100644
--- a/cpp/plugins/cucim.kit.cuslide/.gitignore
+++ b/cpp/plugins/cucim.kit.cuslide/.gitignore
@@ -1,3 +1,2 @@
 cmake-build*
 install
-
diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/.name b/cpp/plugins/cucim.kit.cuslide/.idea/.name
index 2d6ff4e37..cc09966de 100644
--- a/cpp/plugins/cucim.kit.cuslide/.idea/.name
+++ b/cpp/plugins/cucim.kit.cuslide/.idea/.name
@@ -1,1 +1,1 @@
-cuslide
\ No newline at end of file
+cuslide
diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/Project.xml b/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/Project.xml
index f60388162..c8f84c353 100644
--- a/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/Project.xml
+++ b/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/Project.xml
@@ -4,4 +4,4 @@
       <option name="ENABLED" value="true" />
     </clangFormatSettings>
   </code_scheme>
-</component>
\ No newline at end of file
+</component>
diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/codeStyleConfig.xml b/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/codeStyleConfig.xml
index 79ee123c2..0f7bc519d 100644
--- a/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/codeStyleConfig.xml
+++ b/cpp/plugins/cucim.kit.cuslide/.idea/codeStyles/codeStyleConfig.xml
@@ -2,4 +2,4 @@
   <state>
     <option name="USE_PER_PROJECT_SETTINGS" value="true" />
   </state>
-</component>
\ No newline at end of file
+</component>
diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/cucim.kit.cuslide.iml b/cpp/plugins/cucim.kit.cuslide/.idea/cucim.kit.cuslide.iml
index 6d70257c7..08cda128a 100644
--- a/cpp/plugins/cucim.kit.cuslide/.idea/cucim.kit.cuslide.iml
+++ b/cpp/plugins/cucim.kit.cuslide/.idea/cucim.kit.cuslide.iml
@@ -1,2 +1,2 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<module classpath="CMake" type="CPP_MODULE" version="4" />
\ No newline at end of file
+<module classpath="CMake" type="CPP_MODULE" version="4" />
diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h
index b0e223c0a..cf0461d4c 100644
--- a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h
+++ b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h
@@ -12,4 +12,4 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- */
\ No newline at end of file
+ */
diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/CMakeLists.txt.cmake b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/CMakeLists.txt.cmake
index d71d94dba..846356219 100644
--- a/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/CMakeLists.txt.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/.idea/fileTemplates/internal/CMakeLists.txt.cmake
@@ -1,1 +1,1 @@
-#parse("NVIDIA_CMAKE_HEADER.cmake")
\ No newline at end of file
+#parse("NVIDIA_CMAKE_HEADER.cmake")
diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/misc.xml b/cpp/plugins/cucim.kit.cuslide/.idea/misc.xml
index 8822db8f1..2019083a1 100644
--- a/cpp/plugins/cucim.kit.cuslide/.idea/misc.xml
+++ b/cpp/plugins/cucim.kit.cuslide/.idea/misc.xml
@@ -4,4 +4,4 @@
   <component name="JavaScriptSettings">
     <option name="languageLevel" value="ES6" />
   </component>
-</project>
\ No newline at end of file
+</project>
diff --git a/cpp/plugins/cucim.kit.cuslide/.idea/vcs.xml b/cpp/plugins/cucim.kit.cuslide/.idea/vcs.xml
index c2365ab11..fbbc5665e 100644
--- a/cpp/plugins/cucim.kit.cuslide/.idea/vcs.xml
+++ b/cpp/plugins/cucim.kit.cuslide/.idea/vcs.xml
@@ -3,4 +3,4 @@
   <component name="VcsDirectoryMappings">
     <mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
   </component>
-</project>
\ No newline at end of file
+</project>
diff --git a/cpp/plugins/cucim.kit.cuslide/.vscode/settings.json b/cpp/plugins/cucim.kit.cuslide/.vscode/settings.json
index e774417ea..27b5e58d8 100644
--- a/cpp/plugins/cucim.kit.cuslide/.vscode/settings.json
+++ b/cpp/plugins/cucim.kit.cuslide/.vscode/settings.json
@@ -1,4 +1,4 @@
 {
     "cmake.sourceDirectory": "${workspaceFolder}/.",
     "cmake.buildDirectory": "${workspaceFolder}/build-debug"
-}
\ No newline at end of file
+}
diff --git a/cpp/plugins/cucim.kit.cuslide/VERSION b/cpp/plugins/cucim.kit.cuslide/VERSION
index 7c8431de5..6fd949303 100644
--- a/cpp/plugins/cucim.kit.cuslide/VERSION
+++ b/cpp/plugins/cucim.kit.cuslide/VERSION
@@ -1,1 +1,1 @@
-23.02.00
+23.04.00
diff --git a/cpp/plugins/cucim.kit.cuslide/benchmarks/main.cpp b/cpp/plugins/cucim.kit.cuslide/benchmarks/main.cpp
index c10925f6b..01605dca0 100644
--- a/cpp/plugins/cucim.kit.cuslide/benchmarks/main.cpp
+++ b/cpp/plugins/cucim.kit.cuslide/benchmarks/main.cpp
@@ -253,4 +253,4 @@ int main(int argc, char** argv)
         return 1;
     }
     ::benchmark::RunSpecifiedBenchmarks();
-}
\ No newline at end of file
+}
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/cucim.kit.cuslide-config.cmake.in b/cpp/plugins/cucim.kit.cuslide/cmake/cucim.kit.cuslide-config.cmake.in
index e62e35716..0c19065f8 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/cucim.kit.cuslide-config.cmake.in
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/cucim.kit.cuslide-config.cmake.in
@@ -22,4 +22,4 @@ include(CMakeFindDependencyMacro)
 
 if(NOT TARGET cuslide::cuslide)
     include(${CMAKE_CURRENT_LIST_DIR}/cucim.kit.cuslide-targets.cmake)
-endif()
\ No newline at end of file
+endif()
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/boost.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/boost.cmake
index 558740e1e..3fb6db1e9 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/boost.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/boost.cmake
@@ -72,4 +72,4 @@ if (NOT TARGET deps::boost)
 
     set(deps-boost_SOURCE_DIR ${deps-boost_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-boost_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/catch2.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/catch2.cmake
index 56d22a3c3..f5e9ca616 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/catch2.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/catch2.cmake
@@ -37,4 +37,4 @@ if (NOT TARGET deps::catch2)
     target_link_libraries(deps::catch2 INTERFACE Catch2::Catch2)
     set(deps-catch2_SOURCE_DIR ${deps-catch2_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-catch2_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/cli11.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/cli11.cmake
index 343e69e18..185913c8e 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/cli11.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/cli11.cmake
@@ -38,4 +38,4 @@ endif ()
 # Note that library had a failure with nvcc compiler and gcc 9.x headers
 #  ...c++/9/tuple(553): error: pack "_UElements" does not have the same number of elements as "_Elements"
 #            __and_<is_nothrow_assignable<_Elements&, _UElements>...>::value;
-# Not using nvcc for main code that uses cli11 solved the issue.
\ No newline at end of file
+# Not using nvcc for main code that uses cli11 solved the issue.
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/fmt.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/fmt.cmake
index be0fa2ec1..2ca43a98d 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/fmt.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/fmt.cmake
@@ -39,4 +39,4 @@ if (NOT TARGET deps::fmt)
     target_link_libraries(deps::fmt INTERFACE fmt::fmt-header-only)
     set(deps-fmt_SOURCE_DIR ${deps-fmt_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-fmt_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/json.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/json.cmake
index 4f716f120..c3f258ee1 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/json.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/json.cmake
@@ -37,4 +37,4 @@ if (NOT TARGET deps::json)
     target_link_libraries(deps::json INTERFACE nlohmann_json::nlohmann_json)
     set(deps-json_SOURCE_DIR ${deps-json_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-json_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libdeflate.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libdeflate.cmake
index ee04efb00..675a5c718 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libdeflate.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libdeflate.cmake
@@ -56,4 +56,4 @@ if (NOT TARGET deps::libdeflate)
 
     set(deps-libdeflate_SOURCE_DIR ${deps-libdeflate_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-libdeflate_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo-policies-fix.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo-policies-fix.cmake
index ea52c108a..8d6a03021 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo-policies-fix.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo-policies-fix.cmake
@@ -20,4 +20,4 @@ cmake_policy(SET CMP0054 NEW)  # cmake-build-debug/_deps/deps-libjpeg-turbo-src/
 cmake_policy(SET CMP0063 NEW)  # Honor the visibility properties for all target types including static library.
 cmake_policy(SET CMP0077 NEW)  # Use normal variable that is injected, instead of ignoring/clearing normal variable: REQUIRE_SIMD/CMAKE_ASM_NASM_COMPILER.
 # https://cmake.org/cmake/help/v3.18/policy/CMP0065.html : Do not add flags to export symbols from executables without the ENABLE_EXPORTS target property.
-#   : this policy is not handled yet so always enable exports.
\ No newline at end of file
+#   : this policy is not handled yet so always enable exports.
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo.cmake
index e280ade3c..fa0a9f2f5 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libjpeg-turbo.cmake
@@ -75,4 +75,4 @@ if (NOT TARGET deps::libjpeg-turbo)
     mark_as_advanced(deps-libjpeg-turbo_SOURCE_DIR)
     set(deps-libjpeg-turbo_BINARY_DIR ${deps-libjpeg-turbo_BINARY_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-libjpeg-turbo_BINARY_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libtiff.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libtiff.cmake
index 6ef9e6995..8328bbd71 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/libtiff.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/libtiff.cmake
@@ -71,4 +71,4 @@ if (NOT TARGET deps::libtiff)
     target_link_libraries(deps::libtiff INTERFACE tiffxx)
     set(deps-libtiff_SOURCE_DIR ${deps-libtiff_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-libtiff_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/pugixml.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/pugixml.cmake
index 06850918e..827a50ec6 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/pugixml.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/pugixml.cmake
@@ -45,4 +45,4 @@ if (NOT TARGET deps::pugixml)
     target_link_libraries(deps::pugixml INTERFACE pugixml-static)
     set(deps-pugixml_SOURCE_DIR ${deps-pugixml_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-pugixml_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/cpp/plugins/cucim.kit.cuslide/src/cuslide/lzw/lzw_libtiff.cpp b/cpp/plugins/cucim.kit.cuslide/src/cuslide/lzw/lzw_libtiff.cpp
index b86fa8776..5ae35cab6 100644
--- a/cpp/plugins/cucim.kit.cuslide/src/cuslide/lzw/lzw_libtiff.cpp
+++ b/cpp/plugins/cucim.kit.cuslide/src/cuslide/lzw/lzw_libtiff.cpp
@@ -645,4 +645,4 @@ int TIFFInitLZW(TIFF* tif, int scheme)
  * End:
  */
 
-} // namespace cuslide::lzw
\ No newline at end of file
+} // namespace cuslide::lzw
diff --git a/cpp/src/cache/cache_type.cpp b/cpp/src/cache/cache_type.cpp
index eaadc978e..4e1e6c444 100644
--- a/cpp/src/cache/cache_type.cpp
+++ b/cpp/src/cache/cache_type.cpp
@@ -75,4 +75,4 @@ std::string_view lookup_cache_type_str(const CacheType key)
     return map.at(key);
 }
 
-} // namespace cucim::cache
\ No newline at end of file
+} // namespace cucim::cache
diff --git a/cpp/src/cache/image_cache_config.cpp b/cpp/src/cache/image_cache_config.cpp
index 0abaa4441..533a419fc 100644
--- a/cpp/src/cache/image_cache_config.cpp
+++ b/cpp/src/cache/image_cache_config.cpp
@@ -60,4 +60,4 @@ void ImageCacheConfig::load_config(const void* json_obj)
     }
 }
 
-} // namespace cucim::cache
\ No newline at end of file
+} // namespace cucim::cache
diff --git a/cpp/src/codec/base64.cpp b/cpp/src/codec/base64.cpp
index d28a7f727..aa7d37f18 100644
--- a/cpp/src/codec/base64.cpp
+++ b/cpp/src/codec/base64.cpp
@@ -78,4 +78,4 @@ bool decode(const char* src, int src_count, char** out_dst, int* out_count)
     }
     return 0;
 }
-} // namespace cucim::codec::base64
\ No newline at end of file
+} // namespace cucim::codec::base64
diff --git a/cpp/src/config/config.cpp b/cpp/src/config/config.cpp
index c4a41fca9..c534503c2 100644
--- a/cpp/src/config/config.cpp
+++ b/cpp/src/config/config.cpp
@@ -184,4 +184,4 @@ void Config::init_configs()
     // Initialization if needed.
 }
 
-} // namespace cucim::config
\ No newline at end of file
+} // namespace cucim::config
diff --git a/cpp/src/core/cucim_framework.cpp b/cpp/src/core/cucim_framework.cpp
index 026f32f21..7bedfb39e 100644
--- a/cpp/src/core/cucim_framework.cpp
+++ b/cpp/src/core/cucim_framework.cpp
@@ -649,4 +649,4 @@ void CuCIMFramework::set_plugin_root(const char* path)
     plugin_root_path_ = std::string(path);
 }
 
-} // namespace cucim
\ No newline at end of file
+} // namespace cucim
diff --git a/cpp/src/core/cucim_plugin.cpp b/cpp/src/core/cucim_plugin.cpp
index d1a435bd2..f53f44ed6 100644
--- a/cpp/src/core/cucim_plugin.cpp
+++ b/cpp/src/core/cucim_plugin.cpp
@@ -470,4 +470,4 @@ void Plugin::ImplementationDesc::store(const PluginImplDesc& desc)
     hot_reload = desc.hot_reload;
 }
 
-} // namespace cucim
\ No newline at end of file
+} // namespace cucim
diff --git a/cpp/src/cuimage.cpp b/cpp/src/cuimage.cpp
index d9c0ee4dc..e4dd435ea 100644
--- a/cpp/src/cuimage.cpp
+++ b/cpp/src/cuimage.cpp
@@ -1477,4 +1477,4 @@ void CuImageIterator<DataType>::increase_index_()
     }
 }
 
-} // namespace cucim
\ No newline at end of file
+} // namespace cucim
diff --git a/cpp/src/io/device.cpp b/cpp/src/io/device.cpp
index d7fbee5fe..d25f102d3 100644
--- a/cpp/src/io/device.cpp
+++ b/cpp/src/io/device.cpp
@@ -129,4 +129,4 @@ bool Device::validate_device()
     return true;
 }
 
-} // namespace cucim::io
\ No newline at end of file
+} // namespace cucim::io
diff --git a/cpp/src/io/device_type.cpp b/cpp/src/io/device_type.cpp
index d4a41c3b3..f670074e5 100644
--- a/cpp/src/io/device_type.cpp
+++ b/cpp/src/io/device_type.cpp
@@ -82,4 +82,4 @@ std::string_view lookup_device_type_str(const DeviceType key)
     return map.at(key);
 }
 
-} // namespace cucim::io
\ No newline at end of file
+} // namespace cucim::io
diff --git a/cpp/src/memory/memory_manager.cpp b/cpp/src/memory/memory_manager.cpp
index 968c628e3..69474eae0 100644
--- a/cpp/src/memory/memory_manager.cpp
+++ b/cpp/src/memory/memory_manager.cpp
@@ -130,4 +130,4 @@ CUCIM_API bool move_raster_from_device(void** target, size_t size, const cucim::
     return true;
 }
 
-} // namespace cucim::memory
\ No newline at end of file
+} // namespace cucim::memory
diff --git a/cpp/src/util/file.cpp b/cpp/src/util/file.cpp
index 08d659372..7cb109a27 100644
--- a/cpp/src/util/file.cpp
+++ b/cpp/src/util/file.cpp
@@ -27,4 +27,4 @@ bool file_exists(const char* path)
     return stat(path, &st_buff) == 0;
 }
 
-} // namespace cucim::util
\ No newline at end of file
+} // namespace cucim::util
diff --git a/cucim.code-workspace b/cucim.code-workspace
index d53fa8936..d963d8a4c 100644
--- a/cucim.code-workspace
+++ b/cucim.code-workspace
@@ -33,7 +33,7 @@
 					"CUCIM_TESTDATA_FOLDER": "${workspaceDirectory}/test_data",
 					// Add cuslide plugin's library path to LD_LIBRARY_PATH
 					"LD_LIBRARY_PATH": "${workspaceDirectory}/build-debug/lib:${workspaceDirectory}/cpp/plugins/cucim.kit.cuslide/build-debug/lib:${workspaceDirectory}/temp/cuda/lib64:${os_env:LD_LIBRARY_PATH}",
-					"CUCIM_TEST_PLUGIN_PATH": "cucim.kit.cuslide@23.02.00.so"
+					"CUCIM_TEST_PLUGIN_PATH": "cucim.kit.cuslide@23.04.00.so"
 				},
 				"cwd": "${workspaceDirectory}",
 				"catch2": {
@@ -226,7 +226,7 @@
 					},
 					{
 						"name": "CUCIM_TEST_PLUGIN_PATH",
-						"value": "cucim.kit.cuslide@23.02.00.so"
+						"value": "cucim.kit.cuslide@23.04.00.so"
 					}
 				],
 				"console": "externalTerminal",
@@ -254,7 +254,7 @@
 					},
 					{
 						"name": "CUCIM_TEST_PLUGIN_PATH",
-						"value": "cucim.kit.cuslide@23.02.00.so"
+						"value": "cucim.kit.cuslide@23.04.00.so"
 					}
 				],
 				"console": "externalTerminal",
@@ -286,7 +286,7 @@
 					},
 					{
 						"name": "CUCIM_TEST_PLUGIN_PATH",
-						"value": "cucim.kit.cuslide@23.02.00.so"
+						"value": "cucim.kit.cuslide@23.04.00.so"
 					}
 				],
 				"console": "externalTerminal",
diff --git a/dependencies.yaml b/dependencies.yaml
index 1fbff3985..4dcb23f58 100644
--- a/dependencies.yaml
+++ b/dependencies.yaml
@@ -52,13 +52,13 @@ dependencies:
           - matrix:
               arch: x86_64
             packages:
-              - gcc_linux-64=9.*
+              - gcc_linux-64=11.*
               - sysroot_linux-64==2.17
               - yasm
           - matrix:
               arch: aarch64
             packages:
-              - gcc_linux-aarch64=9.*
+              - gcc_linux-aarch64=11.*
               - sysroot_linux-aarch64==2.17
       - output_types: conda
         matrices:
@@ -138,7 +138,7 @@ dependencies:
           - jpeg
           - libwebp-base
           - numpy >=1.21.3
-          - scikit-image >=0.19.0,<0.20.0a0
+          - scikit-image >=0.19.0,<0.21.0a0
           - scipy
           - xz
           - zlib
diff --git a/docker/cufile.json b/docker/cufile.json
index da9f7cee0..55930426c 100644
--- a/docker/cufile.json
+++ b/docker/cufile.json
@@ -73,10 +73,9 @@
                             // specify list of file-systems to prevent IO using cuFile (e.g ["lustre", "wekafs"])
                             "filesystems": [ ]
             },
-        
+
             "miscellaneous": {
                             // enable only for enforcing strict checks at API level for debugging
                             "api_check_aggressive": false
 	    }
 }
-
diff --git a/docker/requirements-claratrain.txt b/docker/requirements-claratrain.txt
index 92cd60dad..485192268 100644
--- a/docker/requirements-claratrain.txt
+++ b/docker/requirements-claratrain.txt
@@ -7,4 +7,4 @@ zarr==2.6.1
 fsspec==0.8.5
 numpy # 1.17.3 already exists in the image
 opencv-contrib-python==4.5.1.48
-imagecodecs>=2021.6.8
\ No newline at end of file
+imagecodecs>=2021.6.8
diff --git a/docs/README.md b/docs/README.md
index e2654d443..5f3f0ed6a 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -16,4 +16,3 @@ make html
 ```
 
 Outputs to `build/html/index.html`
-
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 0a79c6301..3d11b65c1 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -17,4 +17,4 @@ Example
 - 001_choose_database.md
 - 002_format_timestamps.md
 - 003_manage_passwords.md
-- 004_handle_exceptions.md
\ No newline at end of file
+- 004_handle_exceptions.md
diff --git a/docs/adr/example.md b/docs/adr/example.md
index 36a3522c8..74cafd387 100644
--- a/docs/adr/example.md
+++ b/docs/adr/example.md
@@ -214,4 +214,4 @@ Runtime is more valuable than compile time. We are prioritizing customer usage o
 
 ## Notes
 
-Any notes here.
\ No newline at end of file
+Any notes here.
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 1c057a067..f20001f2f 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -72,9 +72,9 @@
 # built documents.
 #
 # The short X.Y version.
-version = '23.02'
+version = '23.04'
 # The full version, including alpha/beta/rc tags.
-release = '23.02.00'
+release = '23.04.00'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
diff --git a/examples/python/gds_whole_slide/README.md b/examples/python/gds_whole_slide/README.md
index f63899e95..fa9ccc57f 100644
--- a/examples/python/gds_whole_slide/README.md
+++ b/examples/python/gds_whole_slide/README.md
@@ -50,7 +50,7 @@ resolution level from an uncompressed multi-resolution TIFF image.
 uncompressed Zarr file with a tile size matching the TIFF image.
 
 - **benchmark_zarr_write.py** : Benchmark writing of a CuPy array to an
-uncompressed Zarr file of the specified chunk size. 
+uncompressed Zarr file of the specified chunk size.
 
 - **benchmark_zarr_write_lz4_via_dask.py** : Use Dask and
 `kvikio.zarr.GDSStore` to write LZ4 lossless compressed Zarr array with the
@@ -98,7 +98,7 @@ cat /proc/driver/nvidia-fs/stats
 ```
 e.g.
 ```
-GDS Version: 1.4.0.29 
+GDS Version: 1.4.0.29
 NVFS statistics(ver: 4.0)
 NVFS Driver(version: 2.13.5)
 Mellanox PeerDirect Supported: False
diff --git a/examples/python/gds_whole_slide/benchmark_zarr_write_lz4_via_dask.py b/examples/python/gds_whole_slide/benchmark_zarr_write_lz4_via_dask.py
index 0483ef7ae..398a4083d 100644
--- a/examples/python/gds_whole_slide/benchmark_zarr_write_lz4_via_dask.py
+++ b/examples/python/gds_whole_slide/benchmark_zarr_write_lz4_via_dask.py
@@ -66,13 +66,16 @@
 write_time_means = np.zeros(((len(dtypes), len(chunk_shapes), len(compressors), 2)), dtype=float)
 write_time_stds = np.zeros_like(write_time_means)
 for i, dtype in enumerate(dtypes):
-    if dtype == 'uint8':
+    dtype = np.dtype(dtype)
+    if dtype == np.uint8:
         img = image_gpu
         assert img.dtype == cp.uint8
-    else:
+    elif dtype == np.uint16:
         img = image_gpu.astype(dtype)
     else:
-        raise NotImplementedError("only testing for uint8 and uint16")
+        raise NotImplementedError(
+            "LZ4 compression can only be tested for uint8 and uint16"
+        )
     for j, chunk_shape in enumerate(chunk_shapes):
         for k, compressor in enumerate(compressors):
             kwargs = dict(
diff --git a/python/.idea/.name b/python/.idea/.name
index 1fec8eaec..106496e4f 100644
--- a/python/.idea/.name
+++ b/python/.idea/.name
@@ -1,1 +1,1 @@
-pycucim
\ No newline at end of file
+pycucim
diff --git a/python/.idea/codeStyles/Project.xml b/python/.idea/codeStyles/Project.xml
index f60388162..c8f84c353 100644
--- a/python/.idea/codeStyles/Project.xml
+++ b/python/.idea/codeStyles/Project.xml
@@ -4,4 +4,4 @@
       <option name="ENABLED" value="true" />
     </clangFormatSettings>
   </code_scheme>
-</component>
\ No newline at end of file
+</component>
diff --git a/python/.idea/codeStyles/codeStyleConfig.xml b/python/.idea/codeStyles/codeStyleConfig.xml
index 79ee123c2..0f7bc519d 100644
--- a/python/.idea/codeStyles/codeStyleConfig.xml
+++ b/python/.idea/codeStyles/codeStyleConfig.xml
@@ -2,4 +2,4 @@
   <state>
     <option name="USE_PER_PROJECT_SETTINGS" value="true" />
   </state>
-</component>
\ No newline at end of file
+</component>
diff --git a/python/.idea/dataSources.xml b/python/.idea/dataSources.xml
index e5ea029d1..ced8a0f77 100644
--- a/python/.idea/dataSources.xml
+++ b/python/.idea/dataSources.xml
@@ -8,4 +8,4 @@
       <jdbc-url>jdbc:sqlite:$PROJECT_DIR$/cucim/.coverage</jdbc-url>
     </data-source>
   </component>
-</project>
\ No newline at end of file
+</project>
diff --git a/python/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h b/python/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h
index b0e223c0a..cf0461d4c 100644
--- a/python/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h
+++ b/python/.idea/fileTemplates/includes/NVIDIA_C_HEADER.h
@@ -12,4 +12,4 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- */
\ No newline at end of file
+ */
diff --git a/python/.idea/fileTemplates/internal/CMakeLists.txt.cmake b/python/.idea/fileTemplates/internal/CMakeLists.txt.cmake
index d71d94dba..846356219 100644
--- a/python/.idea/fileTemplates/internal/CMakeLists.txt.cmake
+++ b/python/.idea/fileTemplates/internal/CMakeLists.txt.cmake
@@ -1,1 +1,1 @@
-#parse("NVIDIA_CMAKE_HEADER.cmake")
\ No newline at end of file
+#parse("NVIDIA_CMAKE_HEADER.cmake")
diff --git a/python/.idea/misc.xml b/python/.idea/misc.xml
index 79b3c9483..c8370a824 100644
--- a/python/.idea/misc.xml
+++ b/python/.idea/misc.xml
@@ -1,4 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
-</project>
\ No newline at end of file
+</project>
diff --git a/python/.idea/pycucim.iml b/python/.idea/pycucim.iml
index f08604bb6..8afe22e01 100644
--- a/python/.idea/pycucim.iml
+++ b/python/.idea/pycucim.iml
@@ -1,2 +1,2 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<module classpath="CMake" type="CPP_MODULE" version="4" />
\ No newline at end of file
+<module classpath="CMake" type="CPP_MODULE" version="4" />
diff --git a/python/.idea/python.iml b/python/.idea/python.iml
index f08604bb6..8afe22e01 100644
--- a/python/.idea/python.iml
+++ b/python/.idea/python.iml
@@ -1,2 +1,2 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<module classpath="CMake" type="CPP_MODULE" version="4" />
\ No newline at end of file
+<module classpath="CMake" type="CPP_MODULE" version="4" />
diff --git a/python/.idea/vcs.xml b/python/.idea/vcs.xml
index 6c0b86358..54e4b961e 100644
--- a/python/.idea/vcs.xml
+++ b/python/.idea/vcs.xml
@@ -3,4 +3,4 @@
   <component name="VcsDirectoryMappings">
     <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
   </component>
-</project>
\ No newline at end of file
+</project>
diff --git a/python/.vscode/settings.json b/python/.vscode/settings.json
index e774417ea..27b5e58d8 100644
--- a/python/.vscode/settings.json
+++ b/python/.vscode/settings.json
@@ -1,4 +1,4 @@
 {
     "cmake.sourceDirectory": "${workspaceFolder}/.",
     "cmake.buildDirectory": "${workspaceFolder}/build-debug"
-}
\ No newline at end of file
+}
diff --git a/python/cmake/deps/fmt.cmake b/python/cmake/deps/fmt.cmake
index 59e9c1fce..ef2797e18 100644
--- a/python/cmake/deps/fmt.cmake
+++ b/python/cmake/deps/fmt.cmake
@@ -40,4 +40,4 @@ if (NOT TARGET deps::fmt)
     target_link_libraries(deps::fmt INTERFACE fmt::fmt-header-only)
     set(deps-fmt_SOURCE_DIR ${deps-fmt_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-fmt_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/python/cmake/deps/json.cmake b/python/cmake/deps/json.cmake
index 4f716f120..c3f258ee1 100644
--- a/python/cmake/deps/json.cmake
+++ b/python/cmake/deps/json.cmake
@@ -37,4 +37,4 @@ if (NOT TARGET deps::json)
     target_link_libraries(deps::json INTERFACE nlohmann_json::nlohmann_json)
     set(deps-json_SOURCE_DIR ${deps-json_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-json_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/python/cmake/deps/pybind11.cmake b/python/cmake/deps/pybind11.cmake
index 6af6bfbd9..de4172935 100644
--- a/python/cmake/deps/pybind11.cmake
+++ b/python/cmake/deps/pybind11.cmake
@@ -17,7 +17,7 @@ if (NOT TARGET deps::pybind11)
     FetchContent_Declare(
             deps-pybind11
             GIT_REPOSITORY https://github.com/pybind/pybind11.git
-            GIT_TAG v2.6.2
+            GIT_TAG v2.10.3
             GIT_SHALLOW TRUE
     )
     FetchContent_GetProperties(deps-pybind11)
diff --git a/python/cmake/deps/pybind11_json.cmake b/python/cmake/deps/pybind11_json.cmake
index fb422be39..9313bb0f0 100644
--- a/python/cmake/deps/pybind11_json.cmake
+++ b/python/cmake/deps/pybind11_json.cmake
@@ -33,4 +33,4 @@ if (NOT TARGET deps::pybind11_json)
     target_link_libraries(deps::pybind11_json INTERFACE pybind11_json)
     set(deps-pybind11_json_SOURCE_DIR ${deps-pybind11_json_SOURCE_DIR} CACHE INTERNAL "" FORCE)
     mark_as_advanced(deps-pybind11_json_SOURCE_DIR)
-endif ()
\ No newline at end of file
+endif ()
diff --git a/python/cucim/README.md b/python/cucim/README.md
index 9a92b8de5..b02dc3ac0 100755
--- a/python/cucim/README.md
+++ b/python/cucim/README.md
@@ -70,7 +70,7 @@ pip install scipy scikit-image cupy-cuda110
 
 ### Notebooks
 
-Please check out our [Welcome](notebooks/Welcome.ipynb) notebook ([NBViewer](https://nbviewer.jupyter.org/github/rapidsai/cucim/blob/branch-23.02/notebooks/Welcome.ipynb))
+Please check out our [Welcome](notebooks/Welcome.ipynb) notebook ([NBViewer](https://nbviewer.jupyter.org/github/rapidsai/cucim/blob/branch-23.04/notebooks/Welcome.ipynb))
 
 #### Downloading sample images
 
diff --git a/python/cucim/VERSION b/python/cucim/VERSION
index 7c8431de5..6fd949303 100644
--- a/python/cucim/VERSION
+++ b/python/cucim/VERSION
@@ -1,1 +1,1 @@
-23.02.00
+23.04.00
diff --git a/python/cucim/docs/getting_started/index.md b/python/cucim/docs/getting_started/index.md
index f03db8a11..3efbe0720 100644
--- a/python/cucim/docs/getting_started/index.md
+++ b/python/cucim/docs/getting_started/index.md
@@ -14,15 +14,15 @@
 
 ## Installation
 
-Please download the latest SDK package (`cuCIM-v23.02.00-linux.tar.gz`).
+Please download the latest SDK package (`cuCIM-v23.04.00-linux.tar.gz`).
 
 Untar the downloaded file.
 
 ```bash
-mkdir -p cuCIM-v23.02.00
-tar -xzvf cuCIM-v23.02.00-linux.tar.gz -C cuCIM-v23.02.00
+mkdir -p cuCIM-v23.04.00
+tar -xzvf cuCIM-v23.04.00-linux.tar.gz -C cuCIM-v23.04.00
 
-cd cuCIM-v23.02.00
+cd cuCIM-v23.04.00
 ```
 
 ## Run command
@@ -147,7 +147,7 @@ Its execution would show some metadata information and create two files -- `outp
 ```
 $ ./bin/tiff_image notebooks/input/image.tif .
 [Plugin: cucim.kit.cuslide] Loading...
-[Plugin: cucim.kit.cuslide] Loading the dynamic library from: cucim.kit.cuslide@23.02.00.so
+[Plugin: cucim.kit.cuslide] Loading the dynamic library from: cucim.kit.cuslide@23.04.00.so
 [Plugin: cucim.kit.cuslide] loaded successfully. Version: 0
 Initializing plugin: cucim.kit.cuslide (interfaces: [cucim::io::IImageFormat v0.1]) (impl: cucim.kit.cuslide)
 is_loaded: true
diff --git a/python/cucim/docs/index.md b/python/cucim/docs/index.md
index 6fc64c1cd..92c5dfcaa 100644
--- a/python/cucim/docs/index.md
+++ b/python/cucim/docs/index.md
@@ -18,7 +18,7 @@ development/index -->
 
 # cuCIM Documentation
 
-Current latest version is [Version 23.02.00](release_notes/v23.02.00.md).
+Current latest version is [Version 23.04.00](release_notes/v23.04.00.md).
 
 **cuCIM** a toolkit to provide GPU accelerated I/O, image  processing & computer vision primitives for N-Dimensional images with a focus on biomedical imaging.
 
diff --git a/python/cucim/docs/release_notes/v0.2.0.md b/python/cucim/docs/release_notes/v0.2.0.md
index d6d16b368..e5817c379 100644
--- a/python/cucim/docs/release_notes/v0.2.0.md
+++ b/python/cucim/docs/release_notes/v0.2.0.md
@@ -30,4 +30,3 @@ The following features are implemented.
 - Fix again for the error that occurs when loading a TIFF image that has `TIFFTAG_JPEGTABLES` tag
   - `ERROR in line 126 while reading JPEG header tables: Not a JPEG file: starts with 0x01 0x00` message can be shown
 - Force-reinstall cucim Python package in the Tox environment whenever `gen_docs` or `gen_docs_dev` command is executed
-
diff --git a/python/cucim/docs/requirements.txt b/python/cucim/docs/requirements.txt
index 5fa61c4ee..c20ae4c7b 100644
--- a/python/cucim/docs/requirements.txt
+++ b/python/cucim/docs/requirements.txt
@@ -20,4 +20,4 @@ pydata_sphinx_theme==0.4.3
 sphinxemoji==0.1.8
 cupy-cuda110==9.0.0b3
 scipy
-scikit-image
\ No newline at end of file
+scikit-image
diff --git a/python/cucim/docs/roadmap/index.md b/python/cucim/docs/roadmap/index.md
index 6d1c334a1..74d5e1b0f 100644
--- a/python/cucim/docs/roadmap/index.md
+++ b/python/cucim/docs/roadmap/index.md
@@ -368,4 +368,3 @@ The following list is on the road |:smile:|
 - Generalize `CuImage::ensure_init()`
   - 'LINUX' path separator is used. Need to make it generalize once filesystem library is available
   - `cucim/cpp/src/cucim.cpp:520`
-
diff --git a/python/cucim/src/cucim/clara/filesystem/__init__.py b/python/cucim/src/cucim/clara/filesystem/__init__.py
index 5e82149fa..fba23f7c9 100644
--- a/python/cucim/src/cucim/clara/filesystem/__init__.py
+++ b/python/cucim/src/cucim/clara/filesystem/__init__.py
@@ -15,4 +15,4 @@
 
 from cucim.clara._cucim.filesystem import *
 
-__all__ = ['open', 'pread', 'pwrite', 'close', 'discard_page_cache', 'CuFileDriver']
\ No newline at end of file
+__all__ = ['open', 'pread', 'pwrite', 'close', 'discard_page_cache', 'CuFileDriver']
diff --git a/python/cucim/src/cucim/core/operations/color/stain_normalizer.py b/python/cucim/src/cucim/core/operations/color/stain_normalizer.py
index 731340664..34475f8fe 100644
--- a/python/cucim/src/cucim/core/operations/color/stain_normalizer.py
+++ b/python/cucim/src/cucim/core/operations/color/stain_normalizer.py
@@ -120,7 +120,7 @@ def _absorbance_to_image_int(absorbance, source_intensity, min_val, max_val):
     rgb = cp.exp(-absorbance) * source_intensity
     # prevent overflow/underflow
     rgb = cp.minimum(cp.maximum(rgb, min_val), max_val)
-    return cp.round(rgb)
+    return cp.around(rgb)
 
 
 @cp.fuse()
@@ -128,7 +128,7 @@ def _absorbance_to_image_uint8(absorbance, source_intensity):
     rgb = cp.exp(-absorbance) * source_intensity
     # prevent overflow/underflow
     rgb = cp.minimum(cp.maximum(rgb, 0), 255)
-    return cp.round(rgb).astype(cp.uint8)
+    return cp.around(rgb).astype(cp.uint8)
 
 
 def absorbance_to_image(absorbance, source_intensity=255, dtype=cp.uint8):
diff --git a/python/cucim/src/cucim/core/operations/intensity/__init__.py b/python/cucim/src/cucim/core/operations/intensity/__init__.py
index 8a046e3f3..000c987bc 100644
--- a/python/cucim/src/cucim/core/operations/intensity/__init__.py
+++ b/python/cucim/src/cucim/core/operations/intensity/__init__.py
@@ -5,6 +5,6 @@
 __all__ = [
     "normalize_data",
     "scale_intensity_range",
-    "zoom", 
+    "zoom",
     "rand_zoom"
 ]
diff --git a/python/cucim/src/cucim/core/operations/morphology/cuda/pba_kernels_2d.h b/python/cucim/src/cucim/core/operations/morphology/cuda/pba_kernels_2d.h
index 200821f06..74e5f0255 100644
--- a/python/cucim/src/cucim/core/operations/morphology/cuda/pba_kernels_2d.h
+++ b/python/cucim/src/cucim/core/operations/morphology/cuda/pba_kernels_2d.h
@@ -1,7 +1,7 @@
 // Euclidean Distance Transform
-// 
-// Kernels for the 2D version of the Parallel Banding Algorithm (PBA+). 
-// 
+//
+// Kernels for the 2D version of the Parallel Banding Algorithm (PBA+).
+//
 // MIT license: see 3rdparty/LICENSE.pba+
 // Copyright: (c) 2019 School of Computing, National University of Singapore
 //
diff --git a/python/cucim/src/cucim/core/operations/morphology/cuda/pba_kernels_3d.h b/python/cucim/src/cucim/core/operations/morphology/cuda/pba_kernels_3d.h
index d4cdec9d6..36dbed985 100644
--- a/python/cucim/src/cucim/core/operations/morphology/cuda/pba_kernels_3d.h
+++ b/python/cucim/src/cucim/core/operations/morphology/cuda/pba_kernels_3d.h
@@ -1,7 +1,7 @@
 // Euclidean Distance Transform
-// 
-// Kernels for the 3D version of the Parallel Banding Algorithm (PBA+). 
-// 
+//
+// Kernels for the 3D version of the Parallel Banding Algorithm (PBA+).
+//
 // MIT license: see 3rdparty/LICENSE.pba+
 //
 // Modifications by Gregory Lee (2022) (NVIDIA)
diff --git a/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters.py b/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters.py
index 633371887..29d3f6707 100644
--- a/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters.py
+++ b/python/cucim/src/cucim/skimage/_vendored/_ndimage_filters.py
@@ -835,6 +835,10 @@ def _min_or_max_filter(input, size, ftprnt, structure, output, mode, cval,
     # structure is used by morphology.grey_erosion() and grey_dilation()
     # and not by the regular min/max filters
 
+    if isinstance(ftprnt, tuple) and size is None:
+        size = ftprnt
+        ftprnt = None
+
     sizes, ftprnt, structure = _filters_core._check_size_footprint_structure(
         input.ndim, size, ftprnt, structure)
     if cval is cupy.nan:
@@ -1106,6 +1110,9 @@ def _rank_filter(input, get_rank, size=None, footprint=None, output=None,
             sizes = footprint.shape
             has_weights = False
 
+    if not has_weights:
+        footprint = None
+
     rank = get_rank(filter_size)
     if rank < 0 or rank >= filter_size:
         raise RuntimeError('rank not within filter footprint size')
@@ -1126,7 +1133,7 @@ def _rank_filter(input, get_rank, size=None, footprint=None, output=None,
     kernel = _get_rank_kernel(filter_size, rank, mode, footprint_shape,
                               offsets, float(cval), int_type,
                               has_weights=has_weights)
-    return _filters_core._call_kernel(kernel, input, None, output,
+    return _filters_core._call_kernel(kernel, input, footprint, output,
                                       weights_dtype=bool)
 
 
diff --git a/python/cucim/src/cucim/skimage/_vendored/_ndimage_morphology.py b/python/cucim/src/cucim/skimage/_vendored/_ndimage_morphology.py
index 5c6462cff..61b957715 100644
--- a/python/cucim/src/cucim/skimage/_vendored/_ndimage_morphology.py
+++ b/python/cucim/src/cucim/skimage/_vendored/_ndimage_morphology.py
@@ -5,6 +5,7 @@
 import numpy
 from cupy import _core
 
+from cucim.skimage._vendored import _internal as internal
 from cucim.skimage._vendored import _ndimage_filters as _filters
 from cucim.skimage._vendored import _ndimage_filters_core as _filters_core
 from cucim.skimage._vendored import _ndimage_util as _util
@@ -66,14 +67,16 @@ def _get_binary_erosion_kernel(
     name = 'binary_erosion'
     if false_val:
         name += '_invert'
+    has_weights = not all_weights_nonzero
+
     return _filters_core._generate_nd_kernel(
         name,
         pre,
         found,
         '',
-        'constant', w_shape, int_type, offsets, 0, ctype='Y', has_weights=True,
-        has_structure=False, has_mask=masked, binary_morphology=True,
-        all_weights_nonzero=all_weights_nonzero)
+        'constant', w_shape, int_type, offsets, 0, ctype='Y',
+        has_weights=has_weights, has_structure=False, has_mask=masked,
+        binary_morphology=True)
 
 
 def _center_is_true(structure, origin):
@@ -158,22 +161,30 @@ def _binary_erosion(input, structure, iterations, mask, output, border_value,
 
     if input.dtype.kind == 'c':
         raise TypeError('Complex type not supported')
+    default_structure = False
     if structure is None:
         structure = generate_binary_structure(input.ndim, 1)
         all_weights_nonzero = input.ndim == 1
         center_is_true = True
-        default_structure = True
+        structure_shape = structure.shape
+    elif isinstance(structure, tuple):
+        # For a structure that is true everywhere, can just provide the shape
+        structure_shape = structure
+        if len(structure_shape) == 0:
+            raise RuntimeError('structure must not be empty')
     else:
         structure = structure.astype(dtype=bool, copy=False)
+        structure_shape = structure.shape
         # transfer to CPU for use in determining if it is fully dense
         # structure_cpu = cupy.asnumpy(structure)
-        default_structure = False
-    if structure.ndim != input.ndim:
-        raise RuntimeError('structure and input must have same dimensionality')
-    if not structure.flags.c_contiguous:
-        structure = cupy.ascontiguousarray(structure)
-    if structure.size < 1:
-        raise RuntimeError('structure must not be empty')
+        if structure.ndim != input.ndim:
+            raise RuntimeError(
+                'structure and input must have same dimensionality'
+            )
+        if not structure.flags.c_contiguous:
+            structure = cupy.ascontiguousarray(structure)
+        if structure.size < 1:
+            raise RuntimeError('structure must not be empty')
 
     if mask is not None:
         if mask.shape != input.shape:
@@ -196,35 +207,47 @@ def _binary_erosion(input, structure, iterations, mask, output, border_value,
         # input and output arrays cannot share memory
         temp = output
         output = _util._get_output(output.dtype, input)
-    if structure.ndim == 0:
+    if len(structure_shape) == 0:
         # kernel doesn't handle ndim=0, so special case it here
-        if float(structure):
+        if isinstance(structure, tuple) or float(structure):
             output[...] = cupy.asarray(input, dtype=bool)
         else:
             output[...] = ~cupy.asarray(input, dtype=bool)
         return output
     origin = tuple(origin)
     int_type = _util._get_inttype(input)
-    offsets = _filters_core._origins_to_offsets(origin, structure.shape)
+    offsets = _filters_core._origins_to_offsets(origin, structure_shape)
     if not default_structure:
-        # synchronize required to determine if all weights are non-zero
-        nnz = int(cupy.count_nonzero(structure))
-        all_weights_nonzero = nnz == structure.size
-        if all_weights_nonzero:
+        if isinstance(structure, tuple):
+            nnz = internal.prod(structure_shape)
+            all_weights_nonzero = True
             center_is_true = True
         else:
-            center_is_true = _center_is_true(structure, origin)
+            # synchronize required to determine if all weights are non-zero
+            nnz = int(cupy.count_nonzero(structure))
+            all_weights_nonzero = nnz == structure.size
+            if all_weights_nonzero:
+                center_is_true = True
+            else:
+                center_is_true = _center_is_true(structure, origin)
 
     erode_kernel = _get_binary_erosion_kernel(
-        structure.shape, int_type, offsets, center_is_true, border_value,
+        structure_shape, int_type, offsets, center_is_true, border_value,
         invert, masked, all_weights_nonzero,
     )
-
-    if iterations == 1:
+    if all_weights_nonzero:
         if masked:
-            output = erode_kernel(input, structure, mask, output)
+            in_args = (input, mask)
         else:
-            output = erode_kernel(input, structure, output)
+            in_args = (input,)
+    else:
+        if masked:
+            in_args = (input, structure, mask)
+        else:
+            in_args = (input, structure)
+
+    if iterations == 1:
+        output = erode_kernel(*in_args, output)
     elif center_is_true and not brute_force:
         raise NotImplementedError(
             'only brute_force iteration has been implemented'
@@ -236,19 +259,23 @@ def _binary_erosion(input, structure, iterations, mask, output, border_value,
         tmp_out = output
         if iterations >= 1 and not iterations & 1:
             tmp_in, tmp_out = tmp_out, tmp_in
-        if masked:
-            tmp_out = erode_kernel(input, structure, mask, tmp_out)
-        else:
-            tmp_out = erode_kernel(input, structure, tmp_out)
+        tmp_out = erode_kernel(*in_args, tmp_out)
         # TODO: kernel doesn't return the changed status, so determine it here
         changed = not (input == tmp_out).all()  # synchronize!
         ii = 1
         while ii < iterations or ((iterations < 1) and changed):
             tmp_in, tmp_out = tmp_out, tmp_in
-            if masked:
-                tmp_out = erode_kernel(tmp_in, structure, mask, tmp_out)
+            if all_weights_nonzero:
+                if masked:
+                    in_args = (tmp_in, mask)
+                else:
+                    in_args = (tmp_in,)
             else:
-                tmp_out = erode_kernel(tmp_in, structure, tmp_out)
+                if masked:
+                    in_args = (tmp_in, structure, mask)
+                else:
+                    in_args = (tmp_in, structure)
+            tmp_out = erode_kernel(*in_args, tmp_out)
             changed = not (tmp_in == tmp_out).all()
             ii += 1
             if not changed and (not ii & 1):  # synchronize!
@@ -262,6 +289,24 @@ def _binary_erosion(input, structure, iterations, mask, output, border_value,
     return output
 
 
+def _prep_structure(structure, ndim):
+    if structure is None:
+        structure = generate_binary_structure(ndim, 1)
+        return structure, structure.shape, True
+    if isinstance(structure, int):
+        structure = (structure,) * ndim
+    elif isinstance(structure, list):
+        structure = tuple(structure)
+    if isinstance(structure, tuple):
+        symmetric_structure = True
+        structure_shape = structure
+    else:
+        # if user-provided, it is not guaranteed to be symmetric
+        symmetric_structure = False
+        structure_shape = structure.shape
+    return structure, structure_shape, symmetric_structure
+
+
 def binary_erosion(input, structure=None, iterations=1, mask=None, output=None,
                    border_value=0, origin=0, brute_force=False):
     """Multidimensional binary erosion with a given structuring element.
@@ -272,10 +317,14 @@ def binary_erosion(input, structure=None, iterations=1, mask=None, output=None,
     Args:
         input(cupy.ndarray): The input binary array_like to be eroded.
             Non-zero (True) elements form the subset to be eroded.
-        structure(cupy.ndarray, optional): The structuring element used for the
-            erosion. Non-zero elements are considered True. If no structuring
-            element is provided an element is generated with a square
-            connectivity equal to one. (Default value = None).
+        structure(cupy.ndarray or tuple or int, optional): The structuring
+            element used for the erosion. Non-zero elements are considered
+            True. If no structuring element is provided an element is generated
+            with a square connectivity equal to one. (Default value = None). If
+            a tuple of integers is provided, a structuring element of the
+            specified shape is used (all elements True). If an integer is
+            provided, the structuring element will have the same size along all
+            axes.
         iterations(int, optional): The erosion is repeated ``iterations`` times
             (one, by default). If iterations is less than 1, the erosion is
             repeated until the result does not change anymore. Only an integer
@@ -304,6 +353,7 @@ def binary_erosion(input, structure=None, iterations=1, mask=None, output=None,
 
     .. seealso:: :func:`scipy.ndimage.binary_erosion`
     """
+    structure, _, _ = _prep_structure(structure, input.ndim)
     return _binary_erosion(input, structure, iterations, mask, output,
                            border_value, origin, 0, brute_force)
 
@@ -315,10 +365,14 @@ def binary_dilation(input, structure=None, iterations=1, mask=None,
     Args:
         input(cupy.ndarray): The input binary array_like to be dilated.
             Non-zero (True) elements form the subset to be dilated.
-        structure(cupy.ndarray, optional): The structuring element used for the
-            dilation. Non-zero elements are considered True. If no structuring
-            element is provided an element is generated with a square
-            connectivity equal to one. (Default value = None).
+        structure(cupy.ndarray or tuple or int, optional): The structuring
+            element used for the erosion. Non-zero elements are considered
+            True. If no structuring element is provided an element is generated
+            with a square connectivity equal to one. (Default value = None). If
+            a tuple of integers is provided, a structuring element of the
+            specified shape is used (all elements True). If an integer is
+            provided, the structuring element will have the same size along all
+            axes.
         iterations(int, optional): The dilation is repeated ``iterations``
             times (one, by default). If iterations is less than 1, the dilation
             is repeated until the result does not change anymore. Only an
@@ -347,13 +401,14 @@ def binary_dilation(input, structure=None, iterations=1, mask=None,
 
     .. seealso:: :func:`scipy.ndimage.binary_dilation`
     """
-    if structure is None:
-        structure = generate_binary_structure(input.ndim, 1)
+    structure, structure_shape, symmetric = _prep_structure(structure,
+                                                            input.ndim)
     origin = _util._fix_sequence_arg(origin, input.ndim, 'origin', int)
-    structure = structure[tuple([slice(None, None, -1)] * structure.ndim)]
+    if not symmetric:
+        structure = structure[tuple([slice(None, None, -1)] * structure.ndim)]
     for ii in range(len(origin)):
         origin[ii] = -origin[ii]
-        if not structure.shape[ii] & 1:
+        if not structure_shape[ii] & 1:
             origin[ii] -= 1
     return _binary_erosion(input, structure, iterations, mask, output,
                            border_value, origin, 1, brute_force)
@@ -370,10 +425,14 @@ def binary_opening(input, structure=None, iterations=1, output=None, origin=0,
     Args:
         input(cupy.ndarray): The input binary array to be opened.
             Non-zero (True) elements form the subset to be opened.
-        structure(cupy.ndarray, optional): The structuring element used for the
-            opening. Non-zero elements are considered True. If no structuring
-            element is provided an element is generated with a square
-            connectivity equal to one. (Default value = None).
+        structure(cupy.ndarray or tuple or int, optional): The structuring
+            element used for the erosion. Non-zero elements are considered
+            True. If no structuring element is provided an element is generated
+            with a square connectivity equal to one. (Default value = None). If
+            a tuple of integers is provided, a structuring element of the
+            specified shape is used (all elements True). If an integer is
+            provided, the structuring element will have the same size along all
+            axes.
         iterations(int, optional): The opening is repeated ``iterations`` times
             (one, by default). If iterations is less than 1, the opening is
             repeated until the result does not change anymore. Only an integer
@@ -402,9 +461,7 @@ def binary_opening(input, structure=None, iterations=1, output=None, origin=0,
 
     .. seealso:: :func:`scipy.ndimage.binary_opening`
     """
-    if structure is None:
-        rank = input.ndim
-        structure = generate_binary_structure(rank, 1)
+    structure, _, _ = _prep_structure(structure, input.ndim)
     tmp = binary_erosion(input, structure, iterations, mask, None,
                          border_value, origin, brute_force)
     return binary_dilation(tmp, structure, iterations, mask, output,
@@ -422,10 +479,14 @@ def binary_closing(input, structure=None, iterations=1, output=None, origin=0,
     Args:
         input(cupy.ndarray): The input binary array to be closed.
             Non-zero (True) elements form the subset to be closed.
-        structure(cupy.ndarray, optional): The structuring element used for the
-            closing. Non-zero elements are considered True. If no structuring
-            element is provided an element is generated with a square
-            connectivity equal to one. (Default value = None).
+        structure(cupy.ndarray or tuple or int, optional): The structuring
+            element used for the erosion. Non-zero elements are considered
+            True. If no structuring element is provided an element is generated
+            with a square connectivity equal to one. (Default value = None). If
+            a tuple of integers is provided, a structuring element of the
+            specified shape is used (all elements True). If an integer is
+            provided, the structuring element will have the same size along all
+            axes.
         iterations(int, optional): The closing is repeated ``iterations`` times
             (one, by default). If iterations is less than 1, the closing is
             repeated until the result does not change anymore. Only an integer
@@ -454,9 +515,7 @@ def binary_closing(input, structure=None, iterations=1, output=None, origin=0,
 
     .. seealso:: :func:`scipy.ndimage.binary_closing`
     """
-    if structure is None:
-        rank = input.ndim
-        structure = generate_binary_structure(rank, 1)
+    structure, _, _ = _prep_structure(structure, input.ndim)
     tmp = binary_dilation(input, structure, iterations, mask, None,
                           border_value, origin, brute_force)
     return binary_erosion(tmp, structure, iterations, mask, output,
@@ -629,7 +688,6 @@ def grey_erosion(input, size=None, footprint=None, structure=None, output=None,
 
     .. seealso:: :func:`scipy.ndimage.grey_erosion`
     """
-
     if size is None and footprint is None and structure is None:
         raise ValueError('size, footprint or structure must be specified')
 
diff --git a/python/cucim/src/cucim/skimage/feature/template.py b/python/cucim/src/cucim/skimage/feature/template.py
index 3eca3e266..63f696759 100644
--- a/python/cucim/src/cucim/skimage/feature/template.py
+++ b/python/cucim/src/cucim/skimage/feature/template.py
@@ -106,13 +106,13 @@ def match_template(image, template, pad_input=False, mode='constant',
            [ 0.,  0.,  0.,  0., -1.,  0.],
            [ 0.,  0.,  0.,  0.,  0.,  0.]])
     >>> result = match_template(image, template)
-    >>> cp.round(result, 3)
+    >>> cp.around(result, 3)
     array([[ 1.   , -0.125,  0.   ,  0.   ],
            [-0.125, -0.125,  0.   ,  0.   ],
            [ 0.   ,  0.   ,  0.125,  0.125],
            [ 0.   ,  0.   ,  0.125, -1.   ]])
     >>> result = match_template(image, template, pad_input=True)
-    >>> cp.round(result, 3)
+    >>> cp.around(result, 3)
     array([[-0.125, -0.125, -0.125,  0.   ,  0.   ,  0.   ],
            [-0.125,  1.   , -0.125,  0.   ,  0.   ,  0.   ],
            [-0.125, -0.125, -0.125,  0.   ,  0.   ,  0.   ],
diff --git a/python/cucim/src/cucim/skimage/measure/_moments.py b/python/cucim/src/cucim/skimage/measure/_moments.py
index 9355e523a..b79b7121c 100644
--- a/python/cucim/src/cucim/skimage/measure/_moments.py
+++ b/python/cucim/src/cucim/skimage/measure/_moments.py
@@ -101,8 +101,8 @@ def moments_coords_central(coords, center=None, order=3):
     point, this no longer holds:
 
     >>> coords2 = cp.concatenate((coords, cp.array([[17, 17]])), axis=0)
-    >>> cp.round(moments_coords_central(coords2),
-    ...          decimals=2)  # doctest: +NORMALIZE_WHITESPACE
+    >>> cp.around(moments_coords_central(coords2),
+    ...           decimals=2)  # doctest: +NORMALIZE_WHITESPACE
     array([[17.  ,  0.  , 22.12, -2.49],
            [ 0.  ,  3.53,  1.73,  7.4 ],
            [25.88,  6.02, 36.63,  8.83],
diff --git a/python/cucim/src/cucim/skimage/morphology/footprints.py b/python/cucim/src/cucim/skimage/morphology/footprints.py
index 6c1afff4f..98a68d2dc 100644
--- a/python/cucim/src/cucim/skimage/morphology/footprints.py
+++ b/python/cucim/src/cucim/skimage/morphology/footprints.py
@@ -28,12 +28,18 @@ def _validate_sequence_element(t):
         return (
             isinstance(t, Sequence)
             and len(t) == 2
-            and hasattr(t[0], '__cuda_array_interface__')
+            and (
+                hasattr(t[0], '__cuda_array_interface__')
+                # can be a shape tuple for square/rectangular footprints
+                or isinstance(t[0], tuple)
+            )
             and isinstance(t[1], Integral)
         )
-
     if isinstance(footprint, Sequence):
-        if not all(_validate_sequence_element(t) for t in footprint):
+        if all(isinstance(t, int) for t in footprint):
+            # allow pass through of a single shape tuple
+            return False
+        elif not all(_validate_sequence_element(t) for t in footprint):
             raise ValueError(
                 "All elements of footprint sequence must be a 2-tuple where "
                 "the first element of the tuple is an ndarray and the second "
@@ -44,6 +50,18 @@ def _validate_sequence_element(t):
     return True
 
 
+def _footprint_shape(footprint):
+    if isinstance(footprint, tuple):
+        return footprint
+    return footprint.shape
+
+
+def _footprint_ndim(footprint):
+    if isinstance(footprint, tuple):
+        return len(footprint)
+    return footprint.ndim
+
+
 def _shape_from_sequence(footprints, require_odd_size=False):
     """Determine the shape of composite footprint
 
@@ -52,7 +70,7 @@ def _shape_from_sequence(footprints, require_odd_size=False):
     """
     if not _footprint_is_sequence(footprints):
         raise ValueError("expected a sequence of footprints")
-    ndim = footprints[0][0].ndim
+    ndim = _footprint_ndim(footprints[0][0])
     shape = [0] * ndim
 
     def _odd_size(size, require_odd_size):
@@ -63,11 +81,13 @@ def _odd_size(size, require_odd_size):
 
     for d in range(ndim):
         fp, nreps = footprints[0]
-        _odd_size(fp.shape[d], require_odd_size)
-        shape[d] = fp.shape[d] + (nreps - 1) * (fp.shape[d] - 1)
+        fp_shape = _footprint_shape(fp)
+        _odd_size(fp_shape[d], require_odd_size)
+        shape[d] = fp_shape[d] + (nreps - 1) * (fp_shape[d] - 1)
         for fp, nreps in footprints[1:]:
-            _odd_size(fp.shape[d], require_odd_size)
-            shape[d] += nreps * (fp.shape[d] - 1)
+            fp_shape = _footprint_shape(fp)
+            _odd_size(fp_shape[d], require_odd_size)
+            shape[d] += nreps * (fp_shape[d] - 1)
     return tuple(shape)
 
 
@@ -95,7 +115,7 @@ def footprint_from_sequence(footprints):
     return morphology.binary_dilation(imag, footprints)
 
 
-def square(width, dtype=cp.uint8, *, decomposition=None):
+def square(width, dtype=None, *, decomposition=None):
     """Generates a flat, square-shaped footprint.
 
     Every pixel along the perimeter has a chessboard distance
@@ -108,8 +128,11 @@ def square(width, dtype=cp.uint8, *, decomposition=None):
 
     Other Parameters
     ----------------
-    dtype : data-type, optional
-        The data type of the footprint.
+    dtype : data-type or None, optional
+        The data type of the footprint. When None, a tuple will be returned in
+        place of the actual footprint array. This can be be passed to grayscale
+        and binary morphology functions in place of an explicit array to avoid
+        array allocation overhead.
     decomposition : {None, 'separable', 'sequence'}, optional
         If None, a single array is returned. For 'sequence', a tuple of smaller
         footprints is returned. Applying this series of smaller footprints will
@@ -144,19 +167,30 @@ def square(width, dtype=cp.uint8, *, decomposition=None):
     The 'sequence' decomposition mode only supports odd valued `width`. If
     `width` is even, the sequence used will be identical to the 'separable'
     mode.
+
     """
     if decomposition is None:
-        return cp.ones((width, width), dtype=dtype)
+        if dtype is None:
+            return (width, width)
+        else:
+            return cp.ones((width, width), dtype=dtype)
 
     if decomposition == 'separable' or width % 2 == 0:
-        sequence = [(cp.ones((width, 1), dtype=dtype), 1),
-                    (cp.ones((1, width), dtype=dtype), 1)]
+        if dtype is None:
+            sequence = (((width, 1), 1), ((1, width), 1))
+        else:
+            sequence = ((cp.ones((width, 1), dtype=dtype), 1),
+                        (cp.ones((1, width), dtype=dtype), 1))
     elif decomposition == 'sequence':
         # only handles odd widths
-        sequence = [(cp.ones((3, 3), dtype=dtype), _decompose_size(width, 3))]
+        if dtype is None:
+            sequence = (((3, 3), _decompose_size(width, 3)),)
+        else:
+            sequence = ((cp.ones((3, 3), dtype=dtype),
+                        _decompose_size(width, 3)),)
     else:
         raise ValueError(f"Unrecognized decomposition: {decomposition}")
-    return tuple(sequence)
+    return sequence
 
 
 def _decompose_size(size, kernel_size=3):
@@ -172,7 +206,7 @@ def _decompose_size(size, kernel_size=3):
     return 1 + (size - kernel_size) // (kernel_size - 1)
 
 
-def rectangle(nrows, ncols, dtype=cp.uint8, *, decomposition=None):
+def rectangle(nrows, ncols, dtype=None, *, decomposition=None):
     """Generates a flat, rectangular-shaped footprint.
 
     Every pixel in the rectangle generated for a given width and given height
@@ -187,8 +221,11 @@ def rectangle(nrows, ncols, dtype=cp.uint8, *, decomposition=None):
 
     Other Parameters
     ----------------
-    dtype : data-type, optional
-        The data type of the footprint.
+    dtype : data-type or None, optional
+        The data type of the footprint. When None, a tuple will be returned in
+        place of the actual footprint array. This can be be passed to grayscale
+        and binary morphology functions in place of an explicit array to avoid
+        array allocation overhead.
     decomposition : {None, 'separable', 'sequence'}, optional
         If None, a single array is returned. For 'sequence', a tuple of smaller
         footprints is returned. Applying this series of smaller footprints will
@@ -228,28 +265,49 @@ def rectangle(nrows, ncols, dtype=cp.uint8, *, decomposition=None):
       version 0.18.0. Use ``nrows`` and ``ncols`` instead.
     """
     if decomposition is None:  # TODO: check optimal width setting here
-        return cp.ones((nrows, ncols), dtype=dtype)
+        if dtype is None:
+            return (nrows, ncols)
+        else:
+            return cp.ones((nrows, ncols), dtype=dtype)
 
     even_rows = nrows % 2 == 0
     even_cols = ncols % 2 == 0
     if decomposition == 'separable' or even_rows or even_cols:
-        sequence = [(cp.ones((nrows, 1), dtype=dtype), 1),
-                    (cp.ones((1, ncols), dtype=dtype), 1)]
+        if dtype is None:
+            sequence = [((nrows, 1), 1), ((1, ncols), 1)]
+        else:
+            sequence = [
+                (cp.ones((nrows, 1), dtype=dtype), 1),
+                (cp.ones((1, ncols), dtype=dtype), 1),
+            ]
     elif decomposition == 'sequence':
         # this branch only support odd nrows, ncols
         sq_size = 3
         sq_reps = _decompose_size(min(nrows, ncols), sq_size)
-        sequence = [(cp.ones((3, 3), dtype=dtype), sq_reps)]
+        if dtype is None:
+            sequence = [((3, 3), sq_reps)]
+        else:
+            sequence = [(cp.ones((3, 3), dtype=dtype), sq_reps)]
         if nrows > ncols:
             nextra = nrows - ncols
-            sequence.append(
-                (cp.ones((nextra + 1, 1), dtype=dtype), 1)
-            )
+            if dtype is None:
+                sequence.append(
+                    ((nextra + 1, 1), 1)
+                )
+            else:
+                sequence.append(
+                    (cp.ones((nextra + 1, 1), dtype=dtype), 1)
+                )
         elif ncols > nrows:
             nextra = ncols - nrows
-            sequence.append(
-                (cp.ones((1, nextra + 1), dtype=dtype), 1)
-            )
+            if dtype is None:
+                sequence.append(
+                    ((1, nextra + 1), 1)
+                )
+            else:
+                sequence.append(
+                    (cp.ones((1, nextra + 1), dtype=dtype), 1)
+                )
     else:
         raise ValueError(f"Unrecognized decomposition: {decomposition}")
     return tuple(sequence)
@@ -313,7 +371,7 @@ def diamond(radius, dtype=cp.uint8, *, decomposition=None):
     return footprint
 
 
-def _nsphere_series_decomposition(radius, ndim, dtype=cp.uint8):
+def _nsphere_series_decomposition(radius, ndim, dtype=None):
     """Generate a sequence of footprints approximating an n-sphere.
 
     Morphological operations with an n-sphere (hypersphere) footprint can be
@@ -368,12 +426,16 @@ def _nsphere_series_decomposition(radius, ndim, dtype=cp.uint8):
     num_t_series, num_diamond, num_square = precomputed_decompositions[radius]
 
     sequence = []
+    if dtype is None:
+        _dtype = cp.uint8
+    else:
+        _dtype = dtype
     if num_t_series > 0:
         # shape (3, ) * ndim "T-shaped" footprints
-        all_t = _t_shaped_element_series(ndim=ndim, dtype=dtype)
+        all_t = _t_shaped_element_series(ndim=ndim, dtype=_dtype)
         [sequence.append((t, num_t_series)) for t in all_t]
     if num_diamond > 0:
-        d = np.zeros((3,) * ndim, dtype=dtype)
+        d = np.zeros((3,) * ndim, dtype=_dtype)
         sl = [slice(1, 2)] * ndim
         for ax in range(ndim):
             sl[ax] = slice(None)
@@ -381,8 +443,10 @@ def _nsphere_series_decomposition(radius, ndim, dtype=cp.uint8):
             sl[ax] = slice(1, 2)
         sequence.append((cp.asarray(d), num_diamond))
     if num_square > 0:
-        sq = cp.ones((3, ) * ndim, dtype=dtype)
-        sequence.append((sq, num_square))
+        if dtype is None:
+            sequence.append(((3, ) * ndim, num_square))
+        else:
+            sequence.append((cp.ones((3, ) * ndim, dtype=_dtype), num_square))
     return tuple(sequence)
 
 
@@ -648,7 +712,7 @@ def ellipse(width, height, dtype=cp.uint8, *, decomposition=None):
     return sequence
 
 
-def cube(width, dtype=cp.uint8, *, decomposition=None):
+def cube(width, dtype=None, *, decomposition=None):
     """ Generates a cube-shaped footprint.
 
     This is the 3D equivalent of a square.
@@ -662,8 +726,11 @@ def cube(width, dtype=cp.uint8, *, decomposition=None):
 
     Other Parameters
     ----------------
-    dtype : data-type, optional
-        The data type of the footprint.
+    dtype : data-type or None, optional
+        The data type of the footprint. When None, a tuple will be returned in
+        place of the actual footprint array. This can be be passed to grayscale
+        and binary morphology functions in place of an explicit array to avoid
+        array allocation overhead.
     decomposition : {None, 'separable', 'sequence'}, optional
         If None, a single array is returned. For 'sequence', a tuple of smaller
         footprints is returned. Applying this series of smaller footprints will
@@ -696,19 +763,34 @@ def cube(width, dtype=cp.uint8, *, decomposition=None):
     mode.
     """
     if decomposition is None:
-        return cp.ones((width, width, width), dtype=dtype)
+        if dtype is None:
+            return (width, width, width)
+        else:
+            return cp.ones((width, width, width), dtype=dtype)
     if decomposition == 'separable' or width % 2 == 0:
-        sequence = [(cp.ones((width, 1, 1), dtype=dtype), 1),
-                    (cp.ones((1, width, 1), dtype=dtype), 1),
-                    (cp.ones((1, 1, width), dtype=dtype), 1)]
+        if dtype is None:
+            sequence = (
+                ((width, 1, 1), 1), ((1, width, 1), 1), ((1, 1, width), 1)
+            )
+        else:
+            sequence = (
+                (cp.ones((width, 1, 1), dtype=dtype), 1),
+                (cp.ones((1, width, 1), dtype=dtype), 1),
+                (cp.ones((1, 1, width), dtype=dtype), 1),
+            )
     elif decomposition == 'sequence':
         # only handles odd widths
-        sequence = [
-            (cp.ones((3, 3, 3), dtype=dtype), _decompose_size(width, 3))
-        ]
+        if dtype is None:
+            sequence = (
+                ((3, 3, 3), _decompose_size(width, 3)),
+            )
+        else:
+            sequence = (
+                (cp.ones((3, 3, 3), dtype=dtype), _decompose_size(width, 3)),
+            )
     else:
         raise ValueError(f"Unrecognized decomposition: {decomposition}")
-    return tuple(sequence)
+    return sequence
 
 
 def octahedron(radius, dtype=cp.uint8, *, decomposition=None):
diff --git a/python/cucim/src/cucim/skimage/morphology/gray.py b/python/cucim/src/cucim/skimage/morphology/gray.py
index 48f87a40e..0de4bb12d 100644
--- a/python/cucim/src/cucim/skimage/morphology/gray.py
+++ b/python/cucim/src/cucim/skimage/morphology/gray.py
@@ -25,13 +25,24 @@ def _iterate_gray_func(gray_func, image, footprints, out):
     (e.g. `scipy.ndimage.binary_erosion`).
     """
     fp, num_iter = footprints[0]
-    gray_func(image, footprint=fp, output=out)
+    tuple_fp = isinstance(fp, tuple)
+    if tuple_fp:
+        gray_func(image, size=fp, output=out)
+    else:
+        gray_func(image, footprint=fp, output=out)
     for _ in range(1, num_iter):
-        gray_func(out.copy(), footprint=fp, output=out)
+        if tuple_fp:
+            gray_func(out.copy(), size=fp, output=out)
+        else:
+            gray_func(out.copy(), footprint=fp, output=out)
     for fp, num_iter in footprints[1:]:
+        tuple_fp = isinstance(fp, tuple)
         # Note: out.copy() because the computation cannot be in-place!
         for _ in range(num_iter):
-            gray_func(out.copy(), footprint=fp, output=out)
+            if tuple_fp:
+                gray_func(out.copy(), size=fp, output=out)
+            else:
+                gray_func(out.copy(), footprint=fp, output=out)
     return out
 
 
@@ -53,6 +64,13 @@ def _shift_footprint(footprint, shift_x, shift_y):
     out : 2D array, shape (M + int(shift_x), N + int(shift_y))
         The shifted footprint.
     """
+    if isinstance(footprint, tuple):
+        if len(footprint) == 2 and any(s % 2 == 0 for s in footprint):
+            # have to use an explicit array to shift the footprint below
+            footprint = cp.ones(footprint, dtype=bool)
+        else:
+            # no shift needed
+            return footprint
     if footprint.ndim != 2:
         # do nothing for 1D or 3D or higher footprints
         return footprint
@@ -101,6 +119,9 @@ def _invert_footprint(footprint):
     ----------
     .. [1] https://github.com/scipy/scipy/blob/ec20ababa400e39ac3ffc9148c01ef86d5349332/scipy/ndimage/morphology.py#L1285  # noqa
     """
+    if isinstance(footprint, tuple):
+        # fully populated rectangle is symmetric
+        return footprint
     inverted = footprint[(slice(None, None, -1),) * footprint.ndim]
     return inverted
 
@@ -135,6 +156,8 @@ def func_out(image, footprint, out=None, *args, **kwargs):
             # Note: in practice none of our built-in footprint sequences will
             #       require padding (all are symmetric and have odd sizes)
             footprint_shape = _shape_from_sequence(footprint)
+        elif isinstance(footprint, tuple):
+            footprint_shape = footprint
         else:
             footprint_shape = footprint.shape
         for axis_len in footprint_shape:
@@ -232,6 +255,18 @@ def erosion(image, footprint=None, out=None, shift_x=False, shift_y=False):
                            for fp, n in footprint)
         return _iterate_gray_func(ndi.grey_erosion, image, footprints, out)
 
+    if isinstance(footprint, tuple):
+        if len(footprint) != image.ndim:
+            raise ValueError(
+                "footprint.ndim={len(footprint)}, image.ndim={image.ndim}"
+            )
+        if image.ndim == 2 and any(s % 2 == 0 for s in footprint):
+            # only odd-shaped footprints are properly handled for tuples
+            footprint = cp.ones(footprint, dtype=bool)
+        else:
+            ndi.grey_erosion(image, size=footprint, output=out)
+            return out
+
     footprint = _shift_footprint(footprint, shift_x, shift_y)
     ndi.grey_erosion(image, footprint=footprint, output=out)
     return out
@@ -315,6 +350,18 @@ def dilation(image, footprint=None, out=None, shift_x=False, shift_y=False):
         )
         return _iterate_gray_func(ndi.grey_dilation, image, footprints, out)
 
+    if isinstance(footprint, tuple):
+        if len(footprint) != image.ndim:
+            raise ValueError(
+                "footprint.ndim={len(footprint)}, image.ndim={image.ndim}"
+            )
+        if image.ndim == 2 and any(s % 2 == 0 for s in footprint):
+            # only odd-shaped footprints are properly handled for tuples
+            footprint = cp.ones(footprint, dtype=bool)
+        else:
+            ndi.grey_dilation(image, size=footprint, output=out)
+            return out
+
     footprint = _shift_footprint(footprint, shift_x, shift_y)
     # Inside ndi.grey_dilation, the footprint is inverted,
     # e.g. `footprint = footprint[::-1, ::-1]` for 2D [1]_, for reasons unknown
@@ -562,6 +609,14 @@ def white_tophat(image, footprint=None, out=None):
         out_ = out
     if _footprint_is_sequence(footprint):
         return _white_tophat_seqence(image_, footprint, out_)
+    elif isinstance(footprint, tuple):
+        if len(footprint) != image.ndim:
+            raise ValueError(
+                "footprint.ndim={len(footprint)}, image.ndim={image.ndim}"
+            )
+        ndi.white_tophat(image, size=footprint, output=out)
+        return out
+
     out_ = ndi.white_tophat(image_, footprint=footprint, output=out_)
     return out
 
diff --git a/python/cucim/src/cucim/skimage/morphology/isotropic.py b/python/cucim/src/cucim/skimage/morphology/isotropic.py
index 4e47b1aaa..1ba6a7d14 100644
--- a/python/cucim/src/cucim/skimage/morphology/isotropic.py
+++ b/python/cucim/src/cucim/skimage/morphology/isotropic.py
@@ -6,6 +6,36 @@
 from cucim.core.operations.morphology import distance_transform_edt
 
 
+def _check_output(out, shape):
+    """Check shape and dtype of output array.
+
+    Parameters
+    ----------
+    out : cp.ndarray or None
+        The array to check
+    shape : tuple of int
+        The expected shape
+
+    Returns
+    -------
+    out : cp.ndarray
+        The original array (or boolean view of a uint8 array).
+    """
+    if out is None:
+        return None
+    if out.shape != shape:
+        raise ValueError("out.shape must match image.shape")
+    if not out.flags.c_contiguous:
+        raise ValueError("out array must have C-contiguous memory layout")
+    if out.dtype == bool:
+        return out
+    elif out.dtype == cp.uint8:
+        # view uint8 as bool
+        return out.view(bool)
+    else:
+        raise ValueError("provided out array should have boolean type")
+
+
 def isotropic_erosion(image, radius, out=None, spacing=None):
     """Return binary morphological erosion of an image.
 
@@ -60,11 +90,10 @@ def isotropic_erosion(image, radius, out=None, spacing=None):
         Volume 13, Issue 3, 1992, Pages 161-166.
         :DOI:`10.1016/0167-8655(92)90055-5`
     """
+    out = _check_output(out, image.shape)
     dist = distance_transform_edt(image, sampling=spacing)
     if out is not None:
-        # Copying instead of using the out= kwarg here since one CI test run
-        # on CentOS 7 failed for test_out_argument otherwise.
-        out[:] = cp.greater(dist, radius)
+        cp.greater(dist, radius, out=out)
     else:
         out = cp.greater(dist, radius)
     return out
@@ -125,11 +154,10 @@ def isotropic_dilation(image, radius, out=None, spacing=None):
         Volume 13, Issue 3, 1992, Pages 161-166.
         :DOI:`10.1016/0167-8655(92)90055-5`
     """
+    out = _check_output(out, image.shape)
     dist = distance_transform_edt(cp.logical_not(image), sampling=spacing)
     if out is not None:
-        # Copying instead of using the out= kwarg here since one CI test run
-        # on CentOS 7 failed for test_out_argument otherwise.
-        out[:] = cp.less_equal(dist, radius)
+        cp.less_equal(dist, radius, out=out)
     else:
         out = cp.less_equal(dist, radius)
     return out
@@ -188,6 +216,7 @@ def isotropic_opening(image, radius, out=None, spacing=None):
         Volume 13, Issue 3, 1992, Pages 161-166.
         :DOI:`10.1016/0167-8655(92)90055-5`
     """
+    out = _check_output(out, image.shape)
     eroded = isotropic_erosion(image, radius, spacing=spacing)
     return isotropic_dilation(eroded, radius, out=out, spacing=spacing)
 
@@ -245,5 +274,6 @@ def isotropic_closing(image, radius, out=None, spacing=None):
         Volume 13, Issue 3, 1992, Pages 161-166.
         :DOI:`10.1016/0167-8655(92)90055-5`
     """
+    out = _check_output(out, image.shape)
     dilated = isotropic_dilation(image, radius, spacing=spacing)
     return isotropic_erosion(dilated, radius, out=out, spacing=spacing)
diff --git a/python/cucim/src/cucim/skimage/segmentation/_expand_labels.py b/python/cucim/src/cucim/skimage/segmentation/_expand_labels.py
index 8ec47a7be..d7261f815 100644
--- a/python/cucim/src/cucim/skimage/segmentation/_expand_labels.py
+++ b/python/cucim/src/cucim/skimage/segmentation/_expand_labels.py
@@ -32,7 +32,7 @@ def expand_labels(label_image, distance=1):
     Where labels are spaced more than ``distance`` pixels are apart, this is
     equivalent to a morphological dilation with a disc or hyperball of radius ``distance``.
     However, in contrast to a morphological dilation, ``expand_labels`` will
-    not expand a label region into a neighboring region.  
+    not expand a label region into a neighboring region.
 
     This implementation of ``expand_labels`` is derived from CellProfiler [1]_, where
     it is known as module "IdentifySecondaryObjects (Distance-N)" [2]_.
diff --git a/python/cucim/src/cucim/skimage/transform/_warps.py b/python/cucim/src/cucim/skimage/transform/_warps.py
index dfc617152..88a395591 100644
--- a/python/cucim/src/cucim/skimage/transform/_warps.py
+++ b/python/cucim/src/cucim/skimage/transform/_warps.py
@@ -67,7 +67,7 @@ def _preprocess_resize_output_shape(image, output_shape):
     return image, output_shape
 
 
-def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=True,
+def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=None,
            preserve_range=False, anti_aliasing=None, anti_aliasing_sigma=None):
     """Resize image to match a certain size.
 
@@ -105,8 +105,8 @@ def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=True,
         the image boundaries.
     clip : bool, optional
         Whether to clip the output to the range of values of the input image.
-        This is enabled by default, since higher order interpolation may
-        produce values outside the given input range.
+        If `order` > 1, this will be enabled by default, since higher order
+        interpolation may produce values outside the given input range.
     preserve_range : bool, optional
         Whether to keep the original range of values. Otherwise, the input
         image is converted according to the conventions of `img_as_float`.
@@ -163,10 +163,11 @@ def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=True,
     if order > 0:
         image = convert_to_float(image, preserve_range)
 
+    if clip is None:
+        clip = True if order > 1 else False
+
     # Save input value range for clip
-    img_bounds = (
-        cp.array([image.min(), image.max()]) if clip else None
-    )
+    img_bounds = (image.min(), image.max()) if clip else None
 
     # Translate modes used by np.pad to those used by scipy.ndimage
     ndi_mode = _to_ndimage_mode(mode)
@@ -197,12 +198,12 @@ def resize(image, output_shape, order=None, mode='reflect', cval=0, clip=True,
     zoom_factors = [1 / f for f in factors]
     out = ndi.zoom(image, zoom_factors, order=order, mode=ndi_mode,
                    cval=cval, grid_mode=True)
-    _clip_warp_output(img_bounds, out, mode, cval, clip)
+    _clip_warp_output(img_bounds, out, mode, cval, order, clip)
     return out
 
 
 @channel_as_last_axis()
-def rescale(image, scale, order=None, mode='reflect', cval=0, clip=True,
+def rescale(image, scale, order=None, mode='reflect', cval=0, clip=None,
             preserve_range=False, anti_aliasing=None, anti_aliasing_sigma=None,
             *, channel_axis=None):
     """Scale image by a certain factor.
@@ -239,8 +240,8 @@ def rescale(image, scale, order=None, mode='reflect', cval=0, clip=True,
         the image boundaries.
     clip : bool, optional
         Whether to clip the output to the range of values of the input image.
-        This is enabled by default, since higher order interpolation may
-        produce values outside the given input range.
+        If `order` > 1, this will be enabled by default, since higher order
+        interpolation may produce values outside the given input range.
     preserve_range : bool, optional
         Whether to keep the original range of values. Otherwise, the input
         image is converted according to the conventions of `img_as_float`.
@@ -335,8 +336,7 @@ def _ndimage_affine(image, matrix, output_shape, order, mode, cval, clip,
                                   mode=ndi_mode, order=order, cval=cval,
                                   output_shape=tuple(output_shape))
 
-    _clip_warp_output(image, warped, mode, cval, clip)
-
+    _clip_warp_output(image, warped, mode, cval, order, clip)
     return warped
 
 
@@ -364,7 +364,7 @@ def _ndimage_rotate(image, angle, resize, order, mode, cval, clip,
     ndi_mode = _to_ndimage_mode(mode)
     warped = ndi.rotate(image, angle, reshape=resize, prefilter=prefilter,
                         mode=ndi_mode, order=order, cval=cval)
-    _clip_warp_output(image, warped, mode, cval, clip)
+    _clip_warp_output(image, warped, mode, cval, order, clip)
     return warped
 
 
@@ -407,8 +407,8 @@ def rotate(image, angle, resize=False, center=None, order=None,
         the image boundaries.
     clip : bool, optional
         Whether to clip the output to the range of values of the input image.
-        This is enabled by default, since higher order interpolation may
-        produce values outside the given input range.
+        If `order` > 1, this will be enabled by default, since higher order
+        interpolation may produce values outside the given input range.
     preserve_range : bool, optional
         Whether to keep the original range of values. Otherwise, the input
         image is converted according to the conventions of `img_as_float`.
@@ -440,7 +440,6 @@ def rotate(image, angle, resize=False, center=None, order=None,
     (512, 512)
 
     """
-
     rows, cols = image.shape[0], image.shape[1]
     if image.dtype == cp.float16:
         image = image.astype(cp.float32)
@@ -585,7 +584,7 @@ def _swirl_mapping(xy, center, rotation, strength, radius):
 
 
 def swirl(image, center=None, strength=1, radius=100, rotation=0,
-          output_shape=None, order=None, mode='reflect', cval=0, clip=True,
+          output_shape=None, order=None, mode='reflect', cval=0, clip=None,
           preserve_range=False):
     """Perform a swirl transformation.
 
@@ -626,8 +625,8 @@ def swirl(image, center=None, strength=1, radius=100, rotation=0,
         the image boundaries.
     clip : bool, optional
         Whether to clip the output to the range of values of the input image.
-        This is enabled by default, since higher order interpolation may
-        produce values outside the given input range.
+        If `order` > 1, this will be enabled by default, since higher order
+        interpolation may produce values outside the given input range.
     preserve_range : bool, optional
         Whether to keep the original range of values. Otherwise, the input
         image is converted according to the conventions of `img_as_float`.
@@ -751,11 +750,12 @@ def warp_coords(coord_map, shape, dtype=cp.float64):
     return coords
 
 
-def _clip_warp_output(input_image, output_image, mode, cval, clip):
+def _clip_warp_output(input_image, output_image, mode, cval, order, clip):
     """Clip output image to range of values of input image.
 
     Note that this function modifies the values of `output_image` in-place
-    and it is only modified if ``clip=True``.
+    and it is only modified if ``clip=True`` (or clip is ``None`` and
+    `order` > 1).
 
     Parameters
     ----------
@@ -766,19 +766,32 @@ def _clip_warp_output(input_image, output_image, mode, cval, clip):
 
     Other parameters
     ----------------
-    mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}, optional
+    mode : {'constant', 'edge', 'symmetric', 'reflect', 'wrap'}
         Points outside the boundaries of the input are filled according
         to the given mode.  Modes match the behaviour of `numpy.pad`.
-    cval : float, optional
+    cval : float
         Used in conjunction with mode 'constant', the value outside
         the image boundaries.
-    clip : bool, optional
+    order : int
+        The interpolation order used. If `clip` is ``None`` and `order` <= 1,
+        no clipping will be applied.
+    clip : bool or None
         Whether to clip the output to the range of values of the input image.
-        This is enabled by default, since higher order interpolation may
-        produce values outside the given input range.
+        If `order` > 1, this will be enabled by default, since higher order
+        interpolation may produce values outside the given input range.
 
     """
-    if clip:
+    if (clip is None and order <= 1) or not clip:
+        # no clipping by default for order = 0 or 1
+        return
+    if isinstance(input_image, tuple) and len(input_image) == 2:
+        min_val, max_val = input_image
+        # copy device scalars to host if necessary
+        if isinstance(min_val, cp.ndarray):
+            min_val = min_val.item()
+        if isinstance(max_val, cp.ndarray):
+            max_val = max_val.item()
+    else:
         min_val = input_image.min().item()
         if np.isnan(min_val):
             # NaNs detected, use NaN-safe min/max
@@ -790,26 +803,26 @@ def _clip_warp_output(input_image, output_image, mode, cval, clip):
             max_func = cp.max
         max_val = max_func(input_image).item()
 
-        # Check if cval has been used such that it expands the effective input
-        # range
-        preserve_cval = (
-            mode == 'constant'
-            and not min_val <= cval <= max_val
-            and min_func(output_image) <= cval <= max_func(output_image)
-        )
+    # Check if cval has been used such that it expands the effective input
+    # range
+    preserve_cval = (
+        mode == 'constant'
+        and not min_val <= cval <= max_val
+        and min_func(output_image) <= cval <= max_func(output_image)
+    )
 
-        # expand min/max range to account for cval
-        if preserve_cval:
-            # cast cval to the same dtype as the input image
-            cval = input_image.dtype.type(cval)
-            min_val = min(min_val, cval)
-            max_val = max(max_val, cval)
+    # expand min/max range to account for cval
+    if preserve_cval:
+        # cast cval to the same dtype as the input image
+        cval = input_image.dtype.type(cval)
+        min_val = min(min_val, cval)
+        max_val = max(max_val, cval)
 
-        cp.clip(output_image, min_val, max_val, out=output_image)
+    cp.clip(output_image, min_val, max_val, out=output_image)
 
 
 def warp(image, inverse_map, map_args={}, output_shape=None, order=None,
-         mode='constant', cval=0., clip=True, preserve_range=False):
+         mode='constant', cval=0., clip=None, preserve_range=False):
     """Warp an image according to a given coordinate transformation.
 
     Parameters
@@ -872,8 +885,8 @@ def warp(image, inverse_map, map_args={}, output_shape=None, order=None,
         the image boundaries.
     clip : bool, optional
         Whether to clip the output to the range of values of the input image.
-        This is enabled by default, since higher order interpolation may
-        produce values outside the given input range.
+        If `order` > 1, this will be enabled by default, since higher order
+        interpolation may produce values outside the given input range.
     preserve_range : bool, optional
         Whether to keep the original range of values. Otherwise, the input
         image is converted according to the conventions of `img_as_float`.
@@ -953,7 +966,6 @@ def warp(image, inverse_map, map_args={}, output_shape=None, order=None,
     if image.size == 0:
         raise ValueError("Cannot warp empty image with dimensions",
                          image.shape)
-
     order = _validate_interpolation_order(image.dtype, order)
 
     if image.dtype.kind == "c":
@@ -1008,8 +1020,7 @@ def coord_map(*args):
     warped = ndi.map_coordinates(image, coords, prefilter=prefilter,
                                  mode=ndi_mode, order=order, cval=cval)
 
-    _clip_warp_output(image, warped, mode, cval, clip)
-
+    _clip_warp_output(image, warped, mode, cval, order, clip)
     return warped
 
 
diff --git a/python/pybind11/cache/cache_py.cpp b/python/pybind11/cache/cache_py.cpp
index 00e3d73a1..6b176e93e 100644
--- a/python/pybind11/cache/cache_py.cpp
+++ b/python/pybind11/cache/cache_py.cpp
@@ -186,4 +186,4 @@ uint32_t py_preferred_memory_capacity(const py::object& img,
                                      bytes_per_pixel);
 }
 
-} // namespace cucim::cache
\ No newline at end of file
+} // namespace cucim::cache
diff --git a/python/pybind11/cache/image_cache_py.cpp b/python/pybind11/cache/image_cache_py.cpp
index df057be17..ba214135a 100644
--- a/python/pybind11/cache/image_cache_py.cpp
+++ b/python/pybind11/cache/image_cache_py.cpp
@@ -24,4 +24,4 @@ namespace py = pybind11;
 namespace cucim::cache
 {
 
-} // namespace cucim::cache
\ No newline at end of file
+} // namespace cucim::cache
diff --git a/python/pybind11/filesystem/cufile_py.cpp b/python/pybind11/filesystem/cufile_py.cpp
index ba2fccec2..52992dd9a 100644
--- a/python/pybind11/filesystem/cufile_py.cpp
+++ b/python/pybind11/filesystem/cufile_py.cpp
@@ -69,4 +69,4 @@ ssize_t fd_pwrite(CuFileDriver& fd, const py::object& obj, size_t count, off_t f
     py::call_guard<py::gil_scoped_release>();
     return fd.pwrite(buf, count, file_offset, buf_offset);
 }
-} // namespace cucim::filesystem
\ No newline at end of file
+} // namespace cucim::filesystem
diff --git a/python/pybind11/filesystem/filesystem_py.cpp b/python/pybind11/filesystem/filesystem_py.cpp
index e11de2bc5..0827a13fe 100644
--- a/python/pybind11/filesystem/filesystem_py.cpp
+++ b/python/pybind11/filesystem/filesystem_py.cpp
@@ -128,4 +128,4 @@ ssize_t py_pwrite(const std::shared_ptr<CuFileDriver>& fd, py::object buf, size_
     }
 }
 
-} // namespace cucim::filesystem
\ No newline at end of file
+} // namespace cucim::filesystem
diff --git a/python/pybind11/io/device_py.cpp b/python/pybind11/io/device_py.cpp
index fbec46333..7b1d82c40 100644
--- a/python/pybind11/io/device_py.cpp
+++ b/python/pybind11/io/device_py.cpp
@@ -25,4 +25,4 @@ namespace py = pybind11;
 namespace cucim::io
 {
 
-} // namespace cucim::io
\ No newline at end of file
+} // namespace cucim::io
diff --git a/python/pybind11/io/io_py.cpp b/python/pybind11/io/io_py.cpp
index 3a79eafdd..969abd9ca 100644
--- a/python/pybind11/io/io_py.cpp
+++ b/python/pybind11/io/io_py.cpp
@@ -42,4 +42,4 @@ void init_io(py::module& io)
         .def_property("index", &Device::index, nullptr, doc::Device::doc_index)
         .def("__repr__", [](const Device& device) { return std::string(device); });
 }
-} // namespace cucim::io
\ No newline at end of file
+} // namespace cucim::io
diff --git a/python/pybind11/memory/memory_py.cpp b/python/pybind11/memory/memory_py.cpp
index 7f5bb7734..9da750c10 100644
--- a/python/pybind11/memory/memory_py.cpp
+++ b/python/pybind11/memory/memory_py.cpp
@@ -139,4 +139,4 @@ void get_memory_info(const py::object& buf_obj,
     }
 }
 
-} // namespace cucim::memory
\ No newline at end of file
+} // namespace cucim::memory


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 5c18a0b1c..3a6641d81 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -16,7 +16,7 @@ on:
 jobs:
   conda-python-tests:
     secrets: inherit
-    uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.02
+    uses: rapidsai/shared-action-workflows/.github/workflows/conda-python-tests.yaml@branch-23.04
     with:
       build_type: nightly
       branch: ${{ inputs.branch }}
diff --git a/3rdparty/LICENSE.googletest b/3rdparty/LICENSE.googletest
index 65c76c50c..1941a11f8 100644
--- a/3rdparty/LICENSE.googletest
+++ b/3rdparty/LICENSE.googletest
@@ -25,4 +25,4 @@ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/3rdparty/LICENSE.pytest b/3rdparty/LICENSE.pytest
index ca658c45a..d14fb7ff4 100644
--- a/3rdparty/LICENSE.pytest
+++ b/3rdparty/LICENSE.pytest
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/3rdparty/LICENSE.pytest-lazy-fixture b/3rdparty/LICENSE.pytest-lazy-fixture
index 7e40ab52e..6c128cff6 100644
--- a/3rdparty/LICENSE.pytest-lazy-fixture
+++ b/3rdparty/LICENSE.pytest-lazy-fixture
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
\ No newline at end of file
+THE SOFTWARE.
diff --git a/ci/test_python.sh b/ci/test_python.sh
index 1b848fc80..6435592bb 100755
--- a/ci/test_python.sh
+++ b/ci/test_python.sh
@@ -1,5 +1,5 @@
 #!/bin/bash
-# Copyright (c) 2022, NVIDIA CORPORATION.
+# Copyright (c) 2022-2023, NVIDIA CORPORATION.
 
 # Common setup steps shared by Python test jobs
 
@@ -27,7 +27,6 @@ PYTHON_CHANNEL=$(rapids-download-conda-from-s3 python)
 RAPIDS_TESTS_DIR=${RAPIDS_TESTS_DIR:-"${PWD}/test-results"}
 RAPIDS_COVERAGE_DIR=${RAPIDS_COVERAGE_DIR:-"${PWD}/coverage-results"}
 mkdir -p "${RAPIDS_TESTS_DIR}" "${RAPIDS_COVERAGE_DIR}"
-SUITEERROR=0
 
 rapids-print-env
 
@@ -39,6 +38,8 @@ rapids-mamba-retry install \
 rapids-logger "Check GPU usage"
 nvidia-smi
 
+EXITCODE=0
+trap "EXITCODE=1" ERR
 set +e
 
 rapids-logger "pytest cucim"
@@ -55,12 +56,7 @@ pytest \
   src \
   tests/unit \
   tests/performance
-exitcode=$?
-
-if (( ${exitcode} != 0 )); then
-    SUITEERROR=${exitcode}
-    echo "FAILED: 1 or more tests in cucim"
-fi
 popd
 
-exit ${SUITEERROR}
+rapids-logger "Test script exiting with value: $EXITCODE"
+exit ${EXITCODE}
diff --git a/cpp/cmake/deps/googletest.cmake b/cpp/cmake/deps/googletest.cmake
index 5d93a95f3..f85dd85b6 100644
--- a/cpp/cmake/deps/googletest.cmake
+++ b/cpp/cmake/deps/googletest.cmake
@@ -42,4 +42,4 @@ endif ()
 #  Policy CMP0082 is not set: Install rules from add_subdirectory() are
 #  interleaved with those in caller.  Run "cmake --help-policy CMP0082" for
 #  policy details.  Use the cmake_policy command to set the policy and
-#  suppress this warning.
\ No newline at end of file
+#  suppress this warning.
diff --git a/cpp/plugins/cucim.kit.cuslide/cmake/deps/googletest.cmake b/cpp/plugins/cucim.kit.cuslide/cmake/deps/googletest.cmake
index 5d93a95f3..f85dd85b6 100644
--- a/cpp/plugins/cucim.kit.cuslide/cmake/deps/googletest.cmake
+++ b/cpp/plugins/cucim.kit.cuslide/cmake/deps/googletest.cmake
@@ -42,4 +42,4 @@ endif ()
 #  Policy CMP0082 is not set: Install rules from add_subdirectory() are
 #  interleaved with those in caller.  Run "cmake --help-policy CMP0082" for
 #  policy details.  Use the cmake_policy command to set the policy and
-#  suppress this warning.
\ No newline at end of file
+#  suppress this warning.
diff --git a/cpp/plugins/cucim.kit.cuslide/tests/test_philips_tiff.cpp b/cpp/plugins/cucim.kit.cuslide/tests/test_philips_tiff.cpp
index 4f48646fd..974ed85ea 100644
--- a/cpp/plugins/cucim.kit.cuslide/tests/test_philips_tiff.cpp
+++ b/cpp/plugins/cucim.kit.cuslide/tests/test_philips_tiff.cpp
@@ -56,4 +56,4 @@ TEST_CASE("Verify philips tiff file", "[test_philips_tiff.cpp]")
     tif->close();
 
     REQUIRE(1 == 1);
-}
\ No newline at end of file
+}
diff --git a/cpp/plugins/cucim.kit.cuslide/tests/test_read_region.cpp b/cpp/plugins/cucim.kit.cuslide/tests/test_read_region.cpp
index 138218e48..b28c6c78b 100644
--- a/cpp/plugins/cucim.kit.cuslide/tests/test_read_region.cpp
+++ b/cpp/plugins/cucim.kit.cuslide/tests/test_read_region.cpp
@@ -115,4 +115,4 @@ TEST_CASE("Verify read_region()", "[test_read_region.cpp]")
          * sy is not multiple of 4, openslide's output was not trivial and performance was low.
          */
     }
-}
\ No newline at end of file
+}
diff --git a/python/cucim/src/cucim/skimage/feature/tests/test_corner.py b/python/cucim/src/cucim/skimage/feature/tests/test_corner.py
index dc752ecd7..649af9ad1 100644
--- a/python/cucim/src/cucim/skimage/feature/tests/test_corner.py
+++ b/python/cucim/src/cucim/skimage/feature/tests/test_corner.py
@@ -19,7 +19,6 @@
                                    structure_tensor,
                                    structure_tensor_eigenvalues)
 from cucim.skimage.feature.corner import _symmetric_image
-from cucim.skimage.morphology import cube
 
 
 @pytest.fixture
@@ -227,9 +226,11 @@ def test_structure_tensor_eigenvalues(dtype):
 
 
 def test_structure_tensor_eigenvalues_3d():
-    image = pad(cube(9), 5, mode='constant') * 1000
-    boundary = (pad(cube(9), 5, mode='constant')
-                - pad(cube(7), 6, mode='constant')).astype(bool)
+    cube9 = cp.ones((9,) * 3, dtype=cp.uint8)
+    cube7 = cp.ones((7,) * 3, dtype=cp.uint8)
+    image = pad(cube9, 5, mode='constant') * 1000
+    boundary = (pad(cube9, 5, mode='constant')
+                - pad(cube7, 6, mode='constant')).astype(bool)
     A_elems = structure_tensor(image, sigma=0.1)
     e0, e1, e2 = structure_tensor_eigenvalues(A_elems)
     # e0 should detect facets
diff --git a/python/cucim/src/cucim/skimage/filters/tests/test_median.py b/python/cucim/src/cucim/skimage/filters/tests/test_median.py
index c510aba20..324ee27c5 100644
--- a/python/cucim/src/cucim/skimage/filters/tests/test_median.py
+++ b/python/cucim/src/cucim/skimage/filters/tests/test_median.py
@@ -4,6 +4,7 @@
 from cupyx.scipy import ndimage
 from skimage import data
 
+from cucim.skimage import morphology
 from cucim.skimage._shared.testing import expected_warnings
 from cucim.skimage.filters import median
 
@@ -249,3 +250,17 @@ def test_median_preserve_dtype(image, dtype):
 )
 def test_median(img, behavior):
     median(img, behavior=behavior)
+
+
+def test_median_nonsquare():
+    """Test non-uniform footprint.
+
+    https://github.com/rapidsai/cucim/issues/520
+    """
+    rng = cp.random.default_rng()
+    img = rng.integers(0, 256, (128, 128), dtype=cp.uint8)
+    footprint = morphology.disk(5)
+    mode = 'nearest'
+    out = median(img, footprint, mode=mode, behavior='ndimage')
+    expected = ndimage.median_filter(img, footprint=footprint, mode=mode)
+    cp.testing.assert_array_equal(out, expected)
diff --git a/python/cucim/src/cucim/skimage/morphology/tests/test_binary.py b/python/cucim/src/cucim/skimage/morphology/tests/test_binary.py
index ccedfa10f..fd075bdac 100755
--- a/python/cucim/src/cucim/skimage/morphology/tests/test_binary.py
+++ b/python/cucim/src/cucim/skimage/morphology/tests/test_binary.py
@@ -60,10 +60,10 @@ def test_binary_opening():
 def _get_decomp_test_data(function, ndim=2):
     if function == 'binary_erosion':
         img = cp.ones((17, ) * ndim, dtype=cp.uint8)
-        img[8, 8] = 0
+        img[(8, ) * ndim] = 0
     elif function == 'binary_dilation':
         img = cp.zeros((17, ) * ndim, dtype=cp.uint8)
-        img[8, 8] = 1
+        img[(8, ) * ndim] = 1
     else:
         img = cp.asarray(data.binary_blobs(32, n_dim=ndim, seed=1))
     return img
@@ -329,3 +329,23 @@ def test_binary_output_3d():
 
     np.testing.assert_equal(int_opened.dtype, np.uint8)
     np.testing.assert_equal(int_closed.dtype, np.uint8)
+
+
+@pytest.mark.parametrize(
+    "function",
+    ["binary_erosion", "binary_dilation", "binary_closing", "binary_opening"],
+)
+@pytest.mark.parametrize("ndim", [1, 2, 3])
+def test_tuple_as_footprint(function, ndim):
+    """Validate footprint decomposition for various shapes.
+
+    comparison is made to the case without decomposition.
+    """
+    footprint_shape = tuple(range(2, ndim + 2))
+    footprint_ndarray = cp.ones(footprint_shape, dtype=bool)
+
+    img = _get_decomp_test_data(function, ndim=ndim)
+    func = getattr(morphology, function)
+    expected = func(img, footprint=footprint_ndarray)
+    out = func(img, footprint=footprint_shape)
+    testing.assert_array_equal(expected, out)
diff --git a/python/cucim/src/cucim/skimage/morphology/tests/test_footprints.py b/python/cucim/src/cucim/skimage/morphology/tests/test_footprints.py
index 0009cae19..160b31bf6 100644
--- a/python/cucim/src/cucim/skimage/morphology/tests/test_footprints.py
+++ b/python/cucim/src/cucim/skimage/morphology/tests/test_footprints.py
@@ -19,7 +19,7 @@ class TestSElem:
     def test_square_footprint(self):
         """Test square footprints"""
         for k in range(0, 5):
-            actual_mask = footprints.square(k)
+            actual_mask = footprints.square(k, dtype=cp.uint8)
             expected_mask = np.ones((k, k), dtype='uint8')
             assert_array_equal(expected_mask, actual_mask)
 
@@ -27,14 +27,14 @@ def test_rectangle_footprint(self):
         """Test rectangle footprints"""
         for i in range(0, 5):
             for j in range(0, 5):
-                actual_mask = footprints.rectangle(i, j)
+                actual_mask = footprints.rectangle(i, j, dtype=cp.uint8)
                 expected_mask = np.ones((i, j), dtype='uint8')
                 assert_array_equal(expected_mask, actual_mask)
 
     def test_cube_footprint(self):
         """Test cube footprints"""
         for k in range(0, 5):
-            actual_mask = footprints.cube(k)
+            actual_mask = footprints.cube(k, dtype=cp.uint8)
             expected_mask = np.ones((k, k, k), dtype='uint8')
             assert_array_equal(expected_mask, actual_mask)
 
diff --git a/python/cucim/src/cucim/skimage/morphology/tests/test_gray.py b/python/cucim/src/cucim/skimage/morphology/tests/test_gray.py
index c06fe9768..e863690ab 100755
--- a/python/cucim/src/cucim/skimage/morphology/tests/test_gray.py
+++ b/python/cucim/src/cucim/skimage/morphology/tests/test_gray.py
@@ -1,6 +1,7 @@
 import cupy as cp
 import numpy as np
 import pytest
+from cupy import testing
 from cupyx.scipy import ndimage as ndi
 from skimage import data
 
@@ -368,14 +369,14 @@ def test_rectangle_decomposition(cam_image, function, nrows, ncols,
                  "black_tophat"],
 )
 @pytest.mark.parametrize("radius", (2, 3))
-@pytest.mark.parametrize("decomposition", ['separable', 'sequence'])
+@pytest.mark.parametrize("decomposition", ['sequence'])
 def test_diamond_decomposition(cam_image, function, radius, decomposition):
     """Validate footprint decomposition for various shapes.
 
     comparison is made to the case without decomposition.
     """
-    footprint_ndarray = morphology.square(radius, decomposition=None)
-    footprint = morphology.square(radius, decomposition=decomposition)
+    footprint_ndarray = morphology.diamond(radius, decomposition=None)
+    footprint = morphology.diamond(radius, decomposition=decomposition)
     func = getattr(morphology, function)
     expected = func(cam_image, footprint=footprint_ndarray)
     out = func(cam_image, footprint=footprint)
@@ -443,3 +444,28 @@ def test_octahedron_decomposition(cell3d_image, function, radius,
     expected = func(cell3d_image, footprint=footprint_ndarray)
     out = func(cell3d_image, footprint=footprint)
     cp.testing.assert_array_equal(expected, out)
+
+
+@pytest.mark.parametrize(
+    "function",
+    ["erosion", "dilation", "closing", "opening"],
+)
+@pytest.mark.parametrize("ndim", [2, 3])
+@pytest.mark.parametrize("odd_only", [False, True])
+def test_tuple_as_footprint(function, ndim, odd_only):
+    """Validate footprint decomposition for various shapes.
+
+    comparison is made to the case without decomposition.
+    """
+    if odd_only:
+        footprint_shape = (3,) * ndim
+    else:
+        footprint_shape = tuple(range(2, 2 + ndim))
+    footprint_ndarray = cp.ones(footprint_shape, dtype=bool)
+
+    rng = cp.random.default_rng(5)
+    img = rng.standard_normal((16,) * ndim, dtype=cp.float32)
+    func = getattr(morphology, function)
+    expected = func(img, footprint=footprint_ndarray)
+    out = func(img, footprint=footprint_shape)
+    testing.assert_array_equal(expected, out)
diff --git a/python/cucim/src/cucim/skimage/morphology/tests/test_isotropic.py b/python/cucim/src/cucim/skimage/morphology/tests/test_isotropic.py
index 3ca66550e..edd67bc7e 100644
--- a/python/cucim/src/cucim/skimage/morphology/tests/test_isotropic.py
+++ b/python/cucim/src/cucim/skimage/morphology/tests/test_isotropic.py
@@ -1,5 +1,8 @@
+import platform
+
 import cupy as cp
 import numpy as np
+import pytest
 from cupy.testing import assert_array_equal
 from skimage import data
 
@@ -9,7 +12,12 @@
 img = color.rgb2gray(cp.asarray(data.astronaut()))
 bw_img = img > 100 / 255.
 
+# TODO: Some tests fail unexpectedly on ARM.
+ON_AARCH64 = platform.machine() == "aarch64"
+ON_AARCH64_REASON = "TODO: Test fails unexpectedly on ARM."
+
 
+@pytest.mark.xfail(ON_AARCH64, reason=ON_AARCH64_REASON)
 def test_non_square_image():
     isotropic_res = morphology.isotropic_erosion(bw_img[:100, :200], 3)
     binary_res = img_as_bool(morphology.binary_erosion(
@@ -17,6 +25,7 @@ def test_non_square_image():
     assert_array_equal(isotropic_res, binary_res)
 
 
+@pytest.mark.xfail(ON_AARCH64, reason=ON_AARCH64_REASON)
 def test_isotropic_erosion():
     isotropic_res = morphology.isotropic_erosion(bw_img, 3)
     binary_res = img_as_bool(
@@ -43,6 +52,7 @@ def _disk_with_spacing(
     return cp.asarray((X ** 2 + Y ** 2) <= radius ** 2, dtype=dtype)
 
 
+@pytest.mark.xfail(ON_AARCH64, reason=ON_AARCH64_REASON)
 def test_isotropic_erosion_spacing():
     isotropic_res = morphology.isotropic_dilation(bw_img, 6, spacing=(1, 2))
     binary_res = img_as_bool(
@@ -53,6 +63,7 @@ def test_isotropic_erosion_spacing():
     assert_array_equal(isotropic_res, binary_res)
 
 
+@pytest.mark.xfail(ON_AARCH64, reason=ON_AARCH64_REASON)
 def test_isotropic_dilation():
     isotropic_res = morphology.isotropic_dilation(bw_img, 3)
     binary_res = img_as_bool(
@@ -61,6 +72,7 @@ def test_isotropic_dilation():
     assert_array_equal(isotropic_res, binary_res)
 
 
+@pytest.mark.xfail(ON_AARCH64, reason=ON_AARCH64_REASON)
 def test_isotropic_closing():
     isotropic_res = morphology.isotropic_closing(bw_img, 3)
     binary_res = img_as_bool(
@@ -69,6 +81,7 @@ def test_isotropic_closing():
     assert_array_equal(isotropic_res, binary_res)
 
 
+@pytest.mark.xfail(ON_AARCH64, reason=ON_AARCH64_REASON)
 def test_isotropic_opening():
     isotropic_res = morphology.isotropic_opening(bw_img, 3)
     binary_res = img_as_bool(
@@ -77,6 +90,7 @@ def test_isotropic_opening():
     assert_array_equal(isotropic_res, binary_res)
 
 
+@pytest.mark.xfail(ON_AARCH64, reason=ON_AARCH64_REASON)
 def test_footprint_overflow():
     img = cp.zeros((20, 20), dtype=bool)
     img[2:19, 2:19] = True
@@ -87,14 +101,20 @@ def test_footprint_overflow():
     assert_array_equal(isotropic_res, binary_res)
 
 
-def test_out_argument():
+@pytest.mark.xfail(ON_AARCH64, reason=ON_AARCH64_REASON)
+@pytest.mark.parametrize('out_dtype', [bool, cp.uint8, cp.int32])
+def test_out_argument(out_dtype):
     for func in (morphology.isotropic_erosion, morphology.isotropic_dilation,
                  morphology.isotropic_opening, morphology.isotropic_closing):
         radius = 3
-        img = cp.ones((10, 10))
+        img = cp.ones((10, 10), dtype=bool)
         img[2:5, 2:5] = 0
-        out = cp.zeros_like(img)
+        out = cp.zeros_like(img, dtype=out_dtype)
         out_saved = out.copy()
-        func(img, radius, out=out)
-        assert cp.any(out != out_saved)
-        assert_array_equal(out, func(img, radius))
+        if out_dtype not in [bool, cp.uint8]:
+            with pytest.raises(ValueError):
+                func(img, radius, out=out)
+        else:
+            func(img, radius, out=out)
+            assert cp.any(out != out_saved)
+            assert_array_equal(out, func(img, radius))
diff --git a/test_data/input/LICENSE-3rdparty b/test_data/input/LICENSE-3rdparty
index bd32fa847..1a16e0556 100644
--- a/test_data/input/LICENSE-3rdparty
+++ b/test_data/input/LICENSE-3rdparty
@@ -316,4 +316,4 @@ Creative Commons Notice
     available upon request from time to time. For the avoidance of doubt,
     this trademark restriction does not form part of this License.
 
-    Creative Commons may be contacted at https://creativecommons.org/.
\ No newline at end of file
+    Creative Commons may be contacted at https://creativecommons.org/.