Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Detection task type #732

Merged
merged 29 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
64008c3
add basic support for detection task
djdameln Nov 24, 2022
bcd03d4
use enum for task type
djdameln Nov 24, 2022
ca427fb
formatting
djdameln Nov 25, 2022
69b83b4
small bugfix
djdameln Nov 25, 2022
30c4368
add unit tests for bounding box conversion
djdameln Nov 25, 2022
3c0cfec
update error message
djdameln Nov 28, 2022
037c1e5
use as_tensor
djdameln Nov 28, 2022
abea835
typing and docstring
djdameln Nov 28, 2022
c060333
explicit keyword arguments
djdameln Nov 28, 2022
bf573d1
simplify bbox handling in video dataset
djdameln Nov 28, 2022
b7f1b66
docstring consistency
djdameln Nov 28, 2022
7f60ea2
add missing licenses
djdameln Nov 28, 2022
eb87358
add whitespace for readability
djdameln Nov 28, 2022
4c3a6b1
add missing license
djdameln Nov 28, 2022
cec6138
Update anomalib/data/utils/boxes.py
djdameln Nov 28, 2022
d13ce5b
Revert "Update anomalib/data/utils/boxes.py"
djdameln Nov 28, 2022
0e0dc80
add test case for custom collate function
djdameln Nov 28, 2022
5ead1ad
docstring
djdameln Nov 28, 2022
44812d6
add integration tests for detection dataloading
djdameln Nov 29, 2022
d9304aa
extend and clean up datamodules tests
djdameln Nov 29, 2022
caf0867
add detection task type to visualizer tests
djdameln Nov 29, 2022
67312fc
Merge branch 'feature/datamodules' into da/detection-task-type
djdameln Nov 29, 2022
d63a7b7
only show pred_boxes during inference
djdameln Nov 30, 2022
7ec5fa4
add detection support for torch inference
djdameln Nov 30, 2022
d74bf41
add detection support for openvino inference
djdameln Nov 30, 2022
39cf0ac
test inference for all task types
djdameln Dec 1, 2022
f3d00d8
pylint
djdameln Dec 1, 2022
9962e8c
merge latest changes
djdameln Dec 5, 2022
5a055f2
merge feature branch
djdameln Dec 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions anomalib/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .folder import Folder
from .inference import InferenceDataset
from .mvtec import MVTec
from .task_type import TaskType
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this be used by models and other components as well? If so, would it be an idea to move it to a place that would be accessible by other components?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm not super happy with the location of TaskType but couldn't think of a better place. Maybe somewhere in anomalib/config would work? @ashwinvaidya17 any ideas?

from .ucsd_ped import UCSDped

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -126,4 +127,5 @@ def get_datamodule(config: Union[DictConfig, ListConfig]) -> AnomalibDataModule:
"MVTec",
"Avenue",
"UCSDped",
"TaskType",
]
9 changes: 5 additions & 4 deletions anomalib/data/avenue.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from torch import Tensor

