Repository: lightly-ai/lightly

Issue:
Udpate docs
### Changes

- Add results for imagenette benchmark to the python script
- Increase the resolution of the benchmark output
- Add softmax to the pizza tutorial to make it work with newer PyTorch lightning versions


Discussion:

Resolving patch:
diff --git a/lightly/api/api_workflow_download_dataset.py b/lightly/api/api_workflow_download_dataset.py
--- a/lightly/api/api_workflow_download_dataset.py
+++ b/lightly/api/api_workflow_download_dataset.py
@@ -9,7 +9,6 @@
 
 from lightly.openapi_generated.swagger_client import TagCreator
 from lightly.openapi_generated.swagger_client.models.sample_create_request import SampleCreateRequest
-from lightly.api.utils import check_filename, check_image, get_thumbnail_from_img, PIL_to_bytes
 from lightly.api.bitmask import BitMask
 from lightly.openapi_generated.swagger_client.models.initial_tag_create_request import InitialTagCreateRequest
 from lightly.openapi_generated.swagger_client.models.image_type import ImageType
diff --git a/lightly/api/api_workflow_upload_dataset.py b/lightly/api/api_workflow_upload_dataset.py
--- a/lightly/api/api_workflow_upload_dataset.py
+++ b/lightly/api/api_workflow_upload_dataset.py
@@ -5,12 +5,14 @@
 
 from lightly.openapi_generated.swagger_client import TagCreator
 from lightly.openapi_generated.swagger_client.models.sample_create_request import SampleCreateRequest
-from lightly.api.utils import check_filename, check_image, get_thumbnail_from_img, PIL_to_bytes
+from lightly.api.utils import check_filename, PIL_to_bytes
 from lightly.openapi_generated.swagger_client.models.initial_tag_create_request import InitialTagCreateRequest
 from lightly.data.dataset import LightlyDataset
 
 from lightly.api.utils import retry
 
+from lightly_utils import image_processing
+
 
 class _UploadDatasetMixin:
 
@@ -129,14 +131,25 @@ def _upload_single_image(self, image, label, filename: str, mode):
             return False
 
         # calculate metadata, and check if corrupted
-        metadata = check_image(image)
+        metadata = image_processing.Metadata(image).to_dict()
+
+        # try to get exif data
+        try:
+            exifdata = image_processing.Exifdata(image)
+        except Exception:
+            exifdata = None
 
         # generate thumbnail if necessary
         thumbname = None
         if not metadata['is_corrupted'] and mode in ["thumbnails", "full"]:
             thumbname = '.'.join(basename.split('.')[:-1]) + '_thumb.webp'
 
-        body = SampleCreateRequest(file_name=basename, thumb_name=thumbname, meta_data=metadata)
+        body = SampleCreateRequest(
+            file_name=basename,
+            thumb_name=thumbname,
+            meta_data=metadata,
+            exif=exifdata if exifdata is None else exifdata.to_dict(),
+        )
         sample_id = retry(
             self.samples_api.create_sample_by_dataset_id,
             body=body,
@@ -144,8 +157,8 @@ def _upload_single_image(self, image, label, filename: str, mode):
         ).id
 
         if not metadata['is_corrupted'] and mode in ["thumbnails", "full"]:
-            thumbnail = get_thumbnail_from_img(image)
-            image_to_upload = PIL_to_bytes(thumbnail, ext='webp', quality=90)
+            thumbnail = image_processing.Thumbnail(image)
+            image_to_upload = thumbnail.to_bytes()
 
             signed_url = retry(
                 self.samples_api.get_sample_image_write_url_by_id,
@@ -161,7 +174,7 @@ def _upload_single_image(self, image, label, filename: str, mode):
                 signed_url
             )
 
-            thumbnail.close()
+            thumbnail.thumbnail.close()
 
         if not metadata['is_corrupted'] and mode == "full":
             image_to_upload = PIL_to_bytes(image)
@@ -173,7 +186,7 @@ def _upload_single_image(self, image, label, filename: str, mode):
                 is_thumbnail=False
             )
 
