def laps(img, lines): """ Lattice points search in the given image. :param img: Image to search. :param lines: Lines detected by slid. :return: Points detected to be part of the chessboard grid. """ intersection_points = __find_intersections(lines) debug.DebugImage(img) \ .lines(lines, color=(0, 0, 255)) \ .points(intersection_points, color=(255, 0, 0), size=2) \ .save("laps_in_queue") points = [] for pt in intersection_points: # Pixels are in integers pt = (int(pt[0]), int(pt[1])) if pt[0] < 0 or pt[1] < 0: continue # Size of our analysis area lx1 = max(0, int(pt[0] - __ANALYSIS_RADIUS - 1)) lx2 = max(0, int(pt[0] + __ANALYSIS_RADIUS)) ly1 = max(0, int(pt[1] - __ANALYSIS_RADIUS)) ly2 = max(0, int(pt[1] + __ANALYSIS_RADIUS + 1)) # Cropping for detector dimg = img[ly1:ly2, lx1:lx2] dimg_shape = np.shape(dimg) # Not valid if dimg_shape[0] <= 0 or dimg_shape[1] <= 0: continue # Detect if it is a lattice point if not __is_lattice_point(dimg): continue points.append(pt) if points: points = __cluster_points(points) debug.DebugImage(img) \ .points(intersection_points, color=(0, 0, 255), size=3) \ .points(points, color=(0, 255, 0)) \ .save("laps_good_points") return points
def simplify_image(img, limit, grid, iters): """Simplify image using CLAHE algorithm (adaptive histogram equalization).""" img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) for _ in range(iters): img = cv2.createCLAHE(clipLimit=limit, tileGridSize=grid).apply(img) debug.DebugImage(img).save("slid_clahe_@1") if limit != 0: kernel = np.ones((10, 10), np.uint8) img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) debug.DebugImage(img).save("slid_clahe_@2") return img
def compute_corners(image_object): """ Compute the coordinates of the board on the original image from the ImageObject obtained in the detection. :param image_object: ImageObject obtained in the detect method. :return: The coordinates in the original image of the chessboard corners and the coordinates of each of the corners of the chessboard squares as a pair of board_corners and square_corners. """ board_corners, square_corners = __original_points_coords( image_object.get_points()) debug.DebugImage(image_object.get_images()[0]['orig']) \ .points(square_corners, size=50, color=(0, 0, 255)) \ .points(board_corners, size=50, color=(0, 255, 0)) \ .save("corner_points") return board_corners, square_corners
def __padcrop(img, four_points): """ Apply a border to the inner four points of the chessboard in order to obtain a frame that contains the full board. """ pco = pyclipper.PyclipperOffset() pco.AddPath(four_points, pyclipper.JT_MITER, pyclipper.ET_CLOSEDPOLYGON) padded = pco.Execute(60)[0] debug.DebugImage(img) \ .points(four_points, color=(0, 0, 255)) \ .points(padded, color=(0, 255, 0)) \ .lines( [[four_points[0], four_points[1]], [four_points[1], four_points[2]], [four_points[2], four_points[3]], [four_points[3], four_points[0]]], color=(255, 255, 255)) \ .lines([[padded[0], padded[1]], [padded[1], padded[2]], [padded[2], padded[3]], [padded[3], padded[0]]], color=(255, 255, 255)) \ .save("cps_final_pad") return __order_points(padded)
def detect(input_image, output_board, board_corners=None): """ Detects the board position in input_image and stores the cropped detected board in output_board. :param input_image: Input chessboard image. :param output_board: Path (including name and extension) where to store the image with the detected chessboard. :param board_corners: A list of the coordinates of the four board corners. If it is not None, first check if the board is in the position given by these corners. If not, runs the full detection. :return: Final ImageObject with which to compute the corners if necessary. """ # Check if we can skip full board detection (if board position is # already known) if board_corners is not None: found, cropped_img = check_board_position(input_image, board_corners) if found: cv2.imwrite(output_board, cropped_img) image = ImageObject(input_image) # For corners calculation image.add_points([[0, 0], [1200, 0], [1200, 1200], [0, 1200]]) image.add_points(board_corners) return image # Read the input image and store the cropped detected board n_layers = 3 image = ImageObject(input_image) for i in range(n_layers): __layer(image) debug.DebugImage(image['orig']).save(f"end_iteration{i}") cv2.imwrite(output_board, image['orig']) return image
def __slid_segments(img): """ Find all segments in the image using different settings. :param img: Image to search. :return: A list of all the segments found. """ def detect_edges(img): """Apply Canny edge detector (automatic threshold).""" sigma = 0.25 v = np.median(img) img = cv2.medianBlur(img, 5) img = cv2.GaussianBlur(img, (7, 7), 2) lower = int(max(0, (1. - sigma) * v)) upper = int(min(255, (1. + sigma) * v)) return cv2.Canny(img, lower, upper) def simplify_image(img, limit, grid, iters): """Simplify image using CLAHE algorithm (adaptive histogram equalization).""" img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) for _ in range(iters): img = cv2.createCLAHE(clipLimit=limit, tileGridSize=grid).apply(img) debug.DebugImage(img).save("slid_clahe_@1") if limit != 0: kernel = np.ones((10, 10), np.uint8) img = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) debug.DebugImage(img).save("slid_clahe_@2") return img def detect_lines(img): """Detect lines using the probabilistic Hough transform.""" beta = 2 lines = cv2.HoughLinesP(img, rho=1, theta=np.pi / 360 * beta, threshold=40, minLineLength=50, maxLineGap=15) # [40, 40, 10] if lines is None: return [] __lines = [] for line in np.reshape(lines, (-1, 4)): __lines.append([[int(line[0]), int(line[1])], [int(line[2]), int(line[3])]]) return __lines clahe_settings = [ [3, (2, 6), 5], # @1 [3, (6, 2), 5], # @2 [5, (3, 3), 5], # @3 [0, (0, 0), 0] ] # EE segments = [] i = 0 for key, arr in enumerate(clahe_settings): tmp = simplify_image(img, limit=arr[0], grid=arr[1], iters=arr[2]) __segments = detect_lines(detect_edges(tmp)) segments += __segments i += 1 debug.DebugImage(detect_edges(tmp)).lines(__segments).save( "pslid_F%d" % i) return segments
def slid(img): """ Straight line detector in the given image from the segments. :param img: Image to search. :return: A list of the detected lines. Each line is a pair of points. """ group = {} hashmap = {} ptp_cache = {} def ptp_distance(a, b): """ Distance from point to point with a cache to avoid multiple calculations. """ idx = hash("__dis" + str(a) + str(b)) if idx in ptp_cache: return ptp_cache[idx] ptp_cache[idx] = math.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2) return ptp_cache[idx] def ptl_distance(line, point, dx): """ Distance from point to line. :param line: Line defined by two points. :param point: Point. :param dx: Distance between the points that define the line. :return: The distance from point to line. """ return abs((line[1][0] - line[0][0]) * (line[0][1] - point[1]) - (line[1][1] - line[0][1]) * (line[0][0] - point[0])) / dx def similar_lines(line1, line2): """Returns if line1 is similar to line2.""" da = ptp_distance(line1[0], line1[1]) db = ptp_distance(line2[0], line2[1]) d1a = ptl_distance(line1, line2[0], da) d2a = ptl_distance(line1, line2[1], da) d1b = ptl_distance(line2, line1[0], db) d2b = ptl_distance(line2, line1[1], db) # Average deviation from the straight line avg_dev = 0.25 * (d1a + d1b + d2a + d2b) + 0.00001 # Allowed matching error delta = 0.0625 * (da + db) return da / avg_dev > delta and db / avg_dev > delta X = {} def __fi(x): if x not in X: X[x] = 0 if X[x] == x or X[x] == 0: X[x] = x else: X[x] = __fi(X[x]) return X[x] def __un(a, b): """Union & find.""" ia, ib = __fi(a), __fi(b) X[ia] = ib group[ib] |= group[ia] def generate_points(a, b, n): """ Returns n equispaced points in the segment given by a and b. """ points = [] t = 1 / n for i in range(n): x = a[0] + (b[0] - a[0]) * (i * t) y = a[1] + (b[1] - a[1]) * (i * t) points.append((int(x), int(y))) return points def merge_group(group, all_points): """Merge the group into a single line.""" points = [] for idx in group: points += generate_points(*hashmap[idx], n=10) all_points += points na_points = np.array(points) _, radius = cv2.minEnclosingCircle(na_points) w = radius * (math.pi / 2) vx, vy, cx, cy = cv2.fitLine(na_points, cv2.DIST_L2, 0, 0.01, 0.01) return ((int(cx - vx * w), int(cy - vy * w)), (int(cx + vx * w), int(cy + vy * w))) # Find all segments in image segments = __slid_segments(img) # Divide segments into vertical and horizontal vh_segments = [[], []] for l in segments: h = hash(str(l)) hashmap[h] = l group[h] = {h} X[h] = h t1 = l[0][0] - l[1][0] t2 = l[0][1] - l[1][1] if abs(t1) < abs(t2): # If l is a vertical segment vh_segments[0].append(l) else: vh_segments[1].append(l) debug.DebugImage(img.shape) \ .lines(vh_segments[0], color=debug.rand_color()) \ .lines(vh_segments[1], color=debug.rand_color()) \ .save("slid_pre_groups") for lines in vh_segments: for i in range(len(lines)): l1 = lines[i] h1 = hash(str(l1)) if X[h1] != h1: # Line already grouped continue for j in range(i + 1, len(lines)): l2 = lines[j] h2 = hash(str(l2)) if X[h2] != h2: continue if similar_lines(l1, l2): __un(h1, h2) if debug.DEBUG: __d = debug.DebugImage(img.shape) for i in group: if X[i] != i: continue ls = [hashmap[h] for h in group[i]] __d.lines(ls, color=debug.rand_color()) __d.save("slid_all_groups") all_points = [] raw_lines = [] for i in group: if X[i] != i: continue raw_lines.append(merge_group(group[i], all_points)) lines = __scale_lines(raw_lines) debug.DebugImage(img.shape) \ .points(all_points, color=(0, 255, 0), size=2) \ .lines(raw_lines).save("slid_raw_lines") debug.DebugImage(img).lines(lines).save("slid_final") return lines
def cps(img, points, lines): """ Chessboard position search in the given image. :param img: Image to search. :param points: Points obtained in laps. :param lines: Lines detected by slid. :return: The four inner points of the detected chessboard. """ ptp_cache = {} def ptp_distance(a, b): """ Distance from point to point with a cache to avoid multiple calculations. """ idx = hash("__dis" + str(a) + str(b)) if idx in ptp_cache: return ptp_cache[idx] ptp_cache[idx] = math.sqrt((a[0] - b[0])**2 + (a[1] - b[1])**2) return ptp_cache[idx] points = __check_correctness(__normalize(points), img.shape) # Clustering __points = {} points = __sort_points(points) __max = 0 __points_max = [] alfa = math.sqrt(cv2.contourArea(np.array(points)) / 49) X = DBSCAN(eps=alfa * 4).fit(points) for i in range(len(points)): __points[i] = [] for i in range(len(points)): if X.labels_[i] != -1: __points[X.labels_[i]].append(points[i]) for i in range(len(points)): if len(__points[i]) > __max: __max = len(__points[i]) __points_max = __points[i] if len(__points) > 0 and len(points) > 49 / 2: points = __points_max n = len(points) beta = n * (5 / 100) # beta = n * (100 - (CPS efectiveness)) alfa = math.sqrt(cv2.contourArea(np.array(points)) / 49) # We are looking for the focal point of the cluster x = [p[0] for p in points] y = [p[1] for p in points] centroid = (sum(x) / len(points), sum(y) / len(points)) def __v(l): y_0, x_0 = l[0][0], l[0][1] y_1, x_1 = l[1][0], l[1][1] x_2 = 0 t = (x_0 - x_2) / (x_0 - x_1 + 0.0001) a = [int((1 - t) * x_0 + t * x_1), int((1 - t) * y_0 + t * y_1)][::-1] x_2 = img.shape[0] t = (x_0 - x_2) / (x_0 - x_1 + 0.0001) b = [int((1 - t) * x_0 + t * x_1), int((1 - t) * y_0 + t * y_1)][::-1] poly1 = __sort_points([[0, 0], [0, img.shape[0]], a, b]) s1 = __polyscore(np.array(poly1), points, centroid, alfa / 2, beta) poly2 = __sort_points( [a, b, [img.shape[1], 0], [img.shape[1], img.shape[0]]]) s2 = __polyscore(np.array(poly2), points, centroid, alfa / 2, beta) return [a, b], s1, s2 def __h(l): x_0, y_0 = l[0][0], l[0][1] x_1, y_1 = l[1][0], l[1][1] x_2 = 0 t = (x_0 - x_2) / (x_0 - x_1 + 0.0001) a = [int((1 - t) * x_0 + t * x_1), int((1 - t) * y_0 + t * y_1)] x_2 = img.shape[1] t = (x_0 - x_2) / (x_0 - x_1 + 0.0001) b = [int((1 - t) * x_0 + t * x_1), int((1 - t) * y_0 + t * y_1)] poly1 = __sort_points([[0, 0], [img.shape[1], 0], a, b]) s1 = __polyscore(np.array(poly1), points, centroid, alfa / 2, beta) poly2 = __sort_points( [a, b, [0, img.shape[0]], [img.shape[1], img.shape[0]]]) s2 = __polyscore(np.array(poly2), points, centroid, alfa / 2, beta) return [a, b], s1, s2 pregroup = [[], []] # Division into 2 groups (for the frame) for l in lines: # We will review all of the lines # We reject lines that pass through the center of the cluster if __ptl_distance(l, centroid, ptp_distance(*l)) > alfa * 2.5: for p in points: # We check that the line passes near a good point if __ptl_distance(l, p, ptp_distance(*l)) < alfa: # The line belongs to the ring tx, ty = l[0][0] - l[1][0], l[0][1] - l[1][1] if abs(tx) < abs(ty): ll, s1, s2 = __v(l) orientation = 0 else: ll, s1, s2 = __h(l) orientation = 1 if s1 == 0 and s2 == 0: continue pregroup[orientation].append(ll) pregroup[0] = __remove_duplicates(pregroup[0]) pregroup[1] = __remove_duplicates(pregroup[1]) if debug.DEBUG: # We create an outer ring def convex_approx(points, alfa=0.01): points = np.array(points) hull = ConvexHull(points).vertices cnt = points[hull] approx = cv2.approxPolyDP(cnt, alfa * cv2.arcLength(cnt, True), True) return __normalize(itertools.chain(*approx)) ring = convex_approx(__sort_points(points)) debug.DebugImage(img) \ .lines(lines, color=(0, 0, 255)) \ .points(points, color=(0, 0, 255)) \ .points(ring, color=(0, 255, 0)) \ .points([centroid], color=(255, 0, 0)) \ .save("cps_debug") debug.DebugImage(img) \ .lines(pregroup[0], color=(0, 0, 255)) \ .lines(pregroup[1], color=(255, 0, 0)) \ .save("cps_pregroups") score = {} # Frame ranking with the result for v in itertools.combinations(pregroup[0], 2): # Horizontal for h in itertools.combinations(pregroup[1], 2): # Vertical poly = [ __intersection(v[0], v[1]), __intersection(v[0], h[0]), __intersection(v[0], h[1]), __intersection(v[1], h[0]), __intersection(v[1], h[1]), __intersection(h[0], h[1]) ] poly = __check_correctness(poly, img.shape) if len(poly) != 4: continue poly = np.array(__sort_points(__normalize(poly))) if not cv2.isContourConvex(poly): continue score[-__polyscore(poly, points, centroid, alfa / 2, beta)] = poly score = collections.OrderedDict(sorted(score.items())) K = next(iter(score)) inner_points = __normalize(score[K]) inner_points = __order_points(inner_points) debug.DebugImage(img) \ .points(points, color=(0, 255, 0)) \ .points(inner_points, color=(0, 0, 255)) \ .points([centroid], color=(255, 0, 0)) \ .lines([[inner_points[0], inner_points[1]], [inner_points[1], inner_points[2]], [inner_points[2], inner_points[3]], [inner_points[3], inner_points[0]]], color=(255, 255, 255)) \ .save("cps_debug_2") return __padcrop(img, inner_points)