Example #1
0
    def perform_test(self, capsule: BaseCapsule,
                     worker_pool: CapsuleThreadPool, num_samples: int) \
            -> datetime.timedelta:

        # Warm things up, such as getting model on GPU if capsule uses it
        warmup_results = worker_pool.map(
            lambda kwargs: capsule.process_frame(**kwargs),
            [self.generate_input_kwargs(capsule) for _ in range(50)]
        )
        for _ in warmup_results:
            pass

        # Generate test args before starting the test, so that the
        # benchmark is purely just for the capsule
        test_inputs = [self.generate_input_kwargs(capsule)
                       for _ in range(num_samples)]

        # Begin the benchmark
        start_time = datetime.datetime.now()
        results = worker_pool.map(
            lambda kwargs: capsule.process_frame(**kwargs),
            test_inputs)

        for _ in results:
            pass

        end_time = datetime.datetime.now()
        duration = end_time - start_time

        return duration
def _run_inference_on_images(images: List[np.ndarray], capsule: BaseCapsule):
    """Run inference on a list of images"""
    # Make multiple parallel requests with different images and different
    # input DetectionNodes to the capsule.
    request_input = []

    with ThreadPoolExecutor(
            thread_name_prefix="InputOutputValidation ") \
            as executor:
        for stream_id, image in enumerate(images):
            if capsule.input_type.size == NodeDescription.Size.NONE:
                input_node = None

            elif capsule.input_type.size == NodeDescription.Size.SINGLE:
                input_node = make_detection_node(image.shape,
                                                 capsule.input_type)

            elif capsule.input_type.size == NodeDescription.Size.ALL:
                input_node = [make_detection_node(image.shape,
                                                  capsule.input_type)
                              for _ in range(random.randint(0, 5))]
            else:
                raise NotImplementedError(
                    "The capsule did not have a NodeDescription.Size that was "
                    "known!")

            options = make_capsule_options(capsule)

            future = executor.submit(
                capsule.process_frame,
                frame=image,
                detection_node=input_node,
                options=options,
                state=capsule.stream_state())

            request_input.append((future, input_node))

        for future, input_node in request_input:
            # Postprocess the results
            prediction = future.result(timeout=90)

            # Verify that the capsule performed correctly

            if isinstance(prediction, DetectionNode):
                # Validate that what was returned by the capsule was valid
                assert (capsule.output_type.size
                        is NodeDescription.Size.SINGLE)
                output_nodes = [prediction]
            elif prediction is None and isinstance(input_node, DetectionNode):
                # If the capsule didn't output something, then it must have
                # modified the input node in-place. Validate the changes.
                assert (capsule.output_type.size
                        is NodeDescription.Size.SINGLE)
                output_nodes = [input_node]
            elif prediction is None and isinstance(input_node, list):
                # If this capsule accepts size ALL as input, then it must
                # have modified the detections within the input list
                assert capsule.input_type.size is NodeDescription.Size.ALL
                assert capsule.output_type.size is NodeDescription.Size.ALL
                output_nodes = input_node
            elif isinstance(prediction, list):
                # Validate that every detection node in the list is correct
                assert capsule.output_type.size is NodeDescription.Size.ALL
                output_nodes = prediction
            else:
                raise RuntimeError(f"Unknown prediction type: {prediction}")

            # Validate every output node against the capsules output_type
            for output_node in output_nodes:
                assert capsule.output_type.describes(output_node), \
                    ("Capsule failed to output a prediction that matches "
                     "the NodeDescription it had for it's output type. "
                     f"Prediction: {prediction}")

                # Assert the nodes "extra_data" attribute can be JSON encoded
                # without errors. Typically this can happen if there's a numpy
                # array carelessly left in the extra_data

                json.loads(json.dumps(output_node.extra_data))

                # If this capsule can encode things, verify that the backend
                # correctly implemented the "distance" function
                if (capsule.capability.encoded
                        and prediction is not None
                        and len(prediction) > 0):
                    # Get one of the predictions
                    pred = prediction[0]

                    # Measure the distance from an encoding to itself
                    # (should be 0)
                    distances = capsule.backends[0].distances(
                        pred.encoding, np.array([pred.encoding]))
                    assert len(distances) == 1
                    assert distances[0] == 0, \
                        ("This assertion can be removed in the case that "
                         "there is some new distance function where two "
                         "encodings that are equal no longer have a distance "
                         "of 0. Until that case exists, keep this assertion.")
