class ConfigurationError(Exception):
    """An error with your stbt configuration file."""
    pass def get_config(section, key, default=NoDefault, type_=str):
    """Read the value of ``key`` from ``section`` of the test-pack
    configuration file.

    For example, if your configuration file looks like this::

        [test_pack]
        stbt_version = 30

        [my_company_name]
        backend_ip = 192.168.1.23

    then you can read the value from your test script like this::

        backend_ip = stbt.get_config("my_company_name", "backend_ip")

    This searches in the ``.stbt.conf`` file at the root of your test-pack, and
    in the ``config/test-farm/<hostname>.conf`` file matching the hostname of
    the stb-tester device where the script is running. Values in the
    host-specific config file override values in ``.stbt.conf``. See
    `Configuration files
    <https://stb-tester.com/manual/advanced-configuration#configuration-files>`__
    for more details.

    Test scripts can use ``get_config`` to read tags that you specify at
    run-time: see `Automatic configuration keys
    <https://stb-tester.com/manual/advanced-configuration#automatic-configuration-keys>`__.

    Raises `ConfigurationError` if the specified ``section`` or ``key`` is not
    found, unless ``default`` is specified (in which case ``default`` is
    returned).

    Changed in v32: Allow specifying ``None`` as the default value (previously
    ``None`` would be treated as if you hadn't specified any default value).
    """

    config = _config_init()

    try:
        if type_ is bool:
            return config.getboolean(section, key)
        elif issubclass(type_, enum.Enum):
            return _to_enum(type_, config.get(section, key), section, key)
        else:
            return type_(config.get(section, key))
    except configparser.Error as e:
        if default is NoDefault:
            raise ConfigurationError(e.message)
        else:
            return default
    except ValueError:
        raise ConfigurationError("'%s.%s' invalid type (must be %s)" % (
            section, key, type_.__name__)) def memoize_iterator(additional_fields=None):
    """
    A decorator like `imgproc_cache.memoize`, but for functions that return an
    iterator.
    """

    def decorator(f):
        func_key = json.dumps([f.__name__, additional_fields],
                              sort_keys=True)

        @functools.wraps(f)
        def inner(*args, **kwargs):
            use_cache = kwargs.pop("use_cache", None)
            try:
                if _cache is None or (not _enabled and not use_cache):
                    raise NotCachable()
                full_kwargs = inspect.getcallargs(f, *args, **kwargs)  # pylint:disable=deprecated-method
                key = _cache_hash((func_key, full_kwargs))
            except NotCachable:
                for x in f(*args, **kwargs):
                    yield x
                return

            for i in itertools.count():
                with _cache.begin() as txn:
                    out = txn.get(key + str(i).encode())
                if out is None:
                    break
                out_, stop_ = json.loads(out)
                if stop_:
                    return
                yield out_

            skip = i  # pylint:disable=undefined-loop-variable
            it = f(**full_kwargs)
            for i in itertools.count():
                try:
                    output = next(it)
                    if i >= skip:
                        _cache_put(key + str(i).encode(), [output, None])
                        yield output
                except StopIteration:
                    _cache_put(key + str(i).encode(), [None, "StopIteration"])
                    return

        return inner
    return decorator def crop(frame, region):
    """Returns an image containing the specified region of ``frame``.

    :type frame: `stbt.Frame` or `numpy.ndarray`
    :param frame: An image in OpenCV format (for example as returned by
      `frames`, `get_frame` and `load_image`, or the ``frame`` parameter of
      `MatchResult`).

    :type Region region: The region to crop.

    :returns: An OpenCV image (`numpy.ndarray`) containing the specified region
      of the source frame. This is a view onto the original data, so if you
      want to modify the cropped image call its ``copy()`` method first.
    """
    r = _validate_region(frame, region)
    return frame[r.y:r.bottom, r.x:r.right] def _frame_repr(frame):
    if frame is None:
        return "None"
    if isinstance(frame, (Image, Frame)):
        return repr(frame)
    if len(frame.shape) == 3:
        return "<%dx%dx%d>" % (frame.shape[1], frame.shape[0], frame.shape[2])
    else:
        return "<%dx%d>" % (frame.shape[1], frame.shape[0]) def _image_region(image):
    s = image.shape
    return Region(0, 0, s[1], s[0]) def limit_time(frames, duration_secs):
    """
    Adapts a frame iterator such that it will return EOS after `duration_secs`
    worth of video has been read.
    """
    import time
    end_time = time.time() + duration_secs
    for frame in frames:
        if frame.time > end_time:
            debug("timed out: %.3f > %.3f" % (frame.time, end_time))
            break
        else:
            yield frame def load_image(filename, flags=None, color_channels=None):
    """Find & read an image from disk.

    If given a relative filename, this will search in the directory of the
    Python file that called ``load_image``, then in the directory of that
    file's caller, etc. This allows you to use ``load_image`` in a helper
    function, and then call that helper function from a different Python file
    passing in a filename relative to the caller.

    Finally this will search in the current working directory. This allows
    loading an image that you had previously saved to disk during the same
    test run.

    This is the same lookup algorithm used by `stbt.match` and similar
    functions.

    :param str filename: A relative or absolute filename.

    :param flags: Flags to pass to :ocv:pyfunc:`cv2.imread`. Deprecated; use
      ``color_channels`` instead.

    :param Tuple[int] color_channels: Tuple of acceptable numbers of color
      channels for the output image: 1 for grayscale, 3 for color, and 4 for
      color with an alpha (transparency) channel. For example,
      ``color_channels=(3, 4)`` will accept color images with or without an
      alpha channel. Defaults to ``(3, 4)``.

      If the image doesn't match the specified ``color_channels`` it will be
      converted to the specified format.

    :rtype: stbt.Image
    :returns: An image in OpenCV format — that is, a `numpy.ndarray` of 8-bit
        values. With the default ``color_channels`` parameter this will be 3
        channels BGR, or 4 channels BGRA if the file has transparent pixels.
    :raises: `IOError` if the specified path doesn't exist or isn't a valid
        image file.

    * Changed in v30: Include alpha (transparency) channel if the file has
      transparent pixels.
    * Changed in v32: Return type is now `stbt.Image`, which is a
      `numpy.ndarray` sub-class with additional attributes ``filename``,
      ``relative_filename`` and ``absolute_filename``.
    * Changed in v32: Allows passing an image (`numpy.ndarray` or `stbt.Image`)
      instead of a string, in which case this function returns the given image.
    * Changed in v33: Added the ``color_channels`` parameter and deprecated
      ``flags``. The image will always be converted to the format specified by
      ``color_channels`` (previously it was only converted to the format
      specified by ``flags`` if it was given as a filename, not as a
      `stbt.Image` or numpy array).
    """
    if flags is not None:
        # Backwards compatibility
        if color_channels is not None:
            raise Exception(
                "flags cannot be specified at the same time as color_channels")
        if flags == cv2.IMREAD_GRAYSCALE:
            color_channels = (1,)
        elif flags == cv2.IMREAD_COLOR:
            color_channels = (3,)
        elif flags == cv2.IMREAD_UNCHANGED:
            color_channels = (1, 3, 4)
        else:
            raise Exception("Unsupported imread flags %s" % flags)
        warnings.warn(
            "load_image: flags=%s argument is deprecated. Use "
            "color_channels=%r instead" % (flags, color_channels),
            DeprecationWarning)

    if color_channels is None:
        color_channels = (3, 4)
    elif isinstance(color_channels, int):
        color_channels = (color_channels,)

    obj = filename
    if isinstance(obj, Image):
        img = obj
        filename = obj.filename
        absolute_filename = obj.absolute_filename
    elif isinstance(obj, numpy.ndarray):
        img = obj  # obj.filename etc. will be None
        filename = None
        absolute_filename = None
    else:
        filename = to_unicode(filename)
        absolute_filename = find_user_file(filename)
        if not absolute_filename:
            raise IOError(to_unicode("No such file: %s" % filename))
        absolute_filename = to_unicode(absolute_filename)
        if color_channels == (3,):
            flags = cv2.IMREAD_COLOR
        elif color_channels == (1,):
            flags = cv2.IMREAD_GRAYSCALE
        else:
            flags = cv2.IMREAD_UNCHANGED
        img = cv2.imread(to_unicode(absolute_filename), flags)
        if img is None:
            raise IOError(to_unicode("Failed to load image: %s" %
                                     absolute_filename))

    if len(img.shape) not in [2, 3]:
        raise ValueError(
            "Invalid shape for image: %r. Shape must have 2 or 3 elements" %
            (img.shape,))

    if img.dtype == numpy.uint16:
        warn("Image %s has 16 bits per channel. Converting to 8 bits."
             % filename)
        img = cv2.convertScaleAbs(img, alpha=1.0 / 256)
    elif img.dtype != numpy.uint8:
        raise ValueError("Image %s must be 8-bits per channel (got %s)"
                         % (filename, img.dtype))

    if len(img.shape) == 2:
        img = img.reshape(img.shape + (1,))
    c = img.shape[2]

    if c == 1:
        if 1 in color_channels:
            pass
        elif 3 in color_channels:
            img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
        elif 4 in color_channels:
            img = numpy.concatenate(
                (cv2.cvtColor(img, cv2.COLOR_GRAY2BGR),
                 numpy.full(img.shape[:2] + (1,), 255, dtype=img.dtype)),
                axis=2)
        else:
            raise ValueError(
                "Can only convert 1 channel image to 1, 3 or 4 channels")
    elif c == 3:
        if 3 in color_channels:
            pass
        elif 4 in color_channels:
            img = numpy.concatenate(
                (img, numpy.full(img.shape[:2] + (1,), 255, dtype=img.dtype)),
                axis=2)
        elif 1 in color_channels:
            img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            img = img.reshape(img.shape + (1,))
        else:
            raise ValueError(
                "Can only convert 3 channel image to 1, 3 or 4 channels")
    elif c == 4:
        if 4 in color_channels:
            # Remove alpha channel if it's 100% opaque
            if 3 in color_channels and numpy.all(img[:, :, 3] == 255):
                img = img[:, :, :3]
        elif 3 in color_channels:
            img = img[:, :, :3]
        elif 1 in color_channels:
            img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            img = img.reshape(img.shape + (1,))
        else:
            raise ValueError(
                "Can only convert 3 channel image to 1, 3 or 4 channels")
    else:
        raise ValueError(
            "load_image can only handle images with 1, 3 or 4 color channels. "
            "%s has %i channels" % (filename, c))

    assert img.shape[2] in color_channels
    if not isinstance(img, Image):
        img = Image(img, filename=filename, absolute_filename=absolute_filename)

    return img def _validate_region(frame, region):
    if region is None:
        raise TypeError(
            "'region=None' means an empty region. To analyse the entire "
            "frame use 'region=Region.ALL' (which is the default)")
    f = _image_region(frame)
    r = Region.intersect(f, region)
    if r is None:
        raise ValueError("%r doesn't overlap with the frame dimensions %ix%i"
                         % (region, frame.shape[1], frame.shape[0]))
    return r class _Annotation(namedtuple("_Annotation", "time region label colour")):
    MATCHED = (32, 0, 255)  # Red
    NO_MATCH = (32, 255, 255)  # Yellow

    @staticmethod
    def from_result(result, label=""):
        colour = _Annotation.MATCHED if result else _Annotation.NO_MATCH
        return _Annotation(result.time, result.region, label, colour) def ddebug(s):
    """Extra verbose debug for stbt developers, not end users"""
    if get_debug_level() > 1:
        sys.stderr.write("%s\n" % (s,)) def debug(msg):
    """Print the given string to stderr if stbt run `--verbose` was given."""
    if get_debug_level() > 0:
        sys.stderr.write("%s\n" % (msg,)) def draw_on(frame, *args, **kwargs):
    draw_sink_ref = getattr(frame, '_draw_sink', None)
    if not draw_sink_ref:
        return
    draw_sink = draw_sink_ref()
    if not draw_sink:
        return
    draw_sink.draw(*args, **kwargs) def get_debug_level():
    global _debug_level
    if _debug_level is None:
        _debug_level = get_config('global', 'verbose', type_=int)
    return _debug_level class ImageLogger():
    """Log intermediate images used in image processing (such as `match`).

    Create a new ImageLogger instance for each frame of video.
    """
    _frame_number = itertools.count(1)

    def __init__(self, name, **kwargs):
        self.jupyter = _jupyter_logging_enabled
        self.enabled = get_debug_level() > 1 or self.jupyter
        if not self.enabled:
            return

        self.name = name
        self.frame_number = next(ImageLogger._frame_number)

        try:
            outdir = os.path.join("stbt-debug", "%05d" % self.frame_number)
            mkdir_p(outdir)
            self.outdir = outdir
        except OSError:
            warn("Failed to create directory '%s'; won't save debug images."
                 % outdir)
            self.enabled = False
            return

        self.images = OrderedDict()
        self.data = {}
        for k, v in kwargs.items():
            self.data[k] = v

    def set(self, **kwargs):
        if not self.enabled:
            return
        for k, v in kwargs.items():
            self.data[k] = v

    def append(self, **kwargs):
        if not self.enabled:
            return
        for k, v in kwargs.items():
            if k not in self.data:
                self.data[k] = []
            self.data[k].append(v)

    def imwrite(self, name, image, regions=None, colours=None, scale=1):
        import cv2
        import numpy
        if not self.enabled:
            return
        if image is None:
            return
        if name in self.images:
            raise ValueError("Image for name '%s' already logged" % name)
        if image is None:
            return
        if image.dtype == numpy.float32:
            # Scale `cv2.matchTemplate` heatmap output in range
            # [0.0, 1.0] to visible grayscale range [0, 255].
            image = cv2.convertScaleAbs(image, alpha=255.0 / scale)
        else:
            image = image.copy()
        self.images[name] = image
        if regions is None:
            regions = []
        elif not isinstance(regions, list):
            regions = [regions]
        if colours is None:
            colours = []
        elif not isinstance(colours, list):
            colours = [colours]
        for region, colour in zip(regions, colours):
            cv2.rectangle(
                image, (region.x, region.y), (region.right, region.bottom),
                colour, thickness=1)

        cv2.imwrite(os.path.join(self.outdir, name + ".png"), image)

    def html(self, template, **kwargs):
        if not self.enabled:
            return

        try:
            import jinja2
        except ImportError:
            warn(
                "Not generating html view of the image-processing debug images "
                "because python 'jinja2' module is not installed.")
            return

        template_kwargs = self.data.copy()
        template_kwargs["images"] = self.images
        template_kwargs.update(kwargs)

        index_html = os.path.join(self.outdir, "index.html")
        with open(index_html, "w") as f:
            f.write(jinja2.Template(_INDEX_HTML_HEADER)
                    .render(frame_number=self.frame_number,
                            jupyter=self.jupyter))
            f.write(jinja2.Template(dedent(template.lstrip("\n")))
                    .render(annotated_image=self._draw_annotated_image,
                            draw=self._draw,
                            jupyter=self.jupyter,
                            **template_kwargs))
            f.write(jinja2.Template(_INDEX_HTML_FOOTER)
                    .render())

        if self.jupyter:
            from IPython.display import display, IFrame
            display(IFrame(src=index_html, width=974, height=600))

    def _draw(self, region, source_size, css_class, title=None):
        import jinja2

        if region is None:
            return ""

        if isinstance(css_class, bool):
            if css_class:
                css_class = "matched"
            else:
                css_class = "nomatch"

        return jinja2.Template(dedent("""\
            <div class="region {{css_class}}"
                 style="left: {{region.x / image.width * 100}}%;
                        top: {{region.y / image.height * 100}}%;
                        width: {{region.width / image.width * 100}}%;
                        height: {{region.height / image.height * 100}}%"
                 {% if title %}
                 title="{{ title | escape }}"
                 {% endif %}
                 ></div>
            """)) \
            .render(css_class=css_class,
                    image=source_size,
                    region=region,
                    title=title)

    def _draw_annotated_image(self, regions=None, source_name="source"):
        import jinja2

        s = self.images[source_name].shape
        source_size = Region(0, 0, s[1], s[0])

        _regions = []
        if "region" in self.data:
            _regions.append((Region.intersect(self.data["region"], source_size),
                             "source_region", None))

        if isinstance(regions, Region):
            _regions.append((regions, True, None))
        elif hasattr(regions, "region"):  # e.g. MotionResult
            _regions.append((regions.region, bool(regions), None))
        elif regions is not None:
            for r in regions:
                if not isinstance(r, tuple) or len(r) != 3:
                    raise ValueError(
                        "_draw_annotated_image expected 3-tuple "
                        "(region, css_class, title); got %r" % (r,))
                _regions.append(r)

        return jinja2.Template(dedent("""\
            <div class="annotated_image">
              <img src="{{source_name}}.png">
              {% for region, css_class, title in regions %}
              {{ draw(region, source_size, css_class, title) }}
              {% endfor %}
            </div>
        """)).render(
            draw=self._draw,
            regions=_regions,
            source_name=source_name,
            source_size=source_size,
        ) class Position(namedtuple('Position', 'x y')):
    """A point with ``x`` and ``y`` coordinates."""
    pass class Region(namedtuple('Region', 'x y right bottom'),
             metaclass=_RegionClsMethods):
    r"""
    ``Region(x, y, width=width, height=height)`` or
    ``Region(x, y, right=right, bottom=bottom)``

    Rectangular region within the video frame.

    For example, given the following regions a, b, and c::

        - 01234567890123
        0 ░░░░░░░░
        1 ░a░░░░░░
        2 ░░░░░░░░
        3 ░░░░░░░░
        4 ░░░░▓▓▓▓░░▓c▓
        5 ░░░░▓▓▓▓░░▓▓▓
        6 ░░░░▓▓▓▓░░░░░
        7 ░░░░▓▓▓▓░░░░░
        8     ░░░░░░b░░
        9     ░░░░░░░░░

    >>> a = Region(0, 0, width=8, height=8)
    >>> b = Region(4, 4, right=13, bottom=10)
    >>> c = Region(10, 4, width=3, height=2)
    >>> a.right
    8
    >>> b.bottom
    10
    >>> b.center
    Position(x=8, y=7)
    >>> b.contains(c), a.contains(b), c.contains(b), c.contains(None)
    (True, False, False, False)
    >>> b.contains(c.center), a.contains(b.center)
    (True, False)
    >>> b.extend(x=6, bottom=-4) == c
    True
    >>> a.extend(right=5).contains(c)
    True
    >>> a.width, a.extend(x=3).width, a.extend(right=-3).width
    (8, 5, 5)
    >>> c.replace(bottom=10)
    Region(x=10, y=4, right=13, bottom=10)
    >>> Region.intersect(a, b)
    Region(x=4, y=4, right=8, bottom=8)
    >>> Region.intersect(a, b) == Region.intersect(b, a)
    True
    >>> Region.intersect(c, b) == c
    True
    >>> print(Region.intersect(a, c))
    None
    >>> print(Region.intersect(None, a))
    None
    >>> Region.intersect(a)
    Region(x=0, y=0, right=8, bottom=8)
    >>> Region.intersect()
    Region.ALL
    >>> quadrant = Region(x=float("-inf"), y=float("-inf"), right=0, bottom=0)
    >>> quadrant.translate(2, 2)
    Region(x=-inf, y=-inf, right=2, bottom=2)
    >>> c.translate(x=-9, y=-3)
    Region(x=1, y=1, right=4, bottom=3)
    >>> Region(2, 3, 2, 1).translate(b)
    Region(x=6, y=7, right=8, bottom=8)
    >>> Region.intersect(Region.ALL, c) == c
    True
    >>> Region.ALL
    Region.ALL
    >>> print(Region.ALL)
    Region.ALL
    >>> c.above()
    Region(x=10, y=-inf, right=13, bottom=4)
    >>> c.below()
    Region(x=10, y=6, right=13, bottom=inf)
    >>> a.right_of()
    Region(x=8, y=0, right=inf, bottom=8)
    >>> a.right_of(width=2)
    Region(x=8, y=0, right=10, bottom=8)
    >>> c.left_of()
    Region(x=-inf, y=4, right=10, bottom=6)

    .. py:attribute:: x

        The x coordinate of the left edge of the region, measured in pixels
        from the left of the video frame (inclusive).

    .. py:attribute:: y

        The y coordinate of the top edge of the region, measured in pixels from
        the top of the video frame (inclusive).

    .. py:attribute:: right

        The x coordinate of the right edge of the region, measured in pixels
        from the left of the video frame (exclusive).

    .. py:attribute:: bottom

        The y coordinate of the bottom edge of the region, measured in pixels
        from the top of the video frame (exclusive).

    .. py:attribute:: width

        The width of the region, measured in pixels.

    .. py:attribute:: height

        The height of the region, measured in pixels.

    ``x``, ``y``, ``right``, ``bottom``, ``width`` and ``height`` can be
    infinite --- that is, ``float("inf")`` or ``-float("inf")``.

    .. py:attribute:: center

        A `stbt.Position` specifying the x & y coordinates of the region's
        center.

    .. py:staticmethod:: from_extents

        Create a Region using right and bottom extents rather than width and
        height.

        Typically you'd use the ``right`` and ``bottom`` parameters of the
        ``Region`` constructor instead, but this factory function is useful
        if you need to create a ``Region`` from a tuple.

        >>> extents = (4, 4, 13, 10)
        >>> Region.from_extents(*extents)
        Region(x=4, y=4, right=13, bottom=10)

    .. py:staticmethod:: bounding_box(*args)

        :returns: The smallest region that contains all the given regions.

        >>> a = Region(50, 20, right=60, bottom=40)
        >>> b = Region(20, 30, right=30, bottom=50)
        >>> c = Region(55, 25, right=70, bottom=35)
        >>> Region.bounding_box(a, b)
        Region(x=20, y=20, right=60, bottom=50)
        >>> Region.bounding_box(b, b)
        Region(x=20, y=30, right=30, bottom=50)
        >>> Region.bounding_box(None, b)
        Region(x=20, y=30, right=30, bottom=50)
        >>> Region.bounding_box(b, None)
        Region(x=20, y=30, right=30, bottom=50)
        >>> Region.bounding_box(b, Region.ALL)
        Region.ALL
        >>> print(Region.bounding_box(None, None))
        None
        >>> print(Region.bounding_box())
        None
        >>> Region.bounding_box(b)
        Region(x=20, y=30, right=30, bottom=50)
        >>> Region.bounding_box(a, b, c) == \
        ...     Region.bounding_box(a, Region.bounding_box(b, c))
        True

        Changed in v30: ``bounding_box`` can take an arbitrary number of region
        arguments, rather than exactly two.

    .. py:staticmethod:: intersect(*args)

        :returns: The intersection of the passed regions, or ``None`` if the
            regions don't intersect.

        Any parameter can be ``None`` (an empty Region) so intersect is
        commutative and associative.

        Changed in v30: ``intersect`` can take an arbitrary number of region
        arguments, rather than exactly two.

    """
    def __new__(cls, x, y, width=None, height=None, right=None, bottom=None):
        if (width is None) == (right is None):
            raise ValueError("You must specify either 'width' or 'right'")
        if (height is None) == (bottom is None):
            raise ValueError("You must specify either 'height' or 'bottom'")
        if right is None:
            right = x + width
        if bottom is None:
            bottom = y + height
        if right <= x:
            raise ValueError("'right' (%r) must be greater than 'x' (%r)"
                             % (right, x))
        if bottom <= y:
            raise ValueError("'bottom' (%r) must be greater than 'y' (%r)"
                             % (bottom, y))
        return super(Region, cls).__new__(cls, x, y, right, bottom)

    def __repr__(self):
        if self == Region.ALL:
            return 'Region.ALL'
        else:
            return 'Region(x=%r, y=%r, right=%r, bottom=%r)' \
                % (self.x, self.y, self.right, self.bottom)

    @property
    def width(self):
        return self.right - self.x

    @property
    def height(self):
        return self.bottom - self.y

    @property
    def center(self):
        return Position((self.x + self.right) // 2,
                        (self.y + self.bottom) // 2)

    @staticmethod
    def from_extents(x, y, right, bottom):
        return Region(x, y, right=right, bottom=bottom)

    def to_slice(self):
        """A 2-dimensional slice suitable for indexing a `stbt.Frame`."""
        return (slice(max(0, self.y),
                      max(0, self.bottom)),
                slice(max(0, self.x),
                      max(0, self.right)))

    def contains(self, other):
        """
        :returns: True if ``other`` (a `Region` or `Position`) is entirely
            contained within self.
        """
        if other is None:
            return False
        elif all(hasattr(other, a) for a in ("x", "y", "right", "bottom")):
            # a Region
            return (self.x <= other.x and other.right <= self.right and
                    self.y <= other.y and other.bottom <= self.bottom)
        elif all(hasattr(other, a) for a in ("x", "y")):  # a Position
            return (self.x <= other.x < self.right and
                    self.y <= other.y < self.bottom)
        else:
            raise TypeError("Region.contains expects a Region, Position, or "
                            "None. Got %r" % (other,))

    def translate(self, x=None, y=None):
        """
        :returns: A new region with the position of the region adjusted by the
            given amounts.  The width and height are unaffected.

        ``translate`` accepts separate x and y arguments, or a single `Region`.

        For example, move the region 1px right and 2px down:

        >>> b = Region(4, 4, 9, 6)
        >>> b.translate(1, 2)
        Region(x=5, y=6, right=14, bottom=12)

        Move the region 1px to the left:

        >>> b.translate(x=-1)
        Region(x=3, y=4, right=12, bottom=10)

        Move the region 3px up:

        >>> b.translate(y=-3)
        Region(x=4, y=1, right=13, bottom=7)

        Move the region by another region.  This can be helpful if `TITLE`
        defines a region relative another UI element on screen.  You can then
        combine the two like so:

        >>> TITLE = Region(20, 5, 160, 40)
        >>> CELL = Region(140, 45, 200, 200)
        >>> TITLE.translate(CELL)
        Region(x=160, y=50, right=320, bottom=90)
        """
        try:
            p = x[0], x[1]
        except TypeError:
            p = x or 0, y or 0
        else:
            if y is not None:
                raise TypeError(
                    "translate() takes either a single Region argument or two "
                    "ints (both given)")
        return Region.from_extents(self.x + p[0], self.y + p[1],
                                   self.right + p[0], self.bottom + p[1])

    def extend(self, x=0, y=0, right=0, bottom=0):
        """
        :returns: A new region with the edges of the region adjusted by the
            given amounts.
        """
        return Region.from_extents(
            self.x + x, self.y + y, self.right + right, self.bottom + bottom)

    def replace(self, x=None, y=None, width=None, height=None, right=None,
                bottom=None):
        """
        :returns: A new region with the edges of the region set to the given
            coordinates.

        This is similar to `extend`, but it takes absolute coordinates within
        the image instead of adjusting by a relative number of pixels.
        """
        def norm_coords(name_x, name_width, name_right,
                        x, width, right,  # or y, height, bottom
                        default_x, _default_width, default_right):
            if all(z is not None for z in (x, width, right)):
                raise ValueError(
                    "Region.replace: Argument conflict: you may only specify "
                    "two of %s, %s and %s.  You specified %s=%s, %s=%s and "
                    "%s=%s" % (name_x, name_width, name_right,
                               name_x, x, name_width, width, name_right, right))
            if x is None:
                if width is not None and right is not None:
                    x = right - width
                else:
                    x = default_x
            if right is None:
                right = x + width if width is not None else default_right
            return x, right

        x, right = norm_coords('x', 'width', 'right', x, width, right,
                               self.x, self.width, self.right)
        y, bottom = norm_coords('y', 'height', 'bottom', y, height, bottom,
                                self.y, self.height, self.bottom)

        return Region(x=x, y=y, right=right, bottom=bottom)

    def dilate(self, n):
        """Expand the region by n px in all directions.

        >>> Region(20, 30, right=30, bottom=50).dilate(3)
        Region(x=17, y=27, right=33, bottom=53)
        """
        return self.extend(x=-n, y=-n, right=n, bottom=n)

    def erode(self, n):
        """Shrink the region by n px in all directions.

        >>> Region(20, 30, right=30, bottom=50).erode(3)
        Region(x=23, y=33, right=27, bottom=47)
        >>> print(Region(20, 30, 10, 20).erode(5))
        None
        """
        if self.width > n * 2 and self.height > n * 2:
            return self.dilate(-n)
        else:
            return None

    def above(self, height=float('inf')):
        """
        :returns: A new region above the current region, extending to the top
            of the frame (or to the specified height).
        """
        return self.replace(y=self.y - height, bottom=self.y)

    def below(self, height=float('inf')):
        """
        :returns: A new region below the current region, extending to the bottom
            of the frame (or to the specified height).
        """
        return self.replace(y=self.bottom, bottom=self.bottom + height)

    def right_of(self, width=float('inf')):
        """
        :returns: A new region to the right of the current region, extending to
            the right edge of the frame (or to the specified width).
        """
        return self.replace(x=self.right, right=self.right + width)

    def left_of(self, width=float('inf')):
        """
        :returns: A new region to the left of the current region, extending to
            the left edge of the frame (or to the specified width).
        """
        return self.replace(x=self.x - width, right=self.x) class UITestFailure(Exception):
    """The test failed because the device under test didn't behave as expected.

    Inherit from this if you need to define your own test-failure exceptions.
    """
    pass def to_unicode(text):
    if isinstance(text, bytes):
        return text.decode("utf-8", errors="replace")
    else:
        return str(text)
import enum
import itertools
import cv2
import numpy
        import stbt_core
from collections import namedtuple
from . import cv2_compat
from .config import ConfigurationError, get_config
from .imgproc_cache import memoize_iterator
from .imgutils import (crop, _frame_repr, _image_region, limit_time, load_image,
                       _validate_region)
from .logging import (_Annotation, ddebug, debug, draw_on, get_debug_level,
                      ImageLogger)
from .types import Position, Region, UITestFailure
from .utils import to_unicode
    from .sqdiff import sqdiff
        from stbt_core import get_frame
        return (
            "MatchResult(time=%s, match=%r, region=%r, first_pass_result=%r, "
            "frame=%s, image=%s)" % (
                "None" if self.time is None else "%.3f" % self.time,
                self.match,
                self.region,
                self.first_pass_result,
                _frame_repr(self.frame),
                _frame_repr(self.image)))

    def __bool__(self):
        return self.match

    @property
    def position(self):
        return Position(self.region.x, self.region.y)


def match(image, frame=None, match_parameters=None, region=Region.ALL):
    """
    Search for an image in a single video frame.

    :type image: string or `numpy.ndarray`
    :param image:
      The image to search for. It can be the filename of a png file on disk, or
      a numpy array containing the pixel data in 8-bit BGR format. If the image
      has an alpha channel, any transparent pixels are ignored.

      Filenames should be relative paths. See `stbt.load_image` for the path
      lookup algorithm.

      8-bit BGR numpy arrays are the same format that OpenCV uses for images.
      This allows generating reference images on the fly (possibly using
      OpenCV) or searching for images captured from the device-under-test
      earlier in the test script.

    :type frame: `stbt.Frame` or `numpy.ndarray`
    :param frame:
      If this is specified it is used as the video frame to search in;
      otherwise a new frame is grabbed from the device-under-test. This is an
      image in OpenCV format (for example as returned by `frames` and
      `get_frame`).

    :type match_parameters: `MatchParameters`
    :param match_parameters:
      Customise the image matching algorithm. See `MatchParameters` for details.

    :type region: `Region`
    :param region:
      Only search within the specified region of the video frame.

    :returns:
      A `MatchResult`, which will evaluate to true if a match was found,
      false otherwise.

    Added in v30: Support transparency in the reference image, and new match
    method ``MatchMethod.SQDIFF``.
    """
    result = next(_match_all(image, frame, match_parameters, region))
    if result.match:
        debug("Match found: %s" % str(result))
    else:
        debug("No match found. Closest match: %s" % str(result))
    return result


def match_all(image, frame=None, match_parameters=None, region=Region.ALL):
    """
    Search for all instances of an image in a single video frame.

    Arguments are the same as `match`.

    :returns:
      An iterator of zero or more `MatchResult` objects (one for each position
      in the frame where ``image`` matches).

    Examples:

    .. code-block:: python

        all_buttons = list(stbt.match_all("button.png"))

    .. code-block:: python

        for match_result in stbt.match_all("button.png"):
            # do something with match_result here
            ...
    """
    any_matches = False
    for result in _match_all(image, frame, match_parameters, region):
        if result.match:
            debug("Match found: %s" % str(result))
            any_matches = True
            yield result
        else:
            if not any_matches:
                debug("No match found. Closest match: %s" % str(result))
            break


def _match_all(image, frame, match_parameters, region):
    """
    Generator that yields a sequence of zero or more truthy MatchResults,
    followed by a falsey MatchResult.
    """
    if match_parameters is None:
        match_parameters = MatchParameters()

    if frame is None:
        frame = get_frame()

    # Normalise single channel images to shape (h, w, 1) rather than just (h, w)
    frame = frame.view()
    if len(frame.shape) == 2:
        frame.shape = frame.shape + (1,)
    if len(frame.shape) != 3:
        raise ValueError(
            "Invalid shape for frame: %r. Shape must have 2 or 3 elements" %
            (frame.shape,))


    t = load_image(image, color_channels={1: 1, 3: (3, 4)}[frame.shape[2]])