def perceive(
        self, to_float=True, new_parent_node: Union[NodePath, None] = None, position=None, hpr=None
    ) -> np.ndarray:
        """
        When to_float is set to False, the image will be represented by unit8 with component value ranging from [0-255].
        Otherwise, it will be float type with component value ranging from [0.-1.]. By default, the reset parameters are
        all None. In this case, the camera will render the result with poses and position set by track() function.

        When the reset parameters are not None, this camera will be mounted to a new place and render corresponding new
        results. After this, the camera will be returned to the original states. This process is like borrowing the
        camera to capture a new image and return the camera to the owner. This usually happens when using one camera to
        render multiple times from different positions and poses.

        new_parent_node should be a NodePath like object.origin or vehicle.origin or self.engine.origin, which
        means the world origin. When new_parent_node is set, both position and hpr have to be set as well. The position
        and hpr are all 3-dim vector representing:
            1) the relative position to the reparent node
            2) the heading/pitch/roll of the sensor

        Args:
            to_float: When to_float is set to False, the image will be represented by unit8 with component value ranging
                from [0-255]. Otherwise, it will be float type with component value ranging from [0.-1.].
            new_parent_node: new_parent_node should be a NodePath like object.origin or vehicle.origin or
                self.engine.origin, which means the world origin. When new_parent_node is set, both position and hpr
                have to be set as well. The position and hpr are all 3-dim vector representing:
            position: the relative position to the reparent node
            hpr: the heading/pitch/roll of the sensor

        Return:
            Array representing the image.
        """

        different_pos_hpr = False
        if new_parent_node:
            if position is None:
                position = constants.DEFAULT_SENSOR_OFFSET
            if hpr is None:
                hpr = constants.DEFAULT_SENSOR_HPR

            # return camera to original state
            original_object = self.cam.getParent()
            original_hpr = self.cam.getHpr()
            original_position = self.cam.getPos()

            # reparent to new parent node
            self.cam.reparentTo(new_parent_node)
            # relative position
            assert len(position) == 3, "The first parameter of camera.perceive() should be a BaseObject instance " \
                                       "or a 3-dim vector representing the (x,y,z) position."
            self.cam.setPos(Vec3(*position))
            assert len(hpr) == 3, "The hpr parameter of camera.perceive() should be  a 3-dim vector representing " \
                                  "the heading/pitch/roll."
            self.cam.setHpr(Vec3(*hpr))

            different_pos_hpr = (original_hpr != Vec3(*hpr)) or (original_position != Vec3(*position))

            self.engine.taskMgr.step()

        if different_pos_hpr:
            # Step the engine to call a new "self.engine.graphicsEngine.renderFrame()"
            # (not sure why need to step twice...
            self.engine.taskMgr.step()

        if self.enable_cuda:
            assert self.cuda_rendered_result is not None
            ret = self.cuda_rendered_result[..., :self.num_channels][..., ::-1][::-1]
        else:
            ret = self.get_rgb_array_cpu()

        if new_parent_node:
            # return camera to original objects
            self.cam.reparentTo(original_object)
            self.cam.setHpr(original_hpr)
            self.cam.setPos(original_position)
        return self._format(ret, to_float)

        When to_float is set to False, the image will be represented by unit8 with component value ranging from [0-255].
        Otherwise, it will be float type with component value ranging from [0.-1.]. By default, the reset parameters are
        all None. In this case, the camera will render the result with poses and position set by track() function.

        When the reset parameters are not None, this camera will be mounted to a new place and render corresponding new
        results. After this, the camera will be returned to the original states. This process is like borrowing the
        camera to capture a new image and return the camera to the owner. This usually happens when using one camera to
        render multiple times from different positions and poses.

        new_parent_node should be a NodePath like object.origin or vehicle.origin or self.engine.origin, which
        means the world origin. When new_parent_node is set, both position and hpr have to be set as well. The position
        and hpr are all 3-dim vector representing:
            1) the relative position to the reparent node
            2) the heading/pitch/roll of the sensor

        Args:
            to_float: When to_float is set to False, the image will be represented by unit8 with component value ranging
                from [0-255]. Otherwise, it will be float type with component value ranging from [0.-1.].
            new_parent_node: new_parent_node should be a NodePath like object.origin or vehicle.origin or
                self.engine.origin, which means the world origin. When new_parent_node is set, both position and hpr
                have to be set as well. The position and hpr are all 3-dim vector representing:
            position: the relative position to the reparent node
            hpr: the heading/pitch/roll of the sensor

        Return:
            Array representing the image.
        
metadrive/component/sensors/base_camera.py