Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
337 changes: 61 additions & 276 deletions docs/tutorials/image_list_processing.ipynb

Large diffs are not rendered by default.

286 changes: 43 additions & 243 deletions docs/tutorials/image_processing.ipynb

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/safeds/data/image/containers/_empty_image_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@ def crop(self, x: int, y: int, width: int, height: int) -> ImageList:
_check_crop_errors(x, y, width, height)
return _EmptyImageList()

def flip_vertically(self) -> ImageList:
def flip_top_and_bottom(self) -> ImageList:
_EmptyImageList._warn_empty_image_list()
return _EmptyImageList()

def flip_horizontally(self) -> ImageList:
def flip_left_and_right(self) -> ImageList:
_EmptyImageList._warn_empty_image_list()
return _EmptyImageList()

Expand Down
8 changes: 4 additions & 4 deletions src/safeds/data/image/containers/_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,9 +502,9 @@ def crop(self, x: int, y: int, width: int, height: int) -> Image:
_check_crop_warnings(x, y, self.width, self.height, plural=False)
return Image(func2.crop(self._image_tensor, y, x, height, width))

def flip_vertically(self) -> Image:
def flip_top_and_bottom(self) -> Image:
"""
Return a new `Image` that is flipped vertically (horizontal axis, flips up-down and vice versa).
Return a new `Image` where top and bottom are flipped along a horizontal axis.

The original image is not modified.

Expand All @@ -519,9 +519,9 @@ def flip_vertically(self) -> Image:

return Image(func2.vertical_flip(self._image_tensor))

def flip_horizontally(self) -> Image:
def flip_left_and_right(self) -> Image:
"""
Return a new `Image` that is flipped horizontally (vertical axis, flips left-right and vice versa).
Return a new `Image` where left and right sides are flipped along a vertical axis.

The original image is not modified.

Expand Down
11 changes: 5 additions & 6 deletions src/safeds/data/image/containers/_image_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,7 @@ def from_files(
im = pil_image_open(filename)
im_channel = len(im.getbands())
im_size = (im.width, im.height)
if im_channel > max_channel:
max_channel = im_channel
max_channel = max(im_channel, max_channel)
if im_size not in image_sizes:
image_sizes[im_size] = {im_channel: [filename]}
image_indices[im_size] = {im_channel: [i]}
Expand Down Expand Up @@ -917,9 +916,9 @@ def crop(self, x: int, y: int, width: int, height: int) -> ImageList:
"""

@abstractmethod
def flip_vertically(self) -> ImageList:
def flip_top_and_bottom(self) -> ImageList:
"""
Return a new `ImageList` with all images flipped vertically (horizontal axis, flips up-down and vice versa).
Return a new `ImageList` where top and bottom of all images are flipped along a horizontal axis.

The original image list is not modified.

Expand All @@ -930,9 +929,9 @@ def flip_vertically(self) -> ImageList:
"""

@abstractmethod
def flip_horizontally(self) -> ImageList:
def flip_left_and_right(self) -> ImageList:
"""
Return a new `ImageList` with all images flipped horizontally (vertical axis, flips left-right and vice versa).
Return a new `ImageList` where left and right sides of all images are flipped along a vertical axis.

The original image list is not modified.

Expand Down
14 changes: 6 additions & 8 deletions src/safeds/data/image/containers/_multi_size_image_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,7 @@ def add_images(self, images: list[Image] | ImageList) -> ImageList:
+ new_indices,
)
elif isinstance(ims, _SingleSizeImageList):
if smallest_channel > ims.channel:
smallest_channel = ims.channel
smallest_channel = min(smallest_channel, ims.channel)
fixed_ims = ims
old_indices = list(fixed_ims._indices_to_tensor_positions.items())
fixed_ims._tensor_positions_to_indices = [
Expand All @@ -383,8 +382,7 @@ def add_images(self, images: list[Image] | ImageList) -> ImageList:
[im._image_tensor for im in ims],
new_indices,
)
if smallest_channel > image_list._image_list_dict[size].channel:
smallest_channel = image_list._image_list_dict[size].channel
smallest_channel = min(smallest_channel, image_list._image_list_dict[size].channel)
for i in new_indices:
image_list._indices_to_image_size_dict[i] = size
max_channel = max(max_channel, image_list._image_list_dict[size].channel)
Expand Down Expand Up @@ -557,16 +555,16 @@ def crop(self, x: int, y: int, width: int, height: int) -> ImageList:
image_list._indices_to_tensor_positions = image_list._calc_new_indices_to_tensor_positions()
return image_list

