Index: tools/telemetry/telemetry/internal/image_processing/screen_finder.py |
diff --git a/tools/telemetry/telemetry/internal/image_processing/screen_finder.py b/tools/telemetry/telemetry/internal/image_processing/screen_finder.py |
deleted file mode 100755 |
index 932d6dfe4fa3104b963c7b5a7ec61cb103fb0633..0000000000000000000000000000000000000000 |
--- a/tools/telemetry/telemetry/internal/image_processing/screen_finder.py |
+++ /dev/null |
@@ -1,857 +0,0 @@ |
-#!/usr/bin/env python |
-# Copyright 2014 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
-# |
-# This script attempts to detect the region of a camera's field of view that |
-# contains the screen of the device we are testing. |
-# |
-# Usage: ./screen_finder.py path_to_video 0 0 --verbose |
- |
-from __future__ import division |
- |
-import copy |
-import logging |
-import os |
-import sys |
- |
-if __name__ == '__main__': |
- sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..')) |
- |
-from telemetry.internal.image_processing import cv_util |
-from telemetry.internal.image_processing import frame_generator as \ |
- frame_generator_module |
-from telemetry.internal.image_processing import video_file_frame_generator |
-from telemetry.internal.util import external_modules |
- |
-np = external_modules.ImportRequiredModule('numpy') |
-cv2 = external_modules.ImportRequiredModule('cv2') |
- |
- |
-class ScreenFinder(object): |
- """Finds and extracts device screens from video. |
- |
- Sample Usage: |
- sf = ScreenFinder(sys.argv[1]) |
- while sf.HasNext(): |
- ret, screen = sf.GetNext() |
- |
- Attributes: |
- _lost_corners: Each index represents whether or not we lost track of that |
- corner on the previous frame. Ordered by [top-right, top-left, |
- bottom-left, bottom-right] |
- _frame: An unmodified copy of the frame we're currently processing. |
- _frame_debug: A copy of the frame we're currently processing, may be |
- modified at any time, used for debugging. |
- _frame_grey: A greyscale copy of the frame we're currently processing. |
- _frame_edges: A Canny Edge detected copy of the frame we're currently |
- processing. |
- _screen_size: The size of device screen in the video when first detected. |
- _avg_corners: Exponentially weighted average of the previous corner |
- locations. |
- _prev_corners: The location of the corners in the previous frame. |
- _lost_corner_frames: A counter of the number of successive frames in which |
- we've lost a corner location. |
- _border: See |border| above. |
- _min_line_length: The minimum length a line must be before we consider it |
- a possible screen edge. |
- _frame_generator: See |frame_generator| above. |
- _width, _height: The width and height of the frame. |
- _anglesp5, _anglesm5: The angles for each point we look at in the grid |
- when computing brightness, constant across frames.""" |
- |
- class ScreenNotFoundError(Exception): |
- pass |
- |
- # Square of the distance a corner can travel in pixels between frames |
- MAX_INTERFRAME_MOTION = 25 |
- # The minimum width line that may be considered a screen edge. |
- MIN_SCREEN_WIDTH = 40 |
- # Number of frames with lost corners before we ignore MAX_INTERFRAME_MOTION |
- RESET_AFTER_N_BAD_FRAMES = 2 |
- # The weight applied to the new screen location when exponentially averaging |
- # screen location. |
- # TODO(mthiesse): This should be framerate dependent, for lower framerates |
- # this value should approach 1. For higher framerates, this value should |
- # approach 0. The current 0.5 value works well in testing with 240 FPS. |
- CORNER_AVERAGE_WEIGHT = 0.5 |
- |
- # TODO(mthiesse): Investigate how to select the constants used here. In very |
- # bright videos, twice as bright may be too high, and the minimum of 60 may |
- # be too low. |
- # The factor by which a quadrant at an intersection must be brighter than |
- # the other quadrants to be considered a screen corner. |
- MIN_RELATIVE_BRIGHTNESS_FACTOR = 1.5 |
- # The minimum average brightness required of an intersection quadrant to |
- # be considered a screen corner (on a scale of 0-255). |
- MIN_CORNER_ABSOLUTE_BRIGHTNESS = 60 |
- |
- # Low and high hysteresis parameters to be passed to the Canny edge |
- # detection algorithm. |
- CANNY_HYSTERESIS_THRESH_LOW = 300 |
- CANNY_HYSTERESIS_THRESH_HIGH = 500 |
- |
- SMALL_ANGLE = 5 / 180 * np.pi # 5 degrees in radians |
- |
- DEBUG = False |
- |
- def __init__(self, frame_generator, border=5): |
- """Initializes the ScreenFinder object. |
- |
- Args: |
- frame_generator: FrameGenerator, An initialized Video Frame Generator. |
- border: int, number of pixels of border to be kept when cropping the |
- detected screen. |
- |
- Raises: |
- FrameReadError: The frame generator may output a read error during |
- initialization.""" |
- assert isinstance(frame_generator, frame_generator_module.FrameGenerator) |
- self._lost_corners = [False, False, False, False] |
- self._frame_debug = None |
- self._frame = None |
- self._frame_grey = None |
- self._frame_edges = None |
- self._screen_size = None |
- self._avg_corners = None |
- self._prev_corners = None |
- self._lost_corner_frames = 0 |
- self._border = border |
- self._min_line_length = self.MIN_SCREEN_WIDTH |
- self._frame_generator = frame_generator |
- self._anglesp5 = None |
- self._anglesm5 = None |
- |
- if not self._InitNextFrame(): |
- logging.warn('Not enough frames in video feed!') |
- return |
- |
- self._height, self._width = self._frame.shape[:2] |
- |
- def _InitNextFrame(self): |
- """Called after processing each frame, reads in the next frame to ensure |
- HasNext() is accurate.""" |
- self._frame_debug = None |
- self._frame = None |
- self._frame_grey = None |
- self._frame_edges = None |
- try: |
- frame = next(self._frame_generator.Generator) |
- except StopIteration: |
- return False |
- self._frame = frame |
- self._frame_debug = copy.copy(frame) |
- self._frame_grey = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) |
- self._frame_edges = cv2.Canny(self._frame_grey, |
- self.CANNY_HYSTERESIS_THRESH_LOW, |
- self.CANNY_HYSTERESIS_THRESH_HIGH) |
- return True |
- |
- def HasNext(self): |
- """True if there are more frames available to process. """ |
- return self._frame is not None |
- |
- def GetNext(self): |
- """Gets the next screen image. |
- |
- Returns: |
- A numpy matrix containing the screen surrounded by the number of border |
- pixels specified in initialization, and the location of the detected |
- screen corners in the current frame, if a screen is found. The returned |
- screen is guaranteed to be the same size at each frame. |
- 'None' and 'None' if no screen was found on the current frame. |
- |
- Raises: |
- FrameReadError: An error occurred in the FrameGenerator. |
- RuntimeError: This method was called when no frames were available.""" |
- if self._frame is None: |
- raise RuntimeError('No more frames available.') |
- |
- logging.info('Processing frame: %d', |
- self._frame_generator.CurrentFrameNumber) |
- |
- # Finds straight lines in the image. |
- hlines = cv2.HoughLinesP(self._frame_edges, 1, np.pi / 180, 60, |
- minLineLength=self._min_line_length, |
- maxLineGap=100) |
- |
- # Extends these straight lines to be long enough to ensure the screen edge |
- # lines intersect. |
- lines = cv_util.ExtendLines(np.float32(hlines[0]), 10000) \ |
- if hlines is not None else [] |
- |
- # Find intersections in the lines; these are likely to be screen corners. |
- intersections = self._FindIntersections(lines) |
- if len(intersections[:, 0]) > 0: |
- points = np.vstack(intersections[:, 0].flat) |
- if (self._prev_corners is not None and len(points) >= 4 and |
- not self._HasMovedFast(points, self._prev_corners)): |
- corners = self._prev_corners |
- missing_corners = 0 |
- else: |
- # Extract the corners from all intersections. |
- corners, missing_corners = self._FindCorners( |
- intersections, self._frame_grey) |
- else: |
- corners = np.empty((4, 2), np.float32) |
- corners[:] = np.nan |
- missing_corners = 4 |
- |
- screen = None |
- found_screen = True |
- final_corners = None |
- try: |
- # Handle the cases where we have missing corners. |
- screen_corners = self._NewScreenLocation( |
- corners, missing_corners, intersections) |
- |
- final_corners = self._SmoothCorners(screen_corners) |
- |
- # Create a perspective transform from our corners. |
- transform, w, h = self._GetTransform(final_corners, self._border) |
- |
- # Apply the perspective transform to get our output. |
- screen = cv2.warpPerspective( |
- self._frame, transform, (int(w + 0.5), int(h + 0.5))) |
- |
- self._prev_corners = final_corners |
- |
- except self.ScreenNotFoundError as e: |
- found_screen = False |
- logging.info(e) |
- |
- if self.DEBUG: |
- self._Debug(lines, corners, final_corners, screen) |
- |
- self._InitNextFrame() |
- if found_screen: |
- return screen, self._prev_corners |
- return None, None |
- |
- def _FindIntersections(self, lines): |
- """Finds intersections in a set of lines. |
- |
- Filters pairs of lines that are less than 45 degrees apart. Filtering |
- these pairs helps dramatically reduce the number of points we have to |
- process, as these points could not represent screen corners anyways. |
- |
- Returns: |
- The intersections, represented as a tuple of (point, line, line) of the |
- points and the lines that intersect there of all lines in the array that |
- are more than 45 degrees apart.""" |
- intersections = np.empty((0, 3), np.float32) |
- for i in xrange(0, len(lines)): |
- for j in xrange(i + 1, len(lines)): |
- # Filter lines that are less than 45 (or greater than 135) degrees |
- # apart. |
- if not cv_util.AreLinesOrthogonal(lines[i], lines[j], (np.pi / 4.0)): |
- continue |
- ret, point = cv_util.FindLineIntersection(lines[i], lines[j]) |
- point = np.float32(point) |
- if not ret: |
- continue |
- # If we know where the previous corners are, we can also filter |
- # intersections that are too far away from the previous corners to be |
- # where the screen has moved. |
- if self._prev_corners is not None and \ |
- self._lost_corner_frames <= self.RESET_AFTER_N_BAD_FRAMES and \ |
- not self._PointIsCloseToPreviousCorners(point): |
- continue |
- intersections = np.vstack((intersections, |
- np.array((point, lines[i], lines[j])))) |
- return intersections |
- |
- def _PointIsCloseToPreviousCorners(self, point): |
- """True if the point is close to the previous corners.""" |
- max_dist = self.MAX_INTERFRAME_MOTION |
- if cv_util.SqDistance(self._prev_corners[0], point) <= max_dist or \ |
- cv_util.SqDistance(self._prev_corners[1], point) <= max_dist or \ |
- cv_util.SqDistance(self._prev_corners[2], point) <= max_dist or \ |
- cv_util.SqDistance(self._prev_corners[3], point) <= max_dist: |
- return True |
- return False |
- |
- def _HasMovedFast(self, corners, prev_corners): |
- min_dist = np.zeros(4, np.float32) |
- for i in xrange(4): |
- dist = np.min(cv_util.SqDistances(corners, prev_corners[i])) |
- min_dist[i] = dist |
- # 3 corners can move up to one pixel before we consider the screen to have |
- # moved. TODO(mthiesse): Should this be relaxed? Resolution dependent? |
- if np.sum(min_dist) < 3: |
- return False |
- return True |
- |
- class CornerData(object): |
- |
- def __init__(self, corner_index, corner_location, brightness_score, line1, |
- line2): |
- self.corner_index = corner_index |
- self.corner_location = corner_location |
- self.brightness_score = brightness_score |
- self.line1 = line1 |
- self.line2 = line2 |
- |
- def __gt__(self, corner_data2): |
- return self.corner_index > corner_data2.corner_index |
- |
- def __repr__(self): |
- return ('\nCorner index: ' + str(self.corner_index) + |
- ',\nCorner location: ' + str(self.corner_location) + |
- ',\nBrightness score: ' + str(self.brightness_score) + |
- ',\nline1: ' + str(self.line1) + ',\nline2: ' + str(self.line2)) |
- |
- def _FindCorners(self, intersections, grey_frame): |
- """Finds the screen corners in the image. |
- |
- Given the set of intersections in the image, finds the intersections most |
- likely to be corners. |
- |
- Args: |
- intersections: The array of intersections in the image. |
- grey_frame: The greyscale frame we're processing. |
- |
- Returns: |
- An array of length 4 containing the positions of the corners, or nan for |
- each index where a corner could not be found, and a count of the number |
- of missing corners. |
- The corners are ordered as follows: |
- 1 | 0 |
- ----- |
- 2 | 3 |
- Ex. 3 corners are found from a square of width 2 centered at the origin, |
- the output would look like: |
- '[[1, 1], [np.nan, np.nan], [-1, -1], [1, -1]], 1'""" |
- filtered = [] |
- corners = np.empty((0, 2), np.float32) |
- for corner_pos, score, point, line1, line2 in \ |
- self._LooksLikeCorner(intersections, grey_frame): |
- if self.DEBUG: |
- center = (int(point[0] + 0.5), int(point[1] + 0.5)) |
- cv2.circle(self._frame_debug, center, 5, (0, 255, 0), 1) |
- point.resize(1, 2) |
- corners = np.append(corners, point, axis=0) |
- point.resize(2,) |
- corner_data = self.CornerData(corner_pos, point, score, line1, line2) |
- filtered.append(corner_data) |
- |
- # De-duplicate corners because we may have found many false positives, or |
- # near-misses. |
- self._DeDupCorners(filtered, corners) |
- |
- # Strip everything but the corner location. |
- filtered_corners = np.array( |
- [corner_data.corner_location for corner_data in filtered]) |
- corner_indices = [corner_data.corner_index for corner_data in filtered] |
- |
- # If we have found a corner to replace a lost corner, we want to check |
- # that the corner is not erroneous by ensuring it makes a rectangle with |
- # the 3 known good corners. |
- if len(filtered) == 4: |
- for i in xrange(4): |
- point_info = (filtered[i].corner_location, |
- filtered[i].line1, |
- filtered[i].line2) |
- if (self._lost_corners[i] and |
- not self._PointConnectsToCorners(filtered_corners, point_info)): |
- filtered_corners = np.delete(filtered_corners, i, 0) |
- corner_indices = np.delete(corner_indices, i, 0) |
- break |
- |
- # Ensure corners are sorted properly, inserting nans for missing corners. |
- sorted_corners = np.empty((4, 2), np.float32) |
- sorted_corners[:] = np.nan |
- for i in xrange(len(filtered_corners)): |
- sorted_corners[corner_indices[i]] = filtered_corners[i] |
- |
- # From this point on, our corners arrays are guaranteed to have 4 |
- # elements, though some may be nan. |
- |
- # Filter corners that have moved too far from the previous corner if we |
- # are not resetting known corner information. |
- reset_corners = ( |
- (self._lost_corner_frames > self.RESET_AFTER_N_BAD_FRAMES) |
- and len(filtered_corners) == 4) |
- if self._prev_corners is not None and not reset_corners: |
- sqdists = cv_util.SqDistances(self._prev_corners, sorted_corners) |
- for i in xrange(4): |
- if np.isnan(sorted_corners[i][0]): |
- continue |
- if sqdists[i] > self.MAX_INTERFRAME_MOTION: |
- sorted_corners[i] = np.nan |
- |
- real_corners = self._FindExactCorners(sorted_corners) |
- missing_corners = np.count_nonzero(np.isnan(real_corners)) / 2 |
- return real_corners, missing_corners |
- |
- def _LooksLikeCorner(self, intersections, grey_frame): |
- """Finds any intersections of lines that look like a screen corner. |
- |
- Args: |
- intersections: The numpy array of points, and the lines that intersect |
- at the given point. |
- grey_frame: The greyscale frame we're processing. |
- |
- Returns: |
- An array of: The corner location (0-3), the relative brightness score |
- (to be used to de-duplicate corners later), the point, and the lines |
- that make up the intersection, for all intersections that look like a |
- corner.""" |
- points = np.vstack(intersections[:, 0].flat) |
- lines1 = np.vstack(intersections[:, 1].flat) |
- lines2 = np.vstack(intersections[:, 2].flat) |
- # Map the image to four quadrants defined as the regions between each of |
- # the lines that make up the intersection. |
- line1a1 = np.pi - np.arctan2(lines1[:, 1] - points[:, 1], |
- lines1[:, 0] - points[:, 0]) |
- line1a2 = np.pi - np.arctan2(lines1[:, 3] - points[:, 1], |
- lines1[:, 2] - points[:, 0]) |
- line2a1 = np.pi - np.arctan2(lines2[:, 1] - points[:, 1], |
- lines2[:, 0] - points[:, 0]) |
- line2a2 = np.pi - np.arctan2(lines2[:, 3] - points[:, 1], |
- lines2[:, 2] - points[:, 0]) |
- line1a1 = line1a1.reshape(-1, 1) |
- line1a2 = line1a2.reshape(-1, 1) |
- line2a1 = line2a1.reshape(-1, 1) |
- line2a2 = line2a2.reshape(-1, 1) |
- |
- line_angles = np.concatenate((line1a1, line1a2, line2a1, line2a2), axis=1) |
- np.ndarray.sort(line_angles) |
- |
- # TODO(mthiesse): Investigate whether these should scale with image or |
- # screen size. My intuition is that these don't scale with image size, |
- # though they may be affected by image quality and how blurry the corners |
- # are. See stackoverflow.com/q/7765810/ for inspiration. |
- avg_range = 8.0 |
- num_points = 7 |
- |
- points_m_avg = points - avg_range |
- points_p_avg = points + avg_range |
- # Exclude points near frame boundaries. |
- include = np.where((points_m_avg[:, 0] > 0) & (points_m_avg[:, 1] > 0) & |
- (points_p_avg[:, 0] < self._width) & |
- (points_p_avg[:, 1] < self._height)) |
- line_angles = line_angles[include] |
- points = points[include] |
- lines1 = lines1[include] |
- lines2 = lines2[include] |
- points_m_avg = points_m_avg[include] |
- points_p_avg = points_p_avg[include] |
- # Perform a 2-d linspace to generate the x, y ranges for each |
- # intersection. |
- arr1 = points_m_avg[:, 0].reshape(-1, 1) |
- arr2 = points_p_avg[:, 0].reshape(-1, 1) |
- lin = np.linspace(0, 1, num_points) |
- x_range = arr1 + (arr2 - arr1) * lin |
- arr1 = points_m_avg[:, 1].reshape(-1, 1) |
- arr2 = points_p_avg[:, 1].reshape(-1, 1) |
- y_range = arr1 + (arr2 - arr1) * lin |
- |
- # The angles for each point we look at in the grid when computing |
- # brightness are constant across frames, so we can generate them once. |
- if self._anglesp5 is None: |
- ind = np.transpose([np.tile(x_range[0], num_points), |
- np.repeat(y_range[0], num_points)]) |
- vectors = ind - points[0] |
- angles = np.arctan2(vectors[:, 1], vectors[:, 0]) + np.pi |
- self._anglesp5 = angles + self.SMALL_ANGLE |
- self._anglesm5 = angles - self.SMALL_ANGLE |
- results = [] |
- for i in xrange(len(y_range)): |
- # Generate our filters for which points belong to which quadrant. |
- one = np.where((self._anglesp5 <= line_angles[i, 1]) & |
- (self._anglesm5 >= line_angles[i, 0])) |
- two = np.where((self._anglesp5 <= line_angles[i, 2]) & |
- (self._anglesm5 >= line_angles[i, 1])) |
- thr = np.where((self._anglesp5 <= line_angles[i, 3]) & |
- (self._anglesm5 >= line_angles[i, 2])) |
- fou = np.where((self._anglesp5 <= line_angles[i, 0]) | |
- (self._anglesm5 >= line_angles[i, 3])) |
- # Take the cartesian product of our x and y ranges to get the full list |
- # of pixels to look at. |
- ind = np.transpose([np.tile(x_range[i], num_points), |
- np.repeat(y_range[i], num_points)]) |
- |
- # Filter the full list by which indices belong to which quadrant, and |
- # convert to integers so we can index with them. |
- one_i = np.int32(np.rint(ind[one[0]])) |
- two_i = np.int32(np.rint(ind[two[0]])) |
- thr_i = np.int32(np.rint(ind[thr[0]])) |
- fou_i = np.int32(np.rint(ind[fou[0]])) |
- |
- # Average the brightness of the pixels that belong to each quadrant. |
- q_1 = np.average(grey_frame[one_i[:, 1], one_i[:, 0]]) |
- q_2 = np.average(grey_frame[two_i[:, 1], two_i[:, 0]]) |
- q_3 = np.average(grey_frame[thr_i[:, 1], thr_i[:, 0]]) |
- q_4 = np.average(grey_frame[fou_i[:, 1], fou_i[:, 0]]) |
- |
- avg_intensity = [(q_4, 0), (q_1, 1), (q_2, 2), (q_3, 3)] |
- # Sort by intensity. |
- avg_intensity.sort(reverse=True) |
- |
- # Treat the point as a corner if one quadrant is at least twice as |
- # bright as the next brightest quadrant, with a minimum brightness |
- # requirement. |
- tau = (2.0 * np.pi) |
- min_factor = self.MIN_RELATIVE_BRIGHTNESS_FACTOR |
- min_brightness = self.MIN_RELATIVE_BRIGHTNESS_FACTOR |
- if avg_intensity[0][0] > avg_intensity[1][0] * min_factor and \ |
- avg_intensity[0][0] > min_brightness: |
- bright_corner = avg_intensity[0][1] |
- if bright_corner == 0: |
- angle = np.pi - (line_angles[i, 0] + line_angles[i, 3]) / 2.0 |
- if angle < 0: |
- angle = angle + tau |
- else: |
- angle = tau - (line_angles[i, bright_corner] + |
- line_angles[i, bright_corner - 1]) / 2.0 |
- score = avg_intensity[0][0] - avg_intensity[1][0] |
- # TODO(mthiesse): int(angle / (pi / 2.0)) will break if the screen is |
- # rotated through 45 degrees. Probably many other things will break as |
- # well, movement of corners from one quadrant to another hasn't been |
- # tested. We should support this eventually, but this is unlikely to |
- # cause issues for any test setups. |
- results.append((int(angle / (np.pi / 2.0)), score, points[i], |
- lines1[i], lines2[i])) |
- return results |
- |
- def _DeDupCorners(self, corner_data, corners): |
- """De-duplicate corners based on corner_index. |
- |
- For each set of points representing a corner: If one point is part of the |
- rectangle and the other is not, filter the other one. If both or none are |
- part of the rectangle, filter based on score (highest relative brightness |
- of a quadrant). The reason we allow for neither to be part of the |
- rectangle is because we may not have found all four corners of the |
- rectangle, and in degenerate cases like this it's better to find 3 likely |
- corners than none. |
- |
- Modifies corner_data directly. |
- |
- Args: |
- corner_data: CornerData for each potential corner in the frame. |
- corners: List of all potential corners in the frame.""" |
- # TODO(mthiesse): Ensure that the corners form a sensible rectangle. For |
- # example, it is currently possible (but unlikely) to detect a 'screen' |
- # where the bottom-left corner is above the top-left corner, while the |
- # bottom-right corner is below the top-right corner. |
- |
- # Sort by corner_index to make de-duping easier. |
- corner_data.sort() |
- |
- # De-dup corners. |
- c_old = None |
- for i in xrange(len(corner_data) - 1, 0, -1): |
- if corner_data[i].corner_index != corner_data[i - 1].corner_index: |
- c_old = None |
- continue |
- if c_old is None: |
- point_info = (corner_data[i].corner_location, |
- corner_data[i].line1, |
- corner_data[i].line2) |
- c_old = self._PointConnectsToCorners(corners, point_info, 2) |
- point_info_new = (corner_data[i - 1].corner_location, |
- corner_data[i - 1].line1, |
- corner_data[i - 1].line2) |
- c_new = self._PointConnectsToCorners(corners, point_info_new, 2) |
- if (not (c_old or c_new)) or (c_old and c_new): |
- if (corner_data[i].brightness_score < |
- corner_data[i - 1].brightness_score): |
- del corner_data[i] |
- c_old = c_new |
- else: |
- del corner_data[i - 1] |
- elif c_old: |
- del corner_data[i - 1] |
- else: |
- del corner_data[i] |
- c_old = c_new |
- |
- def _PointConnectsToCorners(self, corners, point_info, tolerance=1): |
- """Checks if the lines of an intersection intersect with corners. |
- |
- This is useful to check if the point is part of a rectangle specified by |
- |corners|. |
- |
- Args: |
- point_info: A tuple of (point, line, line) representing an intersection |
- of two lines. |
- corners: corners that (hopefully) make up a rectangle. |
- tolerance: The tolerance (approximately in pixels) of the distance |
- between the corners and the lines for detecting if the point is on |
- the line. |
- |
- Returns: |
- True if each of the two lines that make up the intersection where the |
- point is located connect the point to other corners.""" |
- line1_connected = False |
- line2_connected = False |
- point, line1, line2 = point_info |
- for corner in corners: |
- if corner is None: |
- continue |
- |
- # Filter out points that are too close to one another to be different |
- # corners. |
- sqdist = cv_util.SqDistance(corner, point) |
- if sqdist < self.MIN_SCREEN_WIDTH * self.MIN_SCREEN_WIDTH: |
- continue |
- |
- line1_connected = line1_connected or \ |
- cv_util.IsPointApproxOnLine(corner, line1, tolerance) |
- line2_connected = line2_connected or \ |
- cv_util.IsPointApproxOnLine(corner, line2, tolerance) |
- if line1_connected and line2_connected: |
- return True |
- return False |
- |
- def _FindExactCorners(self, sorted_corners): |
- """Attempts to find more accurate corner locations. |
- |
- Args: |
- sorted_corners: The four screen corners, sorted by corner_index. |
- |
- Returns: |
- A list of 4 probably more accurate corners, still sorted.""" |
- real_corners = np.empty((4, 2), np.float32) |
- # Count missing corners, and search in a small area around our |
- # intersections representing corners to see if we can find a more exact |
- # corner, as the position of the intersections is noisy and not always |
- # perfectly accurate. |
- for i in xrange(4): |
- corner = sorted_corners[i] |
- if np.isnan(corner[0]): |
- real_corners[i] = np.nan |
- continue |
- |
- # Almost unbelievably, in edge cases with floating point error, the |
- # width/height of the cropped corner image may be 2 or 4. This is fine |
- # though, as long as the width and height of the cropped corner are not |
- # hard-coded anywhere. |
- corner_image = self._frame_edges[corner[1] - 1:corner[1] + 2, |
- corner[0] - 1:corner[0] + 2] |
- ret, p = self._FindExactCorner(i <= 1, i == 1 or i == 2, corner_image) |
- if ret: |
- if self.DEBUG: |
- self._frame_edges[corner[1] - 1 + p[1]][corner[0] - 1 + p[0]] = 128 |
- real_corners[i] = corner - 1 + p |
- else: |
- real_corners[i] = corner |
- return real_corners |
- |
- def _FindExactCorner(self, top, left, img): |
- """Tries to finds the exact corner location for a given corner. |
- |
- Searches for the top or bottom, left or right most lit |
- pixel in an edge-detected image, which should represent, with pixel |
- precision, as accurate a corner location as possible. (Though perhaps |
- up-sampling using cubic spline interpolation could get sub-pixel |
- precision) |
- |
- TODO(mthiesse): This algorithm could be improved by including a larger |
- region to search in, but would have to be made smarter about which lit |
- pixels are on the detected screen edge and which are a not as it's |
- currently extremely easy to fool by things like notification icons in |
- screen corners. |
- |
- Args: |
- top: boolean, whether or not we're looking for a top corner. |
- left: boolean, whether or not we're looking for a left corner. |
- img: A small cropping of the edge detected image in which to search. |
- |
- Returns: |
- True and the location if a better corner location is found, |
- False otherwise.""" |
- h, w = img.shape[:2] |
- cy = 0 |
- starting_x = w - 1 if left else 0 |
- cx = starting_x |
- if top: |
- y_range = xrange(h - 1, -1, -1) |
- else: |
- y_range = xrange(0, h, 1) |
- if left: |
- x_range = xrange(w - 1, -1, -1) |
- else: |
- x_range = xrange(0, w, 1) |
- for y in y_range: |
- for x in x_range: |
- if img[y][x] == 255: |
- cy = y |
- if (left and x <= cx) or (not left and x >= cx): |
- cx = x |
- if cx == starting_x and cy == 0 and img[0][starting_x] != 255: |
- return False, (0, 0) |
- return True, (cx, cy) |
- |
- def _NewScreenLocation(self, new_corners, missing_corners, intersections): |
- """Computes the new screen location with best effort. |
- |
- Creates the final list of corners that represents the best effort attempt |
- to find the new screen location. Handles degenerate cases where 3 or fewer |
- new corners are present, using previous corner and intersection data. |
- |
- Args: |
- new_corners: The corners found by our search for corners. |
- missing_corners: The count of how many corners we're missing. |
- intersections: The intersections of straight lines found in the current |
- frame. |
- |
- Returns: |
- An array of 4 new_corners hopefully representing the screen, or throws |
- an error if this is not possible. |
- |
- Raises: |
- ValueError: Finding the screen location was not possible.""" |
- screen_corners = copy.copy(new_corners) |
- if missing_corners == 0: |
- self._lost_corner_frames = 0 |
- self._lost_corners = [False, False, False, False] |
- return screen_corners |
- if self._prev_corners is None: |
- raise self.ScreenNotFoundError( |
- 'Could not locate screen on frame %d' % |
- self._frame_generator.CurrentFrameNumber) |
- |
- self._lost_corner_frames += 1 |
- if missing_corners > 1: |
- logging.info('Unable to properly detect screen corners, making ' |
- 'potentially false assumptions on frame %d', |
- self._frame_generator.CurrentFrameNumber) |
- # Replace missing new_corners with either nearest intersection to previous |
- # corner, or previous corner if no intersections are found. |
- for i in xrange(0, 4): |
- if not np.isnan(new_corners[i][0]): |
- self._lost_corners[i] = False |
- continue |
- self._lost_corners[i] = True |
- min_dist = self.MAX_INTERFRAME_MOTION |
- min_corner = None |
- |
- for isection in intersections: |
- dist = cv_util.SqDistance(isection[0], self._prev_corners[i]) |
- if dist >= min_dist: |
- continue |
- if missing_corners == 1: |
- # We know in this case that we have 3 corners present, meaning |
- # all 4 screen lines, and therefore intersections near screen |
- # corners present, so our new corner must connect to these |
- # other corners. |
- if not self._PointConnectsToCorners(new_corners, isection, 3): |
- continue |
- min_corner = isection[0] |
- min_dist = dist |
- screen_corners[i] = min_corner if min_corner is not None else \ |
- self._prev_corners[i] |
- |
- return screen_corners |
- |
- def _SmoothCorners(self, corners): |
- """Smoothes the motion of corners, reduces noise. |
- |
- Smoothes the motion of corners by computing an exponentially weighted |
- moving average of corner positions over time. |
- |
- Args: |
- corners: The corners of the detected screen. |
- |
- Returns: |
- The final corner positions.""" |
- if self._avg_corners is None: |
- self._avg_corners = np.asfarray(corners, np.float32) |
- for i in xrange(0, 4): |
- # Keep an exponential moving average of the corner location to reduce |
- # noise. |
- new_contrib = np.multiply(self.CORNER_AVERAGE_WEIGHT, corners[i]) |
- old_contrib = np.multiply(1 - self.CORNER_AVERAGE_WEIGHT, |
- self._avg_corners[i]) |
- self._avg_corners[i] = np.add(new_contrib, old_contrib) |
- |
- return self._avg_corners |
- |
- def _GetTransform(self, corners, border): |
- """Gets the perspective transform of the screen. |
- |
- Args: |
- corners: The corners of the detected screen. |
- border: The number of pixels of border to crop along with the screen. |
- |
- Returns: |
- A perspective transform and the width and height of the target |
- transform. |
- |
- Raises: |
- ScreenNotFoundError: Something went wrong in detecting the screen.""" |
- if self._screen_size is None: |
- w = np.sqrt(cv_util.SqDistance(corners[1], corners[0])) |
- h = np.sqrt(cv_util.SqDistance(corners[1], corners[2])) |
- if w < 1 or h < 1: |
- raise self.ScreenNotFoundError( |
- 'Screen detected improperly (bad corners)') |
- if min(w, h) < self.MIN_SCREEN_WIDTH: |
- raise self.ScreenNotFoundError('Detected screen was too small.') |
- |
- self._screen_size = (w, h) |
- # Extend min line length, if we can, to reduce the number of extraneous |
- # lines the line finder finds. |
- self._min_line_length = max(self._min_line_length, min(w, h) / 1.75) |
- w = self._screen_size[0] |
- h = self._screen_size[1] |
- |
- target = np.zeros((4, 2), np.float32) |
- width = w + border |
- height = h + border |
- target[0] = np.asfarray((width, border)) |
- target[1] = np.asfarray((border, border)) |
- target[2] = np.asfarray((border, height)) |
- target[3] = np.asfarray((width, height)) |
- transform_w = width + border |
- transform_h = height + border |
- transform = cv2.getPerspectiveTransform(corners, target) |
- return transform, transform_w, transform_h |
- |
- def _Debug(self, lines, corners, final_corners, screen): |
- for line in lines: |
- intline = ((int(line[0]), int(line[1])), |
- (int(line[2]), int(line[3]))) |
- cv2.line(self._frame_debug, intline[0], intline[1], (0, 0, 255), 1) |
- i = 0 |
- for corner in corners: |
- if not np.isnan(corner[0]): |
- cv2.putText( |
- self._frame_debug, str(i), (int(corner[0]), int(corner[1])), |
- cv2.FONT_HERSHEY_COMPLEX_SMALL, 1, (255, 255, 0), 1, cv2.CV_AA) |
- i += 1 |
- if final_corners is not None: |
- for corner in final_corners: |
- cv2.circle(self._frame_debug, |
- (int(corner[0]), int(corner[1])), 5, (255, 0, 255), 1) |
- cv2.imshow('original', self._frame) |
- cv2.imshow('debug', self._frame_debug) |
- if screen is not None: |
- cv2.imshow('screen', screen) |
- cv2.waitKey() |
- |
-# For being run as a script. |
-# TODO(mthiesse): To be replaced with a better standalone script. |
-# Ex: ./screen_finder.py path_to_video 0 5 --verbose |
- |
- |
-def main(): |
- start_frame = int(sys.argv[2]) if len(sys.argv) >= 3 else 0 |
- vf = video_file_frame_generator.VideoFileFrameGenerator(sys.argv[1], |
- start_frame) |
- if len(sys.argv) >= 4: |
- sf = ScreenFinder(vf, int(sys.argv[3])) |
- else: |
- sf = ScreenFinder(vf) |
- # TODO(mthiesse): Use argument parser to improve command line parsing. |
- if len(sys.argv) > 4 and sys.argv[4] == '--verbose': |
- logging.basicConfig(format='%(message)s', level=logging.INFO) |
- else: |
- logging.basicConfig(format='%(message)s', level=logging.WARN) |
- while sf.HasNext(): |
- sf.GetNext() |
- |
-if __name__ == '__main__': |
- main() |