import numpy as np import cv2 def detect_text_bounds(image: np.array) -> (int, int): """ Find the lower and upper bounding lines in an image of a word """ if len(image.shape) >= 3 and image.shape[2] == 3: image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) elif len(image.shape) >= 3 and image.shape[2] == 1: image = np.squeeze(image, axis=-1) _, threshold = cv2.threshold(image, 0.8, 1, cv2.THRESH_BINARY_INV) line_sums = np.sum(threshold, axis=1).astype(float) line_sums = np.convolve(line_sums, np.ones(5) / 5, mode='same') line_sums_d = np.diff(line_sums) std_factor = 0.5 min_threshold = np.mean(line_sums_d[line_sums_d <= 0]) - std_factor * np.std(line_sums_d[line_sums_d <= 0]) bottom_index = np.max(np.where(line_sums_d < min_threshold)) max_threshold = np.mean(line_sums_d[line_sums_d >= 0]) + std_factor * np.std(line_sums_d[line_sums_d >= 0]) top_index = np.min(np.where(line_sums_d > max_threshold)) return bottom_index, top_index def dist(p_one, p_two) -> float: return np.linalg.norm(p_two - p_one) def crop(image: np.array, ratio: float = None, pixels: int = None) -> np.array: assert ratio is not None or pixels is not None, "Please specify either pixels or a ratio to crop" width, height = image.shape[:2] if ratio is not None: width_crop = int(ratio * width) height_crop = int(ratio * height) else: width_crop= pixels height_crop = pixels return image[height_crop:height-height_crop, width_crop:width-width_crop] def find_target_points(top_left, top_right, bottom_left, bottom_right): max_width = max(int(dist(bottom_right, bottom_left)), int(dist(top_right, top_left))) max_height = max(int(dist(top_right, bottom_right)), int(dist(top_left, bottom_left))) destination_corners = [[0, 0], [max_width, 0], [max_width, max_height], [0, max_height]] return order_points(destination_corners) def order_points(points: np.array) -> tuple: """ inspired by: https://learnopencv.com/automatic-document-scanner-using-opencv/ """ sum = np.sum(points, axis=1) top_left = points[np.argmin(sum)] bottom_right = points[np.argmax(sum)] diff = np.diff(points, axis=1) top_right = points[np.argmin(diff)] bottom_left = points[np.argmax(diff)] return top_left, top_right, bottom_left, bottom_right def get_page(image: np.array) -> np.array: """ inspired by: https://github.com/Kakaranish/OpenCV-paper-detection """ filtered = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) filtered = cv2.medianBlur(filtered, 11) canny = cv2.Canny(filtered, 30, 50, 3) contours, _ = cv2.findContours(canny, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) max_perimeter = 0 max_contour = None for contour in contours: contour = np.array(contour) perimeter = cv2.arcLength(contour, True) contour_approx = cv2.approxPolyDP(contour, 0.02 * perimeter, True) if perimeter > max_perimeter and cv2.isContourConvex(contour_approx) and len(contour_approx) == 4: max_perimeter = perimeter max_contour = contour_approx if max_contour is not None: max_contour = np.squeeze(max_contour) points = order_points(max_contour) target_points = find_target_points(*points) M = cv2.getPerspectiveTransform(np.float32(points), np.float32(target_points)) final = cv2.warpPerspective(image, M, (target_points[3][0], target_points[3][1]), flags=cv2.INTER_LINEAR) final = crop(final, pixels=10) return final return image def get_words(page: np.array, dilation_size: int = 3): gray = cv2.cvtColor(page, cv2.COLOR_BGR2GRAY) _, thresholded = cv2.threshold(gray, 125, 1, cv2.THRESH_BINARY_INV) dilation_size = dilation_size element = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * dilation_size + 1, 2 * dilation_size + 1), (dilation_size, dilation_size)) thresholded = cv2.dilate(thresholded, element, iterations=3) contours, _ = cv2.findContours(thresholded, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) words = [] boxes = [] for contour in contours: x, y, w, h = cv2.boundingRect(contour) ratio = w / h if ratio <= 0.1 or ratio >= 10.0: continue boxes.append([x, y, w, h]) words.append(page[y:y+h, x:x+w]) return words, boxes