Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added RandomHorizontalFLip in TF #779

Merged
merged 8 commits into from
Jan 3, 2022
1 change: 1 addition & 0 deletions docs/source/transforms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Here are all transformations that are available through docTR:
.. autoclass:: GaussianBlur
.. autoclass:: ChannelShuffle
.. autoclass:: GaussianNoise
.. autoclass:: RandomHorizontalFlip


Composing transformations
Expand Down
46 changes: 44 additions & 2 deletions doctr/transforms/modules/tensorflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
# See LICENSE or go to <https://www.apache.org/licenses/LICENSE-2.0.txt> for full license details.

import random
from typing import Any, Callable, Iterable, List, Tuple, Union
from typing import Any, Callable, Dict, Iterable, List, Tuple, Union

import numpy as np
import tensorflow as tf
import tensorflow_addons as tfa

from doctr.utils.repr import NestedObject

__all__ = ['Compose', 'Resize', 'Normalize', 'LambdaTransformation', 'ToGray', 'RandomBrightness',
'RandomContrast', 'RandomSaturation', 'RandomHue', 'RandomGamma', 'RandomJpegQuality', 'GaussianBlur',
'ChannelShuffle', 'GaussianNoise']
'ChannelShuffle', 'GaussianNoise', 'RandomHorizontalFlip']


class Compose(NestedObject):
Expand Down Expand Up @@ -375,3 +376,44 @@ def __call__(self, x: tf.Tensor) -> tf.Tensor:

def extra_repr(self) -> str:
return f"mean={self.mean}, std={self.std}"


class RandomHorizontalFlip(NestedObject):
"""Adds random horizontal flip to the input tensor/np.ndarray

Example::
>>> from doctr.transforms import RandomHorizontalFlip
>>> import tensorflow as tf
>>> transfo = RandomHorizontalFlip(p=0.5)
>>> image = tf.random.uniform(shape=[64, 64, 3], minval=0, maxval=1)
>>> target = {
"boxes": np.array([[0.1, 0.1, 0.4, 0.5] ], dtype= np.float32),
"labels": np.ones(1, dtype= np.int64)
}
>>> out = transfo(image, target)

Args:
p : probability of Horizontal Flip"""
def __init__(self, p: float) -> None:
super().__init__()
self.p = p

def __call__(
self,
img: Union[tf.Tensor, np.ndarray],
SiddhantBahuguna marked this conversation as resolved.
Show resolved Hide resolved
target: Dict[str, Any]
) -> Tuple[tf.Tensor, Dict[str, Any]]:
"""
Args:
img: Image to be flipped.
target: Dictionary with boxes (in relative coordinates of shape (N, 4)) and labels as keys
Returns:
Tuple of numpy nd-array or Tensor and target
"""
if np.random.rand(1) <= self.p:
_img = tf.image.flip_left_right(img)
_target = target.copy()
# Changing the relative bbox coordinates
_target["boxes"][:, ::2] = 1 - target["boxes"][:, [2, 0]]
return _img, _target
return img, target
30 changes: 30 additions & 0 deletions tests/tensorflow/test_transforms_tf.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,33 @@ def test_gaussian_noise(input_dtype, input_shape):
assert tf.reduce_all(transformed <= 255)
else:
assert tf.reduce_all(transformed <= 1.)


@pytest.mark.parametrize("p", [1, 0])
def test_randomhorizontalflip(p):
# testing for 2 cases, with flip probability 1 and 0.
transform = T.RandomHorizontalFlip(p)
input_t = np.ones((32, 32, 3))
input_t[:, :16, :] = 0
input_t = tf.convert_to_tensor(input_t)
target = {"boxes": np.array([[0.1, 0.1, 0.3, 0.4]], dtype=np.float32), "labels": np.ones(1, dtype=np.int64)}
transformed, _target = transform(input_t, target)
assert isinstance(transformed, tf.Tensor)
assert transformed.shape == input_t.shape
assert transformed.dtype == input_t.dtype
# integrity check of targets
assert isinstance(_target, dict)
assert all(isinstance(val, np.ndarray) for val in _target.values())
assert _target["boxes"].dtype == np.float32
assert _target["labels"].dtype == np.int64
if p == 1:
assert np.all(_target["boxes"] == np.array([[0.7, 0.1, 0.9, 0.4]], dtype=np.float32))
assert tf.reduce_all(
tf.math.reduce_mean(transformed, (0, 2)) == tf.constant([1] * 16 + [0] * 16, dtype=tf.float64)
)
elif p == 0:
assert np.all(_target["boxes"] == np.array([[0.1, 0.1, 0.3, 0.4]], dtype=np.float32))
assert tf.reduce_all(
tf.math.reduce_mean(transformed, (0, 2)) == tf.constant([0] * 16 + [1] * 16, dtype=tf.float64)
)
assert np.all(_target["labels"] == np.ones(1, dtype=np.int64))