Exemplo n.º 1
0
 def peer_subscribe(self, topic_name, topic_publish, peer_publish):
     """Lazy-start the image-publisher."""
     if self._streaming_thread is None:
         self._streaming_thread = ImageStreamingThread(self)
         self._streaming_thread.start()
     else:
         self._streaming_thread.resume()
Exemplo n.º 2
0
class Axis(rospy.SubscribeListener):
    """The ROS-VAPIX interface for video streaming."""

    def __init__(self, hostname, username, password, width, height, frame_id, camera_info_url, use_encrypted_password,
                 camera_id=1, auto_wakeup_camera=True, compression=0, fps=24, use_color=True,
                 use_square_pixels=False):
        """Create the ROS-VAPIX interface.

        :param hostname: Hostname of the camera (without http://, can be an IP address).
        :type hostname: basestring
        :param username: If login is needed, provide a username here.
        :type username: :py:obj:`basestring` | None
        :param password: If login is needed, provide a password here.
        :type password: :py:obj:`basestring` | None
        :param width: Width of the requested video stream in pixels (can be changed later). Must be one of the supported
                      resolutions. If `None`, the resolution will be chosen by height only. If also `height` is `None`,
                      then the default camera resolution will be used.
        :type width: int|None
        :param height: Height of the requested video stream in pixels (can be changed later). Must be one of the
                       supported resolutions. If `None`, the resolution will be chosen by width only. If also `width` is
                       `None`, then the default camera resolution will be used.
        :type height: int|None
        :param frame_id: The ROS TF frame assigned to the camera.
        :type frame_id: basestring
        :param camera_info_url: The URL pointing to the camera calaibration, if available.
        :type camera_info_url: basestring
        :param use_encrypted_password: Whether to use Plain HTTP Auth (False) or Digest HTTP Auth (True).
        :type use_encrypted_password: bool
        :param camera_id: ID (number) of the camera. Can be 1 to 4.
        :type camera_id: int
        :param auto_wakeup_camera: If True, there will be a wakeup trial after first unsuccessful network command.
        :type auto_wakeup_camera: bool
        :param compression: Compression of the image (0 - no compression, 100 - max compression).
        :type compression: int
        :param fps: The desired frames per second.
        :type fps: int
        :param use_color: If True, send a color stream, otherwise send only grayscale image.
        :type use_color: bool
        :param use_square_pixels: If True, the resolution will be stretched to match 1:1 pixels.
                                  By default, the pixels have a ratio of 11:12.
        :type use_square_pixels: bool
        :raises: :py:exc:`ValueError` if the requested resolution (either the `resolution`, or `width`+`height`
                 is not supported.
        """
        # True every time the video parameters have changed and the URL has to be altered (set from other threads).
        self.video_params_changed = False

        self.__initializing = True

        self._hostname = hostname
        self._camera_id = camera_id

        self.diagnostic_updater = Updater()
        self.diagnostic_updater.setHardwareID(hostname)

        self._api = None
        # autodetect the VAPIX API and connect to it; try it forever
        while self._api is None and not rospy.is_shutdown():
            try:
                self._api = VAPIX.get_api_for_camera(hostname, username, password, camera_id, use_encrypted_password)
            except (IOError, ValueError):
                rospy.loginfo("Retrying connection to VAPIX on host %s, camera %d in 2 seconds." %
                              (hostname, camera_id))
                rospy.sleep(2)
        if rospy.is_shutdown():
            return

        self._allowed_resolutions = self._get_allowed_resolutions()

        rospy.loginfo("The following resolutions are available for camera %d:\n%s" %
                      (camera_id, "\n".join([str(res) for res in self._allowed_resolutions])))
        rospy.set_param("~allowed_resolutions", [res.get_vapix_representation() for res in self._allowed_resolutions])

        # Sometimes the camera falls into power saving mode and stops streaming.
        # This setting allows the script to try to wake up the camera.
        self._auto_wakeup_camera = auto_wakeup_camera

        # dynamic-reconfigurable properties - definitions
        self._width = None  # deprecated
        self._height = None  # deprecated
        self._resolution = None
        self._compression = None
        self._fps = None
        self._use_color = None
        self._use_square_pixels = None

        # treat empty strings as None in width and height params
        width = width if width != "" else None
        height = height if height != "" else None

        # dynamic-reconfigurable properties - defaults
        if width is None and height is None:
            # TODO change to perform default resolution detection from VAPIX
            self.set_resolution(self._allowed_resolutions[0])
        else:
            resolution = self.find_resolution_by_size(width, height)
            self.set_resolution(resolution.get_vapix_representation())

        self.set_compression(compression)
        self.set_fps(fps)
        self.set_use_color(use_color)
        self.set_use_square_pixels(use_square_pixels)

        # only advertise the supported resolutions on dynamic reconfigure
        change_enum_items(
            VideoStreamConfig,
            "resolution",
            [{
                'name': res.name if isinstance(res, CIFVideoResolution) else str(res),
                'value': res.get_vapix_representation(),
                'description': str(res)
            } for res in self._allowed_resolutions],
            self._resolution.get_vapix_representation()
        )

        # dynamic reconfigure server
        self._video_stream_param_change_server = dynamic_reconfigure.server.Server(VideoStreamConfig,
                                                                                   self.reconfigure_video)

        # camera info setup
        self._frame_id = frame_id
        self._camera_info_url = camera_info_url

        # generate a valid camera name based on the hostname
        self._camera_name = camera_info_manager.genCameraName(self._hostname)
        self._camera_info = camera_info_manager.CameraInfoManager(cname=self._camera_name, url=self._camera_info_url)
        self._camera_info.loadCameraInfo()  # required before getCameraInfo()

        # the thread used for streaming images (is instantiated when the first image subscriber subscribes)
        self._streaming_thread = None

        # the publishers are started/stopped lazily in peer_subscribe/peer_unsubscribe
        self._video_publisher_frequency_diagnostic = FrequencyStatusParam({'min': self._fps, 'max': self._fps})
        self._video_publisher = PausableDiagnosedPublisher(
            self,
            rospy.Publisher("image_raw/compressed", CompressedImage, self, queue_size=100),
            self.diagnostic_updater, self._video_publisher_frequency_diagnostic, TimeStampStatusParam()
        )
        self._camera_info_publisher = PausableDiagnosedPublisher(
            self,
            rospy.Publisher("camera_info", CameraInfo, self, queue_size=100),
            self.diagnostic_updater, self._video_publisher_frequency_diagnostic, TimeStampStatusParam()
        )

        self._snapshot_server = rospy.Service("take_snapshot", TakeSnapshot, self.take_snapshot)

        self.diagnostic_updater.add(FunctionDiagnosticTask("Camera parameters", self._camera_diagnostic_callback))

        # BACKWARDS COMPATIBILITY LAYER

        self.username = username  # deprecated
        self.password = password  # deprecated
        self.use_encrypted_password = use_encrypted_password  # deprecated
        self.st = None  # deprecated
        self.pub = self._video_publisher  # deprecated
        self.caminfo_pub = self._camera_info_publisher  # deprecated

        self.__initializing = False

    def __str__(self):
        (width, height) = self._resolution.get_resolution(self._use_square_pixels)
        return 'Axis driver on host %s, camera %d (%dx%d px @ %d FPS)' % \
               (self._hostname, self._api.camera_id, width, height, self._fps)

    def peer_subscribe(self, topic_name, topic_publish, peer_publish):
        """Lazy-start the image-publisher."""
        if self._streaming_thread is None:
            self._streaming_thread = ImageStreamingThread(self)
            self._streaming_thread.start()
        else:
            self._streaming_thread.resume()

    def peer_unsubscribe(self, topic_name, num_peers):
        """Lazy-stop the image-publisher when nobody is interested"""
        if num_peers == 0:
            self._streaming_thread.pause()

    def take_snapshot(self, request):
        """Retrieve a snapshot from the camera.

        :param request: The service request.
        :type request: :py:class:`axis_camera.srv.TakeSnapshotRequest`
        :return: The response containing the image.
        :rtype: :py:class:`axis_camera.srv.TakeSnapshotResponse`
        :raises: :py:exc:`IOError`, :py:exc:`urllib2.URLError`
        """
        image_data = self._api.take_snapshot()

        image = CompressedImage()

        image.header.stamp = rospy.Time.now()
        image.header.frame_id = self._frame_id

        image.format = "jpeg"

        image.data = image_data

        response = TakeSnapshotResponse()
        response.image = image

        return response

    def reconfigure_video(self, config, level):
        """Dynamic reconfigure callback for video parameters.

        :param config: The requested configuration.
        :type config: dict
        :param level: Unused here.
        :type level: int
        :return: The config corresponding to what was really achieved.
        :rtype: dict
        """
        if self.__initializing:
            # in the initialization phase, we want to give precedence to the values given to the constructor
            config.compression = self._compression
            config.fps = self._fps
            config.use_color = self._use_color
            config.use_square_pixels = self._use_square_pixels
            config.resolution = self._resolution.get_vapix_representation()
        else:
            self.__try_set_value_from_config(config, 'compression', self.set_compression)
            self.__try_set_value_from_config(config, 'fps', self.set_fps)
            self.__try_set_value_from_config(config, 'use_color', self.set_use_color)
            self.__try_set_value_from_config(config, 'use_square_pixels', self.set_use_square_pixels)

            try:
                self.set_resolution(config.resolution)
            except ValueError:
                config.resolution = self._resolution.get_vapix_representation()

        return config

    def __try_set_value_from_config(self, config, field, setter):
        """First, try to call `setter(config[field])`, and if this call doesn't succeed. set the field in config to
        its value stored in this class.

        :param config: The dynamic reconfigure config dictionary.
        :type config: dict
        :param field: The field name (both in :py:obj:`config` and in :py:obj:`self`).
        :type field: basestring
        :param setter: The setter to use to set the value.
        :type setter: lambda function
        """
        try:
            setter(config[field])
        except ValueError:
            config[field] = getattr(self, field)

    #################################
    # DYNAMIC RECONFIGURE CALLBACKS #
    #################################

    def set_resolution(self, resolution_value):
        """Request a new resolution for the video stream.

        :param resolution_value: The string of type `width`x`height` or a :py:class:`VideoResolution` object.
        :type resolution_value: basestring|VideoResolution
        :raises: :py:exc:`ValueError` if the resolution is unknown/unsupported.
        """
        resolution = None
        if isinstance(resolution_value, VideoResolution):
            resolution = resolution_value
        elif isinstance(resolution_value, basestring):
            resolution = self._get_resolution_from_param_value(resolution_value)

        if resolution is None:
            raise ValueError("Unsupported resolution type specified: %r" % resolution_value)

        if self._resolution is None or resolution != self._resolution:
            self._resolution = resolution
            self.video_params_changed = True
            # deprecated values
            self._width = resolution.get_resolution(self._use_square_pixels)[0]
            self._height = resolution.get_resolution(self._use_square_pixels)[1]

    def _get_resolution_from_param_value(self, value):
        """Return a :py:class:`VideoResolution` object corresponding to the given video resolution param string.

        :param value: Value of the resolution parameter to parse (of form `width`x`height`).
        :type value: basestring
        :return: The :py:class:`VideoResolution` corresponding to the given resolution param string.
        :rtype: :py:class:`VideoResolution`
        :raises: :py:exc:`ValueError` if the resolution is unknown/unsupported.
        """
        for resolution in self._allowed_resolutions:
            if resolution.get_vapix_representation() == value:
                return resolution

        raise ValueError("%s is not a valid valid resolution." % value)

    def find_resolution_by_size(self, width, height):
        """Return a :py:class:`VideoResolution` object with the given dimensions.

        If there are more resolutions with the same size, any of them may be returned.

        :param width: Image width in pixels. If `None`, resolutions will be matched only by height.
        :type width: int|None
        :param height: Image height in pixels. If `None`, resolutions will be matched only by width.
        :type height: int|None
        :return: The corresponding resolution object.
        :rtype: :py:class:`VideoResolution`
        :raises: :py:exc:`ValueError` if no resolution with the given dimensions can be found.
        :raises: :py:exc:`ValueError` if both `width` and `height` are None.
        """
        if width is None and height is None:
            raise ValueError("Either width or height of the desired resolution must be specified.")

        for resolution in self._allowed_resolutions:
            size = resolution.get_resolution(use_square_pixels=False)
            if (width is None or width == size[0]) and (height is None or height == size[1]):
                return resolution

            size = resolution.get_resolution(use_square_pixels=True)
            if (width is None or width == size[0]) and (height is None or height == size[1]):
                return resolution

        raise ValueError("Cannot find a supported resolution with dimensions %sx%s" % (width, height))

    def _get_allowed_resolutions(self):
        """Return a list of resolutions supported both by the camera.

        :return: The supported resolutions list.
        :rtype: list of :py:class:`VideoResolution`
        """
        camera_resolutions = self._get_resolutions_supported_by_camera()

        return camera_resolutions

    def _get_resolutions_supported_by_camera(self):
        """Return a list of resolutions supported the camera.

        :return: The supported resolutions list.
        :rtype: list of :py:class:`VideoResolution`
        """
        try:
            names = self._api.parse_list_parameter_value(self._api.get_parameter("Properties.Image.Resolution"))
            return [VideoResolution.parse_from_vapix_param_value(name, self._api) for name in names]
        except (IOError, ValueError):
            rospy.logwarn("Could not determine resolutions supported by the camera. Asssuming only CIF.")
            return [CIFVideoResolution("CIF", 384, 288)]

    def set_compression(self, compression):
        """Request the given compression level for the video stream.

        :param compression: Compression of the image (0 - no compression, 100 - max compression).
        :type compression: int
        :raises: :py:exc:`ValueError` if the given compression level is outside the allowed range.
        """
        if compression != self._compression:
            self._compression = self.sanitize_compression(compression)
            self.video_params_changed = True

    @staticmethod
    def sanitize_compression(compression):
        """Make sure the given value can be used as a compression level of the video stream.

        :param compression: Compression of the image (0 - no compression, 100 - max compression).
        :type compression: int
        :return: The given compression converted to an int.
        :rtype: int
        :raises: :py:exc:`ValueError` if the given compression level is outside the allowed range.
        """
        compression = int(compression)
        if not (0 <= compression <= 100):
            raise ValueError("%s is not a valid value for compression." % str(compression))

        return compression

    def set_fps(self, fps):
        """Request the given compression level for the video stream.

        :param fps: The desired frames per second.
        :type fps: int
        :raises: :py:exc:`ValueError` if the given FPS is outside the allowed range.
        """
        if fps != self._fps:
            self._fps = self.sanitize_fps(fps)
            self.video_params_changed = True
            if hasattr(self, "_video_publisher_frequency_diagnostic"):
                self._video_publisher_frequency_diagnostic.freq_bound['min'] = self._fps
                self._video_publisher_frequency_diagnostic.freq_bound['max'] = self._fps

    @staticmethod
    def sanitize_fps(fps):
        """Make sure the given value can be used as FPS of the video stream.

        :param fps: The desired frames per second.
        :type fps: int
        :return: The given FPS converted to an int.
        :rtype: int
        :raises: :py:exc:`ValueError` if the given FPS is outside the allowed range.
        """
        fps = int(fps)
        if not (1 <= fps <= 30):
            raise ValueError("%s is not a valid value for FPS." % str(fps))

        return fps

    def set_use_color(self, use_color):
        """Request using/not using color in the video stream.
        :param use_color: If True, send a color stream, otherwise send only grayscale image.

        :type use_color: bool
        :raises: :py:exc:`ValueError` if the given argument is not a bool.
        """
        if use_color != self._use_color:
            self._use_color = self.sanitize_bool(use_color, "use_color")
            self.video_params_changed = True

    def set_use_square_pixels(self, use_square_pixels):
        """Request using/not using square pixels.

        :param use_square_pixels: If True, the resolution will be stretched to match 1:1 pixels.
                                  By default, the pixels have a ratio of 11:12.
        :type use_square_pixels: bool
        :raises: :py:exc:`ValueError` if the given argument is not a bool.
        """
        if use_square_pixels != self._use_square_pixels:
            self._use_square_pixels = self.sanitize_bool(use_square_pixels, "use_square_pixels")
            self.video_params_changed = True

    @staticmethod
    def sanitize_bool(value, field_name):
        """Convert the given value to a bool.

        :param value: Either True, False,, "1", "0", 1 or 0.
        :type value: :py:class:`basestring` | :py:class:`bool` | :py:class:`int`
        :param field_name: Name of the field this value belongs to (just for debug messages).
        :type field_name: basestring
        :return: The bool value of the given value.
        :rtype: :py:class:`bool`
        :raises: :py:exc:`ValueError` if the given value is not supported in this conversion.
        """
        if value not in (True, False, "1", "0", 1, 0):
            raise ValueError("%s is not a valid value for %s." % (str(value), field_name))

        # bool("0") returns True because it is a nonempty string
        if value == "0":
            return False

        return bool(value)

    def _camera_diagnostic_callback(self, diag_message):
        assert isinstance(diag_message, DiagnosticStatusWrapper)

        diag_message.summary(DiagnosticStatusWrapper.OK, "Video parameters")
        diag_message.add("FPS", self._fps)
        diag_message.add("Resolution", self._resolution)
        diag_message.add("Compression", self._compression)
        diag_message.add("Color image", self._use_color)
        diag_message.add("Square pixels used", self._use_square_pixels)