class FrameDiffer():
    """Interface for different algorithms for diffing frames in a sequence.

    Say you have a sequence of frames A, B, C. Typically you will compare frame
    A against B, and then frame B against C. This is a class (not a function)
    so that you can remember work you've done on frame B, so that you don't
    repeat that work when you need to compare against frame C.
    """

    def __init__(self, initial_frame, region=Region.ALL, mask=None,
                 min_size=None):
        self.prev_frame = initial_frame
        self.region = _validate_region(self.prev_frame, region)
        self.mask = mask
        self.min_size = min_size
        if (self.mask is not None and
                self.mask.shape[:2] != (self.region.height, self.region.width)):
            raise ValueError(
                "The dimensions of the mask %s don't match the %s <%ix%i>"
                % (_frame_repr(self.mask),
                   "frame" if region == Region.ALL else "region",
                   self.region.width, self.region.height))

    def diff(self, frame):
        raise NotImplementedError(
            "%s.diff is not implemented" % self.__class__.__name__) class MotionDiff(FrameDiffer):
    """The `wait_for_motion` diffing algorithm."""

    def __init__(self, initial_frame, region=Region.ALL, mask=None,
                 min_size=None, noise_threshold=None, erode=True):
        super(MotionDiff, self).__init__(initial_frame, region, mask, min_size)

        if noise_threshold is None:
            noise_threshold = get_config(
                'motion', 'noise_threshold', type_=float)
        self.noise_threshold = noise_threshold

        if isinstance(erode, numpy.ndarray):  # For power users
            kernel = erode
        elif erode:
            kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
        else:
            kernel = None
        self.kernel = kernel

        self.prev_frame_gray = self.gray(initial_frame)

    def gray(self, frame):
        return cv2.cvtColor(crop(frame, self.region), cv2.COLOR_BGR2GRAY)

    def diff(self, frame):
        frame_gray = self.gray(frame)

        imglog = ImageLogger("MotionDiff", region=self.region,
                             min_size=self.min_size,
                             noise_threshold=self.noise_threshold)
        imglog.imwrite("source", frame)
        imglog.imwrite("gray", frame_gray)
        imglog.imwrite("previous_frame_gray", self.prev_frame_gray)

        absdiff = cv2.absdiff(self.prev_frame_gray, frame_gray)
        imglog.imwrite("absdiff", absdiff)

        if self.mask is not None:
            absdiff = cv2.bitwise_and(absdiff, self.mask)
            imglog.imwrite("mask", self.mask)
            imglog.imwrite("absdiff_masked", absdiff)

        _, thresholded = cv2.threshold(
            absdiff, int((1 - self.noise_threshold) * 255), 255,
            cv2.THRESH_BINARY)
        imglog.imwrite("absdiff_threshold", thresholded)
        if self.kernel is not None:
            thresholded = cv2.morphologyEx(
                thresholded, cv2.MORPH_OPEN,
                cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))
            imglog.imwrite("absdiff_threshold_erode", thresholded)

        out_region = pixel_bounding_box(thresholded)
        if out_region:
            # Undo crop:
            out_region = out_region.translate(self.region)

        motion = bool(out_region and (
            self.min_size is None or
            (out_region.width >= self.min_size[0] and
             out_region.height >= self.min_size[1])))

        if motion:
            # Only update the comparison frame if it's different to the previous
            # one.  This makes `detect_motion` more sensitive to slow motion
            # because the differences between frames 1 and 2 might be small and
            # the differences between frames 2 and 3 might be small but we'd see
            # the difference by looking between 1 and 3.
            self.prev_frame = frame
            self.prev_frame_gray = frame_gray

        result = MotionResult(getattr(frame, "time", None), motion,
                              out_region, frame)
        ddebug(str(result))
        imglog.html(MOTION_HTML, result=result)
        return result class MotionResult():
    """The result from `detect_motion` and `wait_for_motion`.

    :ivar float time: The time at which the video-frame was captured, in
        seconds since 1970-01-01T00:00Z. This timestamp can be compared with
        system time (``time.time()``).

    :ivar bool motion: True if motion was found. This is the same as evaluating
        ``MotionResult`` as a bool. That is, ``if result:`` will behave the
        same as ``if result.motion:``.

    :ivar Region region: Bounding box where the motion was found, or ``None``
        if no motion was found.

    :ivar Frame frame: The video frame in which motion was (or wasn't) found.
    """
    _fields = ("time", "motion", "region", "frame")

    def __init__(self, time, motion, region, frame):
        self.time = time
        self.motion = motion
        self.region = region
        self.frame = frame

    def __bool__(self):
        return self.motion

    def __nonzero__(self):
        return self.__bool__()

    def __repr__(self):
        return (
            "MotionResult(time=%s, motion=%r, region=%r, frame=%s)" % (
                "None" if self.time is None else "%.3f" % self.time,
                self.motion, self.region, _frame_repr(self.frame))) 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 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 pixel_bounding_box(img):
    """
    Find the smallest region that contains all the non-zero pixels in an image.

    >>> pixel_bounding_box(numpy.array([[0]], dtype=numpy.uint8))
    >>> pixel_bounding_box(numpy.array([[1]], dtype=numpy.uint8))
    Region(x=0, y=0, right=1, bottom=1)
    >>> a = numpy.array([
    ...     [0, 0, 0, 0],
    ...     [0, 1, 1, 1],
    ...     [0, 1, 1, 1],
    ...     [0, 0, 0, 0],
    ... ], dtype=numpy.uint8)
    >>> pixel_bounding_box(a)
    Region(x=1, y=1, right=4, bottom=3)
    >>> pixel_bounding_box(numpy.stack([
    ...     numpy.zeros((4, 4), dtype=numpy.uint8),
    ...     numpy.zeros((4, 4), dtype=numpy.uint8),
    ...     a],
    ...     axis=-1))
    Region(x=1, y=1, right=4, bottom=3)
    >>> pixel_bounding_box(numpy.array([
    ...     [0, 0, 0, 0, 0, 0],
    ...     [0, 0, 0, 1, 0, 0],
    ...     [0, 1, 0, 0, 0, 0],
    ...     [0, 0, 0, 0, 1, 0],
    ...     [0, 0, 0, 0, 0, 0],
    ...     [0, 0, 1, 0, 0, 0],
    ...     [0, 0, 0, 0, 0, 0]
    ... ], dtype=numpy.uint8))
    Region(x=1, y=1, right=5, bottom=6)
    """
    if len(img.shape) == 2:
        pass
    elif len(img.shape) == 3 and img.shape[2] == 3:
        img = img.max(axis=2)
    else:
        raise ValueError("Single-channel or 3-channel (BGR) image required. "
                         "Provided image has shape %r" % (img.shape,))

    out = [None, None, None, None]

    for axis in (0, 1):
        flat = numpy.any(img, axis=axis)
        indices = numpy.where(flat)[0]
        if len(indices) == 0:
            return None
        out[axis] = int(indices[0])
        out[axis + 2] = int(indices[-1] + 1)

    return Region.from_extents(*out) 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) 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)
