From b7328e388ce9f5002e726b8d1051836dc67348cc Mon Sep 17 00:00:00 2001 From: Sravana Neeli Date: Thu, 22 Aug 2024 15:46:43 -0700 Subject: [PATCH 1/9] Bounding box utils --- keras_nlp/api/__init__.py | 1 + keras_nlp/api/bounding_box/__init__.py | 23 + keras_nlp/src/bounding_box/__init__.py | 19 + keras_nlp/src/bounding_box/converters.py | 529 ++++++++++++++++++ keras_nlp/src/bounding_box/converters_test.py | 216 +++++++ keras_nlp/src/bounding_box/to_dense.py | 95 ++++ keras_nlp/src/bounding_box/to_dense_test.py | 34 ++ keras_nlp/src/bounding_box/to_ragged.py | 99 ++++ keras_nlp/src/bounding_box/to_ragged_test.py | 91 +++ keras_nlp/src/bounding_box/validate_format.py | 99 ++++ 10 files changed, 1206 insertions(+) create mode 100644 keras_nlp/api/bounding_box/__init__.py create mode 100644 keras_nlp/src/bounding_box/__init__.py create mode 100644 keras_nlp/src/bounding_box/converters.py create mode 100644 keras_nlp/src/bounding_box/converters_test.py create mode 100644 keras_nlp/src/bounding_box/to_dense.py create mode 100644 keras_nlp/src/bounding_box/to_dense_test.py create mode 100644 keras_nlp/src/bounding_box/to_ragged.py create mode 100644 keras_nlp/src/bounding_box/to_ragged_test.py create mode 100644 keras_nlp/src/bounding_box/validate_format.py diff --git a/keras_nlp/api/__init__.py b/keras_nlp/api/__init__.py index 0c3aa253e5..e9fc85b1bb 100644 --- a/keras_nlp/api/__init__.py +++ b/keras_nlp/api/__init__.py @@ -17,6 +17,7 @@ since your modifications would be overwritten. """ +from keras_nlp.api import bounding_box from keras_nlp.api import layers from keras_nlp.api import metrics from keras_nlp.api import models diff --git a/keras_nlp/api/bounding_box/__init__.py b/keras_nlp/api/bounding_box/__init__.py new file mode 100644 index 0000000000..f7976087ac --- /dev/null +++ b/keras_nlp/api/bounding_box/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2023 The KerasNLP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""DO NOT EDIT. + +This file was autogenerated. Do not edit it by hand, +since your modifications would be overwritten. +""" + +from keras_nlp.src.bounding_box.converters import convert_format +from keras_nlp.src.bounding_box.to_dense import to_dense +from keras_nlp.src.bounding_box.to_ragged import to_ragged +from keras_nlp.src.bounding_box.validate_format import validate_format diff --git a/keras_nlp/src/bounding_box/__init__.py b/keras_nlp/src/bounding_box/__init__.py new file mode 100644 index 0000000000..6f9a928abe --- /dev/null +++ b/keras_nlp/src/bounding_box/__init__.py @@ -0,0 +1,19 @@ +# Copyright 2024 The KerasNLP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from keras_nlp.src.bounding_box.converters import _decode_deltas_to_boxes +from keras_nlp.src.bounding_box.converters import _encode_box_to_deltas +from keras_nlp.src.bounding_box.converters import convert_format +from keras_nlp.src.bounding_box.to_dense import to_dense +from keras_nlp.src.bounding_box.to_ragged import to_ragged diff --git a/keras_nlp/src/bounding_box/converters.py b/keras_nlp/src/bounding_box/converters.py new file mode 100644 index 0000000000..0e363fc6f7 --- /dev/null +++ b/keras_nlp/src/bounding_box/converters.py @@ -0,0 +1,529 @@ +# Copyright 2024 The KerasNLP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Converter functions for working with bounding box formats.""" + +import keras +from keras import ops + +from keras_nlp.src.api_export import keras_nlp_export + +try: + import tensorflow as tf +except ImportError: + tf = None + + +# Internal exception to propagate the fact images was not passed to a converter +# that needs it. +class RequiresImagesException(Exception): + pass + + +ALL_AXES = 4 + + +def _encode_box_to_deltas( + anchors, + boxes, + anchor_format: str, + box_format: str, + variance=None, + image_shape=None, +): + """Converts bounding_boxes from `center_yxhw` to delta format.""" + if variance is not None: + variance = ops.convert_to_tensor(variance, "float32") + var_len = variance.shape[-1] + + if var_len != 4: + raise ValueError(f"`variance` must be length 4, got {variance}") + encoded_anchors = convert_format( + anchors, + source=anchor_format, + target="center_yxhw", + image_shape=image_shape, + ) + boxes = convert_format( + boxes, source=box_format, target="center_yxhw", image_shape=image_shape + ) + anchor_dimensions = ops.maximum( + encoded_anchors[..., 2:], keras.backend.epsilon() + ) + box_dimensions = ops.maximum(boxes[..., 2:], keras.backend.epsilon()) + # anchors be unbatched, boxes can either be batched or unbatched. + boxes_delta = ops.concatenate( + [ + (boxes[..., :2] - encoded_anchors[..., :2]) / anchor_dimensions, + ops.log(box_dimensions / anchor_dimensions), + ], + axis=-1, + ) + if variance is not None: + boxes_delta /= variance + return boxes_delta + + +def _decode_deltas_to_boxes( + anchors, + boxes_delta, + anchor_format: str, + box_format: str, + variance=None, + image_shape=None, +): + """Converts bounding_boxes from delta format to `center_yxhw`.""" + if variance is not None: + variance = ops.convert_to_tensor(variance, "float32") + var_len = variance.shape[-1] + + if var_len != 4: + raise ValueError(f"`variance` must be length 4, got {variance}") + + def decode_single_level(anchor, box_delta): + encoded_anchor = convert_format( + anchor, + source=anchor_format, + target="center_yxhw", + image_shape=image_shape, + ) + if variance is not None: + box_delta = box_delta * variance + # anchors be unbatched, boxes can either be batched or unbatched. + box = ops.concatenate( + [ + box_delta[..., :2] * encoded_anchor[..., 2:] + + encoded_anchor[..., :2], + ops.exp(box_delta[..., 2:]) * encoded_anchor[..., 2:], + ], + axis=-1, + ) + box = convert_format( + box, + source="center_yxhw", + target=box_format, + image_shape=image_shape, + ) + return box + + if isinstance(anchors, dict) and isinstance(boxes_delta, dict): + boxes = {} + for lvl, anchor in anchors.items(): + boxes[lvl] = decode_single_level(anchor, boxes_delta[lvl]) + return boxes + else: + return decode_single_level(anchors, boxes_delta) + + +def _center_yxhw_to_xyxy(boxes, images=None, image_shape=None): + y, x, height, width = ops.split(boxes, ALL_AXES, axis=-1) + return ops.concatenate( + [x - width / 2.0, y - height / 2.0, x + width / 2.0, y + height / 2.0], + axis=-1, + ) + + +def _center_xywh_to_xyxy(boxes, images=None, image_shape=None): + x, y, width, height = ops.split(boxes, ALL_AXES, axis=-1) + return ops.concatenate( + [x - width / 2.0, y - height / 2.0, x + width / 2.0, y + height / 2.0], + axis=-1, + ) + + +def _xywh_to_xyxy(boxes, images=None, image_shape=None): + x, y, width, height = ops.split(boxes, ALL_AXES, axis=-1) + return ops.concatenate([x, y, x + width, y + height], axis=-1) + + +def _xyxy_to_center_yxhw(boxes, images=None, image_shape=None): + left, top, right, bottom = ops.split(boxes, ALL_AXES, axis=-1) + return ops.concatenate( + [ + (top + bottom) / 2.0, + (left + right) / 2.0, + bottom - top, + right - left, + ], + axis=-1, + ) + + +def _rel_xywh_to_xyxy(boxes, images=None, image_shape=None): + image_height, image_width = _image_shape(images, image_shape, boxes) + x, y, width, height = ops.split(boxes, ALL_AXES, axis=-1) + return ops.concatenate( + [ + image_width * x, + image_height * y, + image_width * (x + width), + image_height * (y + height), + ], + axis=-1, + ) + + +def _xyxy_no_op(boxes, images=None, image_shape=None): + return boxes + + +def _xyxy_to_xywh(boxes, images=None, image_shape=None): + left, top, right, bottom = ops.split(boxes, ALL_AXES, axis=-1) + return ops.concatenate( + [left, top, right - left, bottom - top], + axis=-1, + ) + + +def _xyxy_to_rel_xywh(boxes, images=None, image_shape=None): + image_height, image_width = _image_shape(images, image_shape, boxes) + left, top, right, bottom = ops.split(boxes, ALL_AXES, axis=-1) + left, right = ( + left / image_width, + right / image_width, + ) + top, bottom = top / image_height, bottom / image_height + return ops.concatenate( + [left, top, right - left, bottom - top], + axis=-1, + ) + + +def _xyxy_to_center_xywh(boxes, images=None, image_shape=None): + left, top, right, bottom = ops.split(boxes, ALL_AXES, axis=-1) + return ops.concatenate( + [ + (left + right) / 2.0, + (top + bottom) / 2.0, + right - left, + bottom - top, + ], + axis=-1, + ) + + +def _rel_xyxy_to_xyxy(boxes, images=None, image_shape=None): + image_height, image_width = _image_shape(images, image_shape, boxes) + left, top, right, bottom = ops.split( + boxes, + ALL_AXES, + axis=-1, + ) + left, right = left * image_width, right * image_width + top, bottom = top * image_height, bottom * image_height + return ops.concatenate( + [left, top, right, bottom], + axis=-1, + ) + + +def _xyxy_to_rel_xyxy(boxes, images=None, image_shape=None): + image_height, image_width = _image_shape(images, image_shape, boxes) + left, top, right, bottom = ops.split( + boxes, + ALL_AXES, + axis=-1, + ) + left, right = left / image_width, right / image_width + top, bottom = top / image_height, bottom / image_height + return ops.concatenate( + [left, top, right, bottom], + axis=-1, + ) + + +def _yxyx_to_xyxy(boxes, images=None, image_shape=None): + y1, x1, y2, x2 = ops.split(boxes, ALL_AXES, axis=-1) + return ops.concatenate([x1, y1, x2, y2], axis=-1) + + +def _rel_yxyx_to_xyxy(boxes, images=None, image_shape=None): + image_height, image_width = _image_shape(images, image_shape, boxes) + top, left, bottom, right = ops.split( + boxes, + ALL_AXES, + axis=-1, + ) + left, right = left * image_width, right * image_width + top, bottom = top * image_height, bottom * image_height + return ops.concatenate( + [left, top, right, bottom], + axis=-1, + ) + + +def _xyxy_to_yxyx(boxes, images=None, image_shape=None): + x1, y1, x2, y2 = ops.split(boxes, ALL_AXES, axis=-1) + return ops.concatenate([y1, x1, y2, x2], axis=-1) + + +def _xyxy_to_rel_yxyx(boxes, images=None, image_shape=None): + image_height, image_width = _image_shape(images, image_shape, boxes) + left, top, right, bottom = ops.split(boxes, ALL_AXES, axis=-1) + left, right = left / image_width, right / image_width + top, bottom = top / image_height, bottom / image_height + return ops.concatenate( + [top, left, bottom, right], + axis=-1, + ) + + +TO_XYXY_CONVERTERS = { + "xywh": _xywh_to_xyxy, + "center_xywh": _center_xywh_to_xyxy, + "center_yxhw": _center_yxhw_to_xyxy, + "rel_xywh": _rel_xywh_to_xyxy, + "xyxy": _xyxy_no_op, + "rel_xyxy": _rel_xyxy_to_xyxy, + "yxyx": _yxyx_to_xyxy, + "rel_yxyx": _rel_yxyx_to_xyxy, +} + +FROM_XYXY_CONVERTERS = { + "xywh": _xyxy_to_xywh, + "center_xywh": _xyxy_to_center_xywh, + "center_yxhw": _xyxy_to_center_yxhw, + "rel_xywh": _xyxy_to_rel_xywh, + "xyxy": _xyxy_no_op, + "rel_xyxy": _xyxy_to_rel_xyxy, + "yxyx": _xyxy_to_yxyx, + "rel_yxyx": _xyxy_to_rel_yxyx, +} + + +@keras_nlp_export("keras_nlp.bounding_box.convert_format") +def convert_format( + boxes, source, target, images=None, image_shape=None, dtype="float32" +): + f"""Converts bounding_boxes from one format to another. + + Supported formats are: + - `"xyxy"`, also known as `corners` format. In this format the first four + axes represent `[left, top, right, bottom]` in that order. + - `"rel_xyxy"`. In this format, the axes are the same as `"xyxy"` but the x + coordinates are normalized using the image width, and the y axes the + image height. All values in `rel_xyxy` are in the range `(0, 1)`. + - `"xywh"`. In this format the first four axes represent + `[left, top, width, height]`. + - `"rel_xywh". In this format the first four axes represent + [left, top, width, height], just like `"xywh"`. Unlike `"xywh"`, the + values are in the range (0, 1) instead of absolute pixel values. + - `"center_xyWH"`. In this format the first two coordinates represent the x + and y coordinates of the center of the bounding box, while the last two + represent the width and height of the bounding box. + - `"center_yxHW"`. In this format the first two coordinates represent the y + and x coordinates of the center of the bounding box, while the last two + represent the height and width of the bounding box. + - `"yxyx"`. In this format the first four axes represent + [top, left, bottom, right] in that order. + - `"rel_yxyx"`. In this format, the axes are the same as `"yxyx"` but the x + coordinates are normalized using the image width, and the y axes the + image height. All values in `rel_yxyx` are in the range (0, 1). + Formats are case insensitive. It is recommended that you capitalize width + and height to maximize the visual difference between `"xyWH"` and `"xyxy"`. + + Relative formats, abbreviated `rel`, make use of the shapes of the `images` + passed. In these formats, the coordinates, widths, and heights are all + specified as percentages of the host image. `images` may be a ragged + Tensor. Note that using a ragged Tensor for images may cause a substantial + performance loss, as each image will need to be processed separately due to + the mismatching image shapes. + + Example: + + ```python + boxes = load_coco_dataset() + boxes_in_xywh = keras_nlp.bounding_box.convert_format( + boxes, + source='xyxy', + target='xyWH' + ) + ``` + + Args: + boxes: tensor representing bounding boxes in the format specified in + the `source` parameter. `boxes` can optionally have extra + dimensions stacked on the final axis to store metadata. boxes + should be a 3D tensor, with the shape `[batch_size, num_boxes, 4]`. + Alternatively, boxes can be a dictionary with key 'boxes' containing + a tensor matching the aforementioned spec. + source:One of {" ".join([f'"{f}"' for f in TO_XYXY_CONVERTERS.keys()])}. + Used to specify the original format of the `boxes` parameter. + target:One of {" ".join([f'"{f}"' for f in TO_XYXY_CONVERTERS.keys()])}. + Used to specify the destination format of the `boxes` parameter. + images: (Optional) a batch of images aligned with `boxes` on the first + axis. Should be at least 3 dimensions, with the first 3 dimensions + representing: `[batch_size, height, width]`. Used in some + converters to compute relative pixel values of the bounding box + dimensions. Required when transforming from a rel format to a + non-rel format. + dtype: the data type to use when transforming the boxes, defaults to + `"float32"`. + """ + if isinstance(boxes, dict): + converted_boxes = boxes.copy() + converted_boxes["boxes"] = convert_format( + boxes["boxes"], + source=source, + target=target, + images=images, + image_shape=image_shape, + dtype=dtype, + ) + return converted_boxes + + if boxes.shape[-1] is not None and boxes.shape[-1] != 4: + raise ValueError( + "Expected `boxes` to be a Tensor with a final dimension of " + f"`4`. Instead, got `boxes.shape={boxes.shape}`." + ) + if images is not None and image_shape is not None: + raise ValueError( + "convert_format() expects either `images` or `image_shape`, but " + f"not both. Received images={images} image_shape={image_shape}" + ) + + _validate_image_shape(image_shape) + + source = source.lower() + target = target.lower() + if source not in TO_XYXY_CONVERTERS: + raise ValueError( + "`convert_format()` received an unsupported format for the " + "argument `source`. `source` should be one of " + f"{TO_XYXY_CONVERTERS.keys()}. Got source={source}" + ) + if target not in FROM_XYXY_CONVERTERS: + raise ValueError( + "`convert_format()` received an unsupported format for the " + "argument `target`. `target` should be one of " + f"{FROM_XYXY_CONVERTERS.keys()}. Got target={target}" + ) + + boxes = ops.cast(boxes, dtype) + if source == target: + return boxes + + # rel->rel conversions should not require images + if source.startswith("rel") and target.startswith("rel"): + source = source.replace("rel_", "", 1) + target = target.replace("rel_", "", 1) + + boxes, images, squeeze = _format_inputs(boxes, images) + to_xyxy_fn = TO_XYXY_CONVERTERS[source] + from_xyxy_fn = FROM_XYXY_CONVERTERS[target] + + try: + in_xyxy = to_xyxy_fn(boxes, images=images, image_shape=image_shape) + result = from_xyxy_fn(in_xyxy, images=images, image_shape=image_shape) + except RequiresImagesException: + raise ValueError( + "convert_format() must receive `images` or `image_shape` when " + "transforming between relative and absolute formats." + f"convert_format() received source=`{format}`, target=`{format}, " + f"but images={images} and image_shape={image_shape}." + ) + + return _format_outputs(result, squeeze) + + +def _format_inputs(boxes, images): + boxes_rank = len(boxes.shape) + if boxes_rank > 3: + raise ValueError( + "Expected len(boxes.shape)=2, or len(boxes.shape)=3, got " + f"len(boxes.shape)={boxes_rank}" + ) + boxes_includes_batch = boxes_rank == 3 + # Determine if images needs an expand_dims() call + if images is not None: + images_rank = len(images.shape) + if images_rank > 4: + raise ValueError( + "Expected len(images.shape)=2, or len(images.shape)=3, got " + f"len(images.shape)={images_rank}" + ) + images_include_batch = images_rank == 4 + if boxes_includes_batch != images_include_batch: + raise ValueError( + "convert_format() expects both boxes and images to be batched, " + "or both boxes and images to be unbatched. Received " + f"len(boxes.shape)={boxes_rank}, " + f"len(images.shape)={images_rank}. Expected either " + "len(boxes.shape)=2 AND len(images.shape)=3, or " + "len(boxes.shape)=3 AND len(images.shape)=4." + ) + if not images_include_batch: + images = ops.expand_dims(images, axis=0) + + if not boxes_includes_batch: + return ops.expand_dims(boxes, axis=0), images, True + return boxes, images, False + + +def _validate_image_shape(image_shape): + # Escape early if image_shape is None and skip validation. + if image_shape is None: + return + # tuple/list + if isinstance(image_shape, (tuple, list)): + if len(image_shape) != 3: + raise ValueError( + "image_shape should be of length 3, but got " + f"image_shape={image_shape}" + ) + return + + # tensor + if ops.is_tensor(image_shape): + if len(image_shape.shape) > 1: + raise ValueError( + "image_shape.shape should be (3), but got " + f"image_shape.shape={image_shape.shape}" + ) + if image_shape.shape[0] != 3: + raise ValueError( + "image_shape.shape should be (3), but got " + f"image_shape.shape={image_shape.shape}" + ) + return + + # Warn about failure cases + raise ValueError( + "Expected image_shape to be either a tuple, list, Tensor. " + f"Received image_shape={image_shape}" + ) + + +def _format_outputs(boxes, squeeze): + if squeeze: + return ops.squeeze(boxes, axis=0) + return boxes + + +def _image_shape(images, image_shape, boxes): + if images is None and image_shape is None: + raise RequiresImagesException() + + if image_shape is None: + if not isinstance(images, tf.RaggedTensor): + image_shape = ops.shape(images) + height, width = image_shape[1], image_shape[2] + else: + height = ops.reshape(images.row_lengths(), (-1, 1)) + width = ops.reshape(ops.max(images.row_lengths(axis=2), 1), (-1, 1)) + height = ops.expand_dims(height, axis=-1) + width = ops.expand_dims(width, axis=-1) + else: + height, width = image_shape[0], image_shape[1] + return ops.cast(height, boxes.dtype), ops.cast(width, boxes.dtype) diff --git a/keras_nlp/src/bounding_box/converters_test.py b/keras_nlp/src/bounding_box/converters_test.py new file mode 100644 index 0000000000..8ae1d734d6 --- /dev/null +++ b/keras_nlp/src/bounding_box/converters_test.py @@ -0,0 +1,216 @@ +# Copyright 2024 The KerasNLP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools + +import numpy as np +import pytest +import tensorflow as tf +from absl.testing import parameterized + +from keras_nlp.src import bounding_box +from keras_nlp.src.tests.test_case import TestCase + +xyxy_box = np.array([[[10, 20, 110, 120], [20, 30, 120, 130]]], dtype="float32") +yxyx_box = np.array([[[20, 10, 120, 110], [30, 20, 130, 120]]], dtype="float32") +rel_xyxy_box = np.array( + [[[0.01, 0.02, 0.11, 0.12], [0.02, 0.03, 0.12, 0.13]]], dtype="float32" +) +rel_xyxy_box_ragged_images = np.array( + [[[0.10, 0.20, 1.1, 1.20], [0.40, 0.6, 2.40, 2.6]]], dtype="float32" +) +rel_yxyx_box = np.array( + [[[0.02, 0.01, 0.12, 0.11], [0.03, 0.02, 0.13, 0.12]]], dtype="float32" +) +rel_yxyx_box_ragged_images = np.array( + [[[0.2, 0.1, 1.2, 1.1], [0.6, 0.4, 2.6, 2.4]]], dtype="float32" +) +center_xywh_box = np.array( + [[[60, 70, 100, 100], [70, 80, 100, 100]]], dtype="float32" +) +xywh_box = np.array([[[10, 20, 100, 100], [20, 30, 100, 100]]], dtype="float32") +rel_xywh_box = np.array( + [[[0.01, 0.02, 0.1, 0.1], [0.02, 0.03, 0.1, 0.1]]], dtype="float32" +) +rel_xywh_box_ragged_images = np.array( + [[[0.1, 0.2, 1, 1], [0.4, 0.6, 2, 2]]], dtype="float32" +) + +ragged_images = tf.ragged.constant( + [np.ones(shape=[100, 100, 3]), np.ones(shape=[50, 50, 3])], # 2 images + ragged_rank=2, +) + +images = np.ones([2, 1000, 1000, 3]) + +ragged_classes = tf.ragged.constant([[0], [0]], dtype="float32") + +boxes = { + "xyxy": xyxy_box, + "center_xywh": center_xywh_box, + "rel_xywh": rel_xywh_box, + "xywh": xywh_box, + "rel_xyxy": rel_xyxy_box, + "yxyx": yxyx_box, + "rel_yxyx": rel_yxyx_box, +} + +boxes_ragged_images = { + "xyxy": xyxy_box, + "center_xywh": center_xywh_box, + "rel_xywh": rel_xywh_box_ragged_images, + "xywh": xywh_box, + "rel_xyxy": rel_xyxy_box_ragged_images, + "yxyx": yxyx_box, + "rel_yxyx": rel_yxyx_box_ragged_images, +} + +test_cases = [ + (f"{source}_{target}", source, target) + for (source, target) in itertools.permutations(boxes.keys(), 2) +] + [("xyxy_xyxy", "xyxy", "xyxy")] + +test_image_ragged = [ + (f"{source}_{target}", source, target) + for (source, target) in itertools.permutations( + boxes_ragged_images.keys(), 2 + ) +] + [("xyxy_xyxy", "xyxy", "xyxy")] + + +class ConvertersTestCase(TestCase): + @parameterized.named_parameters(*test_cases) + def test_converters(self, source, target): + source_box = boxes[source] + target_box = boxes[target] + + self.assertAllClose( + bounding_box.convert_format( + source_box, source=source, target=target, images=images + ), + target_box, + ) + + @parameterized.named_parameters(*test_image_ragged) + def test_converters_ragged_images(self, source, target): + source_box = _raggify(boxes_ragged_images[source]) + target_box = _raggify(boxes_ragged_images[target]) + self.assertAllClose( + bounding_box.convert_format( + source_box, source=source, target=target, images=ragged_images + ), + target_box, + ) + + @parameterized.named_parameters(*test_cases) + def test_converters_unbatched(self, source, target): + source_box = boxes[source][0] + target_box = boxes[target][0] + + self.assertAllClose( + bounding_box.convert_format( + source_box, source=source, target=target, images=images[0] + ), + target_box, + ) + + def test_raises_with_different_image_rank(self): + source_box = boxes["xyxy"][0] + with self.assertRaises(ValueError): + bounding_box.convert_format( + source_box, source="xyxy", target="xywh", images=images + ) + + def test_without_images(self): + source_box = boxes["xyxy"] + target_box = boxes["xywh"] + self.assertAllClose( + bounding_box.convert_format( + source_box, source="xyxy", target="xywh" + ), + target_box, + ) + + def test_rel_to_rel_without_images(self): + source_box = boxes["rel_xyxy"] + target_box = boxes["rel_yxyx"] + self.assertAllClose( + bounding_box.convert_format( + source_box, source="rel_xyxy", target="rel_yxyx" + ), + target_box, + ) + + @parameterized.named_parameters(*test_cases) + def test_ragged_bounding_box(self, source, target): + source_box = _raggify(boxes[source]) + target_box = _raggify(boxes[target]) + self.assertAllClose( + bounding_box.convert_format( + source_box, source=source, target=target, images=images + ), + target_box, + ) + + @parameterized.named_parameters(*test_image_ragged) + @pytest.mark.tf_keras_only + def test_ragged_bounding_box_ragged_images(self, source, target): + source_box = _raggify(boxes_ragged_images[source]) + target_box = _raggify(boxes_ragged_images[target]) + self.assertAllClose( + bounding_box.convert_format( + source_box, source=source, target=target, images=ragged_images + ), + target_box, + ) + + @parameterized.named_parameters(*test_cases) + def test_ragged_bounding_box_with_image_shape(self, source, target): + source_box = _raggify(boxes[source]) + target_box = _raggify(boxes[target]) + self.assertAllClose( + bounding_box.convert_format( + source_box, + source=source, + target=target, + image_shape=(1000, 1000, 3), + ), + target_box, + ) + + @parameterized.named_parameters(*test_image_ragged) + def test_dense_bounding_box_with_ragged_images(self, source, target): + source_box = _raggify(boxes_ragged_images[source]) + target_box = _raggify(boxes_ragged_images[target]) + source_bounding_boxes = {"boxes": source_box, "classes": ragged_classes} + source_bounding_boxes = bounding_box.to_dense(source_bounding_boxes) + + result_bounding_boxes = bounding_box.convert_format( + source_bounding_boxes, + source=source, + target=target, + images=ragged_images, + ) + result_bounding_boxes = bounding_box.to_ragged(result_bounding_boxes) + + self.assertAllClose( + result_bounding_boxes["boxes"], + target_box, + ) + + +def _raggify(tensor): + tensor = tf.squeeze(tensor, axis=0) + tensor = tf.RaggedTensor.from_row_lengths(tensor, [1, 1]) + return tensor diff --git a/keras_nlp/src/bounding_box/to_dense.py b/keras_nlp/src/bounding_box/to_dense.py new file mode 100644 index 0000000000..3c42d09f4f --- /dev/null +++ b/keras_nlp/src/bounding_box/to_dense.py @@ -0,0 +1,95 @@ +# Copyright 2024 The KerasNLP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import keras_nlp.src.bounding_box.validate_format as validate_format +from keras_nlp.src.api_export import keras_nlp_export + +try: + import tensorflow as tf +except ImportError: + tf = None + + +def _box_shape(batched, boxes_shape, max_boxes): + # ensure we dont drop the final axis in RaggedTensor mode + if max_boxes is None: + shape = list(boxes_shape) + shape[-1] = 4 + return shape + if batched: + return [None, max_boxes, 4] + return [max_boxes, 4] + + +def _classes_shape(batched, classes_shape, max_boxes): + if max_boxes is None: + return None + if batched: + return [None, max_boxes] + classes_shape[2:] + return [max_boxes] + classes_shape[2:] + + +@keras_nlp_export("keras_nlp.bounding_box.to_dense") +def to_dense(bounding_boxes, max_boxes=None, default_value=-1): + """to_dense converts bounding boxes to Dense tensors + + Args: + bounding_boxes: bounding boxes in KerasCV dictionary format. + max_boxes: the maximum number of boxes, used to pad tensors to a given + shape. This can be used to make object detection pipelines TPU + compatible. + default_value: the default value to pad bounding boxes with. defaults + to -1. + """ + info = validate_format.validate_format(bounding_boxes) + + # guards against errors in metrics regarding modification of inputs. + # also guards against unexpected behavior when modifying downstream + bounding_boxes = bounding_boxes.copy() + + # Already running in masked mode + if not info["ragged"]: + # even if already ragged, still copy the dictionary for API consistency + return bounding_boxes + + if isinstance(bounding_boxes["classes"], tf.RaggedTensor): + bounding_boxes["classes"] = bounding_boxes["classes"].to_tensor( + default_value=default_value, + shape=_classes_shape( + info["is_batched"], bounding_boxes["classes"].shape, max_boxes + ), + ) + + if isinstance(bounding_boxes["boxes"], tf.RaggedTensor): + bounding_boxes["boxes"] = bounding_boxes["boxes"].to_tensor( + default_value=default_value, + shape=_box_shape( + info["is_batched"], bounding_boxes["boxes"].shape, max_boxes + ), + ) + + if "confidence" in bounding_boxes: + if isinstance(bounding_boxes["confidence"], tf.RaggedTensor): + bounding_boxes["confidence"] = bounding_boxes[ + "confidence" + ].to_tensor( + default_value=default_value, + shape=_classes_shape( + info["is_batched"], + bounding_boxes["confidence"].shape, + max_boxes, + ), + ) + + return bounding_boxes diff --git a/keras_nlp/src/bounding_box/to_dense_test.py b/keras_nlp/src/bounding_box/to_dense_test.py new file mode 100644 index 0000000000..e5c7eb9d88 --- /dev/null +++ b/keras_nlp/src/bounding_box/to_dense_test.py @@ -0,0 +1,34 @@ +# Copyright 2024 The KerasNLP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from keras_nlp.src import bounding_box +from keras_nlp.src.tests.test_case import TestCase + +try: + import tensorflow as tf +except ImportError: + tf = None + + +class ToDenseTest(TestCase): + def test_converts_to_dense(self): + bounding_boxes = { + "boxes": tf.ragged.constant( + [[[0, 0, 1, 1]], [[0, 0, 1, 1], [0, 0, 1, 1], [0, 0, 1, 1]]] + ), + "classes": tf.ragged.constant([[0], [1, 2, 3]]), + } + bounding_boxes = bounding_box.to_dense(bounding_boxes) + self.assertEqual(bounding_boxes["boxes"].shape, [2, 3, 4]) + self.assertEqual(bounding_boxes["classes"].shape, [2, 3]) diff --git a/keras_nlp/src/bounding_box/to_ragged.py b/keras_nlp/src/bounding_box/to_ragged.py new file mode 100644 index 0000000000..1a8b749509 --- /dev/null +++ b/keras_nlp/src/bounding_box/to_ragged.py @@ -0,0 +1,99 @@ +# Copyright 2024 The KerasNLP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import keras + +import keras_nlp.src.bounding_box.validate_format as validate_format +from keras_nlp.src.api_export import keras_nlp_export + +try: + import tensorflow as tf +except ImportError: + tf = None + + +@keras_nlp_export("keras_nlp.bounding_box.to_ragged") +def to_ragged(bounding_boxes, sentinel=-1, dtype=tf.float32): + """converts a Dense padded bounding box `tf.Tensor` to a `tf.RaggedTensor`. + + Bounding boxes are ragged tensors in most use cases. Converting them to a + dense tensor makes it easier to work with Tensorflow ecosystem. + This function can be used to filter out the masked out bounding boxes by + checking for padded sentinel value of the class_id axis of the + bounding_boxes. + + Example: + ```python + bounding_boxes = { + "boxes": tf.constant([[2, 3, 4, 5], [0, 1, 2, 3]]), + "classes": tf.constant([[-1, 1]]), + } + bounding_boxes = bounding_box.to_ragged(bounding_boxes) + print(bounding_boxes) + # { + # "boxes": [[0, 1, 2, 3]], + # "classes": [[1]] + # } + ``` + + Args: + bounding_boxes: a Tensor of bounding boxes. May be batched, or + unbatched. + sentinel: The value indicating that a bounding box does not exist at the + current index, and the corresponding box is padding, defaults to -1. + dtype: the data type to use for the underlying Tensors. + Returns: + dictionary of `tf.RaggedTensor` or 'tf.Tensor' containing the filtered + bounding boxes. + """ + if keras.config.backend() != "tensorflow": + raise NotImplementedError( + "`bounding_box.to_ragged` was called using a backend which does " + "not support ragged tensors. " + f"Current backend: {keras.backend.backend()}." + ) + + info = validate_format.validate_format(bounding_boxes) + + if info["ragged"]: + return bounding_boxes + + boxes = bounding_boxes.get("boxes") + classes = bounding_boxes.get("classes") + confidence = bounding_boxes.get("confidence", None) + + mask = classes != sentinel + + boxes = tf.ragged.boolean_mask(boxes, mask) + classes = tf.ragged.boolean_mask(classes, mask) + if confidence is not None: + confidence = tf.ragged.boolean_mask(confidence, mask) + + if isinstance(boxes, tf.Tensor): + boxes = tf.RaggedTensor.from_tensor(boxes) + + if isinstance(classes, tf.Tensor) and len(classes.shape) > 1: + classes = tf.RaggedTensor.from_tensor(classes) + + if confidence is not None: + if isinstance(confidence, tf.Tensor) and len(confidence.shape) > 1: + confidence = tf.RaggedTensor.from_tensor(confidence) + + result = bounding_boxes.copy() + result["boxes"] = tf.cast(boxes, dtype) + result["classes"] = tf.cast(classes, dtype) + + if confidence is not None: + result["confidence"] = tf.cast(confidence, dtype) + + return result diff --git a/keras_nlp/src/bounding_box/to_ragged_test.py b/keras_nlp/src/bounding_box/to_ragged_test.py new file mode 100644 index 0000000000..82150a37e9 --- /dev/null +++ b/keras_nlp/src/bounding_box/to_ragged_test.py @@ -0,0 +1,91 @@ +# Copyright 2024 The KerasNLP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import keras +import numpy as np +import pytest + +from keras_nlp.src import bounding_box +from keras_nlp.src.tests.test_case import TestCase + + +class ToRaggedTest(TestCase): + def test_converts_to_ragged(self): + bounding_boxes = { + "boxes": np.array( + [[[0, 0, 0, 0], [0, 0, 0, 0]], [[2, 3, 4, 5], [0, 1, 2, 3]]] + ), + "classes": np.array([[-1, -1], [-1, 1]]), + "confidence": np.array([[0.5, 0.7], [0.23, 0.12]]), + } + bounding_boxes = bounding_box.to_ragged(bounding_boxes) + + self.assertEqual(bounding_boxes["boxes"][1].shape, [1, 4]) + self.assertEqual(bounding_boxes["classes"][1].shape, [1]) + self.assertEqual( + bounding_boxes["confidence"][1].shape, + [ + 1, + ], + ) + + self.assertEqual(bounding_boxes["classes"][0].shape, [0]) + self.assertEqual(bounding_boxes["boxes"][0].shape, [0, 4]) + self.assertEqual( + bounding_boxes["confidence"][0].shape, + [ + 0, + ], + ) + + def test_round_trip(self): + original = { + "boxes": np.array( + [ + [[0, 0, 0, 0], [-1, -1, -1, -1]], + [[-1, -1, -1, -1], [-1, -1, -1, -1]], + ] + ), + "classes": np.array([[1, -1], [-1, -1]]), + "confidence": np.array([[0.5, -1], [-1, -1]]), + } + bounding_boxes = bounding_box.to_ragged(original) + bounding_boxes = bounding_box.to_dense(bounding_boxes, max_boxes=2) + + self.assertEqual(bounding_boxes["boxes"][1].shape, [2, 4]) + self.assertEqual(bounding_boxes["classes"][1].shape, [2]) + self.assertEqual(bounding_boxes["classes"][0].shape, [2]) + self.assertEqual(bounding_boxes["boxes"][0].shape, [2, 4]) + self.assertEqual(bounding_boxes["confidence"][0].shape, [2]) + + self.assertAllEqual(bounding_boxes["boxes"], original["boxes"]) + self.assertAllEqual(bounding_boxes["classes"], original["classes"]) + self.assertAllEqual( + bounding_boxes["confidence"], original["confidence"] + ) + + @pytest.mark.skipif( + keras.config.backend() != "tensorflow", + reason="Only applies to backends which don't support raggeds", + ) + def test_backend_without_raggeds_throws(self): + bounding_boxes = { + "boxes": np.array( + [[[0, 0, 0, 0], [0, 0, 0, 0]], [[2, 3, 4, 5], [0, 1, 2, 3]]] + ), + "classes": np.array([[-1, -1], [-1, 1]]), + "confidence": np.array([[0.5, 0.7], [0.23, 0.12]]), + } + + with self.assertRaisesRegex(NotImplementedError, "support ragged"): + bounding_box.to_ragged(bounding_boxes) diff --git a/keras_nlp/src/bounding_box/validate_format.py b/keras_nlp/src/bounding_box/validate_format.py new file mode 100644 index 0000000000..51fb310807 --- /dev/null +++ b/keras_nlp/src/bounding_box/validate_format.py @@ -0,0 +1,99 @@ +# Copyright 2024 The KerasNLP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from keras_nlp.src.api_export import keras_nlp_export + +try: + import tensorflow as tf +except ImportError: + tf = None + + +@keras_nlp_export("keras_nlp.bounding_box.validate_format") +def validate_format(bounding_boxes, variable_name="bounding_boxes"): + """validates that a given set of bounding boxes complies with KerasNLP + format. + + For a set of bounding boxes to be valid it must satisfy the following + conditions: + - `bounding_boxes` must be a dictionary + - contains keys `"boxes"` and `"classes"` + - each entry must have matching first two dimensions; representing the batch + axis and the number of boxes per image axis. + - either both `"boxes"` and `"classes"` are batched, or both are unbatched. + + Additionally, one of the following must be satisfied: + - `"boxes"` and `"classes"` are both Ragged + - `"boxes"` and `"classes"` are both Dense + - `"boxes"` and `"classes"` are unbatched + + Args: + bounding_boxes: dictionary of bounding boxes according to KerasCV + format. + + Raises: + ValueError if any of the above conditions are not met + """ + if not isinstance(bounding_boxes, dict): + raise ValueError( + f"Expected `{variable_name}` to be a dictionary, got " + f"`{variable_name}={bounding_boxes}`." + ) + if not all([x in bounding_boxes for x in ["boxes", "classes"]]): + raise ValueError( + f"Expected `{variable_name}` to be a dictionary containing keys " + "`'classes'` and `'boxes'`. Got " + f"`{variable_name}.keys()={bounding_boxes.keys()}`." + ) + + boxes = bounding_boxes.get("boxes") + classes = bounding_boxes.get("classes") + info = {} + + is_batched = len(boxes.shape) == 3 + info["is_batched"] = is_batched + info["ragged"] = isinstance(boxes, tf.RaggedTensor) + + if not is_batched: + if boxes.shape[:1] != classes.shape[:1]: + raise ValueError( + "Expected `boxes` and `classes` to have matching dimensions " + "on the first axis when operating in unbatched mode. Got " + f"`boxes.shape={boxes.shape}`, `classes.shape={classes.shape}`." + ) + + info["classes_one_hot"] = len(classes.shape) == 2 + # No Ragged checks needed in unbatched mode. + return info + + info["classes_one_hot"] = len(classes.shape) == 3 + + if isinstance(boxes, tf.RaggedTensor) != isinstance( + classes, tf.RaggedTensor + ): + raise ValueError( + "Either both `boxes` and `classes` " + "should be Ragged, or neither should be ragged." + f" Got `type(boxes)={type(boxes)}`, type(classes)={type(classes)}." + ) + + # Batched mode checks + if boxes.shape[:2] != classes.shape[:2]: + raise ValueError( + "Expected `boxes` and `classes` to have matching dimensions " + "on the first two axes when operating in batched mode. " + f"Got `boxes.shape={boxes.shape}`, `classes.shape={classes.shape}`." + ) + + return info From 5b866cb44b97755cc075df53bbca6c6b67f72deb Mon Sep 17 00:00:00 2001 From: Sravana Neeli Date: Thu, 22 Aug 2024 17:17:55 -0700 Subject: [PATCH 2/9] - Correct test cases --- keras_nlp/src/bounding_box/converters_test.py | 28 +++++++++++++++++-- keras_nlp/src/bounding_box/to_dense_test.py | 7 +++++ keras_nlp/src/bounding_box/to_ragged_test.py | 13 +++++++-- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/keras_nlp/src/bounding_box/converters_test.py b/keras_nlp/src/bounding_box/converters_test.py index 8ae1d734d6..2aecb17d12 100644 --- a/keras_nlp/src/bounding_box/converters_test.py +++ b/keras_nlp/src/bounding_box/converters_test.py @@ -16,12 +16,17 @@ import numpy as np import pytest -import tensorflow as tf from absl.testing import parameterized +from keras import backend from keras_nlp.src import bounding_box from keras_nlp.src.tests.test_case import TestCase +try: + import tensorflow as tf +except ImportError: + tf = None + xyxy_box = np.array([[[10, 20, 110, 120], [20, 30, 120, 130]]], dtype="float32") yxyx_box = np.array([[[20, 10, 120, 110], [30, 20, 130, 120]]], dtype="float32") rel_xyxy_box = np.array( @@ -103,6 +108,10 @@ def test_converters(self, source, target): ) @parameterized.named_parameters(*test_image_ragged) + @pytest.mark.skipif( + backend.backend() != "tensorflow", + reason="Only applies to backends which support raggeds", + ) def test_converters_ragged_images(self, source, target): source_box = _raggify(boxes_ragged_images[source]) target_box = _raggify(boxes_ragged_images[target]) @@ -153,6 +162,10 @@ def test_rel_to_rel_without_images(self): ) @parameterized.named_parameters(*test_cases) + @pytest.mark.skipif( + backend.backend() != "tensorflow", + reason="Only applies to backends which support raggeds", + ) def test_ragged_bounding_box(self, source, target): source_box = _raggify(boxes[source]) target_box = _raggify(boxes[target]) @@ -164,7 +177,10 @@ def test_ragged_bounding_box(self, source, target): ) @parameterized.named_parameters(*test_image_ragged) - @pytest.mark.tf_keras_only + @pytest.mark.skipif( + backend.backend() != "tensorflow", + reason="Only applies to backends which support raggeds", + ) def test_ragged_bounding_box_ragged_images(self, source, target): source_box = _raggify(boxes_ragged_images[source]) target_box = _raggify(boxes_ragged_images[target]) @@ -176,6 +192,10 @@ def test_ragged_bounding_box_ragged_images(self, source, target): ) @parameterized.named_parameters(*test_cases) + @pytest.mark.skipif( + backend.backend() != "tensorflow", + reason="Only applies to backends which support raggeds", + ) def test_ragged_bounding_box_with_image_shape(self, source, target): source_box = _raggify(boxes[source]) target_box = _raggify(boxes[target]) @@ -190,6 +210,10 @@ def test_ragged_bounding_box_with_image_shape(self, source, target): ) @parameterized.named_parameters(*test_image_ragged) + @pytest.mark.skipif( + backend.backend() != "tensorflow", + reason="Only applies to backends which support raggeds", + ) def test_dense_bounding_box_with_ragged_images(self, source, target): source_box = _raggify(boxes_ragged_images[source]) target_box = _raggify(boxes_ragged_images[target]) diff --git a/keras_nlp/src/bounding_box/to_dense_test.py b/keras_nlp/src/bounding_box/to_dense_test.py index e5c7eb9d88..d0110476a9 100644 --- a/keras_nlp/src/bounding_box/to_dense_test.py +++ b/keras_nlp/src/bounding_box/to_dense_test.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest +from keras import backend + from keras_nlp.src import bounding_box from keras_nlp.src.tests.test_case import TestCase @@ -22,6 +25,10 @@ class ToDenseTest(TestCase): + @pytest.mark.skipif( + backend.backend() != "tensorflow", + reason="Only applies to backends which support raggeds", + ) def test_converts_to_dense(self): bounding_boxes = { "boxes": tf.ragged.constant( diff --git a/keras_nlp/src/bounding_box/to_ragged_test.py b/keras_nlp/src/bounding_box/to_ragged_test.py index 82150a37e9..4dbdee8d4f 100644 --- a/keras_nlp/src/bounding_box/to_ragged_test.py +++ b/keras_nlp/src/bounding_box/to_ragged_test.py @@ -11,15 +11,20 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import keras + import numpy as np import pytest +from keras import backend from keras_nlp.src import bounding_box from keras_nlp.src.tests.test_case import TestCase class ToRaggedTest(TestCase): + @pytest.mark.skipif( + backend.backend() != "tensorflow", + reason="Only applies to backends which support raggeds", + ) def test_converts_to_ragged(self): bounding_boxes = { "boxes": np.array( @@ -48,6 +53,10 @@ def test_converts_to_ragged(self): ], ) + @pytest.mark.skipif( + backend.backend() != "tensorflow", + reason="Only applies to backends which support raggeds", + ) def test_round_trip(self): original = { "boxes": np.array( @@ -75,7 +84,7 @@ def test_round_trip(self): ) @pytest.mark.skipif( - keras.config.backend() != "tensorflow", + backend.backend() == "tensorflow", reason="Only applies to backends which don't support raggeds", ) def test_backend_without_raggeds_throws(self): From d3d32f02d296fbb6922632bbf4249d9334d04026 Mon Sep 17 00:00:00 2001 From: Sravana Neeli Date: Thu, 22 Aug 2024 18:32:44 -0700 Subject: [PATCH 3/9] - Remove hard tensorflow dtype --- keras_nlp/src/bounding_box/to_ragged.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keras_nlp/src/bounding_box/to_ragged.py b/keras_nlp/src/bounding_box/to_ragged.py index 1a8b749509..2ebd4a00f4 100644 --- a/keras_nlp/src/bounding_box/to_ragged.py +++ b/keras_nlp/src/bounding_box/to_ragged.py @@ -23,7 +23,7 @@ @keras_nlp_export("keras_nlp.bounding_box.to_ragged") -def to_ragged(bounding_boxes, sentinel=-1, dtype=tf.float32): +def to_ragged(bounding_boxes, sentinel=-1, dtype="float32"): """converts a Dense padded bounding box `tf.Tensor` to a `tf.RaggedTensor`. Bounding boxes are ragged tensors in most use cases. Converting them to a From 3c7f7380a9661d79b213851368e8372511d52bc0 Mon Sep 17 00:00:00 2001 From: Sravana Neeli Date: Thu, 22 Aug 2024 18:39:18 -0700 Subject: [PATCH 4/9] - fix api gen --- keras_nlp/src/bounding_box/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/keras_nlp/src/bounding_box/__init__.py b/keras_nlp/src/bounding_box/__init__.py index 6f9a928abe..d5ec3ca38d 100644 --- a/keras_nlp/src/bounding_box/__init__.py +++ b/keras_nlp/src/bounding_box/__init__.py @@ -17,3 +17,4 @@ from keras_nlp.src.bounding_box.converters import convert_format from keras_nlp.src.bounding_box.to_dense import to_dense from keras_nlp.src.bounding_box.to_ragged import to_ragged +from keras_nlp.src.bounding_box.validate_format import validate_format From 41e597416b685283f7fde7747676670c17ee22dc Mon Sep 17 00:00:00 2001 From: Sravana Neeli Date: Mon, 26 Aug 2024 15:19:22 -0700 Subject: [PATCH 5/9] - Fix import for test cases - Use setup for converters test case --- keras_nlp/src/bounding_box/converters_test.py | 335 ++++++++++++------ keras_nlp/src/bounding_box/to_dense_test.py | 6 +- 2 files changed, 231 insertions(+), 110 deletions(-) diff --git a/keras_nlp/src/bounding_box/converters_test.py b/keras_nlp/src/bounding_box/converters_test.py index 2aecb17d12..39b4932b49 100644 --- a/keras_nlp/src/bounding_box/converters_test.py +++ b/keras_nlp/src/bounding_box/converters_test.py @@ -16,134 +16,185 @@ import numpy as np import pytest +import tensorflow as tf from absl.testing import parameterized from keras import backend from keras_nlp.src import bounding_box from keras_nlp.src.tests.test_case import TestCase -try: - import tensorflow as tf -except ImportError: - tf = None - -xyxy_box = np.array([[[10, 20, 110, 120], [20, 30, 120, 130]]], dtype="float32") -yxyx_box = np.array([[[20, 10, 120, 110], [30, 20, 130, 120]]], dtype="float32") -rel_xyxy_box = np.array( - [[[0.01, 0.02, 0.11, 0.12], [0.02, 0.03, 0.12, 0.13]]], dtype="float32" -) -rel_xyxy_box_ragged_images = np.array( - [[[0.10, 0.20, 1.1, 1.20], [0.40, 0.6, 2.40, 2.6]]], dtype="float32" -) -rel_yxyx_box = np.array( - [[[0.02, 0.01, 0.12, 0.11], [0.03, 0.02, 0.13, 0.12]]], dtype="float32" -) -rel_yxyx_box_ragged_images = np.array( - [[[0.2, 0.1, 1.2, 1.1], [0.6, 0.4, 2.6, 2.4]]], dtype="float32" -) -center_xywh_box = np.array( - [[[60, 70, 100, 100], [70, 80, 100, 100]]], dtype="float32" -) -xywh_box = np.array([[[10, 20, 100, 100], [20, 30, 100, 100]]], dtype="float32") -rel_xywh_box = np.array( - [[[0.01, 0.02, 0.1, 0.1], [0.02, 0.03, 0.1, 0.1]]], dtype="float32" -) -rel_xywh_box_ragged_images = np.array( - [[[0.1, 0.2, 1, 1], [0.4, 0.6, 2, 2]]], dtype="float32" -) - -ragged_images = tf.ragged.constant( - [np.ones(shape=[100, 100, 3]), np.ones(shape=[50, 50, 3])], # 2 images - ragged_rank=2, -) - -images = np.ones([2, 1000, 1000, 3]) - -ragged_classes = tf.ragged.constant([[0], [0]], dtype="float32") - -boxes = { - "xyxy": xyxy_box, - "center_xywh": center_xywh_box, - "rel_xywh": rel_xywh_box, - "xywh": xywh_box, - "rel_xyxy": rel_xyxy_box, - "yxyx": yxyx_box, - "rel_yxyx": rel_yxyx_box, -} - -boxes_ragged_images = { - "xyxy": xyxy_box, - "center_xywh": center_xywh_box, - "rel_xywh": rel_xywh_box_ragged_images, - "xywh": xywh_box, - "rel_xyxy": rel_xyxy_box_ragged_images, - "yxyx": yxyx_box, - "rel_yxyx": rel_yxyx_box_ragged_images, -} - -test_cases = [ - (f"{source}_{target}", source, target) - for (source, target) in itertools.permutations(boxes.keys(), 2) -] + [("xyxy_xyxy", "xyxy", "xyxy")] - -test_image_ragged = [ - (f"{source}_{target}", source, target) - for (source, target) in itertools.permutations( - boxes_ragged_images.keys(), 2 - ) -] + [("xyxy_xyxy", "xyxy", "xyxy")] - class ConvertersTestCase(TestCase): - @parameterized.named_parameters(*test_cases) + def setUp(self): + xyxy_box = np.array( + [[[10, 20, 110, 120], [20, 30, 120, 130]]], dtype="float32" + ) + yxyx_box = np.array( + [[[20, 10, 120, 110], [30, 20, 130, 120]]], dtype="float32" + ) + rel_xyxy_box = np.array( + [[[0.01, 0.02, 0.11, 0.12], [0.02, 0.03, 0.12, 0.13]]], + dtype="float32", + ) + rel_xyxy_box_ragged_images = np.array( + [[[0.10, 0.20, 1.1, 1.20], [0.40, 0.6, 2.40, 2.6]]], dtype="float32" + ) + rel_yxyx_box = np.array( + [[[0.02, 0.01, 0.12, 0.11], [0.03, 0.02, 0.13, 0.12]]], + dtype="float32", + ) + rel_yxyx_box_ragged_images = np.array( + [[[0.2, 0.1, 1.2, 1.1], [0.6, 0.4, 2.6, 2.4]]], dtype="float32" + ) + center_xywh_box = np.array( + [[[60, 70, 100, 100], [70, 80, 100, 100]]], dtype="float32" + ) + xywh_box = np.array( + [[[10, 20, 100, 100], [20, 30, 100, 100]]], dtype="float32" + ) + rel_xywh_box = np.array( + [[[0.01, 0.02, 0.1, 0.1], [0.02, 0.03, 0.1, 0.1]]], dtype="float32" + ) + rel_xywh_box_ragged_images = np.array( + [[[0.1, 0.2, 1, 1], [0.4, 0.6, 2, 2]]], dtype="float32" + ) + + self.ragged_images = tf.ragged.constant( + [ + np.ones(shape=[100, 100, 3]), + np.ones(shape=[50, 50, 3]), + ], # 2 images + ragged_rank=2, + ) + + self.images = np.ones([2, 1000, 1000, 3]) + + self.ragged_classes = tf.ragged.constant([[0], [0]], dtype="float32") + + self.boxes = { + "xyxy": xyxy_box, + "center_xywh": center_xywh_box, + "rel_xywh": rel_xywh_box, + "xywh": xywh_box, + "rel_xyxy": rel_xyxy_box, + "yxyx": yxyx_box, + "rel_yxyx": rel_yxyx_box, + } + + self.boxes_ragged_images = { + "xyxy": xyxy_box, + "center_xywh": center_xywh_box, + "rel_xywh": rel_xywh_box_ragged_images, + "xywh": xywh_box, + "rel_xyxy": rel_xyxy_box_ragged_images, + "yxyx": yxyx_box, + "rel_yxyx": rel_yxyx_box_ragged_images, + } + + @parameterized.named_parameters( + *[ + (f"{source}_{target}", source, target) + for (source, target) in itertools.permutations( + [ + "xyxy", + "center_xywh", + "rel_xywh", + "xywh", + "rel_xyxy", + "yxyx", + "rel_yxyx", + ], + 2, + ) + ] + + [("xyxy_xyxy", "xyxy", "xyxy")] + ) def test_converters(self, source, target): - source_box = boxes[source] - target_box = boxes[target] + source, target + source_box = self.boxes[source] + target_box = self.boxes[target] self.assertAllClose( bounding_box.convert_format( - source_box, source=source, target=target, images=images + source_box, source=source, target=target, images=self.images ), target_box, ) - @parameterized.named_parameters(*test_image_ragged) + @parameterized.named_parameters( + *[ + (f"{source}_{target}", source, target) + for (source, target) in itertools.permutations( + [ + "xyxy", + "center_xywh", + "rel_xywh", + "xywh", + "rel_xyxy", + "yxyx", + "rel_yxyx", + ], + 2, + ) + ] + + [("xyxy_xyxy", "xyxy", "xyxy")] + ) @pytest.mark.skipif( backend.backend() != "tensorflow", reason="Only applies to backends which support raggeds", ) def test_converters_ragged_images(self, source, target): - source_box = _raggify(boxes_ragged_images[source]) - target_box = _raggify(boxes_ragged_images[target]) + source_box = _raggify(self.boxes_ragged_images[source]) + target_box = _raggify(self.boxes_ragged_images[target]) self.assertAllClose( bounding_box.convert_format( - source_box, source=source, target=target, images=ragged_images + source_box, + source=source, + target=target, + images=self.ragged_images, ), target_box, ) - @parameterized.named_parameters(*test_cases) + @parameterized.named_parameters( + *[ + (f"{source}_{target}", source, target) + for (source, target) in itertools.permutations( + [ + "xyxy", + "center_xywh", + "rel_xywh", + "xywh", + "rel_xyxy", + "yxyx", + "rel_yxyx", + ], + 2, + ) + ] + + [("xyxy_xyxy", "xyxy", "xyxy")] + ) def test_converters_unbatched(self, source, target): - source_box = boxes[source][0] - target_box = boxes[target][0] + source_box = self.boxes[source][0] + target_box = self.boxes[target][0] self.assertAllClose( bounding_box.convert_format( - source_box, source=source, target=target, images=images[0] + source_box, source=source, target=target, images=self.images[0] ), target_box, ) def test_raises_with_different_image_rank(self): - source_box = boxes["xyxy"][0] + source_box = self.boxes["xyxy"][0] with self.assertRaises(ValueError): bounding_box.convert_format( - source_box, source="xyxy", target="xywh", images=images + source_box, source="xyxy", target="xywh", images=self.images ) def test_without_images(self): - source_box = boxes["xyxy"] - target_box = boxes["xywh"] + source_box = self.boxes["xyxy"] + target_box = self.boxes["xywh"] self.assertAllClose( bounding_box.convert_format( source_box, source="xyxy", target="xywh" @@ -152,8 +203,8 @@ def test_without_images(self): ) def test_rel_to_rel_without_images(self): - source_box = boxes["rel_xyxy"] - target_box = boxes["rel_yxyx"] + source_box = self.boxes["rel_xyxy"] + target_box = self.boxes["rel_yxyx"] self.assertAllClose( bounding_box.convert_format( source_box, source="rel_xyxy", target="rel_yxyx" @@ -161,44 +212,98 @@ def test_rel_to_rel_without_images(self): target_box, ) - @parameterized.named_parameters(*test_cases) + @parameterized.named_parameters( + *[ + (f"{source}_{target}", source, target) + for (source, target) in itertools.permutations( + [ + "xyxy", + "center_xywh", + "rel_xywh", + "xywh", + "rel_xyxy", + "yxyx", + "rel_yxyx", + ], + 2, + ) + ] + + [("xyxy_xyxy", "xyxy", "xyxy")] + ) @pytest.mark.skipif( backend.backend() != "tensorflow", reason="Only applies to backends which support raggeds", ) def test_ragged_bounding_box(self, source, target): - source_box = _raggify(boxes[source]) - target_box = _raggify(boxes[target]) + source_box = _raggify(self.boxes[source]) + target_box = _raggify(self.boxes[target]) self.assertAllClose( bounding_box.convert_format( - source_box, source=source, target=target, images=images + source_box, source=source, target=target, images=self.images ), target_box, ) - @parameterized.named_parameters(*test_image_ragged) + @parameterized.named_parameters( + *[ + (f"{source}_{target}", source, target) + for (source, target) in itertools.permutations( + [ + "xyxy", + "center_xywh", + "rel_xywh", + "xywh", + "rel_xyxy", + "yxyx", + "rel_yxyx", + ], + 2, + ) + ] + + [("xyxy_xyxy", "xyxy", "xyxy")] + ) @pytest.mark.skipif( backend.backend() != "tensorflow", reason="Only applies to backends which support raggeds", ) def test_ragged_bounding_box_ragged_images(self, source, target): - source_box = _raggify(boxes_ragged_images[source]) - target_box = _raggify(boxes_ragged_images[target]) + source_box = _raggify(self.boxes_ragged_images[source]) + target_box = _raggify(self.boxes_ragged_images[target]) self.assertAllClose( bounding_box.convert_format( - source_box, source=source, target=target, images=ragged_images + source_box, + source=source, + target=target, + images=self.ragged_images, ), target_box, ) - @parameterized.named_parameters(*test_cases) + @parameterized.named_parameters( + *[ + (f"{source}_{target}", source, target) + for (source, target) in itertools.permutations( + [ + "xyxy", + "center_xywh", + "rel_xywh", + "xywh", + "rel_xyxy", + "yxyx", + "rel_yxyx", + ], + 2, + ) + ] + + [("xyxy_xyxy", "xyxy", "xyxy")] + ) @pytest.mark.skipif( backend.backend() != "tensorflow", reason="Only applies to backends which support raggeds", ) def test_ragged_bounding_box_with_image_shape(self, source, target): - source_box = _raggify(boxes[source]) - target_box = _raggify(boxes[target]) + source_box = _raggify(self.boxes[source]) + target_box = _raggify(self.boxes[target]) self.assertAllClose( bounding_box.convert_format( source_box, @@ -209,22 +314,42 @@ def test_ragged_bounding_box_with_image_shape(self, source, target): target_box, ) - @parameterized.named_parameters(*test_image_ragged) + @parameterized.named_parameters( + *[ + (f"{source}_{target}", source, target) + for (source, target) in itertools.permutations( + [ + "xyxy", + "center_xywh", + "rel_xywh", + "xywh", + "rel_xyxy", + "yxyx", + "rel_yxyx", + ], + 2, + ) + ] + + [("xyxy_xyxy", "xyxy", "xyxy")] + ) @pytest.mark.skipif( backend.backend() != "tensorflow", reason="Only applies to backends which support raggeds", ) def test_dense_bounding_box_with_ragged_images(self, source, target): - source_box = _raggify(boxes_ragged_images[source]) - target_box = _raggify(boxes_ragged_images[target]) - source_bounding_boxes = {"boxes": source_box, "classes": ragged_classes} + source_box = _raggify(self.boxes_ragged_images[source]) + target_box = _raggify(self.boxes_ragged_images[target]) + source_bounding_boxes = { + "boxes": source_box, + "classes": self.ragged_classes, + } source_bounding_boxes = bounding_box.to_dense(source_bounding_boxes) result_bounding_boxes = bounding_box.convert_format( source_bounding_boxes, source=source, target=target, - images=ragged_images, + images=self.ragged_images, ) result_bounding_boxes = bounding_box.to_ragged(result_bounding_boxes) diff --git a/keras_nlp/src/bounding_box/to_dense_test.py b/keras_nlp/src/bounding_box/to_dense_test.py index d0110476a9..dfc2bc65a6 100644 --- a/keras_nlp/src/bounding_box/to_dense_test.py +++ b/keras_nlp/src/bounding_box/to_dense_test.py @@ -13,16 +13,12 @@ # limitations under the License. import pytest +import tensorflow as tf from keras import backend from keras_nlp.src import bounding_box from keras_nlp.src.tests.test_case import TestCase -try: - import tensorflow as tf -except ImportError: - tf = None - class ToDenseTest(TestCase): @pytest.mark.skipif( From af5d21f985fe8654fb5f900b924747e9884ccb74 Mon Sep 17 00:00:00 2001 From: Sravana Neeli Date: Mon, 26 Aug 2024 15:57:29 -0700 Subject: [PATCH 6/9] - fix api_gen issue --- keras_nlp/src/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/keras_nlp/src/__init__.py b/keras_nlp/src/__init__.py index 29cb010422..e7a6fd1cce 100644 --- a/keras_nlp/src/__init__.py +++ b/keras_nlp/src/__init__.py @@ -20,6 +20,7 @@ except ImportError: pass +from keras_nlp.src import bounding_box from keras_nlp.src import layers from keras_nlp.src import metrics from keras_nlp.src import models From 051dc8c53aa490f5c29f89cb90f9f128a609e1c6 Mon Sep 17 00:00:00 2001 From: Sravana Neeli Date: Mon, 26 Aug 2024 16:03:25 -0700 Subject: [PATCH 7/9] - FIx api gen --- keras_nlp/src/__init__.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/keras_nlp/src/__init__.py b/keras_nlp/src/__init__.py index e7a6fd1cce..6fe182b8a3 100644 --- a/keras_nlp/src/__init__.py +++ b/keras_nlp/src/__init__.py @@ -15,18 +15,3 @@ # sentencepiece is segfaulting on tf-nightly if tensorflow is imported first. # This is a temporary fix to restore our nightly testing to green, while we look # for a longer term solution. -try: - import sentencepiece -except ImportError: - pass - -from keras_nlp.src import bounding_box -from keras_nlp.src import layers -from keras_nlp.src import metrics -from keras_nlp.src import models -from keras_nlp.src import samplers -from keras_nlp.src import tokenizers -from keras_nlp.src import utils -from keras_nlp.src.utils.preset_utils import upload_preset -from keras_nlp.src.version_utils import __version__ -from keras_nlp.src.version_utils import version From 60b360cd4d2a614cb6867e08c98092994a87135f Mon Sep 17 00:00:00 2001 From: Sravana Neeli Date: Mon, 26 Aug 2024 16:13:07 -0700 Subject: [PATCH 8/9] - Fix api gen error --- keras_nlp/api/bounding_box/__init__.py | 2 +- keras_nlp/src/bounding_box/__init__.py | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/keras_nlp/api/bounding_box/__init__.py b/keras_nlp/api/bounding_box/__init__.py index f7976087ac..18be1cd9aa 100644 --- a/keras_nlp/api/bounding_box/__init__.py +++ b/keras_nlp/api/bounding_box/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2023 The KerasNLP Authors +# Copyright 2024 The KerasNLP Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/keras_nlp/src/bounding_box/__init__.py b/keras_nlp/src/bounding_box/__init__.py index d5ec3ca38d..3364a6bd16 100644 --- a/keras_nlp/src/bounding_box/__init__.py +++ b/keras_nlp/src/bounding_box/__init__.py @@ -11,10 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - -from keras_nlp.src.bounding_box.converters import _decode_deltas_to_boxes -from keras_nlp.src.bounding_box.converters import _encode_box_to_deltas -from keras_nlp.src.bounding_box.converters import convert_format -from keras_nlp.src.bounding_box.to_dense import to_dense -from keras_nlp.src.bounding_box.to_ragged import to_ragged -from keras_nlp.src.bounding_box.validate_format import validate_format From 403609d3a263adc5f8c0c662c949cb2bbad62dfa Mon Sep 17 00:00:00 2001 From: Sravana Neeli Date: Mon, 26 Aug 2024 16:44:08 -0700 Subject: [PATCH 9/9] - Correct test cases as per new api changes --- keras_nlp/src/bounding_box/converters_test.py | 30 +++++++++---------- keras_nlp/src/bounding_box/to_dense_test.py | 4 +-- keras_nlp/src/bounding_box/to_ragged_test.py | 11 +++---- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/keras_nlp/src/bounding_box/converters_test.py b/keras_nlp/src/bounding_box/converters_test.py index 39b4932b49..f6f3adfa17 100644 --- a/keras_nlp/src/bounding_box/converters_test.py +++ b/keras_nlp/src/bounding_box/converters_test.py @@ -20,7 +20,9 @@ from absl.testing import parameterized from keras import backend -from keras_nlp.src import bounding_box +from keras_nlp.src.bounding_box import converters +from keras_nlp.src.bounding_box import to_dense +from keras_nlp.src.bounding_box import to_ragged from keras_nlp.src.tests.test_case import TestCase @@ -115,7 +117,7 @@ def test_converters(self, source, target): target_box = self.boxes[target] self.assertAllClose( - bounding_box.convert_format( + converters.convert_format( source_box, source=source, target=target, images=self.images ), target_box, @@ -147,7 +149,7 @@ def test_converters_ragged_images(self, source, target): source_box = _raggify(self.boxes_ragged_images[source]) target_box = _raggify(self.boxes_ragged_images[target]) self.assertAllClose( - bounding_box.convert_format( + converters.convert_format( source_box, source=source, target=target, @@ -179,7 +181,7 @@ def test_converters_unbatched(self, source, target): target_box = self.boxes[target][0] self.assertAllClose( - bounding_box.convert_format( + converters.convert_format( source_box, source=source, target=target, images=self.images[0] ), target_box, @@ -188,7 +190,7 @@ def test_converters_unbatched(self, source, target): def test_raises_with_different_image_rank(self): source_box = self.boxes["xyxy"][0] with self.assertRaises(ValueError): - bounding_box.convert_format( + converters.convert_format( source_box, source="xyxy", target="xywh", images=self.images ) @@ -196,9 +198,7 @@ def test_without_images(self): source_box = self.boxes["xyxy"] target_box = self.boxes["xywh"] self.assertAllClose( - bounding_box.convert_format( - source_box, source="xyxy", target="xywh" - ), + converters.convert_format(source_box, source="xyxy", target="xywh"), target_box, ) @@ -206,7 +206,7 @@ def test_rel_to_rel_without_images(self): source_box = self.boxes["rel_xyxy"] target_box = self.boxes["rel_yxyx"] self.assertAllClose( - bounding_box.convert_format( + converters.convert_format( source_box, source="rel_xyxy", target="rel_yxyx" ), target_box, @@ -238,7 +238,7 @@ def test_ragged_bounding_box(self, source, target): source_box = _raggify(self.boxes[source]) target_box = _raggify(self.boxes[target]) self.assertAllClose( - bounding_box.convert_format( + converters.convert_format( source_box, source=source, target=target, images=self.images ), target_box, @@ -270,7 +270,7 @@ def test_ragged_bounding_box_ragged_images(self, source, target): source_box = _raggify(self.boxes_ragged_images[source]) target_box = _raggify(self.boxes_ragged_images[target]) self.assertAllClose( - bounding_box.convert_format( + converters.convert_format( source_box, source=source, target=target, @@ -305,7 +305,7 @@ def test_ragged_bounding_box_with_image_shape(self, source, target): source_box = _raggify(self.boxes[source]) target_box = _raggify(self.boxes[target]) self.assertAllClose( - bounding_box.convert_format( + converters.convert_format( source_box, source=source, target=target, @@ -343,15 +343,15 @@ def test_dense_bounding_box_with_ragged_images(self, source, target): "boxes": source_box, "classes": self.ragged_classes, } - source_bounding_boxes = bounding_box.to_dense(source_bounding_boxes) + source_bounding_boxes = to_dense.to_dense(source_bounding_boxes) - result_bounding_boxes = bounding_box.convert_format( + result_bounding_boxes = converters.convert_format( source_bounding_boxes, source=source, target=target, images=self.ragged_images, ) - result_bounding_boxes = bounding_box.to_ragged(result_bounding_boxes) + result_bounding_boxes = to_ragged.to_ragged(result_bounding_boxes) self.assertAllClose( result_bounding_boxes["boxes"], diff --git a/keras_nlp/src/bounding_box/to_dense_test.py b/keras_nlp/src/bounding_box/to_dense_test.py index dfc2bc65a6..4bb795659b 100644 --- a/keras_nlp/src/bounding_box/to_dense_test.py +++ b/keras_nlp/src/bounding_box/to_dense_test.py @@ -16,7 +16,7 @@ import tensorflow as tf from keras import backend -from keras_nlp.src import bounding_box +from keras_nlp.src.bounding_box import to_dense from keras_nlp.src.tests.test_case import TestCase @@ -32,6 +32,6 @@ def test_converts_to_dense(self): ), "classes": tf.ragged.constant([[0], [1, 2, 3]]), } - bounding_boxes = bounding_box.to_dense(bounding_boxes) + bounding_boxes = to_dense.to_dense(bounding_boxes) self.assertEqual(bounding_boxes["boxes"].shape, [2, 3, 4]) self.assertEqual(bounding_boxes["classes"].shape, [2, 3]) diff --git a/keras_nlp/src/bounding_box/to_ragged_test.py b/keras_nlp/src/bounding_box/to_ragged_test.py index 4dbdee8d4f..cbe5146d11 100644 --- a/keras_nlp/src/bounding_box/to_ragged_test.py +++ b/keras_nlp/src/bounding_box/to_ragged_test.py @@ -16,7 +16,8 @@ import pytest from keras import backend -from keras_nlp.src import bounding_box +from keras_nlp.src.bounding_box import to_dense +from keras_nlp.src.bounding_box import to_ragged from keras_nlp.src.tests.test_case import TestCase @@ -33,7 +34,7 @@ def test_converts_to_ragged(self): "classes": np.array([[-1, -1], [-1, 1]]), "confidence": np.array([[0.5, 0.7], [0.23, 0.12]]), } - bounding_boxes = bounding_box.to_ragged(bounding_boxes) + bounding_boxes = to_ragged.to_ragged(bounding_boxes) self.assertEqual(bounding_boxes["boxes"][1].shape, [1, 4]) self.assertEqual(bounding_boxes["classes"][1].shape, [1]) @@ -68,8 +69,8 @@ def test_round_trip(self): "classes": np.array([[1, -1], [-1, -1]]), "confidence": np.array([[0.5, -1], [-1, -1]]), } - bounding_boxes = bounding_box.to_ragged(original) - bounding_boxes = bounding_box.to_dense(bounding_boxes, max_boxes=2) + bounding_boxes = to_ragged.to_ragged(original) + bounding_boxes = to_dense.to_dense(bounding_boxes, max_boxes=2) self.assertEqual(bounding_boxes["boxes"][1].shape, [2, 4]) self.assertEqual(bounding_boxes["classes"][1].shape, [2]) @@ -97,4 +98,4 @@ def test_backend_without_raggeds_throws(self): } with self.assertRaisesRegex(NotImplementedError, "support ragged"): - bounding_box.to_ragged(bounding_boxes) + to_ragged.to_ragged(bounding_boxes)