Repository: deepchecks/deepchecks

Issue:
[FEAT][CV] Enable to create VisionData object from dataset, not only dataloader
It's possible to create a VisionData object from a dataset, and build the dataloader internally (assuming it's default)


Discussion:

Resolving patch:
diff --git a/deepchecks/vision/vision_data.py b/deepchecks/vision/vision_data.py
--- a/deepchecks/vision/vision_data.py
+++ b/deepchecks/vision/vision_data.py
@@ -15,12 +15,12 @@
 from abc import abstractmethod
 from collections import defaultdict
 from copy import copy
-from typing import (Any, Dict, Iterator, List, Optional, Sequence, TypeVar,
-                    Union)
+from typing import (Any, Callable, Dict, Iterator, List, Optional, Sequence,
+                    Type, TypeVar, Union)
 
 import numpy as np
 import torch
-from torch.utils.data import BatchSampler, DataLoader, Sampler
+from torch.utils.data import BatchSampler, DataLoader, Dataset, Sampler
 
 from deepchecks.core.errors import (DeepchecksBaseError,
                                     DeepchecksNotImplementedError,
@@ -30,11 +30,12 @@
 from deepchecks.vision.utils.image_functions import ImageInfo
 from deepchecks.vision.utils.transformations import get_transforms_handler
 
+__all__ = ['VisionData']
+
+
 logger = logging.getLogger('deepchecks')
 VD = TypeVar('VD', bound='VisionData')
 
-__all__ = ['VisionData']
-
 
 class VisionData:
     """VisionData represent a base task in deepchecks. It wraps PyTorch DataLoader together with model related metadata.
@@ -54,12 +55,13 @@ class VisionData:
         Name of transforms field in the dataset which holds transformations of both data and label.
     """
 
-    def __init__(self,
-                 data_loader: DataLoader,
-                 num_classes: Optional[int] = None,
-                 label_map: Optional[Dict[int, str]] = None,
-                 transform_field: Optional[str] = 'transforms'):
-
+    def __init__(
+        self,
+        data_loader: DataLoader,
+        num_classes: Optional[int] = None,
+        label_map: Optional[Dict[int, str]] = None,
+        transform_field: Optional[str] = 'transforms'
+    ):
         # Create data loader that uses IndicesSequentialSampler, which always return batches in the same order
         self._data_loader, self._sampler = self._get_data_loader_sequential(data_loader)
 
@@ -111,6 +113,66 @@ def __init__(self,
         self._classes_indices = None
         self._current_index = None
 
+    @classmethod
+    def from_dataset(
+        cls: Type[VD],
+        data: Dataset,
+        batch_size: int = 64,
+        shuffle: bool = True,
+        num_workers: int = 0,
+        pin_memory: bool = True,
+        collate_fn: Optional[Callable] = None,
+        num_classes: Optional[int] = None,
+        label_map: Optional[Dict[int, str]] = None,
+        transform_field: Optional[str] = 'transforms'
+    ) -> VD:
+        """Create VisionData instance from a Dataset instance.
+
+        Parameters
+        ----------
+        data : Dataset
+            instance of a Dataset.
+        batch_size: int, default 64
+            how many samples per batch to load.
+        shuffle : bool, default True:
+            set to ``True`` to have the data reshuffled at every epoch.
+        num_workers  int, default 0:
+            how many subprocesses to use for data loading.
+            ``0`` means that the data will be loaded in the main process.
+        pin_memory bool, default True
+            If ``True``, the data loader will copy Tensors into CUDA pinned memory
+            before returning them.
+        collate_fn : Optional[Callable]
+            merges a list of samples to form a mini-batch of Tensor(s).
+        num_classes : Optional[int], default None
+            Number of classes in the dataset. If not provided, will be inferred from the dataset.
+        label_map : Optional[Dict[int, str]], default None
+            A dictionary mapping class ids to their names.
+        transform_field : Optional[str], default: 'transforms'
+            Name of transforms field in the dataset which holds transformations of both data and label.
+
+        Returns
+        -------
+        VisionData
+        """
+        def batch_collate(batch):
+            imgs, labels = zip(*batch)
+            return list(imgs), list(labels)
+
+        return cls(
+            data_loader=DataLoader(
+                dataset=data,
+                batch_size=batch_size,
+                shuffle=shuffle,
+                num_workers=num_workers,
+                pin_memory=pin_memory,
+                collate_fn=collate_fn or batch_collate
+            ),
+            num_classes=num_classes,
+            label_map=label_map,
+            transform_field=transform_field
+        )
+
     @abstractmethod
     def get_classes(self, batch_labels: Union[List[torch.Tensor], torch.Tensor]) -> List[List[int]]:
         """Get a labels batch and return classes inside it."""


Test patch:
diff --git a/tests/base/to_wandb_test.py b/tests/base/to_wandb_test.py
--- a/tests/base/to_wandb_test.py
+++ b/tests/base/to_wandb_test.py
@@ -9,9 +9,9 @@
 # ----------------------------------------------------------------------------
 #
 """to wandb tests"""
+import wandb
 from hamcrest import assert_that, equal_to, not_none
 
-import wandb
 from deepchecks.tabular.suites import full_suite
 
 wandb.setup(wandb.Settings(mode="disabled", program=__name__, program_relpath=__name__, disable_code=True))
diff --git a/tests/vision/base/test_vision_data.py b/tests/vision/base/test_vision_data.py
--- a/tests/vision/base/test_vision_data.py
+++ b/tests/vision/base/test_vision_data.py
@@ -416,3 +416,11 @@ def infer_on_batch(self, batch, model, device):
                 raises(ValidationError,
                        'Check requires detection predictions to be a list of 2D tensors, when '
                        'each row has 6 columns: \[x, y, width, height, class_probability, class_id\]'))
+
+
+def test_vision_data_initialization_from_dataset_instance(coco_train_dataloader: DataLoader):
+    visiondata = VisionData.from_dataset(data=coco_train_dataloader.dataset)
+    assert_that(visiondata.data_loader, instance_of(DataLoader))
+    assert_that(len(coco_train_dataloader.dataset) == len(visiondata.data_loader.dataset))
+    assert_that(len(list(visiondata)) == 1)
+    assert_that(visiondata.num_samples == len(visiondata.data_loader.dataset))
diff --git a/tests/vision/checks/performance/robustness_report_test.py b/tests/vision/checks/performance/robustness_report_test.py
--- a/tests/vision/checks/performance/robustness_report_test.py
+++ b/tests/vision/checks/performance/robustness_report_test.py
@@ -12,10 +12,10 @@
 
 import albumentations
 import numpy as np
-from hamcrest import (assert_that, calling, close_to, has_entries, has_items, has_length,
-                      raises)
-from ignite.metrics import Precision
 import torchvision.transforms as T
+from hamcrest import (assert_that, calling, close_to, has_entries, has_items,
+                      has_length, raises)
+from ignite.metrics import Precision
 from PIL import Image
 
 from deepchecks.core.errors import DeepchecksValueError
diff --git a/tests/vision/docs/test_plots.py b/tests/vision/docs/test_plots.py
--- a/tests/vision/docs/test_plots.py
+++ b/tests/vision/docs/test_plots.py
@@ -12,7 +12,6 @@
 from runpy import run_path
 
 import torch
-
 import wandb
 
 # Since we have a plot that's calling `wandb.login` we need to setup first
diff --git a/tests/vision/vision_conftest.py b/tests/vision/vision_conftest.py
--- a/tests/vision/vision_conftest.py
+++ b/tests/vision/vision_conftest.py
@@ -14,21 +14,23 @@
 import numpy as np
 import pytest
 import torch
+import torchvision.transforms as T
 from PIL import Image
 from torch.utils.data import DataLoader, Dataset
 from torch.utils.data.dataloader import default_collate
-import torchvision.transforms as T
 from torchvision import datasets
 
 from deepchecks.core import DatasetKind
 from deepchecks.vision import Batch, Context, VisionData
-from deepchecks.vision.datasets.classification.mnist import MODULE_DIR as mnist_dir
+from deepchecks.vision.datasets.classification.mnist import \
+    MODULE_DIR as mnist_dir
 from deepchecks.vision.datasets.classification.mnist import MNISTData
 from deepchecks.vision.datasets.classification.mnist import \
     load_dataset as load_mnist_dataset
 from deepchecks.vision.datasets.classification.mnist import \
     load_model as load_mnist_net_model
-from deepchecks.vision.datasets.detection.coco import DATA_DIR as coco_root, LABEL_MAP as coco_labels
+from deepchecks.vision.datasets.detection.coco import DATA_DIR as coco_root
+from deepchecks.vision.datasets.detection.coco import LABEL_MAP as coco_labels
 from deepchecks.vision.datasets.detection.coco import COCOData, CocoDataset
 from deepchecks.vision.datasets.detection.coco import \
     load_dataset as load_coco_dataset