from anomalib.data.base import AnomalibDataModule, VideoAnomalibDataset
from anomalib.data.task_type import TaskType
from anomalib.data.utils import DownloadProgressBar, Split, ValSplitMode, hash_check
from anomalib.data.utils.video import ClipsIndexer
from anomalib.pre_processing import PreProcessor
Expand Down Expand Up @@ -124,7 +125,7 @@ class AvenueDataset(VideoAnomalibDataset):
"""Avenue Dataset class.

Args:
task (str): Task type, either 'classification' or 'segmentation'
task (TaskType): Task type, 'classification', 'detection' or 'segmentation'
root (str): Path to the root of the dataset
gt_dir (str): Path to the ground truth files
pre_process (PreProcessor): Pre-processor object
Expand All @@ -135,7 +136,7 @@ class AvenueDataset(VideoAnomalibDataset):

def __init__(
self,
task: str,
task: TaskType,
root: Union[Path, str],
gt_dir: str,
pre_process: PreProcessor,
Expand Down Expand Up @@ -163,7 +164,7 @@ class Avenue(AnomalibDataModule):
gt_dir (str): Path to the ground truth files
clip_length_in_frames (int, optional): Number of video frames in each clip.
frames_between_clips (int, optional): Number of frames between each consecutive video clip.
task (str): Task type, either 'classification' or 'segmentation'
task TaskType): Task type, 'classification', 'detection' or 'segmentation'
image_size (Optional[Union[int, Tuple[int, int]]], optional): Size of the input image.
Defaults to None.
train_batch_size (int, optional): Training batch size. Defaults to 32.
Expand All @@ -184,7 +185,7 @@ def __init__(
gt_dir: str,
clip_length_in_frames: int = 1,
frames_between_clips: int = 1,
task: str = "segmentation",
task: TaskType = TaskType.SEGMENTATION,
image_size: Optional[Union[int, Tuple[int, int]]] = None,
train_batch_size: int = 32,
eval_batch_size: int = 32,
Expand Down
32 changes: 29 additions & 3 deletions anomalib/data/base/datamodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,28 @@
from pandas import DataFrame
from pytorch_lightning import LightningDataModule
from pytorch_lightning.utilities.types import EVAL_DATALOADERS, TRAIN_DATALOADERS
from torch.utils.data import DataLoader
from torch.utils.data import DataLoader, default_collate

from anomalib.data.base.dataset import AnomalibDataset
from anomalib.data.utils import ValSplitMode, random_split

logger = logging.getLogger(__name__)


def collate_fn(batch):
"""Custom collate function that collates bounding boxes as lists."""
elem = batch[0]
djdameln marked this conversation as resolved.
Show resolved Hide resolved
out_dict = {}
if isinstance(elem, dict):
if "boxes" in elem.keys():
# collate boxes as list
out_dict["boxes"] = [item.pop("boxes") for item in batch]
# collate other data normally
out_dict.update({key: default_collate([item[key] for item in batch]) for key in elem})
return out_dict
return default_collate(batch)


class AnomalibDataModule(LightningDataModule, ABC):
"""Base Anomalib data module.

Expand Down Expand Up @@ -101,8 +115,20 @@ def train_dataloader(self) -> TRAIN_DATALOADERS:

def val_dataloader(self) -> EVAL_DATALOADERS:
"""Get validation dataloader."""
return DataLoader(self.val_data, shuffle=False, batch_size=self.eval_batch_size, num_workers=self.num_workers)
return DataLoader(
self.val_data,
shuffle=False,
batch_size=self.eval_batch_size,
num_workers=self.num_workers,
collate_fn=collate_fn,
)
djdameln marked this conversation as resolved.
Show resolved Hide resolved

def test_dataloader(self) -> EVAL_DATALOADERS:
"""Get test dataloader."""
return DataLoader(self.test_data, shuffle=False, batch_size=self.eval_batch_size, num_workers=self.num_workers)
return DataLoader(
self.test_data,
shuffle=False,
batch_size=self.eval_batch_size,
num_workers=self.num_workers,
collate_fn=collate_fn,
)
djdameln marked this conversation as resolved.
Show resolved Hide resolved
22 changes: 14 additions & 8 deletions anomalib/data/base/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@
from torch import Tensor
from torch.utils.data import Dataset

from anomalib.data.utils import read_image
from anomalib.data.task_type import TaskType
from anomalib.data.utils import masks_to_boxes, read_image
from anomalib.pre_processing import PreProcessor

_EXPECTED_COLS_CLASSIFICATION = ["image_path", "split"]
_EXPECTED_COLS_SEGMENTATION = _EXPECTED_COLS_CLASSIFICATION + ["mask_path"]
_EXPECTED_COLS_PERTASK = {
"classification": _EXPECTED_COLS_CLASSIFICATION,
"segmentation": _EXPECTED_COLS_SEGMENTATION,
"detection": _EXPECTED_COLS_SEGMENTATION,
}

logger = logging.getLogger(__name__)
Expand All @@ -34,7 +36,7 @@
class AnomalibDataset(Dataset, ABC):
"""Anomalib dataset."""

