def find_endpoints_in_area(lines_list: list, start_index: int, x: int, y: int, radius: float, main_angle: float) \ -> np.ndarray: """ Find all endpoints in circle area (center and radius given) Also calculate angle difference between each line containing endpoint in area and main endpoint line :param lines_list: list of lines (tuples of 2 endpoints) :param start_index: starting index for search in lines list :param x: center coordinate :param y: center coordinate :param radius: determines area of search :param main_angle: angle of line that search point is in :return: numpy array of endpoint descriptions containing: (if no endpoints found empty list is returned) line_index - line index in list point_index - endpoint index in line (0 or 1) delta - difference of angles between main line, and lines containing in_area endpoints """ in_area_list = [] endpoint = np.array([x, y]) for i in range(start_index, len(lines_list)): if lines_list[i] is None: continue for j in range(0, 2): tmp_endpoint = np.array([lines_list[i][j][0], lines_list[i][j][1]]) if distance_L2(endpoint, tmp_endpoint) <= radius: # calculate distance other_endpoint = np.array([lines_list[i][(j+1) % 2][0], lines_list[i][(j+1) % 2][1]]) tmp_angle = vector_angle(tmp_endpoint, other_endpoint) diff = abs(main_angle - tmp_angle) delta = diff if diff <= 180 else 360 - diff in_area_list.append([i, j, delta]) ret_arr = np.array(in_area_list) if in_area_list else None return ret_arr
def link_nearby_endpoints(lines_list: list, backend: np.ndarray, search_radius: float, angle_threshold: float) \ -> (list, np.ndarray): """ Link edge segments (lines) that are close to each other and go at a similar angle. This operation is performed because sometimes, one edge is separated into multiple lines (e.g. when intersections occur) :param lines_list: list of lines (tuples of 2 endpoints) :param backend: source image with visualised topology recognition backend :param search_radius: radius determining area around endpoint that other endpoint of other line has to be in to be considered a "nearby" endpoint :param angle_threshold: threshold that describes upper limit for angle (in degrees) that lines of 2 nearby endpoints have to be at to be linked :return: list of lines, where nearby line segments are connected into single lines also visualised results """ # sort lines by descending length - longer lines have higher priority (occur closer to the beginning of the list) lines_list = sorted(lines_list, key=functools.cmp_to_key(lines_lengths_compare), reverse=True) i = 0 while i < len(lines_list): if lines_list[i] is None: # skip already linked lines i += 1 continue line = lines_list[i] line_len = distance_L2(line[0], line[1]) search_radius = search_radius if search_radius <= line_len*1.2 else line_len*1.2 for j in range(0, len(line)): # for each one of 2 endpoints main_point = line[j] # search around this endpoint for endpoints from different lines other_point = line[(j+1) % 2] # other endpoint from the same line cv.circle(backend, (main_point[0], main_point[1]), int(search_radius), Color.PINK, 1) cv.line(backend, (main_point[0]-int(search_radius), main_point[1]), (main_point[0]+int(search_radius), main_point[1]), Color.PINK, 1) if main_point is None or other_point is None: break else: main_angle = vector_angle(other_point, main_point) in_area_list = find_endpoints_in_area(lines_list, i+1, main_point[0], main_point[1], search_radius, main_angle) # find all endpoints in area of main point if in_area_list is not None: deltas = in_area_list[:, 2] # extract angle differences into separate array min_delta = np.min(deltas) if min_delta <= angle_threshold: # check if lines are going at a similar angle min_index = np.argmin(deltas) k, l = (int(in_area_list[min_index][0]), int(in_area_list[min_index][1])) # create linked line by assigning new value to main_point place in list lines_list[i][j] = lines_list[k][(l + 1) % 2] # find new endpoint for main line lines_list[k] = None # mark line as linked i -= 1 # take another iteration over current line since it has just changed break # start new iteration with new linked line i += 1 final_lines_list = [] i = 0 for line in lines_list: i += 1 if line is None: # remove fields in list that remained after they were linked to other lines continue else: # print lines final_lines_list.append(line) pt1, pt2 = line cv.line(backend, (pt1[0], pt1[1]), (pt2[0], pt2[1]), Color.ORANGE, 2) return final_lines_list, backend
def extract_circle_area(image: np.ndarray, x: int, y: int, r: int) -> np.ndarray: """ Extract circular area from image into numpy array of pixels :param image: input image :param x: x coordinate of circle center :param y: y coordinate of circle center :param r: circle radius :return: """ pixel_list = [] if x >= 0 and y >= 0: # calculate square area boundaries that contains circle area top = y - r if ((y - r) >= 0) else 0 bottom = y + r if ((y + r) <= image.shape[0]) else image.shape[0] left = x - r if ((x - r) >= 0) else 0 right = x + r if ((x + r) <= image.shape[1]) else image.shape[1] # in inner circle area count black and white pixels to find dominant color for y_iter in range(top, bottom): for x_iter in range(left, right): distance = round(distance_L2([x, y], [x_iter, y_iter])) if distance <= r: pixel_list.append(image[y_iter][x_iter]) return np.array(pixel_list) if len(pixel_list) > 0 else None
def lines_lengths_compare(line1: ([int, int], [int, int]), line2: ([int, int], [int, int])) -> int: """ Compare 2 lines in terms of length :param line1: first line (2 endpoints) :param line2: second line --||-- :return: 1 - first line is longer than second one 0 - lines lengths are equal -1 - second line is longer than first one """ len1 = distance_L2(line1[0], line1[1]) len2 = distance_L2(line2[0], line2[1]) if len1 > len2: return 1 elif len1 == len2: return 0 else: return -1
def point_within_radius(point: np.ndarray, vertex: Vertex, radius_factor: float) -> bool: """ Check if point is within vertex area with radius modified by factor (based on euclidean distance - L2) :param point: x and y coordinates :param vertex: vertex which area is considered :param radius_factor: factor to increase/decrease radius and therefore area :return: True if point is within radius, and False if it is not """ radius = vertex.r * radius_factor return True if distance_L2(point, [vertex.x, vertex.y]) <= radius else False
def find_nearest_vertex(point: np.ndarray, vertices_list: list) -> int: """ Find vertex nearest to a given point (based on euclidean distance - L2) :param point: x and y coordinates from which distance to vertices is measured :param vertices_list: list of detected vertices in segmentation phase :return nearest_index: index in vertices list of vertex nearest to the given point """ # Initialise values with first on list nearest_index = 0 current_vertex = vertices_list[nearest_index] current_center = np.array([current_vertex.x, current_vertex.y]) min_distance = distance_L2(point, current_center) for i in range(0, len(vertices_list)): current_vertex = vertices_list[i] current_center = np.array([current_vertex.x, current_vertex.y]) distance = distance_L2(point, current_center) if distance < min_distance: # Found vertex closer to a point nearest_index = i min_distance = distance return nearest_index
def remove_margins(binary_image: np.ndarray) -> np.ndarray: """ Remove vertical and horizontal margins (lines that go the whole way through a binary image). :param binary_image: input transformed image with notebook margins remaining :return: image with margins removed """ margin_lines = [] # detect horizontal margin lines width = binary_image.shape[1] structure = cv.getStructuringElement(cv.MORPH_RECT, (width // 50, 1)) horizontal = cv.morphologyEx(binary_image, cv.MORPH_OPEN, structure, iterations=1) horizontal = cv.dilate(horizontal, Kernel.k3, iterations=1) margin_lines.append(cv.HoughLines(horizontal, 1, np.pi / 180, 500)) # detect vertical margin lines height = binary_image.shape[0] structure = cv.getStructuringElement(cv.MORPH_RECT, (1, height // 50)) vertical = cv.morphologyEx(binary_image, cv.MORPH_OPEN, structure, iterations=1) vertical = cv.dilate(vertical, Kernel.k3, iterations=1) margin_lines.append(cv.HoughLines(vertical, 1, np.pi / 180, 400)) # remove detected margins processed = binary_image.copy() diagonal_len = distance_L2([0, 0], [processed.shape[1], processed.shape[0]]) for lines in margin_lines: if lines is not None: for line in lines: # convert each line from Polar to Cartesian coordinate system rho, theta = line[0] a, b = (np.cos(theta), np.sin(theta)) x0, y0 = (a * rho, b * rho) pt1 = (int(x0 + diagonal_len * (-b)), int(y0 + diagonal_len * a)) pt2 = (int(x0 - diagonal_len * (-b)), int(y0 - diagonal_len * a)) cv.line(processed, pt1, pt2, Color.BG, 6) # mask margin line in the image return processed
def lines_from_contours(preprocessed: np.ndarray, backend: np.ndarray, min_line_length: float = 10) \ -> (list, np.ndarray): """ From image with removed vertices approximate each contour with straight line :param preprocessed: input preprocessed image with removed vertices :param backend: image with visualisation of topology recognition backend :param min_line_length: all approximated lines of smaller length than this value will be considered noise and not added to the list of lines :return: List of lines (tuples of 2 endpoints) and image with lines visualised """ lines_list = [] contours, hierarchy = cv.findContours(preprocessed, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE) cv.drawContours(backend, contours, -1, Color.YELLOW, 1) for i in range(0, len(contours)): if hierarchy[0][i][3] == -1: # outer contours only cnt = contours[i] pt1, pt2 = fit_line(cnt) # if line has been fitted take lines that are long enough to be an edge if pt1 is not None and pt2 is not None and distance_L2(pt1, pt2) >= min_line_length: cv.circle(backend, (pt1[0], pt1[1]), 4, Color.CYAN, cv.FILLED) cv.circle(backend, (pt2[0], pt2[1]), 4, Color.CYAN, cv.FILLED) lines_list.append([pt1, pt2]) return lines_list, backend