Skip to content

Commit

Permalink
feat: class to store image data
Browse files Browse the repository at this point in the history
  • Loading branch information
lars-reimann committed Apr 4, 2023
1 parent fcda2c6 commit f623c1f
Show file tree
Hide file tree
Showing 16 changed files with 382 additions and 27 deletions.
1 change: 1 addition & 0 deletions src/safeds/data/image/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Work with image data."""
7 changes: 7 additions & 0 deletions src/safeds/data/image/containers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Classes that can store image data."""

from ._image import Image

__all__ = [
'Image',
]
152 changes: 152 additions & 0 deletions src/safeds/data/image/containers/_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from __future__ import annotations

import io
from pathlib import Path
from typing import BinaryIO

from PIL.Image import open as open_image, Image as PillowImage

from safeds.data.image.typing import ImageFormat


class Image:
"""
A container for image data.
Parameters
----------
data : BinaryIO
The image data as bytes.
"""

@staticmethod
def from_jpeg_file(path: str) -> Image:
"""
Create an image from a JPEG file.
Parameters
----------
path : str
The path to the JPEG file.
Returns
-------
image : Image
The image.
"""
return Image(
data=Path(path).open("rb"),
format_=ImageFormat.JPEG,
)

@staticmethod
def from_png_file(path: str) -> Image:
"""
Create an image from a PNG file.
Parameters
----------
path : str
The path to the PNG file.
Returns
-------
image : Image
The image.
"""
return Image(
data=Path(path).open("rb"),
format_=ImageFormat.PNG,
)

def __init__(self, data: BinaryIO, format_: ImageFormat):
data.seek(0)

self._image: PillowImage = open_image(data, formats=[format_.value])
self._format: ImageFormat = format_

# ------------------------------------------------------------------------------------------------------------------
# Properties
# ------------------------------------------------------------------------------------------------------------------

@property
def format(self) -> ImageFormat:
"""
Get the image format.
Returns
-------
format : ImageFormat
The image format.
"""
return self._format

# ------------------------------------------------------------------------------------------------------------------
# Conversion
# ------------------------------------------------------------------------------------------------------------------

def to_jpeg_file(self, path: str) -> None:
"""
Save the image as a JPEG file.
Parameters
----------
path : str
The path to the JPEG file.
"""
Path(path).parent.mkdir(parents=True, exist_ok=True)
self._image.save(path, format="jpeg")

def to_png_file(self, path: str) -> None:
"""
Save the image as a PNG file.
Parameters
----------
path : str
The path to the PNG file.
"""
Path(path).parent.mkdir(parents=True, exist_ok=True)
self._image.save(path, format="png")

# ------------------------------------------------------------------------------------------------------------------
# IPython integration
# ------------------------------------------------------------------------------------------------------------------

def _repr_jpeg_(self) -> bytes | None:
"""
Return a JPEG image as bytes.
If the image is not a JPEG, return None.
Returns
-------
jpeg : bytes
The image as JPEG.
"""
if self._format != ImageFormat.JPEG:
return None

buffer = io.BytesIO()
self._image.save(buffer, format="jpeg")
buffer.seek(0)
return buffer.read()

def _repr_png_(self) -> bytes | None:
"""
Return a PNG image as bytes.
If the image is not a PNG, return None.
Returns
-------
png : bytes
The image as PNG.
"""
if self._format != ImageFormat.PNG:
return None

buffer = io.BytesIO()
self._image.save(buffer, format="png")
buffer.seek(0)
return buffer.read()
7 changes: 7 additions & 0 deletions src/safeds/data/image/typing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Types used to distinguish different image formats."""

from ._image_format import ImageFormat

__all__ = [
'ImageFormat',
]
7 changes: 7 additions & 0 deletions src/safeds/data/image/typing/_image_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from enum import Enum


class ImageFormat(Enum):
"""Images formats supported by us."""
JPEG = 'jpeg'
PNG = 'png'
53 changes: 37 additions & 16 deletions src/safeds/data/tabular/containers/_column.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import io
from numbers import Number
from typing import TYPE_CHECKING, Any

Expand All @@ -9,6 +10,8 @@
import seaborn as sns
from IPython.core.display_functions import DisplayHandle, display

from safeds.data.image.containers import Image
from safeds.data.image.typing import ImageFormat
from safeds.data.tabular.typing import ColumnType
from safeds.exceptions import (
ColumnLengthMismatchError,
Expand Down Expand Up @@ -470,7 +473,7 @@ def variance(self) -> float:
# Plotting
# ------------------------------------------------------------------------------------------------------------------

def boxplot(self) -> None:
def boxplot(self) -> Image:
"""
Plot this column in a boxplot. This function can only plot real numerical data.
Expand All @@ -487,13 +490,22 @@ def boxplot(self) -> None:
"The column contains complex data. Boxplots cannot plot the imaginary part of complex "
"data. Please provide a Column with only real numbers",
)

fig = plt.figure()
ax = sns.boxplot(data=self._data)
ax.set(xlabel=self.name)
plt.tight_layout()
plt.show()

def histogram(self) -> None:
buffer = io.BytesIO()
fig.savefig(buffer, format='png')
plt.close() # Prevents the figure from being displayed directly
buffer.seek(0)
return Image(buffer, ImageFormat.PNG)

def histogram(self) -> Image:
"""Plot a column in a histogram."""

fig = plt.figure()
ax = sns.histplot(data=self._data)
ax.set_xticks(ax.get_xticks())
ax.set(xlabel=self.name)
Expand All @@ -503,23 +515,17 @@ def histogram(self) -> None:
horizontalalignment="right",
) # rotate the labels of the x Axis to prevent the chance of overlapping of the labels
plt.tight_layout()
plt.show()

buffer = io.BytesIO()
fig.savefig(buffer, format='png')
plt.close() # Prevents the figure from being displayed directly
buffer.seek(0)
return Image(buffer, ImageFormat.PNG)

# ------------------------------------------------------------------------------------------------------------------
# Other
# IPython integration
# ------------------------------------------------------------------------------------------------------------------

def _count_missing_values(self) -> int:
"""
Return the number of null values in the column.
Returns
-------
count : int
The number of null values.
"""
return self._data.isna().sum()

def _ipython_display_(self) -> DisplayHandle:
"""
Return a display object for the column to be used in Jupyter Notebooks.
Expand All @@ -534,3 +540,18 @@ def _ipython_display_(self) -> DisplayHandle:

with pd.option_context("display.max_rows", tmp.shape[0], "display.max_columns", tmp.shape[1]):
return display(tmp)

# ------------------------------------------------------------------------------------------------------------------
# Other
# ------------------------------------------------------------------------------------------------------------------

def _count_missing_values(self) -> int:
"""
Return the number of null values in the column.
Returns
-------
count : int
The number of null values.
"""
return self._data.isna().sum()
2 changes: 1 addition & 1 deletion src/safeds/data/tabular/containers/_row.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def count(self) -> int:
return len(self._data)

# ------------------------------------------------------------------------------------------------------------------
# Other
# IPython integration
# ------------------------------------------------------------------------------------------------------------------

def _ipython_display_(self) -> DisplayHandle:
Expand Down
Loading

0 comments on commit f623c1f

Please sign in to comment.