text: # nuScenes dev-kit.
  # Code written by Holger Caesar, 2018.
  # Licensed under the Creative Commons [see licence.txt]
  
  from typing import List, Dict, Optional
  
  import numpy as np
  from pyquaternion import Quaternion
  
  from second.data.nuscenes.eval.detection.data_classes import EvalBox
  from second.data.nuscenes.utils.data_classes import Box
  
  
  def category_to_detection_name(category_name: str) -> Optional[str]:
      """
      Default label mapping from nuScenes to nuScenes detection classes.
      Note that pedestrian does not include personal_mobility, stroller and wheelchair.
      :param category_name: Generic nuScenes class.
      :return: nuScenes detection classes.
      """
      detection_mapping = {
          'movable_object.barrier': 'barrier',
          'vehicle.bicycle': 'bicycle',
          'vehicle.bus.bendy': 'bus',
          'vehicle.bus.rigid': 'bus',
          'vehicle.car': 'car',
          'vehicle.construction': 'construction_vehicle',
          'vehicle.motorcycle': 'motorcycle',
          'human.pedestrian.adult': 'pedestrian',
          'human.pedestrian.child': 'pedestrian',
          'human.pedestrian.construction_worker': 'pedestrian',
          'human.pedestrian.police_officer': 'pedestrian',
          'movable_object.trafficcone': 'traffic_cone',
          'vehicle.trailer': 'trailer',
          'vehicle.truck': 'truck'
      }
  
      if category_name in detection_mapping:
          return detection_mapping[category_name]
      else:
          return None
  
  
  def detection_name_to_rel_attributes(detection_name: str) -> List[str]:
      """
      Returns a list of relevant attributes for a given detection class.
      :param detection_name: The detection classs.
      :return: List of relevant attributes.
      """
      if detection_name in ['pedestrian']:
          rel_attributes = ['pedestrian.moving', 'pedestrian.sitting_lying_down', 'pedestrian.standing']
      elif detection_name in ['bicycle', 'motorcycle']:
          rel_attributes = ['cycle.with_rider', 'cycle.without_rider']
      elif detection_name in ['car', 'bus', 'construction_vehicle', 'trailer', 'truck']:
          rel_attributes = ['vehicle.moving', 'vehicle.parked', 'vehicle.stopped']
      elif detection_name in ['barrier', 'traffic_cone']:
          # Classes without attributes: barrier, traffic_cone.
          rel_attributes = []
      else:
          raise ValueError('Error: %s is not a valid detection class.' % detection_name)
  
      return rel_attributes
  
  
  def center_distance(gt_box: EvalBox, pred_box: EvalBox) -> float:
      """
      L2 distance between the box centers (xy only).
      :param gt_box: GT annotation sample.
      :param pred_box: Predicted sample.
      :return: L2 distance.
      """
      return np.linalg.norm(np.array(pred_box.translation[:2]) - np.array(gt_box.translation[:2]))
  
  
  def velocity_l2(gt_box: EvalBox, pred_box: EvalBox) -> float:
      """
      L2 distance between the velocity vectors (xy only).
      If the predicted velocities are nan, we return inf, which is subsequently clipped to 1.
      :param gt_box: GT annotation sample.
      :param pred_box: Predicted sample.
      :return: L2 distance.
      """
      return np.linalg.norm(np.array(pred_box.velocity) - np.array(gt_box.velocity))
  
  
  def yaw_diff(gt_box: EvalBox, eval_box: EvalBox, period: float = 2*np.pi) -> float:
      """
      Returns the yaw angle difference between the orientation of two boxes.
      :param gt_box: Ground truth box.
      :param eval_box: Predicted box.
      :param period: Periodicity in radians for assessing angle difference.
      :return: Yaw angle difference in radians in [0, pi].
      """
      yaw_gt = quaternion_yaw(Quaternion(gt_box.rotation))
      yaw_est = quaternion_yaw(Quaternion(eval_box.rotation))
  
      return abs(angle_diff(yaw_gt, yaw_est, period))
  
  
  def angle_diff(x: float, y: float, period: float):
      """
      Get the smallest angle difference between 2 angles: the angle from y to x.
      :param x: To angle.
      :param y: From angle.
      :param period: Periodicity in radians for assessing angle difference.
      :return: <float>. Signed smallest between-angle difference in range (-pi, pi).
      """
  
      # calculate angle difference, modulo to [0, 2*pi]
      diff = (x - y + period / 2) % period - period / 2
      if diff > np.pi:
          diff = diff - (2 * np.pi)  # shift (pi, 2*pi] to (-pi, 0]
  
      return diff
  
  
  def attr_acc(gt_box: EvalBox, pred_box: EvalBox) -> float:
      """
      Computes the classification accuracy for the attribute of this class (if any).
      If the GT class has no attributes or the annotation is missing attributes, we assign an accuracy of nan, which is
      ignored later on.
      :param gt_box: GT annotation sample.
      :param pred_box: Predicted sample.
      :return: Attribute classification accuracy (0 or 1) or nan if GT annotation does not have any attributes.
      """
      if gt_box.attribute_name == '':
          # If the class does not have attributes or this particular sample is missing attributes, return nan, which is
          # ignored later. Note that about 0.4% of the sample_annotations have no attributes, although they should.
          acc = np.nan
      else:
          # Check that label is correct.
          acc = float(gt_box.attribute_name == pred_box.attribute_name)
      return acc
  
  
  def scale_iou(sample_annotation: EvalBox, sample_result: EvalBox) -> float:
      """
      This method compares predictions to the ground truth in terms of scale.
      It is equivalent to intersection over union (IOU) between the two boxes in 3D,
      if we assume that the boxes are aligned, i.e. translation and rotation are considered identical.
      :param sample_annotation: GT annotation sample.
      :param sample_result: Predicted sample.
      :return: Scale IOU.
      """
      # Validate inputs.
      sa_size = np.array(sample_annotation.size)
      sr_size = np.array(sample_result.size)
      assert all(sa_size > 0), 'Error: sample_annotation sizes must be >0.'
      assert all(sr_size > 0), 'Error: sample_result sizes must be >0.'
  
      # Compute IOU.
      min_wlh = np.minimum(sa_size, sr_size)
      volume_annotation = np.prod(sa_size)
      volume_result = np.prod(sr_size)
      intersection = np.prod(min_wlh)  # type: float
      union = volume_annotation + volume_result - intersection  # type: float
      iou = intersection / union
  
      return iou
  
  
  def quaternion_yaw(q: Quaternion) -> float:
      """
      Calculate the yaw angle from a quaternion.
      Note that this only works for a quaternion that represents a box in lidar or global coordinate frame.
      It does not work for a box in the camera frame.
      :param q: Quaternion of interest.
      :return: Yaw angle in radians.
      """
  
      # Project into xy plane.
      v = np.dot(q.rotation_matrix, np.array([1, 0, 0]))
  
      # Measure yaw using arctan.
      yaw = np.arctan2(v[1], v[0])
  
      return yaw
  
  
  def boxes_to_sensor(boxes: List[EvalBox], pose_record: Dict, cs_record: Dict):
      """
      Map boxes from global coordinates to the vehicle's sensor coordinate system.
      :param boxes: The boxes in global coordinates.
      :param pose_record: The pose record of the vehicle at the current timestamp.
      :param cs_record: The calibrated sensor record of the sensor.
      :return: The transformed boxes.
      """
      boxes_out = []
      for box in boxes:
          # Create Box instance.
          box = Box(box.translation, box.size, Quaternion(box.rotation))
  
          # Move box to ego vehicle coord system.
          box.translate(-np.array(pose_record['translation']))
          box.rotate(Quaternion(pose_record['rotation']).inverse)
  
          #  Move box to sensor coord system.
          box.translate(-np.array(cs_record['translation']))
          box.rotate(Quaternion(cs_record['rotation']).inverse)
  
          boxes_out.append(box)
  
      return boxes_out
  
  
  def cummean(x: np.array) -> np.array:
      """
      Computes the cumulative mean up to each position in a NaN sensitive way
      - If all values are NaN return an array of ones.
      - If some values are NaN, accumulate arrays discording those entries.
      """
      if sum(np.isnan(x)) == len(x):
          # Is all numbers in array are NaN's.
          return np.ones(len(x))  # If all errors are NaN set to error to 1 for all operating points.
      else:
          # Accumulate in a nan-aware manner.
          sum_vals = np.nancumsum(x.astype(float))  # Cumulative sum ignoring nans.
          count_vals = np.cumsum(~np.isnan(x))  # Number of non-nans up to each position.
          return np.divide(sum_vals, count_vals, out=np.zeros_like(sum_vals), where=count_vals != 0)
  