-            # try to upload thumbnail
+            # try to upload full image
             retry(
                 self.upload_file_with_signed_url,
                 image_to_upload,
diff --git a/lightly/api/utils.py b/lightly/api/utils.py
--- a/lightly/api/utils.py
+++ b/lightly/api/utils.py
@@ -16,9 +16,7 @@
 
 JpegImagePlugin._getmp = lambda: None
 
-LEGAL_IMAGE_FORMATS = ['jpg', 'jpeg', 'png', 'tiff', 'bmp']
 MAXIMUM_FILENAME_LENGTH = 80
-MAX_WIDTH, MAX_HEIGHT = 2048, 2048
 
 
 def retry(func, *args, **kwargs):
@@ -89,149 +87,6 @@ def PIL_to_bytes(img, ext: str = 'png', quality: int = None):
     return bytes_io
 
 
-def image_mean(np_img: np.ndarray):
-    """Return mean of each channel.
-
-    """
-    return np_img.mean(axis=(0, 1))
-
-
-def image_std(np_img: np.ndarray):
-    """Return standard deviation of each channel.
-
-    """
-    return np_img.std(axis=(0, 1))
-
-
-def sum_of_values(np_img: np.ndarray):
-    """Return the sum of the pixel values of each channel.
-
-    """
-    return np_img.sum(axis=(0, 1))
-
-
-def sum_of_squares(np_img: np.ndarray):
-    """Return the sum of the squared pixel values of each channel.
-
-    """
-    return (np_img ** 2).sum(axis=(0, 1))
-
-
-def signal_to_noise_ratio(img, axis: int = None, ddof: int = 0):
-    """Calculate the signal to noise ratio of the image.
-
-    """
-    np_img = np.asanyarray(img)
-    mean = np_img.mean(axis=axis)
-    std = np_img.std(axis=axis, ddof=ddof)
-    return np.where(std == 0, 0, mean / std)
-
-
-def sharpness(img):
-    """Calculate the sharpness of the image using a Laplacian Kernel.
-
-    """
-    img_bw = img.convert('L')
-    filtered = img_bw.filter(
-        ImageFilter.Kernel(
-            (3, 3),
-            # Laplacian Kernel:
-            (-1, -1, -1, -1, 8, -1, -1, -1, -1),
-            1,
-            0,
-        )
-    )
-    return np.std(filtered)
-
-
-def size_in_bytes(img):
-    """Return the size of the image in bytes.
-
-    """
-    img_file = io.BytesIO()
-    img.save(img_file, format='png')
-    return img_file.tell()
-
-
-def shape(np_img: np.ndarray):
-    """Shape of the image as np.ndarray.
-
-    """
-    return np_img.shape
-
-
-def get_meta_from_img(img):
-    """Calculates metadata from PIL image.
-
-        - Mean
-        - Standard Deviation
-        - Signal To Noise
-        - Sharpness
-        - Size in Bytes
-        - Shape
-        - Sum of Values
-        - Sum of Squares
-
-    Args:
-        PIL Image.
-
-    Returns:
-        A dictionary containing the metadata of the image.
-    """
-
-    np_img = np.array(img) / 255.0
-    metadata = {
-        'mean': image_mean(np_img).tolist(),
-        'std': image_std(np_img).tolist(),
-        'snr': float(signal_to_noise_ratio(img)),
-        'sharpness': float(sharpness(img)),
-        'sizeInBytes': size_in_bytes(img),
-        'shape': list(shape(np_img)),
-        'sumOfValues': sum_of_values(np_img).tolist(),
-        'sumOfSquares': sum_of_squares(np_img).tolist(),
-    }
-    return metadata
-
-
-def get_thumbnail_from_img(img, size: int = 128):
-    """Compute the thumbnail of the image.
-
-    Args:
-        img:
-            PIL Image.
-        size:
-            Size of the thumbnail.
-
-    Returns:
-        Thumbnail as PIL Image.
-
-    """
-    thumbnail = img.copy()
-    thumbnail.thumbnail((size, size), Image.LANCZOS)
-    return thumbnail
-
-
-def resize_image(image, max_width: int, max_height: int):
-    """Resize the image if it is too large for the web-app.
-
-    """
-    width = image.width
-    height = image.height
-    new_image = image.copy()
-    new_image.format = image.format
-
-    width_factor = max_width / width
-    height_factor = max_height / height
-    factor = min(width_factor, height_factor)
-    new_image.resize(
-        (
-            np.floor(factor * width).astype(int),
-            np.floor(factor * height).astype(int)
-        )
-    )
-    return new_image
-
-
 def check_filename(basename):
     """Checks the length of the filename.
 
@@ -241,37 +96,3 @@ def check_filename(basename):
 
     """
     return len(basename) <= MAXIMUM_FILENAME_LENGTH
-
-
-def check_image(image):
-    """Checks whether an image is corrupted or not.
-
-    The function reports the metadata, and opens the file to check whether
-    it is corrupt or not.
-
-    Args:
-        image:
-            PIL image from which metadata will be computed.
-
-    Returns:
-        A dictionary of metadata of the image.
-    """
-
-    # try to load the image to see whether it's corrupted or not
-    try:
-        image.load()
-        is_corrupted = False
-        corruption = ''
-    except IOError as e:
-        is_corrupted = True
-        corruption = e
-
-    # calculate metadata from image
-    if is_corrupted:
-        metadata = { 'corruption': corruption }
-    else:
-        metadata = get_meta_from_img(image)
-        metadata['corruption'] = ''
-
-    metadata['is_corrupted'] = is_corrupted
-    return metadata


Test patch:
diff --git a/tests/api/test_utils.py b/tests/api/test_utils.py
--- a/tests/api/test_utils.py
+++ b/tests/api/test_utils.py
@@ -1,8 +1,14 @@
 import unittest
 
+import os
+from PIL import Image
+
+from PIL import Image
+
 import lightly
 from lightly.api.utils import retry
-
+from lightly.api.utils import getenv
+from lightly.api.utils import PIL_to_bytes
 
 class TestUtils(unittest.TestCase):
 
@@ -21,3 +27,24 @@ def my_func():
         
         with self.assertRaises(RuntimeError):
             retry(my_func)
+
+    def test_getenv(self):
+        os.environ['TEST_ENV_VARIABLE'] = 'hello world'
+        env = getenv('TEST_ENV_VARIABLE', 'default')
+        self.assertEqual(env, 'hello world')
+
+    def test_getenv_fail(self):
+        env = getenv('TEST_ENV_VARIABLE_WHICH_DOES_NOT_EXIST', 'hello world')
+        self.assertEqual(env, 'hello world')
+
+    def test_PIL_to_bytes(self):
+        image = Image.new('RGB', (128, 128))
+
+        # test with quality=None
+        PIL_to_bytes(image)
+
+        # test with quality=90
+        PIL_to_bytes(image, quality=90)
+
+        # test with quality=90 and ext=jpg
+        PIL_to_bytes(image, ext='JPEG', quality=90)
diff --git a/tests/api_workflow/test_api_workflow_upload_dataset.py b/tests/api_workflow/test_api_workflow_upload_dataset.py
--- a/tests/api_workflow/test_api_workflow_upload_dataset.py
+++ b/tests/api_workflow/test_api_workflow_upload_dataset.py
@@ -1,5 +1,6 @@
 import os
 import tempfile
+import pathlib
 
 import torchvision
 from lightly.data.dataset import LightlyDataset
@@ -27,6 +28,12 @@ def create_fake_dataset(self):
             path = os.path.join(self.folder_path, sample_names[sample_idx])
             data[0].save(path)
 
+    def corrupt_fake_dataset(self):
+        n_data = self.n_data
+        sample_names = [f'img_{i}.jpg' for i in range(n_data)]
+        for sample_name in sample_names:
+            pathlib.Path(os.path.join(self.folder_path, sample_name)).touch()
+
     def test_upload_dataset_over_quota(self):
         quota = self.n_data-1
         def get_quota_reduced():
@@ -35,7 +42,6 @@ def get_quota_reduced():
         with self.assertRaises(ValueError):
             self.api_workflow_client.upload_dataset(input=self.folder_path)
 
-
     def test_upload_dataset_from_folder(self):
         self.api_workflow_client.upload_dataset(input=self.folder_path)
 
@@ -47,3 +53,8 @@ def test_upload_existing_dataset(self):
     def test_upload_dataset_from_dataset(self):
         dataset = LightlyDataset.from_torch_dataset(self.dataset)
         self.api_workflow_client.upload_dataset(input=dataset)
+
+    def test_corrupt_dataset_from_folder(self):
+        self.corrupt_fake_dataset()
+        self.api_workflow_client.upload_dataset(input=self.folder_path)
+        self.api_workflow_client.upload_dataset(input=self.folder_path)