Skip to content

Commit

Permalink
Keep EXIF rotation during YOLO importing (#27)
Browse files Browse the repository at this point in the history
* keep exif rotation during yolo importing

* remove unnecessary changes

* sort imports

* update license headers

* mark tests as xfail

* no sec

* nosec

* black linter

* constraint for version of dvc dependencies

* add test

* fix linters
  • Loading branch information
Kirill Sizov committed Sep 14, 2023
1 parent ff83c00 commit 8a14a99
Show file tree
Hide file tree
Showing 19 changed files with 74 additions and 23 deletions.
2 changes: 1 addition & 1 deletion datumaro/components/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ def match_items(datasets):
item_map = {} # id(item) -> (item, id(dataset))

matches = OrderedDict()
for (item_id, item_subset) in sorted(item_ids, key=lambda e: e[0]):
for item_id, item_subset in sorted(item_ids, key=lambda e: e[0]):
items = {}
for d in datasets:
item = d.get(item_id, subset=item_subset)
Expand Down
2 changes: 1 addition & 1 deletion datumaro/plugins/camvid_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ def save_segm_lists(self, subset_name, segm_list):
return

with open(ann_file, "w", encoding="utf-8") as f:
for (image_path, mask_path) in segm_list.values():
for image_path, mask_path in segm_list.values():
image_path = "/" + image_path.replace("\\", "/")
mask_path = mask_path.replace("\\", "/")
if 1 < len(image_path.split()) or 1 < len(mask_path.split()):
Expand Down
1 change: 0 additions & 1 deletion datumaro/plugins/imagenet_txt_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ def build_cmdline_parser(cls, **kwargs):
@classmethod
def find_sources_with_params(cls, path, **extra_params):
if "labels" not in extra_params or extra_params["labels"] == _LabelsSource.file.name:

labels_file_name = osp.basename(
extra_params.get("labels_file") or ImagenetTxtPath.LABELS_FILE
)
Expand Down
1 change: 0 additions & 1 deletion datumaro/plugins/mpii_format/mpii_mat.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ def _load_items(self, path):
)

if x1 is not None and x2 is not None and y1 is not None and y2 is not None:

annotations.append(Bbox(x1, y1, x2 - x1, y2 - y1, label=0, group=group_num))

group_num += 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def process_outputs(inputs, outputs):

results = []
for input_, detections in zip(inputs, outputs["detection_out"]):

input_height, input_width = input_.shape[:2]

