Repository: microsoft/torchgeo

Issue:
Fix failing minimum tests
This PR fixes the cause for the minimum tests failing in #854 (and presumably any PR created after Dec 1st). 

`scikit-learn` has officially begun deprecating `sklearn` to prevent malicious actors from using it. So any dependency that attempts to install `sklearn` would install the new `sklearn` package which throws an exception  at certain intervals - Refer https://pypi.org/project/sklearn/ for more details.

`open3d=0.11.2` was our problem package. The open3d team had switched from `sklearn` to `scikit-learn` in version `0.13.0`. This PR bumps the minimum version to the same.

It seems there was a PR prepped to replace `open3d` with `pyvista` - #663. If this lands for the `0.4.0` release, this PR can be ignored.



Discussion:

Resolving patch:
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -110,6 +110,7 @@
     "numpy": ("https://numpy.org/doc/stable/", None),
     "python": ("https://docs.python.org/3", None),
     "pytorch-lightning": ("https://pytorch-lightning.readthedocs.io/en/stable/", None),
+    "pyvista": ("https://docs.pyvista.org/", None),
     "rasterio": ("https://rasterio.readthedocs.io/en/stable/", None),
     "rtree": ("https://rtree.readthedocs.io/en/stable/", None),
     "segmentation_models_pytorch": ("https://smp.readthedocs.io/en/stable/", None),
diff --git a/torchgeo/datasets/idtrees.py b/torchgeo/datasets/idtrees.py
--- a/torchgeo/datasets/idtrees.py
+++ b/torchgeo/datasets/idtrees.py
@@ -8,7 +8,6 @@
 from typing import Any, Callable, Dict, List, Optional, Tuple, cast, overload
 
 import fiona
-import matplotlib as mpl
 import matplotlib.pyplot as plt
 import numpy as np
 import rasterio
@@ -567,25 +566,28 @@ def normalize(x: Tensor) -> Tensor:
 
         return fig
 
-    def plot_las(self, index: int, colormap: Optional[str] = None) -> Any:
+    def plot_las(
+        self, index: int
+    ) -> "pyvista.Plotter":  # type: ignore[name-defined] # noqa: F821
         """Plot a sample point cloud at the index.
 
         Args:
             index: index to plot
-            colormap: a valid matplotlib colormap
 
         Returns:
-            a open3d.visualizer.Visualizer object. Use
-                Visualizer.run() to display
+            pyvista.PolyData object. Run pyvista.plot(point_cloud, ...) to display
 
         Raises:
-            ImportError: if open3d is not installed
+            ImportError: if pyvista is not installed
+
+        .. versionchanged:: 0.4
+           Ported from Open3D to PyVista, *colormap* parameter removed.
         """
         try:
-            import open3d  # noqa: F401
+            import pyvista  # noqa: F401
         except ImportError:
             raise ImportError(
-                "open3d is not installed and is required to plot point clouds"
+                "pyvista is not installed and is required to plot point clouds"
             )
         import laspy
 
@@ -595,27 +597,12 @@ def plot_las(self, index: int, colormap: Optional[str] = None) -> Any:
         points: "np.typing.NDArray[np.int_]" = np.stack(
             [las.x, las.y, las.z], axis=0
         ).transpose((1, 0))
+        point_cloud = pyvista.PolyData(points)  # type: ignore[attr-defined]
 
-        if colormap:
-            if hasattr(mpl, "colormaps"):
-                cm = mpl.colormaps[colormap]
-            else:
-                cm = plt.cm.get_cmap(colormap)
-            norm = plt.Normalize()
-            colors = cm(norm(points[:, 2]))[:, :3]
-        else:
-            # Some point cloud files have no color->points mapping
-            if hasattr(las, "red"):
-                colors = np.stack([las.red, las.green, las.blue], axis=0)
-                colors = colors.transpose((1, 0)) / 65535
-            # Default to no colormap if no colors exist in las file
-            else:
-                colors = np.zeros_like(points)
-
-        pcd = open3d.geometry.PointCloud()
-        pcd.points = open3d.utility.Vector3dVector(points)
-        pcd.colors = open3d.utility.Vector3dVector(colors)
-        vis = open3d.visualization.Visualizer()
-        vis.create_window()
-        vis.add_geometry(pcd)
-        return vis
+        # Some point cloud files have no color->points mapping
+        if hasattr(las, "red"):
+            colors = np.stack([las.red, las.green, las.blue], axis=0)
+            colors = colors.transpose((1, 0)) / np.iinfo(np.uint16).max
+            point_cloud["colors"] = colors
+
+        return point_cloud


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -44,6 +44,8 @@ jobs:
       uses: actions/setup-python@v4.3.0
       with:
         python-version: ${{ matrix.python-version }}
+    - name: Setup headless display for pyvista
+      uses: pyvista/setup-headless-display-action@v1
     - name: Install apt dependencies (Linux)
       run: sudo apt-get install unrar
       if: ${{ runner.os == 'Linux' }}
diff --git a/tests/datasets/test_idtrees.py b/tests/datasets/test_idtrees.py
--- a/tests/datasets/test_idtrees.py
+++ b/tests/datasets/test_idtrees.py
@@ -5,7 +5,6 @@
 import glob
 import os
 import shutil
-import sys
 from pathlib import Path
 from typing import Any
 
@@ -52,7 +51,7 @@ def dataset(
         transforms = nn.Identity()
         return IDTReeS(root, split, task, transforms, download=True, checksum=True)
 
-    @pytest.fixture(params=["pandas", "laspy", "open3d"])
+    @pytest.fixture(params=["pandas", "laspy", "pyvista"])
     def mock_missing_module(self, monkeypatch: MonkeyPatch, request: SubRequest) -> str:
         import_orig = builtins.__import__
         package = str(request.param)
@@ -117,7 +116,7 @@ def test_mock_missing_module(
                 match=f"{package} is not installed and is required to use this dataset",
             ):
                 IDTReeS(dataset.root, dataset.split, dataset.task)
-        elif package in ["open3d"]:
+        elif package in ["pyvista"]:
             with pytest.raises(
                 ImportError,
                 match=f"{package} is not installed and is required to plot point cloud",
@@ -140,15 +139,9 @@ def test_plot(self, dataset: IDTReeS) -> None:
             dataset.plot(x, show_titles=False)
             plt.close()
 
-    @pytest.mark.skipif(
-        sys.platform in ["darwin", "win32"],
-        reason="segmentation fault on macOS and windows",
-    )
     def test_plot_las(self, dataset: IDTReeS) -> None:
-        pytest.importorskip("open3d", minversion="0.11.2")
-        vis = dataset.plot_las(index=0, colormap="BrBG")
-        vis.close()
-        vis = dataset.plot_las(index=0, colormap=None)
-        vis.close()
-        vis = dataset.plot_las(index=1, colormap=None)
-        vis.close()
+        pyvista = pytest.importorskip("pyvista", minversion="0.35.1")
+
+        # Test point cloud without colors
+        point_cloud = dataset.plot_las(index=0)
+        pyvista.plot(point_cloud, scalars=point_cloud.points, cpos="yz", cmap="viridis")