def flip_vertically(self) -> ImageList:
def flip_top_and_bottom(self) -> ImageList:
image_list = self._clone_without_image_dict()
for image_list_key, image_list_original in self._image_list_dict.items():
image_list._image_list_dict[image_list_key] = image_list_original.flip_vertically()
image_list._image_list_dict[image_list_key] = image_list_original.flip_top_and_bottom()
return image_list

def flip_horizontally(self) -> ImageList:
def flip_left_and_right(self) -> ImageList:
image_list = self._clone_without_image_dict()
for image_list_key, image_list_original in self._image_list_dict.items():
image_list._image_list_dict[image_list_key] = image_list_original.flip_horizontally()
image_list._image_list_dict[image_list_key] = image_list_original.flip_left_and_right()
return image_list

def adjust_brightness(self, factor: float) -> ImageList:
Expand Down
10 changes: 4 additions & 6 deletions src/safeds/data/image/containers/_single_size_image_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,7 @@ def _create_image_list(images: list[Tensor], indices: list[int]) -> ImageList:
max_channel = 0
for image, index in zip(images, indices, strict=False):
current_channel = image.size(dim=-3)
if max_channel < current_channel:
max_channel = current_channel
max_channel = max(max_channel, current_channel)
if current_channel not in images_with_channels:
images_with_channels[current_channel] = [image]
indices_with_channels[current_channel] = [index]
Expand Down Expand Up @@ -575,8 +574,7 @@ def add_images(self, images: list[Image] | ImageList) -> ImageList:
for image in images:
current_size = (image.width, image.height)
current_channel = image.channel
if max_channel < current_channel:
max_channel = current_channel
max_channel = max(max_channel, current_channel)
if current_size not in images_with_sizes_with_channel:
images_with_sizes_with_channel[current_size] = {}
indices_with_sizes_with_channel[current_size] = {}
Expand Down Expand Up @@ -965,7 +963,7 @@ def crop(self, x: int, y: int, width: int, height: int) -> ImageList:
image_list._tensor = func2.crop(self._tensor, x, y, height, width)
return image_list

def flip_vertically(self) -> ImageList:
def flip_top_and_bottom(self) -> ImageList:
from torchvision.transforms.v2 import functional as func2

_init_default_device()
Expand All @@ -974,7 +972,7 @@ def flip_vertically(self) -> ImageList:
image_list._tensor = func2.vertical_flip(self._tensor)
return image_list

def flip_horizontally(self) -> ImageList:
def flip_left_and_right(self) -> ImageList:
from torchvision.transforms.v2 import functional as func2

_init_default_device()
Expand Down
26 changes: 13 additions & 13 deletions tests/safeds/data/image/containers/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
import PIL.Image
import pytest
import torch
from syrupy import SnapshotAssertion
from torch.types import Device

from safeds._config import _get_device
from safeds.data.image.containers import Image
from safeds.data.image.typing import ImageSize
from safeds.data.tabular.containers import Table
from safeds.exceptions import IllegalFormatError, OutOfBoundsError
from syrupy import SnapshotAssertion
from torch.types import Device