confs = outputs["Softmax_189/Softmax_"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ def process_outputs(inputs, outputs):
for input_, confs, detections in zip(
inputs, outputs["do_ExpandDims_conf/sigmoid"], outputs["DetectionOutput"]
):

input_height, input_width = input_.shape[:2]

confs = confs[0].reshape(-1, model_class_num)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def process_outputs(inputs, outputs):

results = []
for input_, detections in zip(inputs, outputs["detection_out"]):

input_height, input_width = input_.shape[:2]

confs = outputs["Softmax_189/Softmax_"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def process_outputs(inputs, outputs):

results = []
for input_, detections in zip(inputs, outputs["detection_out"]):

input_height, input_width = input_.shape[:2]

confs = outputs["Softmax_189/Softmax_"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ def process_outputs(inputs, outputs):

results = []
for input_, detections in zip(inputs, outputs["detection_out"]):

input_height, input_width = input_.shape[:2]

confs = outputs["Softmax_189/Softmax_"]
Expand Down
20 changes: 18 additions & 2 deletions datumaro/plugins/yolo_format/extractor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright (C) 2019-2022 Intel Corporation
# Copyright (C) 2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

Expand All @@ -18,7 +19,12 @@
from datumaro.components.extractor import DatasetItem, Extractor, Importer, SourceExtractor
from datumaro.components.format_detection import FormatDetectionContext
from datumaro.components.media import Image
from datumaro.util.image import DEFAULT_IMAGE_META_FILE_NAME, ImageMeta, load_image_meta_file
from datumaro.util.image import (
DEFAULT_IMAGE_META_FILE_NAME,
ImageMeta,
load_image,
load_image_meta_file,
)
from datumaro.util.meta_file_util import has_meta_file, parse_meta_file
from datumaro.util.os_util import split_path

Expand Down Expand Up @@ -156,14 +162,23 @@ def name_from_path(cls, path: str) -> str:

return osp.splitext(path)[0]

@classmethod
def _image_loader(cls, *args, **kwargs):
return load_image(*args, **kwargs, keep_exif=True)

def _get(self, item_id: str, subset_name: str) -> Optional[DatasetItem]:
subset = self._subsets[subset_name]
item = subset.items[item_id]

if isinstance(item, str):
try:
image_size = self._image_info.get(item_id)
image = Image(path=osp.join(self._path, item), size=image_size)
image_path = osp.join(self._path, item)

if image_size:
image = Image(path=image_path, size=image_size)
else:
image = Image(path=image_path, data=self._image_loader)

anno_path = osp.splitext(image.path)[0] + ".txt"
annotations = self._parse_annotations(
Expand Down Expand Up @@ -228,6 +243,7 @@ def _parse_annotations(
h = self._parse_field(h, float, "bbox height")
x = self._parse_field(xc, float, "bbox center x") - w * 0.5
y = self._parse_field(yc, float, "bbox center y") - h * 0.5

annotations.append(
Bbox(
x * image_width,
Expand Down
16 changes: 12 additions & 4 deletions datumaro/util/image.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright (C) 2019-2021 Intel Corporation
# Copyright (C) 2023 CVAT.ai Corporation
#
# SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -56,7 +57,7 @@ def __getattr__(name: str):
raise AttributeError(f"module {__name__} has no attribute {name}")


def load_image(path: str, dtype: DTypeLike = np.float32):
def load_image(path: str, dtype: DTypeLike = np.float32, **kwargs):
"""
Reads an image in the HWC Grayscale/BGR(A) float [0; 255] format.
"""
Expand All @@ -69,11 +70,18 @@ def load_image(path: str, dtype: DTypeLike = np.float32):
with open(path, "rb") as f:
image_bytes = f.read()

if kwargs.get("keep_exif"):
return decode_image(image_bytes, dtype=dtype, cv2_read_flag=1)

return decode_image(image_bytes, dtype=dtype)
elif _IMAGE_BACKEND == _IMAGE_BACKENDS.PIL:
from PIL import Image
from PIL import Image, ImageOps

image = Image.open(path)

if kwargs.get("keep_exif"):
image = ImageOps.exif_transpose(image)

image = np.asarray(image, dtype=dtype)
if len(image.shape) == 3 and image.shape[2] in {3, 4}:
image[:, :, :3] = image[:, :, 2::-1] # RGB to BGR
Expand Down Expand Up @@ -176,12 +184,12 @@ def encode_image(image: np.ndarray, ext: str, dtype: DTypeLike = np.uint8, **kwa
raise NotImplementedError()


def decode_image(image_bytes: bytes, dtype: DTypeLike = np.float32) -> np.ndarray:
def decode_image(image_bytes: bytes, dtype: DTypeLike = np.float32, **kwargs) -> np.ndarray:
if _IMAGE_BACKEND == _IMAGE_BACKENDS.cv2:
import cv2

image = np.frombuffer(image_bytes, dtype=np.uint8)
image = cv2.imdecode(image, cv2.IMREAD_UNCHANGED)
image = cv2.imdecode(image, kwargs.get("cv2_read_flag", cv2.IMREAD_UNCHANGED))
image = image.astype(dtype)
elif _IMAGE_BACKEND == _IMAGE_BACKENDS.PIL:
from PIL import Image
Expand Down
2 changes: 1 addition & 1 deletion requirements-default.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
dvc>=2.7.0
dvc>=2.7.0,<3.0.0
fsspec<2023.1; python_version == '3.7' # remove after 3.7 EOL. https://stackoverflow.com/a/75197382

GitPython>=3.1.18,!=3.1.25 # https://github.com/openvinotoolkit/datumaro/issues/612
2 changes: 1 addition & 1 deletion site/build_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def git_checkout(tagname, repo, temp_dir):
repo.git.archive(tagname, "--", subdir, output_stream=archive)
archive.seek(0)
with tarfile.open(fileobj=archive) as tar:
tar.extractall(temp_dir)
tar.extractall(temp_dir) # nosec


def change_version_menu_toml(filename, version):
Expand Down
3 changes: 3 additions & 0 deletions tests/cli/test_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@
import os.path as osp
from unittest import TestCase

import pytest

import datumaro.util.image as image_module
from datumaro.util.test_utils import TestDir
from datumaro.util.test_utils import run_datum as run

from ..requirements import Requirements, mark_requirement


@pytest.mark.xfail(reason="Cannot download the model file from the source")
class ImageGeneratorTest(TestCase):
def check_images_shape(self, img_dir, expected_shape):
exp_h, exp_w, exp_c = expected_shape
Expand Down
2 changes: 1 addition & 1 deletion tests/cli/test_image_zip_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

def make_zip_archive(src_path, dst_path):
with ZipFile(dst_path, "w") as archive:
for (dirpath, _, filenames) in os.walk(src_path):
for dirpath, _, filenames in os.walk(src_path):
for name in filenames:
path = osp.join(dirpath, name)
archive.write(path, osp.relpath(path, src_path))
Expand Down
2 changes: 2 additions & 0 deletions tests/test_fractal_image_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from unittest import TestCase

import numpy as np
import pytest

from datumaro.plugins.synthetic_data import FractalImageGenerator
from datumaro.util.image import load_image
Expand All @@ -11,6 +12,7 @@
from .requirements import Requirements, mark_requirement


@pytest.mark.xfail(reason="Cannot download the model file from the source")
class FractalImageGeneratorTest(TestCase):
@mark_requirement(Requirements.DATUM_677)
def test_save_image_can_create_dir(self):
Expand Down
3 changes: 0 additions & 3 deletions tests/test_splitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,6 @@ def test_no_subset_name_and_count_restriction(self):

@mark_requirement(Requirements.DATUM_GENERAL_REQ)
def test_split_for_segmentation(self):

with self.subTest("mask annotation"):
dtypes = ["coco", "voc", "labelme", "mot"]
task = splitter.SplitTask.segmentation.name
Expand Down Expand Up @@ -1041,7 +1040,6 @@ def test_split_for_segmentation(self):

@mark_requirement(Requirements.DATUM_GENERAL_REQ)
def test_split_for_segmentation_with_unlabeled(self):

with self.subTest("mask annotation"):
source, _ = self._generate_detection_segmentation_dataset(
annotation_type=self._get_append_mask("coco"),
Expand Down Expand Up @@ -1076,7 +1074,6 @@ def test_split_for_segmentation_with_unlabeled(self):

@mark_requirement(Requirements.DATUM_GENERAL_REQ)
def test_split_for_segmentation_gives_error(self):

with self.subTest("mask annotation"):
source, _ = self._generate_detection_segmentation_dataset(
annotation_type=self._get_append_mask("coco"),
Expand Down
1 change: 0 additions & 1 deletion tests/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,6 @@ def test_check_far_from_attr_mean(self):


class TestValidateAnnotations(_TestValidatorBase):

extra_args = {
"few_samples_thr": 1,
"imbalance_ratio_thr": 50,
Expand Down
35 changes: 34 additions & 1 deletion tests/test_yolo_format.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import os
import os.path as osp
import pickle # nosec - disable B403:import_pickle check
import shutil
from unittest import TestCase

import numpy as np
from PIL import Image as PILImage

from datumaro.components.annotation import Bbox
from datumaro.components.dataset import Dataset
Expand Down Expand Up @@ -289,7 +291,6 @@ def test_can_save_and_load_with_custom_subset_name(self):
@mark_requirement(Requirements.DATUM_565)
def test_cant_save_with_reserved_subset_name(self):
for subset in ["backup", "classes"]:

dataset = Dataset.from_iterable(
[
DatasetItem(
Expand Down Expand Up @@ -366,6 +367,38 @@ def test_can_import(self):

compare_datasets(self, expected_dataset, dataset)

@mark_requirement(Requirements.DATUM_GENERAL_REQ)
def test_can_import_with_exif_rotated_images(self):
expected_dataset = Dataset.from_iterable(
[
DatasetItem(
id=1,
subset="train",
media=Image(data=np.ones((15, 10, 3))),
annotations=[
Bbox(0, 3, 2.67, 3.0, label=2),
Bbox(2, 4.5, 1.33, 4.5, label=4),
],
),
],
categories=["label_" + str(i) for i in range(10)],
)

with TestDir() as test_dir:
dataset_path = osp.join(test_dir, "dataset")
shutil.copytree(DUMMY_DATASET_DIR, dataset_path)

# Add exif rotation for image
image_path = osp.join(dataset_path, "obj_train_data", "1.jpg")
img = PILImage.open(image_path)
exif = img.getexif()
exif.update([(296, 3), (282, 28.0), (531, 1), (274, 6), (283, 28.0)])
img.save(image_path, exif=exif)

dataset = Dataset.import_from(dataset_path, "yolo")

compare_datasets(self, expected_dataset, dataset, require_media=True)

@mark_requirement(Requirements.DATUM_673)
def test_can_pickle(self):
source = Dataset.import_from(DUMMY_DATASET_DIR, format="yolo")
Expand Down

0 comments on commit 8a14a99

Please sign in to comment.