Repository: openvinotoolkit/datumaro

Issue:
Add transform to change dataset image resolution
I annotated the dataset using cvat. But I found that the resolution of my image was too high. Is there any way to reduce the resolution of the marked image?


Discussion:
@kankanzheli , you can try Datumaro (https://github.com/openvinotoolkit/datumaro) for the task.

@zhiltsov-max , could you please advise?
Hi, could you say the format name?
coco
I am working in coco format. And I did the labeled work by cvat.
Ok, try the following script:

```python
from datumaro.components.dataset import Dataset
from datumaro.components.annotation import Bbox, Label, Mask, Points, PolyLine, Polygon, RleMask
from datumaro.components.extractor import DatasetItem, ItemTransform
from datumaro.util.mask_tools import mask_to_rle

import pycocotools.mask as mask_util

import cv2
import numpy as np

class ScaleTransform(ItemTransform):
    def __init__(self, extractor, scale=1):
        super().__init__(extractor)

        self._scale = scale

    def transform_item(self, item):
        if item.has_image and item.image.has_data:
            method = cv2.INTER_LINEAR if self._scale < 1 else cv2.INTER_LANCZOS4
            image = item.image.data / 255.0
            rescaled_image = cv2.resize(image, (0, 0),
                fx=self._scale, fy=self._scale, interpolation=method)

        rescaled_annotations = []
        for ann in item.annotations:
            if isinstance(ann, Bbox):
                rescaled_annotations.append(ann.wrap(
                    x=ann.x * self._scale,
                    y=ann.y * self._scale,
                    w=ann.w * self._scale,
                    h=ann.h * self._scale,
                ))
            elif isinstance(ann, (Polygon, Points, PolyLine)):
                rescaled_annotations.append(ann.wrap(
                    points=[p * self._scale for p in ann.points]
                ))
            elif isinstance(ann, Mask):
                # Can only use NEAREST for masks
                rescaled_mask = cv2.resize(ann.image, (0, 0),
                    fx=self._scale, fy=self._scale,
                    interpolation=cv2.INTER_NEAREST)

                if isinstance(ann, RleMask):
                    rle = mask_to_rle(rescaled_mask)
                    rescaled_annotations.append(ann.wrap(
                        rle=mask_util.frPyObjects(rle, *rle['size'])))
                else:
                    rescaled_annotations.append(ann.wrap(image=rescaled_mask))
            else:
                rescaled_annotations.append(ann)

        return self.wrap_item(item,
            image=rescaled_image, annotations=rescaled_annotations)

dataset = Dataset.import_from('path/', 'coco')
dataset.transform(ScaleTransform, scale=0.5) # you can specify the desired scale here
dataset.export('path2/', 'coco', save_images=True)
```

@kirill-sizov, please integrate this transform into Datumaro, it looks quite useful.
How to get the rescaled image at the same time?
I use the annotation result (JSON file) obtained by this method on the image whose resolution I modified by other methods. At this time, the annotation result is misaligne
![2021-11-23 20-21-32屏幕截图](https://user-images.githubusercontent.com/58200245/143015578-48c4aeb6-1b40-42b3-9a34-799e5168ce0f.png)
d.
I only modify these four parameters during use:

rescaled_ image = cv2.resize(image, (1280, 720)

rescaled_mask = cv2.resize(ann.image, (1280, 720),

dataset = Dataset.import_from('/home/zhaowentao/coco/', 'coco')

dataset.export('/home/zhaowentao/1/', 'coco', save_images=True)
If you have fixed resulting image size, try this one instead:

```python
from datumaro.components.dataset import Dataset
from datumaro.components.annotation import Bbox, Label, Mask, Points, PolyLine, Polygon, RleMask
from datumaro.components.extractor import DatasetItem, ItemTransform
from datumaro.util.mask_tools import mask_to_rle
from datumaro.util import take_by

import pycocotools.mask as mask_util

import cv2
import numpy as np

class ScaleTransform(ItemTransform):
    def __init__(self, extractor, size):
        super().__init__(extractor)

        self._size = size

    def transform_item(self, item):
        assert item.has_image
        h, w = item.image.size
        xscale = self._size[0] / w
        yscale = self._size[1] / h

        if item.image.has_data:
            method = cv2.INTER_LINEAR if (xscale * yscale) < 1 else cv2.INTER_LANCZOS4
            image = item.image.data / 255.0
            rescaled_image = cv2.resize(image, (0, 0),
                fx=xscale, fy=yscale, interpolation=method)

        rescaled_annotations = []
        for ann in item.annotations:
            if isinstance(ann, Bbox):
                rescaled_annotations.append(ann.wrap(
                    x=ann.x * xscale,
                    y=ann.y * yscale,
                    w=ann.w * xscale,
                    h=ann.h * yscale,
                ))
            elif isinstance(ann, (Polygon, Points, PolyLine)):
                rescaled_annotations.append(ann.wrap(
                    points=[p
                        for t in ((x * xscale, y * yscale) for x, y in take_by(ann.points, 2))
                        for p in t
                    ]
                ))
            elif isinstance(ann, Mask):
                # Can only use NEAREST for masks
                rescaled_mask = cv2.resize(ann.image, (0, 0),
                    fx=xscale, fy=yscale,
                    interpolation=cv2.INTER_NEAREST)

                if isinstance(ann, RleMask):
                    rle = mask_to_rle(rescaled_mask)
                    rescaled_annotations.append(ann.wrap(
                        rle=mask_util.frPyObjects(rle, *rle['size'])))
                else:
                    rescaled_annotations.append(ann.wrap(image=rescaled_mask))
            else:
                rescaled_annotations.append(ann)

        return self.wrap_item(item,
            image=rescaled_image,
            annotations=rescaled_annotations)

dataset = Dataset.import_from('path/', 'coco')
dataset.transform(ScaleTransform, size=(1280, 720))
dataset.export('path2/', 'coco', save_images=True)


```
error is happened:
    File "Change_resolution.py", line 41, in transform_item

    points=[p for p in (x * xscale, y * yscale)

NameError: name 'x' is not defined
I've updated the answer above.
It is worked. Thanks！
I'll keep it open as a feature request.

Resolving patch:
diff --git a/datumaro/plugins/transforms.py b/datumaro/plugins/transforms.py
--- a/datumaro/plugins/transforms.py
+++ b/datumaro/plugins/transforms.py
@@ -12,17 +12,19 @@
 import random
 import re
 
+import cv2
+import numpy as np
 import pycocotools.mask as mask_utils
 
 from datumaro.components.annotation import (
-    AnnotationType, Bbox, Label, LabelCategories, MaskCategories,
-    PointsCategories, Polygon, RleMask,
+    AnnotationType, Bbox, Caption, Label, LabelCategories, Mask, MaskCategories,
+    Points, PointsCategories, Polygon, PolyLine, RleMask,
 )
 from datumaro.components.cli_plugin import CliPlugin
 from datumaro.components.extractor import (
     DEFAULT_SUBSET_NAME, IExtractor, ItemTransform, Transform,
 )
-from datumaro.util import NOTSET, parse_str_enum_value
+from datumaro.util import NOTSET, parse_str_enum_value, take_by
 from datumaro.util.annotation_util import find_group_leader, find_instances
 import datumaro.util.mask_tools as mask_tools
 
@@ -734,3 +736,85 @@ def transform_item(self, item):
                 label=bbox.label, attributes=bbox.attributes))
 
         return item.wrap(annotations=annotations)
+
+class ResizeTransform(ItemTransform):
+    """
+    Resizes images and annotations in the dataset to the specified size.
+    Supports upscaling, downscaling and mixed variants.|n
+    |n
+    Examples:|n
+    - Resize all images to 256x256 size|n
+    |s|s%(prog)s -dw 256 -dh 256
+    """
+
+    @classmethod
+    def build_cmdline_parser(cls, **kwargs):
+        parser = super().build_cmdline_parser(**kwargs)
+        parser.add_argument('-dw', '--width', type=int,
+            help="Destination image width")
+        parser.add_argument('-dh', '--height', type=int,
+            help="Destination image height")
+        return parser
+
+    def __init__(self, extractor: IExtractor, width: int, height: int) -> None:
+        super().__init__(extractor)
+
+        assert width > 0 and height > 0
+        self._width = width
+        self._height = height
+
+    def transform_item(self, item):
+        if not item.has_image:
+            raise Exception("Image info is required for this transform")
+
+        h, w = item.image.size
+        xscale = self._width / float(w)
+        yscale = self._height / float(h)
+
+        if item.image.has_data:
+            # LANCZOS4 is preferable for upscaling, but it works quite slow
+            method = cv2.INTER_AREA if (xscale * yscale) < 1 \
+                else cv2.INTER_CUBIC
+            image = item.image.data / 255.0
+            resized_image = cv2.resize(image, (self._width, self._height),
+                interpolation=method)
+
+        resized_annotations = []
+        for ann in item.annotations:
+            if isinstance(ann, Bbox):
+                resized_annotations.append(ann.wrap(
+                    x=ann.x * xscale,
+                    y=ann.y * yscale,
+                    w=ann.w * xscale,
+                    h=ann.h * yscale,
+                ))
+            elif isinstance(ann, (Polygon, Points, PolyLine)):
+                resized_annotations.append(ann.wrap(
+                    points=[p
+                        for t in ((x * xscale, y * yscale)
+                            for x, y in take_by(ann.points, 2)
+                        )
+                        for p in t
+                    ]
+                ))
+            elif isinstance(ann, Mask):
+                # Can use only NEAREST for masks,
+                # because we can't have interpolated values
+                rescaled_mask = cv2.resize(ann.image.astype(np.float32),
+                    (self._width, self._height),
+                    interpolation=cv2.INTER_NEAREST).astype(np.uint8)
+
+                if isinstance(ann, RleMask):
+                    rle = mask_tools.mask_to_rle(rescaled_mask)
+                    resized_annotations.append(ann.wrap(
+                        rle=mask_utils.frPyObjects(rle, *rle['size'])))
+                else:
+                    resized_annotations.append(ann.wrap(image=rescaled_mask))
+            elif isinstance(ann, (Caption, Label)):
+                resized_annotations.append(ann)
+            else:
+                assert False, f"Unexpected annotation type {type(ann)}"
+
+        return self.wrap_item(item,
+            image=resized_image,
+            annotations=resized_annotations)


Test patch:
diff --git a/tests/test_transforms.py b/tests/test_transforms.py
--- a/tests/test_transforms.py
+++ b/tests/test_transforms.py
@@ -566,3 +566,49 @@ def test_bboxes_values_decrement_transform(self):
         actual = transforms.BboxValuesDecrement(src_dataset)
 
         compare_datasets(self, dst_dataset, actual)
+
+    @mark_requirement(Requirements.DATUM_GENERAL_REQ)
+    def test_can_resize(self):
+        small_dataset = Dataset.from_iterable([
+            DatasetItem(id=1, image=np.zeros((4, 4)), annotations=[
+                Label(1),
+                Bbox(1, 1, 2, 2, label=2),
+                Polygon([1, 1, 1, 2, 2, 2, 2, 1], label=1),
+                PolyLine([1, 1, 1, 2, 2, 2, 2, 1], label=2),
+                Points([1, 1, 1, 2, 2, 2, 2, 1], label=2),
+                Mask(np.array([
+                    [0, 0, 1, 1],
+                    [1, 0, 0, 1],
+                    [0, 1, 1, 0],
+                    [1, 1, 0, 0],
+                ]))
+            ])
+        ], categories=['a', 'b', 'c'])
+
+        big_dataset = Dataset.from_iterable([
+            DatasetItem(id=1, image=np.zeros((8, 8)), annotations=[
+                Label(1),
+                Bbox(2, 2, 4, 4, label=2),
+                Polygon([2, 2, 2, 4, 4, 4, 4, 2], label=1),
+                PolyLine([2, 2, 2, 4, 4, 4, 4, 2], label=2),
+                Points([2, 2, 2, 4, 4, 4, 4, 2], label=2),
+                Mask(np.array([
+                    [0, 0, 0, 0, 1, 1, 1, 1],
+                    [0, 0, 0, 0, 1, 1, 1, 1],
+                    [1, 1, 0, 0, 0, 0, 1, 1],
+                    [1, 1, 0, 0, 0, 0, 1, 1],
+                    [0, 0, 1, 1, 1, 1, 0, 0],
+                    [0, 0, 1, 1, 1, 1, 0, 0],
+                    [1, 1, 1, 1, 0, 0, 0, 0],
+                    [1, 1, 1, 1, 0, 0, 0, 0],
+                ]))
+            ])
+        ], categories=['a', 'b', 'c'])
+
+        with self.subTest('upscale'):
+            actual = transforms.ResizeTransform(small_dataset, width=8, height=8)
+            compare_datasets(self, big_dataset, actual)
+
+        with self.subTest('downscale'):
+            actual = transforms.ResizeTransform(big_dataset, width=4, height=4)
+            compare_datasets(self, small_dataset, actual)