def _validate_capsule(capsule: BaseCapsule):
    """This will try calling different attributes on a capsule to make sure
    they are there. This is based on the API compatibility version.

    :raises: InvalidCapsuleError
    """

    def check_arg_names(func: Callable,
                        correct: List[str],
                        ignore: Optional[List[str]] = None) \
            -> bool:
        """Return False if a function has the wrong argument names. Return
        true if they are correct.
        Usage:
        >>> def my_func(self, frame, detection_node):
        ...     pass
        >>> check_arg_names(my_func, ['self'], ['frame', 'detection_node'])
        True
        """
        # noinspection PyUnresolvedReferences
        code = func.__code__

        ignore = [] if ignore is None else ignore
        all_var_names = code.co_varnames
        arg_names = all_var_names[:code.co_argcount]
        filtered = [n for n in arg_names if n not in ignore]
        return filtered == correct

    try:
        # If the ASCII flag is used, only [a-zA-Z0-9_] is matched for \w.
        # Otherwise, some other Unicode characters can be matched, depending on
        # the locale. We don't want that.
        if re.fullmatch(r"\w+", capsule.name, flags=re.ASCII) is None:
            raise InvalidCapsuleError(
                "Capsule names must only contain alphanumeric characters and "
                "underscores")

        # Validate the capsule class attributes
        capsule_assertions = [
            isinstance(capsule.name, str),
            callable(capsule.backend_loader),
            isinstance(capsule.version, int),
            isinstance(capsule.input_type, NodeDescription),
            isinstance(capsule.output_type, NodeDescription),
            isinstance(capsule.options, dict)
        ]

        if not all(capsule_assertions):
            raise InvalidCapsuleError(
                f"The capsule has an invalid internal configuration!\n" +
                f"Capsule Assertions: {capsule_assertions}")

        # Make sure that certain things are NOT attributes (we don't want
        # accidental leftover code from previous capsule versions)
        unwanted_attributes = ["backend_config"]
        for unwanted_attr in unwanted_attributes:
            try:
                # This should throw an attribute error
                capsule.__getattribute__(unwanted_attr)
                raise InvalidCapsuleError(
                    f"The capsule has leftover attributes from a previous "
                    f"OpenVisionCapsules API version. Attribute name: "
                    f"{unwanted_attr}")
            except AttributeError:
                pass

        # Validate the capsule's backend_loader takes the right args
        loader = capsule.backend_loader
        loader_assertions = [
            callable(loader),
            check_arg_names(func=loader, correct=["capsule_files", "device"])
        ]
        if not all(loader_assertions):
            raise InvalidCapsuleError(
                f"The capsule's backend_loader has an invalid configuration!\n"
                f"Loader Assertions: {loader_assertions}")

        # Validate the backend class attributes
        if capsule.backends is not None:
            backend = capsule.backends[0]
            backend_assertions = [
                callable(backend.batch_predict),
                callable(backend.process_frame),
                callable(backend.close),
                isinstance(capsule.backends[0], BaseBackend)
            ]
            if not all(backend_assertions):
                raise InvalidCapsuleError(
                    f"The capsule's backend has an invalid configuration!\n"
                    f"Backend Assertions: {backend_assertions}")

        # Validate the stream state
        stream_state = capsule.stream_state
        stream_state_assertions = [
            (stream_state is BaseStreamState
             or BaseStreamState in stream_state.__bases__)
        ]
        if not all(stream_state_assertions):
            raise InvalidCapsuleError(
                "The capsule's stream_state has an invalid configuration!\n"
                f"Stream State Assertions: {stream_state_assertions}")

        # Validate that if the capsule is an encoder, it has a threshold option
        if capsule.capability.encoded:
            if "recognition_threshold" not in capsule.options.keys():
                raise InvalidCapsuleError(
                    "This capsule can encode, but doesn't have a option "
                    "called \"recognition_threshold\"!")
    except InvalidCapsuleError:
        # Don't catch this exception type in the "except Exception" below
        raise
    except AttributeError as e:
        message = f"The capsule did not describe the necessary attributes. " \
                  f"Error: {e}"
        raise InvalidCapsuleError(message)
    except Exception as e:
        message = f"This capsule is invalid for unknown reasons. Error: {e}"
        raise InvalidCapsuleError(message)
Example #4
0
def _run_inference_on_images(images: List[np.ndarray], capsule: BaseCapsule):
    """Run inference on a list of images"""
    # Make multiple parallel requests with different images and different
    # input DetectionNodes to the capsule.
    request_input = []

    with ThreadPoolExecutor(
            thread_name_prefix="InputOutputValidation ") \
            as executor:
        for stream_id, image in enumerate(images):
            if capsule.input_type.size == NodeDescription.Size.NONE:
                node = None

            elif capsule.input_type.size == NodeDescription.Size.SINGLE:
                node = make_detection_node(image.shape,
                                           capsule.input_type)

            elif capsule.input_type.size == NodeDescription.Size.ALL:
                node = [make_detection_node(image.shape,
                                            capsule.input_type)
                        for _ in range(random.randint(0, 5))]
            else:
                raise NotImplementedError(
                    "The capsule did not have a NodeDescription.Size that was "
                    "known!")

            options = make_capsule_options(capsule)

            future = executor.submit(
                capsule.process_frame,
                frame=image,
                detection_node=node,
                options=options,
                state=capsule.stream_state())

            request_input.append((future, node))

        for future, node in request_input:
            # Postprocess the results
            prediction = future.result(timeout=90)

            # Verify that the capsule performed correctly
            if node is not None and not isinstance(node, list):
                assert capsule.output_type.describes(node)
            elif prediction is not None:
                assert all(capsule.output_type.describes(detection_node)
                           for detection_node in prediction), \
                    ("Capsule failed to output a prediction that matches "
                     "the NodeDescription it had for it's output type. "
                     f"Prediction: {prediction}")
            # If this capsule can encode things, verify that the backend
            # correctly implemented the "distance" function
            if (capsule.capability.encoded
                    and prediction is not None
                    and len(prediction) > 0):
                # Get one of the predictions
                pred = prediction[0]

                # Measure the distance from an encoding to itself
                # (should be 0)
                distances = capsule.backends[0].distances(
                    pred.encoding, np.array([pred.encoding]))
                assert len(distances) == 1
                assert distances[0] == 0, \
                    ("This assertion can be removed in the case that "
                     "there is some new distance function where two "
                     "encodings that are equal no longer have a distance "
                     "of 0. Until that case exists, keep this assertion.")