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

feat: Add ImageDataset and Layer for ConvolutionalNeuralNetworks #645

Merged
merged 49 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
bc33cc0
feat: added `ImageDataset`
Marsmaennchen221 Apr 19, 2024
1dca8f0
Merge branch 'main' of https://github.com/Safe-DS/Stdlib into 579-add…
Marsmaennchen221 Apr 19, 2024
312feec
feat: added `Convolutional2DLayer`, `FlattenLayer`, `MaxPooling2DLaye…
Marsmaennchen221 Apr 23, 2024
7b97fb7
Merge branch 'main' of https://github.com/Safe-DS/Stdlib into 579-add…
Marsmaennchen221 Apr 23, 2024
7ae5b56
test: fixed one test
Marsmaennchen221 Apr 23, 2024
83a008e
test: added tests for `ImageSize` and `Image.size`
Marsmaennchen221 Apr 23, 2024
cf7bfa4
test: added tests for `ImageList.sizes`
Marsmaennchen221 Apr 24, 2024
a40ed0e
feat: changed `ImageDataset` to have generic output type
Marsmaennchen221 Apr 24, 2024
9137eac
test: corrected test in `ImageList`
Marsmaennchen221 Apr 24, 2024
ac452e4
test: corrected cnn workflow test to be os independent
Marsmaennchen221 Apr 24, 2024
3350ba6
Merge branch 'main' of https://github.com/Safe-DS/Stdlib into 579-add…
Marsmaennchen221 Apr 24, 2024
c3bcc20
feat: added `ConvolutionalTranspose2DLayer`
Marsmaennchen221 Apr 24, 2024
8312d99
test: made `TestImageToColumn.test_should_train_and_predict_model` os…
Marsmaennchen221 Apr 24, 2024
031677a
feat: added `Image.__array__` to convert a `Image` to a `numpy.ndarray`
Marsmaennchen221 Apr 28, 2024
c567b55
feat: added checks and errors for invalid CNNs
Marsmaennchen221 Apr 29, 2024
137d658
feat: added equals check to `OneHotEncoder`
Marsmaennchen221 Apr 29, 2024
732b1ce
test: added tests for `Convolutional2DLayer`, `ConvolutionalTranspose…
Marsmaennchen221 Apr 29, 2024
cf497a4
test: added tests for `OneHotEncoder.__eq__`
Marsmaennchen221 Apr 29, 2024
4d07c6a
refactor: ruff linter
Marsmaennchen221 Apr 29, 2024
99e0d54
refactor: mypy linter
Marsmaennchen221 Apr 30, 2024
75cce0a
Merge branch 'main' of https://github.com/Safe-DS/Stdlib into 579-add…
Marsmaennchen221 May 1, 2024
a43ecda
refactor: finish merge
Marsmaennchen221 May 1, 2024
e4e5239
refactor: linter
Marsmaennchen221 May 1, 2024
d87bc92
refactor: linter
Marsmaennchen221 May 1, 2024
5bdce23
refactor: linter
Marsmaennchen221 May 1, 2024
032f58f
refactor: mypy linter
Marsmaennchen221 May 1, 2024
4b30a58
refactor: mypy linter
Marsmaennchen221 May 1, 2024
33e8db8
refactor: mypy linter
Marsmaennchen221 May 1, 2024
19f1f24
refactor: mypy linter
Marsmaennchen221 May 1, 2024
5503312
refactor: mypy linter
Marsmaennchen221 May 1, 2024
aedf2be
refactor: mypy linter
Marsmaennchen221 May 1, 2024
2953f36
refactor: mypy linter
Marsmaennchen221 May 1, 2024
0974807
refactor: mypy linter
Marsmaennchen221 May 1, 2024
ff407cd
style: apply automated linter fixes
megalinter-bot May 1, 2024
8b50d3b
refactor: ruff linter
Marsmaennchen221 May 1, 2024
8bd40b1
style: apply automated linter fixes
megalinter-bot May 1, 2024
a1e7415
refactor: codecov
Marsmaennchen221 May 1, 2024
c717cdc
Merge branch '579-add-a-new-imagedataset-class' of https://github.com…
Marsmaennchen221 May 1, 2024
5cf56cd
Merge branch 'main' of https://github.com/Safe-DS/Stdlib into 579-add…
Marsmaennchen221 May 2, 2024
a3b9336
refactor: finish merge
Marsmaennchen221 May 3, 2024
de465aa
feat: added and improved various `__hash__`, `__sizeof__` and `__eq__…
Marsmaennchen221 May 3, 2024
3c800ea
refactor: mypy
Marsmaennchen221 May 3, 2024
c8437bb
test: added missing tests
Marsmaennchen221 May 3, 2024
3204c26
style: apply automated linter fixes
megalinter-bot May 3, 2024
b8cfd66
style: apply automated linter fixes
megalinter-bot May 3, 2024
16871cf
feat: disabled warning from `OneHotEncoder` in `ImageDataset`
Marsmaennchen221 May 6, 2024
06e1af9
Merge branch 'main' of https://github.com/Safe-DS/Stdlib into 579-add…
Marsmaennchen221 May 6, 2024
0397251
refactor: completed merge
Marsmaennchen221 May 6, 2024
701091b
style: apply automated linter fixes
megalinter-bot May 6, 2024
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
11 changes: 8 additions & 3 deletions src/safeds/data/image/containers/_empty_image_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
from typing import TYPE_CHECKING, Self

from safeds._utils import _structural_hash
from safeds.data.image.containers._image_list import ImageList
from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList
from safeds.data.image.utils._image_transformation_error_and_warning_checks import (
from safeds.data.image._utils._image_transformation_error_and_warning_checks import (
_check_add_noise_errors,
_check_adjust_brightness_errors_and_warnings,
_check_adjust_color_balance_errors_and_warnings,
Expand All @@ -17,6 +15,8 @@
_check_resize_errors,
_check_sharpen_errors_and_warnings,
)
from safeds.data.image.containers._image_list import ImageList
from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList
from safeds.exceptions import IndexOutOfBoundsError

if TYPE_CHECKING:
Expand All @@ -25,6 +25,7 @@
from torch import Tensor

from safeds.data.image.containers import Image
from safeds.data.image.typing import ImageSize


class _EmptyImageList(ImageList):
Expand Down Expand Up @@ -91,6 +92,10 @@ def heights(self) -> list[int]:
def channel(self) -> int:
return NotImplemented

@property
def sizes(self) -> list[ImageSize]:
return []

@property
def number_of_sizes(self) -> int:
return 0
Expand Down
37 changes: 35 additions & 2 deletions src/safeds/data/image/containers/_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from safeds._config import _get_device
from safeds._utils import _structural_hash
from safeds.data.image.utils._image_transformation_error_and_warning_checks import (
from safeds.data.image._utils._image_transformation_error_and_warning_checks import (
_check_add_noise_errors,
_check_adjust_brightness_errors_and_warnings,
_check_adjust_color_balance_errors_and_warnings,
Expand All @@ -18,9 +18,11 @@
_check_resize_errors,
_check_sharpen_errors_and_warnings,
)
from safeds.data.image.typing import ImageSize
from safeds.exceptions import IllegalFormatError

if TYPE_CHECKING:
from numpy import dtype, ndarray
from torch import Tensor
from torch.types import Device

Expand Down Expand Up @@ -137,7 +139,7 @@ def __eq__(self, other: object) -> bool:

if not isinstance(other, Image):
return NotImplemented
return (
return (self is other) or (
self._image_tensor.size() == other._image_tensor.size()
and torch.all(torch.eq(self._image_tensor, other._set_device(self.device)._image_tensor)).item()
)
Expand All @@ -164,6 +166,25 @@ def __sizeof__(self) -> int:
"""
return sys.getsizeof(self._image_tensor) + self._image_tensor.element_size() * self._image_tensor.nelement()

def __array__(self, numpy_dtype: str | dtype = None) -> ndarray:
"""
Return the image as a numpy array.

Returns
-------
numpy_array:
The image as numpy array.
"""
from numpy import uint8

return (
self._image_tensor.permute(1, 2, 0)
.detach()
.cpu()
.numpy()
.astype(uint8 if numpy_dtype is None else numpy_dtype)
)

def _repr_jpeg_(self) -> bytes | None:
"""
Return a JPEG image as bytes.
Expand Down Expand Up @@ -261,6 +282,18 @@ def channel(self) -> int:
"""
return self._image_tensor.size(dim=0)

@property
def size(self) -> ImageSize:
"""
Get the `ImageSize` of the image.

Returns
-------
image_size:
The size of the image.
"""
return ImageSize(self.width, self.height, self.channel)

@property
def device(self) -> Device:
"""
Expand Down
57 changes: 52 additions & 5 deletions src/safeds/data/image/containers/_image_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os
from abc import ABCMeta, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal, overload

from safeds.data.image.containers._image import Image

Expand All @@ -16,6 +16,7 @@

from safeds.data.image.containers._multi_size_image_list import _MultiSizeImageList
from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList
from safeds.data.image.typing import ImageSize


class ImageList(metaclass=ABCMeta):
Expand Down Expand Up @@ -80,7 +81,32 @@ def from_images(images: list[Image]) -> ImageList:
return _SingleSizeImageList._create_image_list([image._image_tensor for image in images], indices)

@staticmethod
def from_files(path: str | Path | Sequence[str | Path]) -> ImageList:
@overload
def from_files(path: str | Path | Sequence[str | Path]) -> ImageList: ...

@staticmethod
@overload
def from_files(path: str | Path | Sequence[str | Path], return_filenames: Literal[False]) -> ImageList: ...

@staticmethod
@overload
def from_files(
path: str | Path | Sequence[str | Path],
return_filenames: Literal[True],
) -> tuple[ImageList, list[str]]: ...

@staticmethod
@overload
def from_files(
path: str | Path | Sequence[str | Path],
return_filenames: bool,
) -> ImageList | tuple[ImageList, list[str]]: ...

@staticmethod
def from_files(
path: str | Path | Sequence[str | Path],
return_filenames: bool = False,
) -> ImageList | tuple[ImageList, list[str]]:
"""
Create an ImageList from a directory or a list of files.

Expand All @@ -90,6 +116,8 @@ def from_files(path: str | Path | Sequence[str | Path]) -> ImageList:
----------
path:
the path to the directory or a list of files
return_filenames:
if True the output will be a tuple which contains a list of the filenames in order of the images

Returns
-------
Expand All @@ -102,7 +130,7 @@ def from_files(path: str | Path | Sequence[str | Path]) -> ImageList:
If the directory or one of the files of the path cannot be found
"""
from PIL.Image import open as pil_image_open
from torchvision.transforms.functional import pil_to_tensor
from torchvision.transforms.v2.functional import pil_to_tensor

from safeds.data.image.containers._empty_image_list import _EmptyImageList
from safeds.data.image.containers._multi_size_image_list import _MultiSizeImageList
Expand All @@ -112,6 +140,7 @@ def from_files(path: str | Path | Sequence[str | Path]) -> ImageList:
return _EmptyImageList()

image_tensors = []
file_names = []
fixed_size = True

path_list: list[str | Path]
Expand All @@ -125,6 +154,7 @@ def from_files(path: str | Path | Sequence[str | Path]) -> ImageList:
path_list += sorted([p / name for name in os.listdir(p)])
else:
image_tensors.append(pil_to_tensor(pil_image_open(p)))
file_names.append(str(p))
if fixed_size and (
image_tensors[0].size(dim=2) != image_tensors[-1].size(dim=2)
or image_tensors[0].size(dim=1) != image_tensors[-1].size(dim=1)
Expand All @@ -137,9 +167,14 @@ def from_files(path: str | Path | Sequence[str | Path]) -> ImageList:
indices = list(range(len(image_tensors)))

if fixed_size:
return _SingleSizeImageList._create_image_list(image_tensors, indices)
image_list = _SingleSizeImageList._create_image_list(image_tensors, indices)
else:
image_list = _MultiSizeImageList._create_image_list(image_tensors, indices)

if return_filenames:
return image_list, file_names
else:
return _MultiSizeImageList._create_image_list(image_tensors, indices)
return image_list

@abstractmethod
def _clone(self) -> ImageList:
Expand Down Expand Up @@ -300,6 +335,18 @@ def channel(self) -> int:
The channel of all images
"""

@property
@abstractmethod
def sizes(self) -> list[ImageSize]:
"""
Return the sizes of all images.

Returns
-------
sizes:
The sizes of all images
"""

@property
@abstractmethod
def number_of_sizes(self) -> int:
Expand Down
16 changes: 14 additions & 2 deletions src/safeds/data/image/containers/_multi_size_image_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
from typing import TYPE_CHECKING

from safeds._utils import _structural_hash
from safeds.data.image.containers import Image, ImageList
from safeds.data.image.utils._image_transformation_error_and_warning_checks import (
from safeds.data.image._utils._image_transformation_error_and_warning_checks import (
_check_blur_errors_and_warnings,
_check_remove_images_with_size_errors,
)
from safeds.data.image.containers import Image, ImageList
from safeds.exceptions import (
DuplicateIndexError,
IllegalFormatError,
Expand All @@ -23,6 +23,7 @@
from torch import Tensor

from safeds.data.image.containers._single_size_image_list import _SingleSizeImageList
from safeds.data.image.typing import ImageSize


class _MultiSizeImageList(ImageList):
Expand Down Expand Up @@ -111,6 +112,8 @@ def __eq__(self, other: object) -> bool:
return NotImplemented
if not isinstance(other, _MultiSizeImageList) or set(other._image_list_dict) != set(self._image_list_dict):
return False
if self is other:
return True
for image_list_key, image_list_value in self._image_list_dict.items():
if image_list_value != other._image_list_dict[image_list_key]:
return False
Expand Down Expand Up @@ -158,6 +161,15 @@ def heights(self) -> list[int]:
def channel(self) -> int:
return next(iter(self._image_list_dict.values())).channel

@property
def sizes(self) -> list[ImageSize]:
sizes = {}
for image_list in self._image_list_dict.values():
indices = image_list._as_single_size_image_list()._tensor_positions_to_indices
for i, index in enumerate(indices):
sizes[index] = image_list.sizes[i]
return [sizes[index] for index in sorted(sizes)]

@property
def number_of_sizes(self) -> int:
return len(self._image_list_dict)
Expand Down
58 changes: 54 additions & 4 deletions src/safeds/data/image/containers/_single_size_image_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
from typing import TYPE_CHECKING

from safeds._utils import _structural_hash
from safeds.data.image.containers._image import Image
from safeds.data.image.containers._image_list import ImageList
from safeds.data.image.utils._image_transformation_error_and_warning_checks import (
from safeds.data.image._utils._image_transformation_error_and_warning_checks import (
_check_add_noise_errors,
_check_adjust_brightness_errors_and_warnings,
_check_adjust_color_balance_errors_and_warnings,
Expand All @@ -20,6 +18,9 @@
_check_resize_errors,
_check_sharpen_errors_and_warnings,
)
from safeds.data.image.containers._image import Image
from safeds.data.image.containers._image_list import ImageList
from safeds.data.image.typing import ImageSize
from safeds.exceptions import (
DuplicateIndexError,
IllegalFormatError,
Expand Down Expand Up @@ -49,6 +50,9 @@ class _SingleSizeImageList(ImageList):
def __init__(self) -> None:
import torch

self._next_batch_index = 0
self._batch_size = 1

self._tensor: Tensor = torch.empty(0)
self._tensor_positions_to_indices: list[int] = [] # list[tensor_position] = index
self._indices_to_tensor_positions: dict[int, int] = {} # {index: tensor_position}
Expand Down Expand Up @@ -95,6 +99,46 @@ def _create_image_list(images: list[Tensor], indices: list[int]) -> ImageList:

return image_list

@staticmethod
def _create_from_tensor(images_tensor: Tensor, indices: list[int]) -> _SingleSizeImageList:
if images_tensor.dim() == 3:
images_tensor = images_tensor.unsqueeze(dim=1)
if images_tensor.dim() != 4:
raise ValueError(f"Invalid Tensor. This Tensor requires 3 or 4 dimensions but has {images_tensor.dim()}")

image_list = _SingleSizeImageList()
image_list._tensor = images_tensor.detach().clone()
image_list._tensor_positions_to_indices = indices
image_list._indices_to_tensor_positions = image_list._calc_new_indices_to_tensor_positions()

return image_list

def __iter__(self) -> _SingleSizeImageList:
im_ds = copy.copy(self)
im_ds._next_batch_index = 0
return im_ds

def __next__(self) -> Tensor:
if self._next_batch_index * self._batch_size >= len(self):
raise StopIteration
self._next_batch_index += 1
return self._get_batch(self._next_batch_index - 1)

def _get_batch(self, batch_number: int, batch_size: int | None = None) -> Tensor:
import torch

if batch_size is None:
batch_size = self._batch_size
if batch_size * batch_number >= len(self):
raise IndexOutOfBoundsError(batch_size * batch_number)
max_index = batch_size * (batch_number + 1) if batch_size * (batch_number + 1) < len(self) else len(self)
return (
self._tensor[
[self._indices_to_tensor_positions[index] for index in range(batch_size * batch_number, max_index)]
].to(torch.float32)
/ 255
)

def _clone(self) -> ImageList:
cloned_image_list = self._clone_without_tensor()
cloned_image_list._tensor = self._tensor.detach().clone()
Expand Down Expand Up @@ -135,7 +179,7 @@ def __eq__(self, other: object) -> bool:
return NotImplemented
if not isinstance(other, _SingleSizeImageList):
return False
return (
return (self is other) or (
self._tensor.size() == other._tensor.size()
and set(self._tensor_positions_to_indices) == set(self._tensor_positions_to_indices)
and set(self._indices_to_tensor_positions) == set(self._indices_to_tensor_positions)
Expand Down Expand Up @@ -183,6 +227,12 @@ def heights(self) -> list[int]:
def channel(self) -> int:
return self._tensor.size(dim=1)

@property
def sizes(self) -> list[ImageSize]:
return [
ImageSize(self._tensor.size(dim=3), self._tensor.size(dim=2), self._tensor.size(dim=1)),
] * self.number_of_images

@property
def number_of_sizes(self) -> int:
return 1
Expand Down
Loading