diff --git a/requirements.txt b/requirements.txt index 0a70f16b..a184080d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ matplotlib numpy>=1.18.5 opencv-python-headless==4.10.0.84 Pillow>=7.1.2 +pillow-heif>=0.18.0 python-dateutil python-dotenv requests diff --git a/roboflow/__init__.py b/roboflow/__init__.py index c64a62c1..ce9289d5 100644 --- a/roboflow/__init__.py +++ b/roboflow/__init__.py @@ -15,7 +15,7 @@ from roboflow.models import CLIPModel, GazeModel # noqa: F401 from roboflow.util.general import write_line -__version__ = "1.1.57" +__version__ = "1.1.58" def check_key(api_key, model, notebook, num_retries=0): diff --git a/roboflow/core/project.py b/roboflow/core/project.py index e9da7f6a..16fd789c 100644 --- a/roboflow/core/project.py +++ b/roboflow/core/project.py @@ -22,6 +22,9 @@ "image/jpeg", "image/png", "image/webp", + "image/tiff", + "image/avif", + "image/heic", } diff --git a/roboflow/util/folderparser.py b/roboflow/util/folderparser.py index bf469e84..eff2a118 100644 --- a/roboflow/util/folderparser.py +++ b/roboflow/util/folderparser.py @@ -7,7 +7,7 @@ from .image_utils import load_labelmap -IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp"} +IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif", ".avif", ".heic"} ANNOTATION_EXTENSIONS = {".txt", ".json", ".xml", ".csv", ".jsonl"} LABELMAPS_EXTENSIONS = {".labels", ".yaml", ".yml"} diff --git a/roboflow/util/image_utils.py b/roboflow/util/image_utils.py index af071eee..c9ca6b2a 100644 --- a/roboflow/util/image_utils.py +++ b/roboflow/util/image_utils.py @@ -1,12 +1,18 @@ +# Standard library imports import base64 import io import os import urllib +# Third-party imports +import pillow_heif # type: ignore[import-untyped] import requests import yaml from PIL import Image +pillow_heif.register_heif_opener(thumbnails=False) # Register for HEIF/HEIC +pillow_heif.register_avif_opener(thumbnails=False) # Register for AVIF + def check_image_path(image_path): """ @@ -74,9 +80,18 @@ def validate_image_path(image_path): def file2jpeg(image_path): import cv2 + # OpenCV will handle standard formats efficiently img = cv2.imread(image_path) - image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - pilImage = Image.fromarray(image) + if img is not None: + # Convert BGR to RGB for PIL + image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + pilImage = Image.fromarray(image) + else: + # If OpenCV fails, the format might be HEIC/AVIF which are handled by PIL + pilImage = Image.open(image_path) + if pilImage.mode != "RGB": + pilImage = pilImage.convert("RGB") + buffered = io.BytesIO() pilImage.save(buffered, quality=100, format="JPEG") return buffered.getvalue() diff --git a/tests/images/file_example_TIFF_1MB.tiff b/tests/images/file_example_TIFF_1MB.tiff new file mode 100644 index 00000000..e8826452 Binary files /dev/null and b/tests/images/file_example_TIFF_1MB.tiff differ diff --git a/tests/images/whatsnew.avif b/tests/images/whatsnew.avif new file mode 100644 index 00000000..86540657 Binary files /dev/null and b/tests/images/whatsnew.avif differ diff --git a/tests/test_project.py b/tests/test_project.py index afa69437..4949fde1 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -15,6 +15,8 @@ def test_check_valid_image_with_accepted_formats(self): "rabbit2.jpg", "hand-rabbit.PNG", "woodland-rabbit.png", + "file_example_TIFF_1MB.tiff", + "sky-rabbit.heic", ] for image in images_to_test: @@ -23,7 +25,6 @@ def test_check_valid_image_with_accepted_formats(self): def test_check_valid_image_with_unaccepted_formats(self): images_to_test = [ "sky-rabbit.gif", - "sky-rabbit.heic", ] for image in images_to_test: @@ -32,7 +33,7 @@ def test_check_valid_image_with_unaccepted_formats(self): def test_upload_raises_upload_image_error(self): responses.add( responses.POST, - f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}" f"&batch={DEFAULT_BATCH_NAME}", + f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}&batch={DEFAULT_BATCH_NAME}", json={ "error": { "message": "Invalid image.", @@ -58,7 +59,7 @@ def test_upload_raises_upload_annotation_error(self): # Image upload responses.add( responses.POST, - f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}" f"&batch={DEFAULT_BATCH_NAME}", + f"{API_URL}/dataset/{PROJECT_NAME}/upload?api_key={ROBOFLOW_API_KEY}&batch={DEFAULT_BATCH_NAME}", json={"success": True, "id": image_id}, status=200, ) @@ -66,7 +67,7 @@ def test_upload_raises_upload_annotation_error(self): # Annotation responses.add( responses.POST, - f"{API_URL}/dataset/{PROJECT_NAME}/annotate/{image_id}?api_key={ROBOFLOW_API_KEY}" f"&name={image_name}", + f"{API_URL}/dataset/{PROJECT_NAME}/annotate/{image_id}?api_key={ROBOFLOW_API_KEY}&name={image_name}", json={ "error": { "message": "Image was already annotated.",