From 068f5fdf9b69183d80ef1e967363e8adc39ef192 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 28 Mar 2022 08:40:40 +0300 Subject: [PATCH 01/19] draft implementation --- cvat-canvas/src/typescript/canvasModel.ts | 7 +++++-- cvat-canvas/src/typescript/canvasView.ts | 10 ++++++++++ cvat-core/src/frames.js | 20 +++++++++++++++++++ cvat/apps/engine/media_extractors.py | 10 ++++++++-- .../migrations/0052_exif_orientation.py | 18 +++++++++++++++++ cvat/apps/engine/models.py | 1 + cvat/apps/engine/serializers.py | 1 + cvat/apps/engine/task.py | 8 ++++---- cvat/apps/engine/views.py | 1 + 9 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 cvat/apps/engine/migrations/0052_exif_orientation.py diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index bc43af8fec4..218140a9685 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -13,6 +13,7 @@ export interface Size { export interface Image { renderWidth: number; renderHeight: number; + orientation: number; imageData: ImageData | CanvasImageSource; } @@ -429,8 +430,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { height: frameData.height as number, width: frameData.width as number, }; - - this.data.image = data; + this.data.image = { + ...data, + orientation: frameData.orientation as number, + }; this.notify(UpdateReasons.IMAGE_CHANGED); this.data.zLayer = zLayer; this.data.objects = objectStates; diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 4cc1377f54b..4189187d85e 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1277,6 +1277,16 @@ export class CanvasViewImpl implements CanvasView, Listener { // Transformation matrix must not affect the putImageData() method. // By this reason need to redraw the image to apply scale. // https://www.w3.org/TR/2dcontext/#dom-context-2d-putimagedata + switch (image.orientation) { + case 2: ctx.transform(-1, 0, 0, 1, image.imageData.width, 0); break; + case 3: ctx.transform(-1, 0, 0, -1, image.imageData.width, image.imageData.height); break; + case 4: ctx.transform(1, 0, 0, -1, 0, image.imageData.height); break; + case 5: ctx.transform(0, 1, 1, 0, 0, 0); break; + case 6: ctx.transform(0, 1, -1, 0, image.imageData.height, 0); break; + case 7: ctx.transform(0, -1, -1, 0, image.imageData.height, image.imageData.width); break; + case 8: ctx.transform(0, -1, 1, 0, 0, image.imageData.width); break; + default: break; + } ctx.drawImage(this.background, 0, 0); } else { ctx.drawImage(image.imageData, 0, 0); diff --git a/cvat-core/src/frames.js b/cvat-core/src/frames.js index fef97285892..7eadf1f62d8 100644 --- a/cvat-core/src/frames.js +++ b/cvat-core/src/frames.js @@ -21,6 +21,7 @@ constructor({ width, height, + orientation, name, taskID, jobID, @@ -66,6 +67,25 @@ value: height, writable: false, }, + /** + * @name orientation + * @type {integer} + * @memberof module:API.cvat.classes.FrameData + * @readonly + * @instance + */ + orientation: { + value: orientation, + writable: false, + }, + /** + * task ID + * @name tid + * @type {integer} + * @memberof module:API.cvat.classes.FrameData + * @readonly + * @instance + */ tid: { value: taskID, writable: false, diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 3e64490281d..8f42d26a5b0 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -173,7 +173,10 @@ def get_image_size(self, i): properties = ValidateDimension.get_pcd_properties(f) return int(properties["WIDTH"]), int(properties["HEIGHT"]) img = Image.open(self._source_path[i]) - return img.width, img.height + try: + return img.width, img.height, img._getexif().get(274, 0) + except Exception: + return img.width, img.height, 0 def reconcile(self, source_files, step=1, start=0, stop=None, dimension=DimensionType.DIM_2D, sorting_method=None): # FIXME @@ -314,7 +317,10 @@ def get_image_size(self, i): properties = ValidateDimension.get_pcd_properties(f) return int(properties["WIDTH"]), int(properties["HEIGHT"]) img = Image.open(io.BytesIO(self._zip_source.read(self._source_path[i]))) - return img.width, img.height + try: + return img.width, img.height, img._getexif().get(274, 0) + except Exception: + return img.width, img.height, 0 def get_image(self, i): if self._dimension == DimensionType.DIM_3D: diff --git a/cvat/apps/engine/migrations/0052_exif_orientation.py b/cvat/apps/engine/migrations/0052_exif_orientation.py new file mode 100644 index 00000000000..6f1a4e416c6 --- /dev/null +++ b/cvat/apps/engine/migrations/0052_exif_orientation.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.12 on 2022-03-25 09:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('engine', '0051_auto_20220220_1824'), + ] + + operations = [ + migrations.AddField( + model_name='image', + name='orientation', + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 540d8ee9153..914c6aa0489 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -223,6 +223,7 @@ class Image(models.Model): frame = models.PositiveIntegerField() width = models.PositiveIntegerField() height = models.PositiveIntegerField() + orientation = models.PositiveIntegerField(default=0) class Meta: default_permissions = () diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 471a12824c5..db488e146ff 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -672,6 +672,7 @@ class AboutSerializer(serializers.Serializer): class FrameMetaSerializer(serializers.Serializer): width = serializers.IntegerField() height = serializers.IntegerField() + orientation = serializers.IntegerField() name = serializers.CharField(max_length=1024) has_related_context = serializers.BooleanField() diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 33464493de3..e8b45de21dd 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -490,7 +490,7 @@ def update_progress(progress): if db_data.chunk_size is None: if isinstance(compressed_chunk_writer, ZipCompressedChunkWriter): if not (db_data.storage == models.StorageChoice.CLOUD_STORAGE): - w, h = extractor.get_image_size(0) + w, h, _ = extractor.get_image_size(0) else: img_properties = manifest[0] w, h = img_properties['width'], img_properties['height'] @@ -593,7 +593,7 @@ def _update_status(msg): if not chunk_path.endswith(f"{properties['name']}{properties['extension']}"): raise Exception('Incorrect file mapping to manifest content') if db_task.dimension == models.DimensionType.DIM_2D: - resolution = (properties['width'], properties['height']) + resolution = (properties['width'], properties['height'], extractor.get_image_size(frame_id)[2]) else: resolution = extractor.get_image_size(frame_id) img_sizes.append(resolution) @@ -601,8 +601,8 @@ def _update_status(msg): db_images.extend([ models.Image(data=db_data, path=os.path.relpath(path, upload_dir), - frame=frame, width=w, height=h) - for (path, frame), (w, h) in zip(chunk_paths, img_sizes) + frame=frame, width=w, height=h, orientation=o) + for (path, frame), (w, h, o) in zip(chunk_paths, img_sizes) ]) if db_data.storage_method == models.StorageMethodChoice.FILE_SYSTEM or not settings.USE_CACHE: diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index f44a73ac19e..7b582be5288 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -931,6 +931,7 @@ def data_info(request, pk): frame_meta = [{ 'width': item.width, 'height': item.height, + 'orientation': item.orientation, 'name': item.path, 'has_related_context': hasattr(item, 'related_files') and item.related_files.exists() } for item in media] From c349c1e5be26e2672769669980159a55e7f729a4 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 29 Mar 2022 00:43:54 +0300 Subject: [PATCH 02/19] Added for non chunk --- cvat-canvas/src/typescript/canvasModel.ts | 6 ++- cvat-canvas/src/typescript/canvasView.ts | 36 ++++++++++-------- cvat/apps/engine/media_extractors.py | 38 +++++++++++++++---- .../migrations/0052_exif_orientation.py | 2 +- cvat/apps/engine/models.py | 2 +- cvat/apps/engine/task.py | 13 ++++--- 6 files changed, 64 insertions(+), 33 deletions(-) diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index 218140a9685..dc7aeb13719 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -427,8 +427,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } this.data.imageSize = { - height: frameData.height as number, - width: frameData.width as number, + height: frameData.orientation as number < 5 ? + frameData.height as number : frameData.width as number, + width: frameData.orientation as number < 5 ? + frameData.width as number : frameData.height as number, }; this.data.image = { ...data, diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 4189187d85e..8360eaabf12 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1264,29 +1264,33 @@ export class CanvasViewImpl implements CanvasView, Listener { } else { this.loadingAnimation.classList.add('cvat_canvas_hidden'); const ctx = this.background.getContext('2d'); - this.background.setAttribute('width', `${image.renderWidth}px`); - this.background.setAttribute('height', `${image.renderHeight}px`); + const [width, height, renderWidth, renderHeight] = image.orientation < 5 ? [ + image.imageData.width as number, image.imageData.height as number, + image.renderWidth, image.renderHeight, + ] : [ + image.imageData.height as number, image.imageData.width as number, + image.renderHeight, image.renderWidth, + ]; + this.background.setAttribute('width', `${renderWidth}px`); + this.background.setAttribute('height', `${renderHeight}px`); if (ctx) { + switch (image.orientation) { + case 2: ctx.transform(-1, 0, 0, 1, width, 0); break; + case 3: ctx.transform(-1, 0, 0, -1, width, height); break; + case 4: ctx.transform(1, 0, 0, -1, 0, height); break; + case 5: ctx.transform(0, 1, 1, 0, 0, 0); break; + case 6: ctx.transform(0, 1, -1, 0, width, 0); break; + case 7: ctx.transform(0, -1, -1, 0, width, height); break; + case 8: ctx.transform(0, -1, 1, 0, 0, height); break; + default: break; + } if (image.imageData instanceof ImageData) { - ctx.scale( - image.renderWidth / image.imageData.width, - image.renderHeight / image.imageData.height, - ); + ctx.scale(renderWidth / width, renderHeight / height); ctx.putImageData(image.imageData, 0, 0); // Transformation matrix must not affect the putImageData() method. // By this reason need to redraw the image to apply scale. // https://www.w3.org/TR/2dcontext/#dom-context-2d-putimagedata - switch (image.orientation) { - case 2: ctx.transform(-1, 0, 0, 1, image.imageData.width, 0); break; - case 3: ctx.transform(-1, 0, 0, -1, image.imageData.width, image.imageData.height); break; - case 4: ctx.transform(1, 0, 0, -1, 0, image.imageData.height); break; - case 5: ctx.transform(0, 1, 1, 0, 0, 0); break; - case 6: ctx.transform(0, 1, -1, 0, image.imageData.height, 0); break; - case 7: ctx.transform(0, -1, -1, 0, image.imageData.height, image.imageData.width); break; - case 8: ctx.transform(0, -1, 1, 0, 0, image.imageData.width); break; - default: break; - } ctx.drawImage(this.background, 0, 0); } else { ctx.drawImage(image.imageData, 0, 0); diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 8f42d26a5b0..5866c297644 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -90,6 +90,15 @@ def _get_preview(obj): else: preview = obj preview.thumbnail(PREVIEW_SIZE) + orientation = preview._getexif().get(274, 1) if preview._getexif() else 1 + if orientation in [3, 4]: + preview = preview.rotate(180, expand=True) + elif orientation in [5, 8]: + preview = preview.rotate(90, expand=True) + elif orientation in [6, 7]: + preview = preview.rotate(270, expand=True) + if orientation in [2, 4, 5 ,7]: + preview = preview.transpose(Image.FLIP_LEFT_RIGHT) return preview.convert('RGB') @@ -97,6 +106,10 @@ def _get_preview(obj): def get_image_size(self, i): pass + @abstractmethod + def get_orientation(self, i): + pass + def __len__(self): return len(self.frame_range) @@ -173,10 +186,17 @@ def get_image_size(self, i): properties = ValidateDimension.get_pcd_properties(f) return int(properties["WIDTH"]), int(properties["HEIGHT"]) img = Image.open(self._source_path[i]) + return img.width, img.height + + def get_orientation(self, i): + if self._dimension == DimensionType.DIM_3D: + raise NotImplementedError() + img = Image.open(self._source_path[i]) try: - return img.width, img.height, img._getexif().get(274, 0) + return img._getexif().get(274, 1) if img._getexif() else 1 except Exception: - return img.width, img.height, 0 + return 1 + def reconcile(self, source_files, step=1, start=0, stop=None, dimension=DimensionType.DIM_2D, sorting_method=None): # FIXME @@ -317,10 +337,16 @@ def get_image_size(self, i): properties = ValidateDimension.get_pcd_properties(f) return int(properties["WIDTH"]), int(properties["HEIGHT"]) img = Image.open(io.BytesIO(self._zip_source.read(self._source_path[i]))) + return img.width, img.height + + def get_orientation(self, i): + if self._dimension == DimensionType.DIM_3D: + raise NotImplementedError() + img = Image.open(self._source_path[i]) try: - return img.width, img.height, img._getexif().get(274, 0) + return img._getexif().get(274, 1) if img._getexif() else 1 except Exception: - return img.width, img.height, 0 + return 1 def get_image(self, i): if self._dimension == DimensionType.DIM_3D: @@ -435,10 +461,6 @@ def get_preview(self): ).to_image() ) - def get_image_size(self, i): - image = (next(iter(self)))[0] - return image.width, image.height - class FragmentMediaReader: def __init__(self, chunk_number, chunk_size, start, stop, step=1): self._start = start diff --git a/cvat/apps/engine/migrations/0052_exif_orientation.py b/cvat/apps/engine/migrations/0052_exif_orientation.py index 6f1a4e416c6..03deb3895e3 100644 --- a/cvat/apps/engine/migrations/0052_exif_orientation.py +++ b/cvat/apps/engine/migrations/0052_exif_orientation.py @@ -13,6 +13,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='image', name='orientation', - field=models.PositiveIntegerField(default=0), + field=models.PositiveIntegerField(default=1), ), ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 914c6aa0489..8e82c1ba4c5 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -223,7 +223,7 @@ class Image(models.Model): frame = models.PositiveIntegerField() width = models.PositiveIntegerField() height = models.PositiveIntegerField() - orientation = models.PositiveIntegerField(default=0) + orientation = models.PositiveIntegerField(default=1) class Meta: default_permissions = () diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index e8b45de21dd..775af18ad71 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -490,7 +490,7 @@ def update_progress(progress): if db_data.chunk_size is None: if isinstance(compressed_chunk_writer, ZipCompressedChunkWriter): if not (db_data.storage == models.StorageChoice.CLOUD_STORAGE): - w, h, _ = extractor.get_image_size(0) + w, h = extractor.get_image_size(0) else: img_properties = manifest[0] w, h = img_properties['width'], img_properties['height'] @@ -584,7 +584,7 @@ def _update_status(msg): counter = itertools.count() for _, chunk_frames in itertools.groupby(extractor.frame_range, lambda x: next(counter) // db_data.chunk_size): chunk_paths = [(extractor.get_path(i), i) for i in chunk_frames] - img_sizes = [] + imgs_info = [] for chunk_path, frame_id in chunk_paths: properties = manifest[manifest_index(frame_id)] @@ -593,16 +593,19 @@ def _update_status(msg): if not chunk_path.endswith(f"{properties['name']}{properties['extension']}"): raise Exception('Incorrect file mapping to manifest content') if db_task.dimension == models.DimensionType.DIM_2D: - resolution = (properties['width'], properties['height'], extractor.get_image_size(frame_id)[2]) + resolution = (properties['width'], properties['height']) + orientation = properties.get('orientation', 1) else: resolution = extractor.get_image_size(frame_id) - img_sizes.append(resolution) + orientation = 1 + + imgs_info.append((resolution, orientation)) db_images.extend([ models.Image(data=db_data, path=os.path.relpath(path, upload_dir), frame=frame, width=w, height=h, orientation=o) - for (path, frame), (w, h, o) in zip(chunk_paths, img_sizes) + for (path, frame), ((w, h), o) in zip(chunk_paths, imgs_info) ]) if db_data.storage_method == models.StorageMethodChoice.FILE_SYSTEM or not settings.USE_CACHE: From b35abe486227d536fd17674c7a2d484f488b65bd Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 29 Mar 2022 10:35:58 +0300 Subject: [PATCH 03/19] Added for chunks --- cvat/apps/engine/media_extractors.py | 16 ++++++++++------ cvat/apps/engine/task.py | 6 +++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 5866c297644..34ef7571e77 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -566,6 +566,10 @@ def __init__(self, quality, dimension=DimensionType.DIM_2D): @staticmethod def _compress_image(image_path, quality): image = image_path.to_image() if isinstance(image_path, av.VideoFrame) else Image.open(image_path) + try: + orientation = image._getexif().get(274, 1) if image._getexif() else 1 + except Exception: + orientation = 1 # Ensure image data fits into 8bit per pixel before RGB conversion as PIL clips values on conversion if image.mode == "I": # Image mode is 32bit integer pixels. @@ -580,7 +584,7 @@ def _compress_image(image_path, quality): buf.seek(0) width, height = converted_image.size converted_image.close() - return width, height, buf + return width, height, orientation, buf @abstractmethod def save_as_chunk(self, images, chunk_path): @@ -601,23 +605,23 @@ def save_as_chunk(self, images, chunk_path): class ZipCompressedChunkWriter(IChunkWriter): def save_as_chunk(self, images, chunk_path): - image_sizes = [] + image_sizes_orientations = [] with zipfile.ZipFile(chunk_path, 'x') as zip_chunk: for idx, (image, _, _) in enumerate(images): if self._dimension == DimensionType.DIM_2D: - w, h, image_buf = self._compress_image(image, self._image_quality) + w, h, o, image_buf = self._compress_image(image, self._image_quality) extension = "jpeg" else: image_buf = open(image, "rb") if isinstance(image, str) else image properties = ValidateDimension.get_pcd_properties(image_buf) - w, h = int(properties["WIDTH"]), int(properties["HEIGHT"]) + w, h, o = int(properties["WIDTH"]), int(properties["HEIGHT"]), 1 extension = "pcd" image_buf.seek(0, 0) image_buf = io.BytesIO(image_buf.read()) - image_sizes.append((w, h)) + image_sizes_orientations.append((w, h, o)) arcname = '{:06d}.{}'.format(idx, extension) zip_chunk.writestr(arcname, image_buf.getvalue()) - return image_sizes + return image_sizes_orientations class Mpeg4ChunkWriter(IChunkWriter): def __init__(self, quality=67): diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 775af18ad71..19a48438476 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -626,9 +626,9 @@ def _update_status(msg): path=os.path.relpath(data[1], upload_dir), frame=data[2], width=size[0], - height=size[1]) - - for data, size in zip(chunk_data, img_sizes) + height=size[1], + orientation=size[2], + ) for data, size in zip(chunk_data, img_sizes) ]) else: video_size = img_sizes[0] From cf41398f8369ee7c2df164bcc0699d8fd5ad8509 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 29 Mar 2022 11:23:08 +0300 Subject: [PATCH 04/19] Added CHANGELOG increased packages version --- CHANGELOG.md | 1 + cvat-canvas/package-lock.json | 4 ++-- cvat-canvas/package.json | 2 +- cvat-core/package-lock.json | 4 ++-- cvat-core/package.json | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fceccd4714a..c555a334952 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Permission error occured when accessing the JobCommits () - job assignee can remove or update any issue created by the task owner () - Bug: Incorrect point deletion with keyboard shortcut () +- Bug: Exif orientation information handled incorrectly () ### Security - TDB diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index 4c915d045a7..6ca22becc19 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,12 +1,12 @@ { "name": "cvat-canvas", - "version": "2.13.2", + "version": "2.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cvat-canvas", - "version": "2.13.2", + "version": "2.14.0", "license": "MIT", "dependencies": { "@types/polylabel": "^1.0.5", diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 4c5d7da82d5..ac8e9138360 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.13.2", + "version": "2.14.0", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 43b2323cafd..5dfb3959f38 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,12 +1,12 @@ { "name": "cvat-core", - "version": "5.0.1", + "version": "5.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cvat-core", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "axios": "^0.21.4", diff --git a/cvat-core/package.json b/cvat-core/package.json index 55f6408e92c..fddf989d128 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "5.0.1", + "version": "5.1.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "babel.config.js", "scripts": { From 051492e3ed38a92cd42d3de5ce2418c2e8a56a77 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 30 Mar 2022 13:20:56 +0300 Subject: [PATCH 05/19] Added consts to server --- cvat/apps/engine/media_extractors.py | 35 +++++++++++++++++++++------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 34ef7571e77..96d1fd690f4 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -2,14 +2,17 @@ # # SPDX-License-Identifier: MIT +from ast import Or import os import tempfile import shutil +from tkinter.font import NORMAL import zipfile import io import itertools import struct from abc import ABC, abstractmethod +from enum import Enum from contextlib import closing import av @@ -29,6 +32,9 @@ from cvat.apps.engine.mime_types import mimetypes from utils.dataset_manifest import VideoManifestManager, ImageManifestManager +ORIENTATION_EXIF_TAG = 274 + + def get_mime(name): for type_name, type_def in MEDIA_TYPES.items(): if type_def['has_mime_type'](name): @@ -85,19 +91,32 @@ def get_progress(self, pos): @staticmethod def _get_preview(obj): PREVIEW_SIZE = (256, 256) + class ORIENTATION(Enum): + NORMAL_HORIZONTAL=1 + MIRROR_HORIZONTAL=2 + NORAMAL_180_ROTATED=3 + MIRROR_VERTICAL=4 + MIRROR_HORIZONTAL_270_ROTATED=5 + NORAMAL_90_ROTATED=6 + MIRROR_HORIZONTAL_90_ROTATED=7 + NORAMAL_270_ROTATED=8 + if isinstance(obj, io.IOBase): preview = Image.open(obj) else: preview = obj preview.thumbnail(PREVIEW_SIZE) - orientation = preview._getexif().get(274, 1) if preview._getexif() else 1 - if orientation in [3, 4]: + orientation = preview._getexif().get(ORIENTATION_EXIF_TAG, 1) if preview._getexif() else 1 + if orientation in [ORIENTATION.NORAMAL_180_ROTATED, ORIENTATION.MIRROR_VERTICAL]: preview = preview.rotate(180, expand=True) - elif orientation in [5, 8]: + elif orientation in [ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED, ORIENTATION.NORAMAL_270_ROTATED]: preview = preview.rotate(90, expand=True) - elif orientation in [6, 7]: + elif orientation in [ORIENTATION.NORAMAL_90_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED]: preview = preview.rotate(270, expand=True) - if orientation in [2, 4, 5 ,7]: + if orientation in [ + ORIENTATION.MIRROR_HORIZONTAL, ORIENTATION.MIRROR_VERTICAL, + ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED ,ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED, + ]: preview = preview.transpose(Image.FLIP_LEFT_RIGHT) return preview.convert('RGB') @@ -193,7 +212,7 @@ def get_orientation(self, i): raise NotImplementedError() img = Image.open(self._source_path[i]) try: - return img._getexif().get(274, 1) if img._getexif() else 1 + return img._getexif().get(ORIENTATION_EXIF_TAG, 1) if img._getexif() else 1 except Exception: return 1 @@ -344,7 +363,7 @@ def get_orientation(self, i): raise NotImplementedError() img = Image.open(self._source_path[i]) try: - return img._getexif().get(274, 1) if img._getexif() else 1 + return img._getexif().get(ORIENTATION_EXIF_TAG, 1) if img._getexif() else 1 except Exception: return 1 @@ -567,7 +586,7 @@ def __init__(self, quality, dimension=DimensionType.DIM_2D): def _compress_image(image_path, quality): image = image_path.to_image() if isinstance(image_path, av.VideoFrame) else Image.open(image_path) try: - orientation = image._getexif().get(274, 1) if image._getexif() else 1 + orientation = image._getexif().get(ORIENTATION_EXIF_TAG, 1) if image._getexif() else 1 except Exception: orientation = 1 # Ensure image data fits into 8bit per pixel before RGB conversion as PIL clips values on conversion From 2b862f7611faf9d846ad47d569e3c31e4f27e256 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 30 Mar 2022 13:27:00 +0300 Subject: [PATCH 06/19] Added comments for orientation --- cvat-canvas/src/typescript/canvasView.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 8360eaabf12..2df73797899 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1276,12 +1276,20 @@ export class CanvasViewImpl implements CanvasView, Listener { if (ctx) { switch (image.orientation) { + // 1 - normal nothing to do + // 2 - horizontal mirrored case 2: ctx.transform(-1, 0, 0, 1, width, 0); break; + // 3 - 180 cw rotated case 3: ctx.transform(-1, 0, 0, -1, width, height); break; + // 4 - vertical mirrored case 4: ctx.transform(1, 0, 0, -1, 0, height); break; + // 5 - horizontal mirrored and 270 cw rotated case 5: ctx.transform(0, 1, 1, 0, 0, 0); break; + // 6 - 90 cw rotated case 6: ctx.transform(0, 1, -1, 0, width, 0); break; + // 7 - horizontal mirrored and 90 cw rotated case 7: ctx.transform(0, -1, -1, 0, width, height); break; + // 8 - 270 cw rotated case 8: ctx.transform(0, -1, 1, 0, 0, height); break; default: break; } From 8799b2da26d61abe741f280b064e8e1b10cba03d Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Wed, 30 Mar 2022 19:18:13 +0300 Subject: [PATCH 07/19] Use prerotated images --- cvat-canvas/package-lock.json | 4 +- cvat-canvas/package.json | 2 +- cvat-canvas/src/typescript/canvasModel.ts | 13 +-- cvat-canvas/src/typescript/canvasView.ts | 34 ++------ cvat-core/package-lock.json | 4 +- cvat-core/package.json | 2 +- cvat-core/src/frames.js | 12 --- cvat/apps/engine/media_extractors.py | 84 +++++++------------ .../migrations/0052_exif_orientation.py | 18 ---- cvat/apps/engine/models.py | 1 - cvat/apps/engine/serializers.py | 1 - cvat/apps/engine/task.py | 17 ++-- cvat/apps/engine/views.py | 1 - 13 files changed, 54 insertions(+), 139 deletions(-) delete mode 100644 cvat/apps/engine/migrations/0052_exif_orientation.py diff --git a/cvat-canvas/package-lock.json b/cvat-canvas/package-lock.json index 6ca22becc19..4c915d045a7 100644 --- a/cvat-canvas/package-lock.json +++ b/cvat-canvas/package-lock.json @@ -1,12 +1,12 @@ { "name": "cvat-canvas", - "version": "2.14.0", + "version": "2.13.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cvat-canvas", - "version": "2.14.0", + "version": "2.13.2", "license": "MIT", "dependencies": { "@types/polylabel": "^1.0.5", diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index ac8e9138360..4c5d7da82d5 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -1,6 +1,6 @@ { "name": "cvat-canvas", - "version": "2.14.0", + "version": "2.13.2", "description": "Part of Computer Vision Annotation Tool which presents its canvas library", "main": "src/canvas.ts", "scripts": { diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts index dc7aeb13719..bc43af8fec4 100644 --- a/cvat-canvas/src/typescript/canvasModel.ts +++ b/cvat-canvas/src/typescript/canvasModel.ts @@ -13,7 +13,6 @@ export interface Size { export interface Image { renderWidth: number; renderHeight: number; - orientation: number; imageData: ImageData | CanvasImageSource; } @@ -427,15 +426,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel { } this.data.imageSize = { - height: frameData.orientation as number < 5 ? - frameData.height as number : frameData.width as number, - width: frameData.orientation as number < 5 ? - frameData.width as number : frameData.height as number, - }; - this.data.image = { - ...data, - orientation: frameData.orientation as number, + height: frameData.height as number, + width: frameData.width as number, }; + + this.data.image = data; this.notify(UpdateReasons.IMAGE_CHANGED); this.data.zLayer = zLayer; this.data.objects = objectStates; diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index 2df73797899..4cc1377f54b 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -1264,37 +1264,15 @@ export class CanvasViewImpl implements CanvasView, Listener { } else { this.loadingAnimation.classList.add('cvat_canvas_hidden'); const ctx = this.background.getContext('2d'); - const [width, height, renderWidth, renderHeight] = image.orientation < 5 ? [ - image.imageData.width as number, image.imageData.height as number, - image.renderWidth, image.renderHeight, - ] : [ - image.imageData.height as number, image.imageData.width as number, - image.renderHeight, image.renderWidth, - ]; - this.background.setAttribute('width', `${renderWidth}px`); - this.background.setAttribute('height', `${renderHeight}px`); + this.background.setAttribute('width', `${image.renderWidth}px`); + this.background.setAttribute('height', `${image.renderHeight}px`); if (ctx) { - switch (image.orientation) { - // 1 - normal nothing to do - // 2 - horizontal mirrored - case 2: ctx.transform(-1, 0, 0, 1, width, 0); break; - // 3 - 180 cw rotated - case 3: ctx.transform(-1, 0, 0, -1, width, height); break; - // 4 - vertical mirrored - case 4: ctx.transform(1, 0, 0, -1, 0, height); break; - // 5 - horizontal mirrored and 270 cw rotated - case 5: ctx.transform(0, 1, 1, 0, 0, 0); break; - // 6 - 90 cw rotated - case 6: ctx.transform(0, 1, -1, 0, width, 0); break; - // 7 - horizontal mirrored and 90 cw rotated - case 7: ctx.transform(0, -1, -1, 0, width, height); break; - // 8 - 270 cw rotated - case 8: ctx.transform(0, -1, 1, 0, 0, height); break; - default: break; - } if (image.imageData instanceof ImageData) { - ctx.scale(renderWidth / width, renderHeight / height); + ctx.scale( + image.renderWidth / image.imageData.width, + image.renderHeight / image.imageData.height, + ); ctx.putImageData(image.imageData, 0, 0); // Transformation matrix must not affect the putImageData() method. // By this reason need to redraw the image to apply scale. diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index db819671bcf..54bd177dc55 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,12 +1,12 @@ { "name": "cvat-core", - "version": "5.1.0", + "version": "5.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cvat-core", - "version": "5.1.0", + "version": "5.0.1", "license": "MIT", "dependencies": { "axios": "^0.21.4", diff --git a/cvat-core/package.json b/cvat-core/package.json index fddf989d128..55f6408e92c 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "5.1.0", + "version": "5.0.1", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "babel.config.js", "scripts": { diff --git a/cvat-core/src/frames.js b/cvat-core/src/frames.js index 7eadf1f62d8..53279522123 100644 --- a/cvat-core/src/frames.js +++ b/cvat-core/src/frames.js @@ -21,7 +21,6 @@ constructor({ width, height, - orientation, name, taskID, jobID, @@ -67,17 +66,6 @@ value: height, writable: false, }, - /** - * @name orientation - * @type {integer} - * @memberof module:API.cvat.classes.FrameData - * @readonly - * @instance - */ - orientation: { - value: orientation, - writable: false, - }, /** * task ID * @name tid diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 96d1fd690f4..28d5057115f 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -2,11 +2,9 @@ # # SPDX-License-Identifier: MIT -from ast import Or import os import tempfile import shutil -from tkinter.font import NORMAL import zipfile import io import itertools @@ -35,6 +33,17 @@ ORIENTATION_EXIF_TAG = 274 +class ORIENTATION(Enum): + NORMAL_HORIZONTAL=1 + MIRROR_HORIZONTAL=2 + NORAMAL_180_ROTATED=3 + MIRROR_VERTICAL=4 + MIRROR_HORIZONTAL_270_ROTATED=5 + NORAMAL_90_ROTATED=6 + MIRROR_HORIZONTAL_90_ROTATED=7 + NORAMAL_270_ROTATED=8 + + def get_mime(name): for type_name, type_def in MEDIA_TYPES.items(): if type_def['has_mime_type'](name): @@ -68,6 +77,21 @@ def sort(images, sorting_method=SortingMethod.LEXICOGRAPHICAL, func=None): else: raise NotImplementedError() +def rotate_within_exif(img: Image): + orientation = img.getexif().get(ORIENTATION_EXIF_TAG, 1) + if orientation in [ORIENTATION.NORAMAL_180_ROTATED, ORIENTATION.MIRROR_VERTICAL]: + img = img.rotate(180, expand=True) + elif orientation in [ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED, ORIENTATION.NORAMAL_270_ROTATED]: + img = img.rotate(90, expand=True) + elif orientation in [ORIENTATION.NORAMAL_90_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_90_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): def __init__(self, source_path, step, start, stop, dimension): self._source_path = source_path @@ -91,33 +115,13 @@ def get_progress(self, pos): @staticmethod def _get_preview(obj): PREVIEW_SIZE = (256, 256) - class ORIENTATION(Enum): - NORMAL_HORIZONTAL=1 - MIRROR_HORIZONTAL=2 - NORAMAL_180_ROTATED=3 - MIRROR_VERTICAL=4 - MIRROR_HORIZONTAL_270_ROTATED=5 - NORAMAL_90_ROTATED=6 - MIRROR_HORIZONTAL_90_ROTATED=7 - NORAMAL_270_ROTATED=8 if isinstance(obj, io.IOBase): preview = Image.open(obj) else: preview = obj preview.thumbnail(PREVIEW_SIZE) - orientation = preview._getexif().get(ORIENTATION_EXIF_TAG, 1) if preview._getexif() else 1 - if orientation in [ORIENTATION.NORAMAL_180_ROTATED, ORIENTATION.MIRROR_VERTICAL]: - preview = preview.rotate(180, expand=True) - elif orientation in [ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED, ORIENTATION.NORAMAL_270_ROTATED]: - preview = preview.rotate(90, expand=True) - elif orientation in [ORIENTATION.NORAMAL_90_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED]: - preview = preview.rotate(270, expand=True) - if orientation in [ - ORIENTATION.MIRROR_HORIZONTAL, ORIENTATION.MIRROR_VERTICAL, - ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED ,ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED, - ]: - preview = preview.transpose(Image.FLIP_LEFT_RIGHT) + preview = rotate_within_exif(preview) return preview.convert('RGB') @@ -125,10 +129,6 @@ class ORIENTATION(Enum): def get_image_size(self, i): pass - @abstractmethod - def get_orientation(self, i): - pass - def __len__(self): return len(self.frame_range) @@ -207,16 +207,6 @@ def get_image_size(self, i): img = Image.open(self._source_path[i]) return img.width, img.height - def get_orientation(self, i): - if self._dimension == DimensionType.DIM_3D: - raise NotImplementedError() - img = Image.open(self._source_path[i]) - try: - return img._getexif().get(ORIENTATION_EXIF_TAG, 1) if img._getexif() else 1 - except Exception: - return 1 - - def reconcile(self, source_files, step=1, start=0, stop=None, dimension=DimensionType.DIM_2D, sorting_method=None): # FIXME ImageListReader.__init__(self, @@ -358,15 +348,6 @@ def get_image_size(self, i): img = Image.open(io.BytesIO(self._zip_source.read(self._source_path[i]))) return img.width, img.height - def get_orientation(self, i): - if self._dimension == DimensionType.DIM_3D: - raise NotImplementedError() - img = Image.open(self._source_path[i]) - try: - return img._getexif().get(ORIENTATION_EXIF_TAG, 1) if img._getexif() else 1 - except Exception: - return 1 - def get_image(self, i): if self._dimension == DimensionType.DIM_3D: return self.get_path(i) @@ -585,10 +566,7 @@ def __init__(self, quality, dimension=DimensionType.DIM_2D): @staticmethod def _compress_image(image_path, quality): image = image_path.to_image() if isinstance(image_path, av.VideoFrame) else Image.open(image_path) - try: - orientation = image._getexif().get(ORIENTATION_EXIF_TAG, 1) if image._getexif() else 1 - except Exception: - orientation = 1 + image = rotate_within_exif(image) # Ensure image data fits into 8bit per pixel before RGB conversion as PIL clips values on conversion if image.mode == "I": # Image mode is 32bit integer pixels. @@ -603,7 +581,7 @@ def _compress_image(image_path, quality): buf.seek(0) width, height = converted_image.size converted_image.close() - return width, height, orientation, buf + return width, height, buf @abstractmethod def save_as_chunk(self, images, chunk_path): @@ -628,12 +606,12 @@ def save_as_chunk(self, images, chunk_path): with zipfile.ZipFile(chunk_path, 'x') as zip_chunk: for idx, (image, _, _) in enumerate(images): if self._dimension == DimensionType.DIM_2D: - w, h, o, image_buf = self._compress_image(image, self._image_quality) + w, h, image_buf = self._compress_image(image, self._image_quality) extension = "jpeg" else: image_buf = open(image, "rb") if isinstance(image, str) else image properties = ValidateDimension.get_pcd_properties(image_buf) - w, h, o = int(properties["WIDTH"]), int(properties["HEIGHT"]), 1 + w, h = int(properties["WIDTH"]), int(properties["HEIGHT"]) extension = "pcd" image_buf.seek(0, 0) image_buf = io.BytesIO(image_buf.read()) diff --git a/cvat/apps/engine/migrations/0052_exif_orientation.py b/cvat/apps/engine/migrations/0052_exif_orientation.py deleted file mode 100644 index 03deb3895e3..00000000000 --- a/cvat/apps/engine/migrations/0052_exif_orientation.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.12 on 2022-03-25 09:49 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('engine', '0051_auto_20220220_1824'), - ] - - operations = [ - migrations.AddField( - model_name='image', - name='orientation', - field=models.PositiveIntegerField(default=1), - ), - ] diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index ebb22ba3dda..ff86d304574 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -223,7 +223,6 @@ class Image(models.Model): frame = models.PositiveIntegerField() width = models.PositiveIntegerField() height = models.PositiveIntegerField() - orientation = models.PositiveIntegerField(default=1) class Meta: default_permissions = () diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 470053feb38..1ca899667be 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -672,7 +672,6 @@ class AboutSerializer(serializers.Serializer): class FrameMetaSerializer(serializers.Serializer): width = serializers.IntegerField() height = serializers.IntegerField() - orientation = serializers.IntegerField() name = serializers.CharField(max_length=1024) has_related_context = serializers.BooleanField() diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index 19a48438476..33464493de3 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -584,7 +584,7 @@ def _update_status(msg): counter = itertools.count() for _, chunk_frames in itertools.groupby(extractor.frame_range, lambda x: next(counter) // db_data.chunk_size): chunk_paths = [(extractor.get_path(i), i) for i in chunk_frames] - imgs_info = [] + img_sizes = [] for chunk_path, frame_id in chunk_paths: properties = manifest[manifest_index(frame_id)] @@ -594,18 +594,15 @@ def _update_status(msg): raise Exception('Incorrect file mapping to manifest content') if db_task.dimension == models.DimensionType.DIM_2D: resolution = (properties['width'], properties['height']) - orientation = properties.get('orientation', 1) else: resolution = extractor.get_image_size(frame_id) - orientation = 1 - - imgs_info.append((resolution, orientation)) + img_sizes.append(resolution) db_images.extend([ models.Image(data=db_data, path=os.path.relpath(path, upload_dir), - frame=frame, width=w, height=h, orientation=o) - for (path, frame), ((w, h), o) in zip(chunk_paths, imgs_info) + frame=frame, width=w, height=h) + for (path, frame), (w, h) in zip(chunk_paths, img_sizes) ]) if db_data.storage_method == models.StorageMethodChoice.FILE_SYSTEM or not settings.USE_CACHE: @@ -626,9 +623,9 @@ def _update_status(msg): path=os.path.relpath(data[1], upload_dir), frame=data[2], width=size[0], - height=size[1], - orientation=size[2], - ) for data, size in zip(chunk_data, img_sizes) + height=size[1]) + + for data, size in zip(chunk_data, img_sizes) ]) else: video_size = img_sizes[0] diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 7b582be5288..f44a73ac19e 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -931,7 +931,6 @@ def data_info(request, pk): frame_meta = [{ 'width': item.width, 'height': item.height, - 'orientation': item.orientation, 'name': item.path, 'has_related_context': hasattr(item, 'related_files') and item.related_files.exists() } for item in media] From 16f128cef751dc129c4cce1f03bbf8b910f39f07 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Thu, 31 Mar 2022 07:50:29 +0300 Subject: [PATCH 08/19] Fixed parsing sizes --- cvat/apps/engine/media_extractors.py | 21 ++++++++++++++------- utils/dataset_manifest/core.py | 8 ++++++-- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 28d5057115f..9b408ed0144 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -10,7 +10,6 @@ import itertools import struct from abc import ABC, abstractmethod -from enum import Enum from contextlib import closing import av @@ -33,7 +32,7 @@ ORIENTATION_EXIF_TAG = 274 -class ORIENTATION(Enum): +class ORIENTATION: NORMAL_HORIZONTAL=1 MIRROR_HORIZONTAL=2 NORAMAL_180_ROTATED=3 @@ -205,7 +204,11 @@ def get_image_size(self, i): properties = ValidateDimension.get_pcd_properties(f) return int(properties["WIDTH"]), int(properties["HEIGHT"]) img = Image.open(self._source_path[i]) - return img.width, img.height + width, height = img.width, img.height + orientation = img.getexif().get(ORIENTATION_EXIF_TAG, 1) + if orientation > 4: + width, height = height, width + return width, height def reconcile(self, source_files, step=1, start=0, stop=None, dimension=DimensionType.DIM_2D, sorting_method=None): # FIXME @@ -346,7 +349,11 @@ def get_image_size(self, i): properties = ValidateDimension.get_pcd_properties(f) return int(properties["WIDTH"]), int(properties["HEIGHT"]) img = Image.open(io.BytesIO(self._zip_source.read(self._source_path[i]))) - return img.width, img.height + width, height = img.width, img.height + orientation = img.getexif().get(ORIENTATION_EXIF_TAG, 1) + if orientation > 4: + width, height = height, width + return width, height def get_image(self, i): if self._dimension == DimensionType.DIM_3D: @@ -602,7 +609,7 @@ def save_as_chunk(self, images, chunk_path): class ZipCompressedChunkWriter(IChunkWriter): def save_as_chunk(self, images, chunk_path): - image_sizes_orientations = [] + image_sizes = [] with zipfile.ZipFile(chunk_path, 'x') as zip_chunk: for idx, (image, _, _) in enumerate(images): if self._dimension == DimensionType.DIM_2D: @@ -615,10 +622,10 @@ def save_as_chunk(self, images, chunk_path): extension = "pcd" image_buf.seek(0, 0) image_buf = io.BytesIO(image_buf.read()) - image_sizes_orientations.append((w, h, o)) + image_sizes.append((w, h)) arcname = '{:06d}.{}'.format(idx, extension) zip_chunk.writestr(arcname, image_buf.getvalue()) - return image_sizes_orientations + return image_sizes class Mpeg4ChunkWriter(IChunkWriter): def __init__(self, quality=67): diff --git a/utils/dataset_manifest/core.py b/utils/dataset_manifest/core.py index d2e9da5ff02..36d70eab927 100644 --- a/utils/dataset_manifest/core.py +++ b/utils/dataset_manifest/core.py @@ -194,14 +194,18 @@ def __iter__(self): if idx in self.range_: image = next(sources) 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 \ else os.path.basename(image) name, extension = os.path.splitext(img_name) + width, height = img.width, img.height + if orientation > 4: + width, height = height, width image_properties = { 'name': name.replace('\\', '/'), 'extension': extension, - 'width': img.width, - 'height': img.height, + 'width': width, + 'height': height, } if self._meta and img_name in self._meta: image_properties['meta'] = self._meta[img_name] From b9b92d9fe3d2614a9e6b41095d468584e4ab066d Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Thu, 31 Mar 2022 12:16:36 +0300 Subject: [PATCH 09/19] Fixed missed method --- cvat/apps/engine/media_extractors.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 9b408ed0144..5d44928bba2 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -468,6 +468,10 @@ def get_preview(self): ).to_image() ) + def get_image_size(self, i): + image = (next(iter(self)))[0] + return image.width, image.height + class FragmentMediaReader: def __init__(self, chunk_number, chunk_size, start, stop, step=1): self._start = start From 19844e9e4a7faa11f9d8e8077568cb58634a21f2 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 1 Apr 2022 09:12:27 +0300 Subject: [PATCH 10/19] Update cvat/apps/engine/media_extractors.py Co-authored-by: Andrey Zhavoronkov --- cvat/apps/engine/media_extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 5d44928bba2..6887ff0bc6c 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -77,7 +77,7 @@ def sort(images, sorting_method=SortingMethod.LEXICOGRAPHICAL, func=None): raise NotImplementedError() def rotate_within_exif(img: Image): - orientation = img.getexif().get(ORIENTATION_EXIF_TAG, 1) + orientation = img.getexif().get(ORIENTATION_EXIF_TAG, Orientation.NORMAL_HORIZONTAL) if orientation in [ORIENTATION.NORAMAL_180_ROTATED, ORIENTATION.MIRROR_VERTICAL]: img = img.rotate(180, expand=True) elif orientation in [ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED, ORIENTATION.NORAMAL_270_ROTATED]: From 6af6b0b830365c9f622904dc1330f3cb52a355c2 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Fri, 1 Apr 2022 11:29:25 +0300 Subject: [PATCH 11/19] Fixed comments --- cvat/apps/engine/media_extractors.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 6887ff0bc6c..ff4af5d5708 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -9,6 +9,7 @@ import io import itertools import struct +from enum import IntEnum from abc import ABC, abstractmethod from contextlib import closing @@ -32,7 +33,7 @@ ORIENTATION_EXIF_TAG = 274 -class ORIENTATION: +class ORIENTATION(IntEnum): NORMAL_HORIZONTAL=1 MIRROR_HORIZONTAL=2 NORAMAL_180_ROTATED=3 @@ -76,8 +77,14 @@ def sort(images, sorting_method=SortingMethod.LEXICOGRAPHICAL, func=None): else: raise NotImplementedError() +def image_size_within_orientation(img: Image): + orientation = img.getexif().get(ORIENTATION_EXIF_TAG, 1) + 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) + orientation = img.getexif().get(ORIENTATION_EXIF_TAG, ORIENTATION.NORMAL_HORIZONTAL) if orientation in [ORIENTATION.NORAMAL_180_ROTATED, ORIENTATION.MIRROR_VERTICAL]: img = img.rotate(180, expand=True) elif orientation in [ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED, ORIENTATION.NORAMAL_270_ROTATED]: @@ -204,11 +211,7 @@ def get_image_size(self, i): properties = ValidateDimension.get_pcd_properties(f) return int(properties["WIDTH"]), int(properties["HEIGHT"]) img = Image.open(self._source_path[i]) - width, height = img.width, img.height - orientation = img.getexif().get(ORIENTATION_EXIF_TAG, 1) - if orientation > 4: - width, height = height, width - return width, 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): # FIXME @@ -349,11 +352,7 @@ def get_image_size(self, i): properties = ValidateDimension.get_pcd_properties(f) return int(properties["WIDTH"]), int(properties["HEIGHT"]) img = Image.open(io.BytesIO(self._zip_source.read(self._source_path[i]))) - width, height = img.width, img.height - orientation = img.getexif().get(ORIENTATION_EXIF_TAG, 1) - if orientation > 4: - width, height = height, width - return width, height + return image_size_within_orientation(img) def get_image(self, i): if self._dimension == DimensionType.DIM_3D: From 99f96628061a08c28adb8def531e527ee4eb1da2 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 4 Apr 2022 12:16:12 +0300 Subject: [PATCH 12/19] Fixed orientation --- cvat/apps/engine/media_extractors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index ff4af5d5708..b9317fb67e7 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -87,9 +87,9 @@ def rotate_within_exif(img: Image): orientation = img.getexif().get(ORIENTATION_EXIF_TAG, ORIENTATION.NORMAL_HORIZONTAL) if orientation in [ORIENTATION.NORAMAL_180_ROTATED, ORIENTATION.MIRROR_VERTICAL]: img = img.rotate(180, expand=True) - elif orientation in [ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED, ORIENTATION.NORAMAL_270_ROTATED]: + elif orientation in [ORIENTATION.NORAMAL_270_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED]: img = img.rotate(90, expand=True) - elif orientation in [ORIENTATION.NORAMAL_90_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED]: + elif orientation in [ORIENTATION.NORAMAL_90_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED]: img = img.rotate(270, expand=True) if orientation in [ ORIENTATION.MIRROR_HORIZONTAL, ORIENTATION.MIRROR_VERTICAL, From 5b205cc68feddd6298239e04660c21dcd2655e89 Mon Sep 17 00:00:00 2001 From: Nikita Manovich Date: Mon, 4 Apr 2022 14:56:21 +0300 Subject: [PATCH 13/19] Update cvat/apps/engine/media_extractors.py --- cvat/apps/engine/media_extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index b9317fb67e7..f11318dcf5e 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -78,7 +78,7 @@ def sort(images, sorting_method=SortingMethod.LEXICOGRAPHICAL, func=None): raise NotImplementedError() def image_size_within_orientation(img: Image): - orientation = img.getexif().get(ORIENTATION_EXIF_TAG, 1) + orientation = img.getexif().get(ORIENTATION_EXIF_TAG, ORIENTATION.NORMAL_HORIZONTAL) if orientation > 4: return img.height, img.width return img.width, img.height From 1b6e47bef921108025b5ac96a9dfe88bf17b72c9 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 4 Apr 2022 19:38:18 +0300 Subject: [PATCH 14/19] Update cvat/apps/engine/media_extractors.py Co-authored-by: Maria Khrustaleva --- cvat/apps/engine/media_extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index f11318dcf5e..ec33573eb3e 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -36,7 +36,7 @@ class ORIENTATION(IntEnum): NORMAL_HORIZONTAL=1 MIRROR_HORIZONTAL=2 - NORAMAL_180_ROTATED=3 + NORMAL_180_ROTATED=3 MIRROR_VERTICAL=4 MIRROR_HORIZONTAL_270_ROTATED=5 NORAMAL_90_ROTATED=6 From d88853ecdec40e39b84c8608482989426f4b537e Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 4 Apr 2022 19:38:27 +0300 Subject: [PATCH 15/19] Update cvat/apps/engine/media_extractors.py Co-authored-by: Maria Khrustaleva --- cvat/apps/engine/media_extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index ec33573eb3e..5bd3f1c29ce 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -85,7 +85,7 @@ def image_size_within_orientation(img: Image): def rotate_within_exif(img: Image): orientation = img.getexif().get(ORIENTATION_EXIF_TAG, ORIENTATION.NORMAL_HORIZONTAL) - if orientation in [ORIENTATION.NORAMAL_180_ROTATED, ORIENTATION.MIRROR_VERTICAL]: + if orientation in [ORIENTATION.NORMAL_180_ROTATED, ORIENTATION.MIRROR_VERTICAL]: img = img.rotate(180, expand=True) elif orientation in [ORIENTATION.NORAMAL_270_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED]: img = img.rotate(90, expand=True) From 06d32b77f8422ef009a6914dd158352a457e77a3 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 4 Apr 2022 19:38:38 +0300 Subject: [PATCH 16/19] Update cvat/apps/engine/media_extractors.py Co-authored-by: Maria Khrustaleva --- cvat/apps/engine/media_extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 5bd3f1c29ce..ac61ef8cf1b 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -39,7 +39,7 @@ class ORIENTATION(IntEnum): NORMAL_180_ROTATED=3 MIRROR_VERTICAL=4 MIRROR_HORIZONTAL_270_ROTATED=5 - NORAMAL_90_ROTATED=6 + NORMAL_90_ROTATED=6 MIRROR_HORIZONTAL_90_ROTATED=7 NORAMAL_270_ROTATED=8 From 46f206a5eb86d930f7a3f692cb6968443f62785b Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 4 Apr 2022 19:38:46 +0300 Subject: [PATCH 17/19] Update cvat/apps/engine/media_extractors.py Co-authored-by: Maria Khrustaleva --- cvat/apps/engine/media_extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index ac61ef8cf1b..d031ed408c0 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -41,7 +41,7 @@ class ORIENTATION(IntEnum): MIRROR_HORIZONTAL_270_ROTATED=5 NORMAL_90_ROTATED=6 MIRROR_HORIZONTAL_90_ROTATED=7 - NORAMAL_270_ROTATED=8 + NORMAL_270_ROTATED=8 def get_mime(name): From f7dc27ba3ccb82d88c6ffa71cac29bec8faec555 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 4 Apr 2022 19:38:57 +0300 Subject: [PATCH 18/19] Update cvat/apps/engine/media_extractors.py Co-authored-by: Maria Khrustaleva --- cvat/apps/engine/media_extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index d031ed408c0..1bac40308ac 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -87,7 +87,7 @@ 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.NORAMAL_270_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED]: + elif orientation in [ORIENTATION.NORMAL_270_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_90_ROTATED]: img = img.rotate(90, expand=True) elif orientation in [ORIENTATION.NORAMAL_90_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED]: img = img.rotate(270, expand=True) From 6330f5d55e1f0aec30b6e6b4ef9d79c944e39a80 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Mon, 4 Apr 2022 19:39:03 +0300 Subject: [PATCH 19/19] Update cvat/apps/engine/media_extractors.py Co-authored-by: Maria Khrustaleva --- cvat/apps/engine/media_extractors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvat/apps/engine/media_extractors.py b/cvat/apps/engine/media_extractors.py index 1bac40308ac..1a7fa04d2c7 100644 --- a/cvat/apps/engine/media_extractors.py +++ b/cvat/apps/engine/media_extractors.py @@ -89,7 +89,7 @@ def rotate_within_exif(img: Image): 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.NORAMAL_90_ROTATED, ORIENTATION.MIRROR_HORIZONTAL_270_ROTATED]: + 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,