EXIF orientation support (#4529)

Co-authored-by: Andrey Zhavoronkov <andrey.zhavoronkov@intel.com>
Co-authored-by: Nikita Manovich <nikita.manovich@intel.com>
Co-authored-by: Maria Khrustaleva <maya17grd@gmail.com>
main
Dmitry Kalinin 4 years ago committed by GitHub
parent 2b6fe74e1a
commit d22b12df6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- some AI Tools were not sending responses properly (<https://github.com/openvinotoolkit/cvat/issues/4432>) - some AI Tools were not sending responses properly (<https://github.com/openvinotoolkit/cvat/issues/4432>)
- Unable to upload annotations (<https://github.com/openvinotoolkit/cvat/pull/4513>) - Unable to upload annotations (<https://github.com/openvinotoolkit/cvat/pull/4513>)
- Fix build dependencies for Siammask (<https://github.com/openvinotoolkit/cvat/pull/4486>) - Fix build dependencies for Siammask (<https://github.com/openvinotoolkit/cvat/pull/4486>)
- Bug: Exif orientation information handled incorrectly (<https://github.com/openvinotoolkit/cvat/pull/4529>)
### Security ### Security
- TDB - TDB

@ -66,6 +66,14 @@
value: height, value: height,
writable: false, writable: false,
}, },
/**
* task ID
* @name tid
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
tid: { tid: {
value: taskID, value: taskID,
writable: false, writable: false,

@ -9,6 +9,7 @@ import zipfile
import io import io
import itertools import itertools
import struct import struct
from enum import IntEnum
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from contextlib import closing from contextlib import closing
@ -29,6 +30,20 @@ ImageFile.LOAD_TRUNCATED_IMAGES = True
from cvat.apps.engine.mime_types import mimetypes from cvat.apps.engine.mime_types import mimetypes
from utils.dataset_manifest import VideoManifestManager, ImageManifestManager from utils.dataset_manifest import VideoManifestManager, ImageManifestManager
ORIENTATION_EXIF_TAG = 274
class ORIENTATION(IntEnum):
NORMAL_HORIZONTAL=1
MIRROR_HORIZONTAL=2
NORMAL_180_ROTATED=3
MIRROR_VERTICAL=4
MIRROR_HORIZONTAL_270_ROTATED=5
NORMAL_90_ROTATED=6
MIRROR_HORIZONTAL_90_ROTATED=7
NORMAL_270_ROTATED=8
def get_mime(name): def get_mime(name):
for type_name, type_def in MEDIA_TYPES.items(): for type_name, type_def in MEDIA_TYPES.items():
if type_def['has_mime_type'](name): if type_def['has_mime_type'](name):
@ -62,6 +77,27 @@ def sort(images, sorting_method=SortingMethod.LEXICOGRAPHICAL, func=None):
else: else:
raise NotImplementedError() raise NotImplementedError()
def image_size_within_orientation(img: Image):
orientation = img.getexif().get(ORIENTATION_EXIF_TAG, ORIENTATION.NORMAL_HORIZONTAL)
if orientation > 4:
return img.height, img.width
return img.width, img.height
def rotate_within_exif(img: Image):
orientation = img.getexif().get(ORIENTATION_EXIF_TAG, ORIENTATION.NORMAL_HORIZONTAL)
if orientation in [ORIENTATION.NORMAL_180_ROTATED, ORIENTATION.MIRROR_VERTICAL]:
img = img.rotate(180, expand=True)
elif orientation in [ORIENTATION.NORMAL_270_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED]:
img = img.rotate(90, expand=True)
elif orientation in [ORIENTATION.NORMAL_90_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED]:
img = img.rotate(270, expand=True)
if orientation in [
ORIENTATION.MIRROR_HORIZONTAL, ORIENTATION.MIRROR_VERTICAL,
ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED ,ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED,
]:
img = img.transpose(Image.FLIP_LEFT_RIGHT)
return img
class IMediaReader(ABC): class IMediaReader(ABC):
def __init__(self, source_path, step, start, stop, dimension): def __init__(self, source_path, step, start, stop, dimension):
self._source_path = source_path self._source_path = source_path
@ -85,11 +121,13 @@ class IMediaReader(ABC):
@staticmethod @staticmethod
def _get_preview(obj): def _get_preview(obj):
PREVIEW_SIZE = (256, 256) PREVIEW_SIZE = (256, 256)
if isinstance(obj, io.IOBase): if isinstance(obj, io.IOBase):
preview = Image.open(obj) preview = Image.open(obj)
else: else:
preview = obj preview = obj
preview.thumbnail(PREVIEW_SIZE) preview.thumbnail(PREVIEW_SIZE)
preview = rotate_within_exif(preview)
return preview.convert('RGB') return preview.convert('RGB')
@ -173,7 +211,7 @@ class ImageListReader(IMediaReader):
properties = ValidateDimension.get_pcd_properties(f) properties = ValidateDimension.get_pcd_properties(f)
return int(properties["WIDTH"]), int(properties["HEIGHT"]) return int(properties["WIDTH"]), int(properties["HEIGHT"])
img = Image.open(self._source_path[i]) img = Image.open(self._source_path[i])
return img.width, img.height return image_size_within_orientation(img)
def reconcile(self, source_files, step=1, start=0, stop=None, dimension=DimensionType.DIM_2D, sorting_method=None): def reconcile(self, source_files, step=1, start=0, stop=None, dimension=DimensionType.DIM_2D, sorting_method=None):
# FIXME # FIXME
@ -314,7 +352,7 @@ class ZipReader(ImageListReader):
properties = ValidateDimension.get_pcd_properties(f) properties = ValidateDimension.get_pcd_properties(f)
return int(properties["WIDTH"]), int(properties["HEIGHT"]) return int(properties["WIDTH"]), int(properties["HEIGHT"])
img = Image.open(io.BytesIO(self._zip_source.read(self._source_path[i]))) img = Image.open(io.BytesIO(self._zip_source.read(self._source_path[i])))
return img.width, img.height return image_size_within_orientation(img)
def get_image(self, i): def get_image(self, i):
if self._dimension == DimensionType.DIM_3D: if self._dimension == DimensionType.DIM_3D:
@ -538,6 +576,7 @@ class IChunkWriter(ABC):
@staticmethod @staticmethod
def _compress_image(image_path, quality): def _compress_image(image_path, quality):
image = image_path.to_image() if isinstance(image_path, av.VideoFrame) else Image.open(image_path) image = image_path.to_image() if isinstance(image_path, av.VideoFrame) else Image.open(image_path)
image = rotate_within_exif(image)
# Ensure image data fits into 8bit per pixel before RGB conversion as PIL clips values on conversion # Ensure image data fits into 8bit per pixel before RGB conversion as PIL clips values on conversion
if image.mode == "I": if image.mode == "I":
# Image mode is 32bit integer pixels. # Image mode is 32bit integer pixels.

@ -194,14 +194,18 @@ class DatasetImagesReader:
if idx in self.range_: if idx in self.range_:
image = next(sources) image = next(sources)
img = Image.open(image, mode='r') img = Image.open(image, mode='r')
orientation = img.getexif().get(274, 1)
img_name = os.path.relpath(image, self._data_dir) if self._data_dir \ img_name = os.path.relpath(image, self._data_dir) if self._data_dir \
else os.path.basename(image) else os.path.basename(image)
name, extension = os.path.splitext(img_name) name, extension = os.path.splitext(img_name)
width, height = img.width, img.height
if orientation > 4:
width, height = height, width
image_properties = { image_properties = {
'name': name.replace('\\', '/'), 'name': name.replace('\\', '/'),
'extension': extension, 'extension': extension,
'width': img.width, 'width': width,
'height': img.height, 'height': height,
} }
if self._meta and img_name in self._meta: if self._meta and img_name in self._meta:
image_properties['meta'] = self._meta[img_name] image_properties['meta'] = self._meta[img_name]

Loading…
Cancel
Save