def __init__(self, task: str, pre_process: PreProcessor):
def __init__(self, task: TaskType, pre_process: PreProcessor):
super().__init__()
self.task = task
self.pre_process = pre_process
Expand Down Expand Up @@ -107,16 +109,16 @@ def __getitem__(self, index: int) -> Dict[str, Union[str, Tensor]]:
"""

image_path = self._samples.iloc[index].image_path
image = read_image(image_path)
mask_path = self._samples.iloc[index].mask_path
label_index = self._samples.iloc[index].label_index

image = read_image(image_path)
item = dict(image_path=image_path, label=label_index)

if self.task == "classification":
if self.task == TaskType.CLASSIFICATION:
pre_processed = self.pre_process(image=image)
elif self.task == "segmentation":
mask_path = self._samples.iloc[index].mask_path

item["image"] = pre_processed["image"]
elif self.task in [TaskType.DETECTION, TaskType.SEGMENTATION]:
# Only Anomalous (1) images have masks in anomaly datasets
# Therefore, create empty mask for Normal (0) images.
if label_index == 0:
Expand All @@ -126,11 +128,15 @@ def __getitem__(self, index: int) -> Dict[str, Union[str, Tensor]]:

pre_processed = self.pre_process(image=image, mask=mask)

item["image"] = pre_processed["image"]
item["mask_path"] = mask_path
item["mask"] = pre_processed["mask"]

if self.task == TaskType.DETECTION:
# create boxes from masks for detection task
item["boxes"] = masks_to_boxes(item["mask"])[0]
else:
raise ValueError(f"Unknown task type: {self.task}")
item["image"] = pre_processed["image"]

return item

Expand Down
14 changes: 12 additions & 2 deletions anomalib/data/base/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from torch import Tensor

from anomalib.data.base.dataset import AnomalibDataset
from anomalib.data.task_type import TaskType
from anomalib.data.utils import masks_to_boxes
from anomalib.data.utils.video import ClipsIndexer
from anomalib.pre_processing import PreProcessor

Expand All @@ -21,7 +23,9 @@ class VideoAnomalibDataset(AnomalibDataset, ABC):
frames_between_clips (int): Number of frames between each consecutive video clip.
"""

def __init__(self, task: str, pre_process: PreProcessor, clip_length_in_frames: int, frames_between_clips: int):
def __init__(
self, task: TaskType, pre_process: PreProcessor, clip_length_in_frames: int, frames_between_clips: int
):
super().__init__(task, pre_process)