import enum
import cv2
import numpy
        import stbt_core
        import stbt_core
from .diff import FrameDiffer, MotionDiff, MotionResult
from .imgutils import crop, load_image, pixel_bounding_box
from .logging import ddebug, debug, draw_on
from .types import Region
# coding: utf-8

"""Detection & frame-accurate measurement of animations and transitions.

For example a selection that moves from one menu item to another or loading a
new screen such as a Guide and waiting for it to populate fully.

Because we want these measurements to be frame-accurate, we don't do expensive
image processing, relying instead on diffs between successive frames.

Copyright 2017-2018 Stb-tester.com Ltd.
License: LGPL v2.1 or (at your option) any later version (see
https://github.com/stb-tester/stb-tester/blob/master/LICENSE for details).
"""





def press_and_wait(
        key, region=Region.ALL, mask=None, timeout_secs=10, stable_secs=1,
        min_size=None, _dut=None):

    """Press a key, then wait for the screen to change, then wait for it to stop
    changing.

    This can be used to wait for a menu selection to finish moving before
    attempting to OCR at the selection's new position; or to measure the
    duration of animations; or to measure how long it takes for a screen (such
    as an EPG) to finish populating.

    :param str key: The name of the key to press (passed to `stbt.press`).

    :param stbt.Region region: Only look at the specified region of the video
        frame.

    :param mask:
        A black & white image that specifies which part of the video frame to
        look at. White pixels select the area to analyse; black pixels select
        the area to ignore.

        This can be a string (a filename that will be resolved as per
        `load_image`) or a single-channel image in OpenCV format.

        If you specify ``region``, the mask must be the same size as the
        region. Otherwise the mask must be the same size as the frame.
    :type mask: str or `numpy.ndarray`

    :param timeout_secs: A timeout in seconds. This function will return a
        falsey value if the transition didn't complete within this number of
        seconds from the key-press.
    :type timeout_secs: int or float

    :param stable_secs: A duration in seconds. The screen must stay unchanged
        (within the specified region or mask) for this long, for the transition
        to be considered "complete".
    :type timeout_secs: int or float

    :param min_size: A tuple of ``(width, height)``, in pixels, for differences
        to be considered as "motion". Use this to ignore small differences,
        such as the blinking text cursor in a search box.
    :type min_size: Tuple[int, int]

    :returns:
        An object that will evaluate to true if the transition completed, false
        otherwise. It has the following attributes:

        * **key** (*str*) – The name of the key that was pressed.
        * **frame** (`stbt.Frame`) – If successful, the first video frame when
          the transition completed; if timed out, the last frame seen.
        * **status** (`stbt.TransitionStatus`) – Either ``START_TIMEOUT``,
          ``STABLE_TIMEOUT``, or ``COMPLETE``. If it's ``COMPLETE``, the whole
          object will evaluate as true.
        * **press_time** (*float*) – When the key-press completed.
        * **animation_start_time** (*float*) – When animation started after the
          key-press (or ``None`` if timed out).
        * **end_time** (*float*) – When animation completed (or ``None`` if
          timed out).
        * **duration** (*float*) – Time from ``press_time`` to ``end_time`` (or
          ``None`` if timed out).
        * **animation_duration** (*float*) – Time from ``animation_start_time``
          to ``end_time`` (or ``None`` if timed out).

        All times are measured in seconds since 1970-01-01T00:00Z; the
        timestamps can be compared with system time (the output of
        ``time.time()``).

    Changed in v32: Use the same difference-detection algorithm as
    `wait_for_motion`; ``region`` and ``mask`` can both be specified at the
    same time.
    """
    if _dut is None:
        _dut = stbt_core

    t = _Transition(region, mask, timeout_secs, stable_secs, min_size, _dut)
    press_result = _dut.press(key)

    debug("transition: %.3f: Pressed %s" % (press_result.end_time, key))