from tests.helpers import (
configure_test_with_device,
device_cpu,
Expand Down Expand Up @@ -547,21 +547,21 @@ def test_should_warn_if_coordinates_outsize_image(


@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids())
class TestFlipVertically:
class TestFlipTopAndBottom:
@pytest.mark.parametrize(
"resource_path",
images_asymmetric(),
ids=images_asymmetric_ids(),
)
def test_should_flip_vertically(
def test_should_flip_top_and_bottom(
self,
resource_path: str,
snapshot_png_image: SnapshotAssertion,
device: Device,
) -> None:
configure_test_with_device(device)
image = Image.from_file(resolve_resource_path(resource_path))
image_flip_v = image.flip_vertically()
image_flip_v = image.flip_top_and_bottom()
assert image != image_flip_v
assert image_flip_v == snapshot_png_image
_assert_width_height_channel(image, image_flip_v)
Expand All @@ -574,26 +574,26 @@ def test_should_flip_vertically(
def test_should_be_original(self, resource_path: str, device: Device) -> None:
configure_test_with_device(device)
image = Image.from_file(resolve_resource_path(resource_path))
image_flip_v_v = image.flip_vertically().flip_vertically()
image_flip_v_v = image.flip_top_and_bottom().flip_top_and_bottom()
assert image == image_flip_v_v


@pytest.mark.parametrize("device", get_devices(), ids=get_devices_ids())
class TestFlipHorizontally:
class TestFlipLeftAndRight:
@pytest.mark.parametrize(
"resource_path",
images_asymmetric(),
ids=images_asymmetric_ids(),
)
def test_should_flip_horizontally(
def test_should_flip_left_and_right(
self,
resource_path: str,
snapshot_png_image: SnapshotAssertion,
device: Device,
) -> None:
configure_test_with_device(device)
image = Image.from_file(resolve_resource_path(resource_path))
image_flip_h = image.flip_horizontally()
image_flip_h = image.flip_left_and_right()
assert image != image_flip_h
assert image_flip_h == snapshot_png_image
_assert_width_height_channel(image, image_flip_h)
Expand All @@ -606,7 +606,7 @@ def test_should_flip_horizontally(
def test_should_be_original(self, resource_path: str, device: Device) -> None:
configure_test_with_device(device)
image = Image.from_file(resolve_resource_path(resource_path))
image_flip_h_h = image.flip_horizontally().flip_horizontally()
image_flip_h_h = image.flip_left_and_right().flip_left_and_right()
assert image == image_flip_h_h


Expand Down Expand Up @@ -1011,8 +1011,8 @@ def test_should_return_flipped_image(self, resource_path: str, device: Device) -
image = Image.from_file(resolve_resource_path(resource_path))
image_left_rotated = image.rotate_left().rotate_left()
image_right_rotated = image.rotate_right().rotate_right()
image_flipped_h_v = image.flip_horizontally().flip_vertically()
image_flipped_v_h = image.flip_horizontally().flip_vertically()
image_flipped_h_v = image.flip_left_and_right().flip_top_and_bottom()
image_flipped_v_h = image.flip_left_and_right().flip_top_and_bottom()
assert image_left_rotated == image_right_rotated
assert image_left_rotated == image_flipped_h_v
assert image_left_rotated == image_flipped_v_h
Expand Down
24 changes: 12 additions & 12 deletions tests/safeds/data/image/containers/test_image_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@

import pytest
import torch
from syrupy import SnapshotAssertion
from torch import Tensor
from torch.types import Device

from safeds.data.image.containers import Image, ImageList
from safeds.data.image.containers._empty_image_list import _EmptyImageList
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.tabular.containers import Table
from safeds.exceptions import DuplicateIndexError, IllegalFormatError, IndexOutOfBoundsError, OutOfBoundsError
from syrupy import SnapshotAssertion
from torch import Tensor
from torch.types import Device

from tests.helpers import (
configure_test_with_device,
device_cpu,
Expand Down Expand Up @@ -892,8 +892,8 @@ class TestTransformsEqualImageTransforms:
("resize", [700, 400]),
("convert_to_grayscale", None),
("crop", [0, 0, 100, 100]),
("flip_vertically", None),
("flip_horizontally", None),
("flip_top_and_bottom", None),
("flip_left_and_right", None),
("adjust_brightness", [0]),
("adjust_brightness", [0.5]),
("adjust_brightness", [10]),
Expand All @@ -917,8 +917,8 @@ class TestTransformsEqualImageTransforms:
"resize-(700, 400)",
"grayscale",
"crop-(0, 0, 100, 100)",
"flip_vertically",
"flip_horizontally",
"flip_top_and_bottom",
"flip_left_and_right",
"adjust_brightness-zero factor",
"adjust_brightness-small factor",
"adjust_brightness-large factor",
Expand Down Expand Up @@ -1631,8 +1631,8 @@ def test_remove_image_by_index(self, device: Device) -> None:
("resize", [700, 400]),
("convert_to_grayscale", None),
("crop", [0, 0, 100, 100]),
("flip_vertically", None),
("flip_horizontally", None),
("flip_top_and_bottom", None),
("flip_left_and_right", None),
("adjust_brightness", [0]),
("adjust_brightness", [0.5]),
("adjust_brightness", [10]),
Expand Down Expand Up @@ -1663,8 +1663,8 @@ def test_remove_image_by_index(self, device: Device) -> None:
"resize-(700, 400)",
"grayscale",
"crop-(0, 0, 100, 100)",
"flip_vertically",
"flip_horizontally",
"flip_top_and_bottom",
"flip_left_and_right",
"adjust_brightness-zero factor",
"adjust_brightness-small factor",
"adjust_brightness-large factor",
Expand Down