self.clip_length_in_frames = clip_length_in_frames
Expand Down Expand Up @@ -74,9 +78,15 @@ def __getitem__(self, index: int) -> Dict[str, Union[str, Tensor]]:
self.pre_process(image=frame.numpy(), mask=mask) for frame, mask in zip(item["image"], item["mask"])
]
item["image"] = torch.stack([item["image"] for item in processed_frames]).squeeze(0)
mask = item["mask"]
mask = Tensor(item["mask"])
djdameln marked this conversation as resolved.
Show resolved Hide resolved
item["mask"] = torch.stack([item["mask"] for item in processed_frames]).squeeze(0)
item["label"] = Tensor([1 in frame for frame in mask]).int().squeeze(0)
if self.task == TaskType.DETECTION:
item["boxes"] = [
torch.empty((0, 4)) if frame.max() == 0 else masks_to_boxes(frame)
for frame in item["mask"].view((-1, 1) + item["mask"].shape[-2:])
]
djdameln marked this conversation as resolved.
Show resolved Hide resolved
item["boxes"] = item["boxes"][0] if len(item["boxes"]) == 1 else item["boxes"]
else:
item["image"] = torch.stack(
[self.pre_process(image=frame.numpy())["image"] for frame in item["image"]]
Expand Down
9 changes: 5 additions & 4 deletions anomalib/data/btech.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from tqdm import tqdm

from anomalib.data.base import AnomalibDataModule, AnomalibDataset
from anomalib.data.task_type import TaskType
from anomalib.data.utils import DownloadProgressBar, Split, ValSplitMode, hash_check
from anomalib.pre_processing import PreProcessor

Expand Down Expand Up @@ -114,7 +115,7 @@ def __init__(
category: str,
pre_process: PreProcessor,
split: Optional[Union[Split, str]] = None,
task: str = "segmentation",
task: TaskType = TaskType.SEGMENTATION,
) -> None:
"""Btech Dataset class.

Expand All @@ -123,7 +124,7 @@ def __init__(
category: Name of the BTech category.
pre_process: List of pre_processing object containing albumentation compose.
split: 'train', 'val' or 'test'
task: ``classification`` or ``segmentation``
task: ``classification``, ``detection`` or ``segmentation``
create_validation_set: Create a validation subset in addition to the train and test subsets

Examples:
Expand Down Expand Up @@ -177,7 +178,7 @@ def __init__(
train_batch_size: int = 32,
eval_batch_size: int = 32,
num_workers: int = 8,
task: str = "segmentation",
task: TaskType = TaskType.SEGMENTATION,
transform_config_train: Optional[Union[str, A.Compose]] = None,
transform_config_eval: Optional[Union[str, A.Compose]] = None,
val_split_mode: ValSplitMode = ValSplitMode.SAME_AS_TEST,
Expand All @@ -192,7 +193,7 @@ def __init__(
train_batch_size: Training batch size.
test_batch_size: Testing batch size.
num_workers: Number of workers.
task: ``classification`` or ``segmentation``
task: ``classification``, ``detection`` or ``segmentation``
transform_config_train: Config for pre-processing during training.
transform_config_val: Config for pre-processing during validation.
create_validation_set: Create a validation subset in addition to the train and test subsets
Expand Down
11 changes: 6 additions & 5 deletions anomalib/data/folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from torchvision.datasets.folder import IMG_EXTENSIONS

from anomalib.data.base import AnomalibDataModule, AnomalibDataset
from anomalib.data.task_type import TaskType
from anomalib.data.utils import Split, ValSplitMode, random_split
from anomalib.pre_processing.pre_process import PreProcessor

Expand Down Expand Up @@ -141,7 +142,7 @@ class FolderDataset(AnomalibDataset):
"""Folder dataset.

Args:
task (str): Task type. (classification or segmentation).
task (TaskType): Task type. (classification, detection or segmentation).
djdameln marked this conversation as resolved.
Show resolved Hide resolved
pre_process (PreProcessor): Image Pre-processor to apply transform.
split (Optional[Union[Split, str]]): Fixed subset split that follows from folder structure on file system.
Choose from [Split.FULL, Split.TRAIN, Split.TEST]
Expand All @@ -165,7 +166,7 @@ class FolderDataset(AnomalibDataset):

def __init__(
self,
task: str,
task: TaskType,
pre_process: PreProcessor,
root: Union[str, Path],
normal_dir: Union[str, Path],
Expand Down Expand Up @@ -222,8 +223,8 @@ class Folder(AnomalibDataModule):
train_batch_size (int, optional): Training batch size. Defaults to 32.
test_batch_size (int, optional): Test batch size. Defaults to 32.
num_workers (int, optional): Number of workers. Defaults to 8.
task (str, optional): Task type. Could be either classification or segmentation.
Defaults to "classification".
task (TaskType, optional): Task type. Could be classification, detection or segmentation.
Defaults to segmentation.
djdameln marked this conversation as resolved.
Show resolved Hide resolved
transform_config_train (Optional[Union[str, A.Compose]], optional): Config for pre-processing
during training.
Defaults to None.
Expand All @@ -248,7 +249,7 @@ def __init__(
train_batch_size: int = 32,
eval_batch_size: int = 32,
num_workers: int = 8,
task: str = "segmentation",
task: TaskType = TaskType.SEGMENTATION,
transform_config_train: Optional[Union[str, A.Compose]] = None,
transform_config_eval: Optional[Union[str, A.Compose]] = None,
val_split_mode: ValSplitMode = ValSplitMode.FROM_TEST,
Expand Down
7 changes: 4 additions & 3 deletions anomalib/data/mvtec.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from pandas import DataFrame

from anomalib.data.base import AnomalibDataModule, AnomalibDataset
from anomalib.data.task_type import TaskType
from anomalib.data.utils import DownloadProgressBar, Split, ValSplitMode, hash_check
from anomalib.pre_processing import PreProcessor

Expand Down Expand Up @@ -123,7 +124,7 @@ class MVTecDataset(AnomalibDataset):
"""MVTec dataset class.

Args:
task (str): Task type, either 'classification' or 'segmentation'
task (TaskType): Task type,'classification', 'detection' or 'segmentation'
djdameln marked this conversation as resolved.
Show resolved Hide resolved
pre_process (PreProcessor): Pre-processor object
split (Optional[Union[Split, str]]): Split of the dataset, usually Split.TRAIN or Split.TEST
root (str): Path to the root of the dataset
Expand All @@ -132,7 +133,7 @@ class MVTecDataset(AnomalibDataset):

def __init__(
self,
task: str,
task: TaskType,
pre_process: PreProcessor,
root: str,
category: str,
Expand All @@ -158,7 +159,7 @@ def __init__(
train_batch_size: int = 32,
eval_batch_size: int = 32,
num_workers: int = 8,
task: str = "segmentation",
task: TaskType = TaskType.SEGMENTATION,
transform_config_train: Optional[Union[str, A.Compose]] = None,
transform_config_eval: Optional[Union[str, A.Compose]] = None,
val_split_mode: ValSplitMode = ValSplitMode.SAME_AS_TEST,
Expand Down
11 changes: 11 additions & 0 deletions anomalib/data/task_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Task type enum."""

djdameln marked this conversation as resolved.
Show resolved Hide resolved
from enum import Enum


class TaskType(str, Enum):
"""Task type used when generating predictions on the dataset."""

CLASSIFICATION = "classification"
DETECTION = "detection"
SEGMENTATION = "segmentation"
9 changes: 5 additions & 4 deletions anomalib/data/ucsd_ped.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from anomalib.data.base import AnomalibDataModule
from anomalib.data.base.video import VideoAnomalibDataset
from anomalib.data.task_type import TaskType
from anomalib.data.utils import (
DownloadProgressBar,
Split,
Expand Down Expand Up @@ -138,7 +139,7 @@ class UCSDpedDataset(VideoAnomalibDataset):
"""UCSDped Dataset class.

Args:
task (str): Task type, either 'classification' or 'segmentation'
task (TaskType): Task type, 'classification', 'detection' or 'segmentation'
root (str): Path to the root of the dataset
category (str): Sub-category of the dataset, e.g. 'bottle'
pre_process (PreProcessor): Pre-processor object
Expand All @@ -149,7 +150,7 @@ class UCSDpedDataset(VideoAnomalibDataset):

def __init__(
self,
task: str,
task: TaskType,
root: Union[Path, str],
category: str,
pre_process: PreProcessor,
Expand All @@ -176,7 +177,7 @@ class UCSDped(AnomalibDataModule):
category (str): Sub-category of the dataset, e.g. 'bottle'
clip_length_in_frames (int, optional): Number of video frames in each clip.
frames_between_clips (int, optional): Number of frames between each consecutive video clip.
task (str): Task type, either 'classification' or 'segmentation'
task (TaskType): Task type, 'classification', 'detection' or 'segmentation'
image_size (Optional[Union[int, Tuple[int, int]]], optional): Size of the input image.
Defaults to None.
train_batch_size (int, optional): Training batch size. Defaults to 32.
Expand All @@ -197,7 +198,7 @@ def __init__(
category: str,
clip_length_in_frames: int = 1,
frames_between_clips: int = 1,
task: str = "segmentation",
task: TaskType = TaskType.SEGMENTATION,
image_size: Optional[Union[int, Tuple[int, int]]] = None,
train_batch_size: int = 32,
eval_batch_size: int = 32,
Expand Down
Loading