text: import tensorflow as tf
  
  
  class BatchNormalization(tf.keras.layers.BatchNormalization):
      """
      Identical to tf.keras.layers.BatchNormalization, but adds the option to freeze parameters.
      """
  
      def __init__(self, freeze, *args, **kwargs):
          self.freeze = freeze
          super(BatchNormalization, self).__init__(*args, **kwargs)
  
          # set to non-trainable if freeze is true
          self.trainable = not self.freeze
  
      def call(self, inputs, training=None, **kwargs):
          # return super.call, but set training
          if not training:
              return super(BatchNormalization, self).call(inputs, training=False)
          else:
              return super(BatchNormalization, self).call(
                  inputs, training=(not self.freeze)
              )
  
      def get_config(self):
          config = super(BatchNormalization, self).get_config()
          config.update({"freeze": self.freeze})
          return config
  
  
  class wBiFPNAdd(tf.keras.layers.Layer):
      def __init__(self, epsilon=1e-4, **kwargs):
          super(wBiFPNAdd, self).__init__(**kwargs)
          self.epsilon = epsilon
  
      def build(self, input_shape):
          num_in = len(input_shape)
          self.w = self.add_weight(
              name=self.name,
              shape=(num_in,),
              initializer=tf.keras.initializers.constant(1 / num_in),
              trainable=True,
              dtype=tf.float32,
          )
  
      def call(self, inputs, **kwargs):
          w = tf.keras.activations.relu(self.w)
          x = tf.reduce_sum([w[i] * inputs[i] for i in range(len(inputs))], axis=0)
          x = x / (tf.reduce_sum(w) + self.epsilon)
          return x
  
      def compute_output_shape(self, input_shape):
          return input_shape[0]
  
      def get_config(self):
          config = super(wBiFPNAdd, self).get_config()
          config.update({"epsilon": self.epsilon})
          return config
  
  
  def bbox_transform_inv(boxes, deltas, scale_factors=None):
      cxa = (boxes[..., 0] + boxes[..., 2]) / 2
      cya = (boxes[..., 1] + boxes[..., 3]) / 2
      wa = boxes[..., 2] - boxes[..., 0]
      ha = boxes[..., 3] - boxes[..., 1]
      ty, tx, th, tw = deltas[..., 0], deltas[..., 1], deltas[..., 2], deltas[..., 3]
      if scale_factors:
          ty *= scale_factors[0]
          tx *= scale_factors[1]
          th *= scale_factors[2]
          tw *= scale_factors[3]
      w = tf.exp(tw) * wa
      h = tf.exp(th) * ha
      cy = ty * ha + cya
      cx = tx * wa + cxa
      ymin = cy - h / 2.0
      xmin = cx - w / 2.0
      ymax = cy + h / 2.0
      xmax = cx + w / 2.0
      return tf.stack([xmin, ymin, xmax, ymax], axis=-1)
  
  
  class ClipBoxes(tf.keras.layers.Layer):
      def call(self, inputs, **kwargs):
          image, boxes = inputs
          shape = tf.keras.backend.cast(
              tf.keras.backend.shape(image), tf.keras.backend.floatx()
          )
          height = shape[1]
          width = shape[2]
          x1 = tf.clip_by_value(boxes[:, :, 0], 0, width - 1)
          y1 = tf.clip_by_value(boxes[:, :, 1], 0, height - 1)
          x2 = tf.clip_by_value(boxes[:, :, 2], 0, width - 1)
          y2 = tf.clip_by_value(boxes[:, :, 3], 0, height - 1)
  
          return tf.keras.backend.stack([x1, y1, x2, y2], axis=2)
  
      def compute_output_shape(self, input_shape):
          return input_shape[1]
  
  
  class RegressBoxes(tf.keras.layers.Layer):
      def __init__(self, *args, **kwargs):
          super(RegressBoxes, self).__init__(*args, **kwargs)
  
      def call(self, inputs, **kwargs):
          anchors, regression = inputs
          return bbox_transform_inv(anchors, regression)
  
      def compute_output_shape(self, input_shape):
          return input_shape[0]
  
      def get_config(self):
          config = super(RegressBoxes, self).get_config()
          return config
  
  
  def filter_detections(
      boxes,
      classification,
      alphas=None,
      ratios=None,
      class_specific_filter=True,
      nms=True,
      score_threshold=0.01,
      max_detections=100,
      nms_threshold=0.5,
      detect_quadrangle=False,
  ):
      """
      Filter detections using the boxes and classification values.
  
      Args
          boxes: Tensor of shape (num_boxes, 4) containing the boxes in (x1, y1, x2, y2) format.
          classification: Tensor of shape (num_boxes, num_classes) containing the classification scores.
          other: List of tensors of shape (num_boxes, ...) to filter along with the boxes and classification scores.
          class_specific_filter: Whether to perform filtering per class, or take the best scoring class and filter those.
          nms: Flag to enable/disable non maximum suppression.
          score_threshold: Threshold used to prefilter the boxes with.
          max_detections: Maximum number of detections to keep.
          nms_threshold: Threshold for the IoU value to determine when a box should be suppressed.
  
      Returns
          A list of [boxes, scores, labels, other[0], other[1], ...].
          boxes is shaped (max_detections, 4) and contains the (x1, y1, x2, y2) of the non-suppressed boxes.
          scores is shaped (max_detections,) and contains the scores of the predicted class.
          labels is shaped (max_detections,) and contains the predicted label.
          other[i] is shaped (max_detections, ...) and contains the filtered other[i] data.
          In case there are less than max_detections detections, the tensors are padded with -1's.
      """
  
      def _filter_detections(scores_, labels_):
          # threshold based on score
          # (num_score_keeps, 1)
          indices_ = tf.where(tf.keras.backend.greater(scores_, score_threshold))
  
          if nms:
              # (num_score_keeps, 4)
              filtered_boxes = tf.gather_nd(boxes, indices_)
              # In [4]: scores = np.array([0.1, 0.5, 0.4, 0.2, 0.7, 0.2])
              # In [5]: tf.greater(scores, 0.4)
              # Out[5]: <tf.Tensor: id=2, shape=(6,), dtype=bool, numpy=array([False,  True, False, False,  True, False])>
              # In [6]: tf.where(tf.greater(scores, 0.4))
              # Out[6]:
              # <tf.Tensor: id=7, shape=(2, 1), dtype=int64, numpy=
              # array([[1],
              #        [4]])>
              #
              # In [7]: tf.gather(scores, tf.where(tf.greater(scores, 0.4)))
              # Out[7]:
              # <tf.Tensor: id=15, shape=(2, 1), dtype=float64, numpy=
              # array([[0.5],
              #        [0.7]])>
              filtered_scores = tf.keras.backend.gather(scores_, indices_)[:, 0]
  
              # perform NMS
              # filtered_boxes = tf.concat([filtered_boxes[..., 1:2], filtered_boxes[..., 0:1],
              #                             filtered_boxes[..., 3:4], filtered_boxes[..., 2:3]], axis=-1)
              nms_indices = tf.image.non_max_suppression(
                  filtered_boxes,
                  filtered_scores,
                  max_output_size=max_detections,
                  iou_threshold=nms_threshold,
              )
  
              # filter indices based on NMS
              # (num_score_nms_keeps, 1)
              indices_ = tf.keras.backend.gather(indices_, nms_indices)
  
          # add indices to list of all indices
          # (num_score_nms_keeps, )
          labels_ = tf.gather_nd(labels_, indices_)
          # (num_score_nms_keeps, 2)
          indices_ = tf.keras.backend.stack([indices_[:, 0], labels_], axis=1)
  
          return indices_
  
      if class_specific_filter:
          all_indices = []
          # perform per class filtering
          for c in range(int(classification.shape[1])):
              scores = classification[:, c]
              labels = c * tf.ones((tf.keras.backend.shape(scores)[0],), dtype="int64")
              all_indices.append(_filter_detections(scores, labels))
  
          # concatenate indices to single tensor
          # (concatenated_num_score_nms_keeps, 2)
          indices = tf.keras.backend.concatenate(all_indices, axis=0)
      else:
          scores = tf.keras.backend.max(classification, axis=1)
          labels = tf.keras.backend.argmax(classification, axis=1)
          indices = _filter_detections(scores, labels)
  
      # select top k
      scores = tf.gather_nd(classification, indices)
      labels = indices[:, 1]
      scores, top_indices = tf.nn.top_k(
          scores,
          k=tf.keras.backend.minimum(max_detections, tf.keras.backend.shape(scores)[0]),
      )
  
      # filter input using the final set of indices
      indices = tf.keras.backend.gather(indices[:, 0], top_indices)
      boxes = tf.keras.backend.gather(boxes, indices)
      labels = tf.keras.backend.gather(labels, top_indices)
  
      # zero pad the outputs
      pad_size = tf.keras.backend.maximum(
          0, max_detections - tf.keras.backend.shape(scores)[0]
      )
      boxes = tf.pad(boxes, [[0, pad_size], [0, 0]], constant_values=-1)
      scores = tf.pad(scores, [[0, pad_size]], constant_values=-1)
      labels = tf.pad(labels, [[0, pad_size]], constant_values=-1)
      labels = tf.keras.backend.cast(labels, "int32")
  
      # set shapes, since we know what they are
      boxes.set_shape([max_detections, 4])
      scores.set_shape([max_detections])
      labels.set_shape([max_detections])
  
      if detect_quadrangle:
          alphas = tf.keras.backend.gather(alphas, indices)
          ratios = tf.keras.backend.gather(ratios, indices)
          alphas = tf.pad(alphas, [[0, pad_size], [0, 0]], constant_values=-1)
          ratios = tf.pad(ratios, [[0, pad_size]], constant_values=-1)
          alphas.set_shape([max_detections, 4])
          ratios.set_shape([max_detections])
          return [boxes, scores, alphas, ratios, labels]
      else:
          return [boxes, scores, labels]
  
  
  class FilterDetections(tf.keras.layers.Layer):
      """
      Keras layer for filtering detections using score threshold and NMS.
      """
  
      def __init__(
          self,
          nms=True,
          class_specific_filter=True,
          nms_threshold=0.5,
          score_threshold=0.01,
          max_detections=100,
          parallel_iterations=32,
          detect_quadrangle=False,
          **kwargs
      ):
          """
          Filters detections using score threshold, NMS and selecting the top-k detections.
  
          Args
              nms: Flag to enable/disable NMS.
              class_specific_filter: Whether to perform filtering per class, or take the best scoring class and filter those.
              nms_threshold: Threshold for the IoU value to determine when a box should be suppressed.
              score_threshold: Threshold used to prefilter the boxes with.
              max_detections: Maximum number of detections to keep.
              parallel_iterations: Number of batch items to process in parallel.
          """
          self.nms = nms
          self.class_specific_filter = class_specific_filter
          self.nms_threshold = nms_threshold
          self.score_threshold = score_threshold
          self.max_detections = max_detections
          self.parallel_iterations = parallel_iterations
          self.detect_quadrangle = detect_quadrangle
          super(FilterDetections, self).__init__(**kwargs)
  
      def call(self, inputs, **kwargs):
          """
          Constructs the NMS graph.
  
          Args
              inputs : List of [boxes, classification, other[0], other[1], ...] tensors.
          """
          boxes = inputs[0]
          classification = inputs[1]
          if self.detect_quadrangle:
              alphas = inputs[2]
              ratios = inputs[3]
  
          # wrap nms with our parameters
          def _filter_detections(args):
              boxes_ = args[0]
              classification_ = args[1]
              alphas_ = args[2] if self.detect_quadrangle else None
              ratios_ = args[3] if self.detect_quadrangle else None
  
              return filter_detections(
                  boxes_,
                  classification_,
                  alphas_,
                  ratios_,
                  nms=self.nms,
                  class_specific_filter=self.class_specific_filter,
                  score_threshold=self.score_threshold,
                  max_detections=self.max_detections,
                  nms_threshold=self.nms_threshold,
                  detect_quadrangle=self.detect_quadrangle,
              )
  
          # call filter_detections on each batch item
          if self.detect_quadrangle:
              outputs = tf.map_fn(
                  _filter_detections,
                  elems=[boxes, classification, alphas, ratios],
                  dtype=["float32", "float32", "float32", "float32", "int32"],
                  parallel_iterations=self.parallel_iterations,
              )
          else:
              outputs = tf.map_fn(
                  _filter_detections,
                  elems=[boxes, classification],
                  dtype=["float32", "float32", "int32"],
                  parallel_iterations=self.parallel_iterations,
              )
  
          return outputs
  
      def compute_output_shape(self, input_shape):
          """
          Computes the output shapes given the input shapes.
  
          Args
              input_shape : List of input shapes [boxes, classification].
  
          Returns
              List of tuples representing the output shapes:
              [filtered_boxes.shape, filtered_scores.shape, filtered_labels.shape, filtered_other[0].shape, filtered_other[1].shape, ...]
          """
          if self.detect_quadrangle:
              return [
                  (input_shape[0][0], self.max_detections, 4),
                  (input_shape[1][0], self.max_detections),
                  (input_shape[1][0], self.max_detections, 4),
                  (input_shape[1][0], self.max_detections),
                  (input_shape[1][0], self.max_detections),
              ]
          else:
              return [
                  (input_shape[0][0], self.max_detections, 4),
                  (input_shape[1][0], self.max_detections),
                  (input_shape[1][0], self.max_detections),
              ]
  
      def compute_mask(self, inputs, mask=None):
          """
          This is required in Keras when there is more than 1 output.
          """
          return (len(inputs) + 1) * [None]
  
      def get_config(self):
          """
          Gets the configuration of this layer.
  
          Returns
              Dictionary containing the parameters of this layer.
          """
          config = super(FilterDetections, self).get_config()
          config.update(
              {
                  "nms": self.nms,
                  "class_specific_filter": self.class_specific_filter,
                  "nms_threshold": self.nms_threshold,
                  "score_threshold": self.score_threshold,
                  "max_detections": self.max_detections,
                  "parallel_iterations": self.parallel_iterations,
              }
          )
  
          return config
  