"""
Wrapper implementation for GIoU-Loss.
Fork of tensorflow_addons.losses.giou_loss
https://github.com/tensorflow/addons/blob/b2dafcfa74c5de268b8a5c53813bc0b89cadf386/tensorflow_addons/losses/giou_loss.py
Forked to bypass version issue with TF 2.4 and unavailable package tensorflow_addons.
"""
from typing import Optional
import tensorflow as tf
from tensorflow.python.keras.utils.tf_utils import is_tensor_or_variable
[docs]class LossFunctionWrapper(tf.keras.losses.Loss):
"""Wraps a loss function in the `Loss` class."""
def __init__(
self, fn, reduction=tf.keras.losses.Reduction.AUTO, name=None, **kwargs
):
"""Initializes `LossFunctionWrapper` class.
Args:
fn: The loss function to wrap, with signature `fn(y_true, y_pred,
**kwargs)`.
reduction: (Optional) Type of `tf.keras.losses.Reduction` to apply to
loss. Default value is `AUTO`. `AUTO` indicates that the reduction
option will be determined by the usage context. For almost all cases
this defaults to `SUM_OVER_BATCH_SIZE`. When used with
`tf.distribute.Strategy`, outside of built-in training loops such as
`tf.keras` `compile` and `fit`, using `AUTO` or `SUM_OVER_BATCH_SIZE`
will raise an error. Please see this custom training [tutorial](
https://www.tensorflow.org/tutorials/distribute/custom_training)
for more details.
name: (Optional) name for the loss.
**kwargs: The keyword arguments that are passed on to `fn`.
"""
super().__init__(reduction=reduction, name=name)
## The underlying function
self.fn = fn
## kwargs forwarded to `fn`
self._fn_kwargs = kwargs
[docs] def call(self, y_true, y_pred):
"""Invokes the `LossFunctionWrapper` instance.
Args:
y_true: Ground truth values.
y_pred: The predicted values.
Returns:
Loss values per sample.
"""
return self.fn(y_true, y_pred, **self._fn_kwargs)
[docs] def get_config(self):
"""Configuration getter for LossWrapper"""
config = {}
for k, v in iter(self._fn_kwargs.items()):
config[k] = tf.keras.backend.eval(v) if is_tensor_or_variable(v) else v
base_config = super().get_config()
return {**base_config, **config}
def _calculate_giou(b1, b2, mode: str = 'giou') -> tf.Tensor:
"""
Args:
b1: bounding box. The coordinates of the each bounding box in boxes are
encoded as [y_min, x_min, y_max, x_max].
b2: the other bounding box. The coordinates of the each bounding box
in boxes are encoded as [y_min, x_min, y_max, x_max].
mode: one of ['giou', 'iou'], decided to calculate GIoU or IoU loss.
Returns:
GIoU loss float `Tensor`.
"""
zero = tf.convert_to_tensor(0.0, b1.dtype)
b1_ymin, b1_xmin, b1_ymax, b1_xmax = tf.unstack(b1, 4, axis=-1)
b2_ymin, b2_xmin, b2_ymax, b2_xmax = tf.unstack(b2, 4, axis=-1)
b1_width = tf.maximum(zero, b1_xmax - b1_xmin)
b1_height = tf.maximum(zero, b1_ymax - b1_ymin)
b2_width = tf.maximum(zero, b2_xmax - b2_xmin)
b2_height = tf.maximum(zero, b2_ymax - b2_ymin)
b1_area = b1_width * b1_height
b2_area = b2_width * b2_height
intersect_ymin = tf.maximum(b1_ymin, b2_ymin)
intersect_xmin = tf.maximum(b1_xmin, b2_xmin)
intersect_ymax = tf.minimum(b1_ymax, b2_ymax)
intersect_xmax = tf.minimum(b1_xmax, b2_xmax)
intersect_width = tf.maximum(zero, intersect_xmax - intersect_xmin)
intersect_height = tf.maximum(zero, intersect_ymax - intersect_ymin)
intersect_area = intersect_width * intersect_height
union_area = b1_area + b2_area - intersect_area
iou = tf.math.divide_no_nan(intersect_area, union_area)
if mode == 'iou':
return iou
enclose_ymin = tf.minimum(b1_ymin, b2_ymin)
enclose_xmin = tf.minimum(b1_xmin, b2_xmin)
enclose_ymax = tf.maximum(b1_ymax, b2_ymax)
enclose_xmax = tf.maximum(b1_xmax, b2_xmax)
enclose_width = tf.maximum(zero, enclose_xmax - enclose_xmin)
enclose_height = tf.maximum(zero, enclose_ymax - enclose_ymin)
enclose_area = enclose_width * enclose_height
giou = iou - tf.math.divide_no_nan((enclose_area - union_area), enclose_area)
return giou
[docs]def tf_giou_loss(y_true, y_pred, mode: str = 'giou') -> tf.Tensor:
"""
Implements the GIoU loss function.
GIoU loss was first introduced in the
[Generalized Intersection over Union:
A Metric and A Loss for Bounding Box Regression]
(https://giou.stanford.edu/GIoU.pdf).
GIoU is an enhancement for models which use IoU in object detection.
:param y_true: true targets tensor. The coordinates of the each bounding box in boxes are encoded as [y_min, x_min, y_max, x_max].
:param y_pred: predictions tensor. The coordinates of the each bounding box in boxes are encoded as [y_min, x_min, y_max, x_max].
:param mode: one of ['giou', 'iou'], decided to calculate GIoU or IoU loss.
:returns: GIoU loss float `Tensor`.
"""
if mode not in ['giou', 'iou']:
raise ValueError("Value of mode should be 'iou' or 'giou'")
y_pred = tf.convert_to_tensor(y_pred)
if not y_pred.dtype.is_floating:
y_pred = tf.cast(y_pred, tf.float32)
y_true = tf.cast(y_true, y_pred.dtype)
giou = tf.squeeze(_calculate_giou(y_pred, y_true, mode))
return 1 - giou
"""
END Fork
"""
[docs]class GIoULoss(LossFunctionWrapper):
"""
GIoULoss as class instance
"""
def __init__(self,
mode: str = 'giou',
reduction: str = tf.keras.losses.Reduction.AUTO,
name: Optional[str] = 'giou_loss'):
"""
:param mode: either 'giou' or 'iou'
:param reduction: tf.keras reduction
:param name: verbose name for loss
"""
super().__init__(giou_loss, name=name, reduction=reduction, mode=mode)
[docs]def convert_values(data) -> tf.Tensor:
"""
converts values of shape [y, x, h, w] into [y_min, x_min, y_max, x_max]
:param data: bounding box coordinates
:return: converted bounding box coordinates
"""
y, x, h, w = tf.unstack(data, 4, axis=-1)
element_vals = tf.stack([y, x, tf.add(y, h), tf.add(x, w)], axis=-1)
return element_vals
[docs]def giou_loss(bb1, bb2, mode: str = 'giou') -> tf.Tensor:
"""
fork of `tensorflow_addons.losses.giou_loss.giou_loss` internally converts
`[y, x, h, w] -> [y_min, x_min, y_max, x_max]`
:param bb1: ground truth bounding box
:param bb2: predicted bounding box
:param mode: Mode to use, either 'giou' (default) or 'iou'
:return: computed giou-loss
"""
return tf_giou_loss(
y_true=convert_values(bb1),
y_pred=convert_values(bb2),
mode=mode
)