from operator import itemgetter from pathlib import Path from typing import Callable, Optional, Tuple import cv2 import matplotlib.pyplot as plt import numpy as np from kn_utils.logging import logger # type: ignore from numpy import ndarray as Array from scipy.stats import norm # type: ignore def show_multiple(arrs: Tuple[Array], title: str = ""): plt.clf() plt.cla() plt.close() for a in arrs: plt.plot(a) plt.title(title) plt.show() def show(arr: Array, title: str = ""): plt.clf() plt.cla() plt.close() plt.plot(arr) plt.title(title) plt.show() def save_plot(arr: Array, name: str, title: str = "") -> None: plt.clf() plt.cla() plt.close() plt.plot(arr) plt.title(title) plt.savefig(Path(str(name) + ".png")) def save_lines(img: Array, lines: list[dict[str, int]]) -> None: img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) getter = itemgetter("x1", "y1", "x2", "y2") for line in lines: x1, y1, x2, y2 = getter(line) img = cv2.line(img, (x1, y1), (x2, y2), color=(0, 255, 0), thickness=3) cv2.imwrite("/tmp/lines.png", img) def make_gaussian_kernel(kernel_size: int, sd: float) -> Array: kernel_size += int(not kernel_size % 2) wing_size = int((kernel_size - 1) / 2) xvals = np.arange(-wing_size, wing_size + 1) kernel = norm.pdf(xvals, scale=sd) kernel /= np.sum(kernel) return kernel def make_gaussian_nonpositive_kernel(kernel_size: int, sd: float) -> Array: kernel_size += int(not kernel_size % 2) wing_size = int((kernel_size - 1) / 2) xvals = np.arange(-wing_size, wing_size + 1) kernel = norm.pdf(xvals, scale=sd) kernel /= np.sum(kernel) return kernel def make_quadratic_kernel(kernel_size: int, ratio: float) -> Array: kernel_size += int(not kernel_size % 2) wing_size = int((kernel_size - 1) / 2) kernel = np.array(list(map(lambda x: float(-(x**2)), range(-wing_size, wing_size + 1)))) maxval, minval = np.max(kernel), np.min(kernel) diff = maxval - minval kernel += diff / (1 - ratio) kernel /= np.sum(kernel) return kernel def min_avg_for_interval(filtered: Array, interval: int) -> tuple[float, int]: n = len(filtered) avgs: list[float] = [np.mean(filtered[range(start, n, interval)]) for start in range(interval)] best: float = min(avgs) return best, avgs.index(best) def search_intervals(filtered: Array, min_interval: int, max_interval: int): performance = [ (interval, *min_avg_for_interval(filtered, interval)) for interval in range(min_interval, max_interval + 1) ] best = min(performance, key=lambda x: x[1]) return best[0], best[2] def filter_array( array: Array, sum_filter: Array | None, padding: Optional[Array] = None, pad_value_function: Callable[[Array], float] = lambda x: 255.0, # np.mean, ) -> Array: if sum_filter is None: return array fsize = len(sum_filter) assert fsize % 2 if padding is None: # ensures that output size matches the input size pad = int((fsize - 1) / 2) padding = np.full(pad, pad_value_function(array)) return np.convolve(np.concatenate((padding, array, padding)), sum_filter, "valid") ROW_FILTER1_WIDTH = 30 ROW_FILTER1_SD = 6 ROW_FILTER2_WIDTH = 20 ROW_FILTER2_SD = 4 COL_FILTER1_WIDTH = 90 COL_FILTER1_SD = 15 COL_FILTER2_WIDTH = 70 COL_FILTER2_SD = 12 COL_FILTER3_WIDTH = 200 COL_FILTER3_SD = 20 FILTERS: dict[str, dict[int, np.ndarray | None]] = { "row": { 1: make_gaussian_kernel(ROW_FILTER1_WIDTH, ROW_FILTER1_SD), 2: make_gaussian_kernel(ROW_FILTER2_WIDTH, ROW_FILTER2_SD), 3: None, }, "col": { 1: make_gaussian_kernel(COL_FILTER1_WIDTH, COL_FILTER1_SD), 2: make_gaussian_kernel(COL_FILTER2_WIDTH, COL_FILTER2_SD), 3: make_gaussian_kernel(COL_FILTER3_WIDTH, COL_FILTER3_SD), }, } def filter_fp_col_lines(line_list: list[int], filt_sums: Array) -> list[int]: if not list(line_list): return [] centers = list(np.where((filt_sums[1:-1] < filt_sums[:-2]) * (filt_sums[1:-1] < filt_sums[2:]))[0] + 1) if not centers: return [] if line_list[0] > centers[0]: centers = centers[1:] + [len(filt_sums) - 1] mindiff = np.std(filt_sums) line_list = [ maxidx for maxidx, minidx in zip(line_list, centers) if (filt_sums[maxidx] - filt_sums[minidx]) > mindiff ] return line_list def get_lines_either(table_array: Array, horizontal=True) -> list[int]: key = "row" if horizontal else "col" sums = np.mean(table_array, axis=int(horizontal)) threshold = 0.3 * 255 # np.mean(sums) - (1 + 2 * horizontal) * np.std(sums) predicate = 1000.0 * (sums < threshold) sums = np.maximum( np.maximum(sums[1:-1], predicate[1:-1]), np.maximum(predicate[:-2], predicate[2:]), ) filtered_sums = filter_array(sums, FILTERS[key][1]) filtered_sums = filter_array(filtered_sums, FILTERS[key][2]) filtered_sums = filter_array(filtered_sums, FILTERS[key][3]) lines = list( np.where((filtered_sums[1:-1] > filtered_sums[:-2]) * (filtered_sums[1:-1] > filtered_sums[2:]))[0] + 1 ) if not horizontal: lines = filter_fp_col_lines(lines, filtered_sums) return lines def img_bytes_to_array(img_bytes: bytes) -> Array: img_np = cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_GRAYSCALE) return img_np def infer_lines(img: Array) -> dict[str, dict[str, int] | list[dict[str, int]]]: cv2.imwrite("/tmp/table.png", img) _, img = cv2.threshold(img, 220, 255, cv2.THRESH_BINARY) cv2.imwrite("/tmp/table_bin.png", img) h, w = map(int, img.shape) row_vals = map(int, get_lines_either(img, horizontal=True)) col_vals = map(int, get_lines_either(img, horizontal=False)) lines = [{"x1": 0, "y1": r, "x2": w, "y2": r} for r in row_vals] + [ {"x1": c, "y1": 0, "x2": c, "y2": h} for c in col_vals ] save_lines(img, lines) return {"tableLines": lines, "imageInfo": {"height": h, "width": w}}