234 lines
8.1 KiB
Python
234 lines
8.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import cv2
|
|
import numpy as np
|
|
|
|
|
|
def adaptive_threshold(imagename, process_background=False, blocksize=15, c=-2, process_color_background=False, saturation_threshold=5):
|
|
"""Thresholds an image using OpenCV's adaptiveThreshold.
|
|
|
|
Parameters
|
|
----------
|
|
imagename : string
|
|
Path to image file.
|
|
process_background : bool, optional (default: False)
|
|
Whether or not to process lines that are in background.
|
|
blocksize : int, optional (default: 15)
|
|
Size of a pixel neighborhood that is used to calculate a
|
|
threshold value for the pixel: 3, 5, 7, and so on.
|
|
|
|
For more information, refer `OpenCV's adaptiveThreshold <https://docs.opencv.org/2.4/modules/imgproc/doc/miscellaneous_transformations.html#adaptivethreshold>`_.
|
|
c : int, optional (default: -2)
|
|
Constant subtracted from the mean or weighted mean.
|
|
Normally, it is positive but may be zero or negative as well.
|
|
|
|
For more information, refer `OpenCV's adaptiveThreshold <https://docs.opencv.org/2.4/modules/imgproc/doc/miscellaneous_transformations.html#adaptivethreshold>`_.
|
|
|
|
Returns
|
|
-------
|
|
img : object
|
|
numpy.ndarray representing the original image.
|
|
threshold : object
|
|
numpy.ndarray representing the thresholded image.
|
|
|
|
"""
|
|
img = cv2.imread(imagename)
|
|
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
|
|
|
if process_background:
|
|
if process_color_background:
|
|
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
|
initial = hsv[:, :, 1]
|
|
hsv[initial > saturation_threshold, 0] = 0
|
|
hsv[initial > saturation_threshold, 1] = 255
|
|
hsv[initial > saturation_threshold, 2] = 0
|
|
hsv[initial <= saturation_threshold, 0] = 128
|
|
hsv[initial <= saturation_threshold, 1] = 0
|
|
hsv[initial <= saturation_threshold, 2] = 255
|
|
hsv[initial == 255, 1] = 0
|
|
gray = cv2.cvtColor(hsv, cv2.COLOR_BGR2GRAY)
|
|
threshold = cv2.adaptiveThreshold(
|
|
gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, blocksize, c
|
|
)
|
|
else:
|
|
threshold = cv2.adaptiveThreshold(
|
|
np.invert(gray),
|
|
255,
|
|
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
|
cv2.THRESH_BINARY,
|
|
blocksize,
|
|
c,
|
|
)
|
|
return img, threshold
|
|
|
|
|
|
def find_lines(
|
|
threshold, regions=None, direction="horizontal", line_scale=15, iterations=0
|
|
):
|
|
"""Finds horizontal and vertical lines by applying morphological
|
|
transformations on an image.
|
|
|
|
Parameters
|
|
----------
|
|
threshold : object
|
|
numpy.ndarray representing the thresholded image.
|
|
regions : list, optional (default: None)
|
|
List of page regions that may contain tables of the form x1,y1,x2,y2
|
|
where (x1, y1) -> left-top and (x2, y2) -> right-bottom
|
|
in image coordinate space.
|
|
direction : string, optional (default: 'horizontal')
|
|
Specifies whether to find vertical or horizontal lines.
|
|
line_scale : int, optional (default: 15)
|
|
Factor by which the page dimensions will be divided to get
|
|
smallest length of lines that should be detected.
|
|
|
|
The larger this value, smaller the detected lines. Making it
|
|
too large will lead to text being detected as lines.
|
|
iterations : int, optional (default: 0)
|
|
Number of times for erosion/dilation is applied.
|
|
|
|
For more information, refer `OpenCV's dilate <https://docs.opencv.org/2.4/modules/imgproc/doc/filtering.html#dilate>`_.
|
|
|
|
Returns
|
|
-------
|
|
dmask : object
|
|
numpy.ndarray representing pixels where vertical/horizontal
|
|
lines lie.
|
|
lines : list
|
|
List of tuples representing vertical/horizontal lines with
|
|
coordinates relative to a left-top origin in
|
|
image coordinate space.
|
|
|
|
"""
|
|
lines = []
|
|
|
|
if direction == "vertical":
|
|
size = threshold.shape[0] // line_scale
|
|
el = cv2.getStructuringElement(cv2.MORPH_RECT, (1, size))
|
|
elif direction == "horizontal":
|
|
size = threshold.shape[1] // line_scale
|
|
el = cv2.getStructuringElement(cv2.MORPH_RECT, (size, 1))
|
|
elif direction is None:
|
|
raise ValueError("Specify direction as either 'vertical' or 'horizontal'")
|
|
|
|
if regions is not None:
|
|
region_mask = np.zeros(threshold.shape)
|
|
for region in regions:
|
|
x, y, w, h = region
|
|
region_mask[y : y + h, x : x + w] = 1
|
|
threshold = np.multiply(threshold, region_mask)
|
|
|
|
threshold = cv2.erode(threshold, el)
|
|
threshold = cv2.dilate(threshold, el)
|
|
dmask = cv2.dilate(threshold, el, iterations=iterations)
|
|
|
|
try:
|
|
_, contours, _ = cv2.findContours(
|
|
threshold.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
|
)
|
|
except ValueError:
|
|
# for opencv backward compatibility
|
|
contours, _ = cv2.findContours(
|
|
threshold.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
|
)
|
|
|
|
for c in contours:
|
|
x, y, w, h = cv2.boundingRect(c)
|
|
x1, x2 = x, x + w
|
|
y1, y2 = y, y + h
|
|
if direction == "vertical":
|
|
lines.append(((x1 + x2) // 2, y2, (x1 + x2) // 2, y1))
|
|
elif direction == "horizontal":
|
|
lines.append((x1, (y1 + y2) // 2, x2, (y1 + y2) // 2))
|
|
|
|
return dmask, lines
|
|
|
|
|
|
def find_contours(vertical, horizontal):
|
|
"""Finds table boundaries using OpenCV's findContours.
|
|
|
|
Parameters
|
|
----------
|
|
vertical : object
|
|
numpy.ndarray representing pixels where vertical lines lie.
|
|
horizontal : object
|
|
numpy.ndarray representing pixels where horizontal lines lie.
|
|
|
|
Returns
|
|
-------
|
|
cont : list
|
|
List of tuples representing table boundaries. Each tuple is of
|
|
the form (x, y, w, h) where (x, y) -> left-top, w -> width and
|
|
h -> height in image coordinate space.
|
|
|
|
"""
|
|
mask = vertical + horizontal
|
|
|
|
try:
|
|
__, contours, __ = cv2.findContours(
|
|
mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
|
)
|
|
except ValueError:
|
|
# for opencv backward compatibility
|
|
contours, __ = cv2.findContours(
|
|
mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
|
|
)
|
|
# sort in reverse based on contour area and use first 10 contours
|
|
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:10]
|
|
|
|
cont = []
|
|
for c in contours:
|
|
c_poly = cv2.approxPolyDP(c, 3, True)
|
|
x, y, w, h = cv2.boundingRect(c_poly)
|
|
cont.append((x, y, w, h))
|
|
return cont
|
|
|
|
|
|
def find_joints(contours, vertical, horizontal):
|
|
"""Finds joints/intersections present inside each table boundary.
|
|
|
|
Parameters
|
|
----------
|
|
contours : list
|
|
List of tuples representing table boundaries. Each tuple is of
|
|
the form (x, y, w, h) where (x, y) -> left-top, w -> width and
|
|
h -> height in image coordinate space.
|
|
vertical : object
|
|
numpy.ndarray representing pixels where vertical lines lie.
|
|
horizontal : object
|
|
numpy.ndarray representing pixels where horizontal lines lie.
|
|
|
|
Returns
|
|
-------
|
|
tables : dict
|
|
Dict with table boundaries as keys and list of intersections
|
|
in that boundary as their value.
|
|
Keys are of the form (x1, y1, x2, y2) where (x1, y1) -> lb
|
|
and (x2, y2) -> rt in image coordinate space.
|
|
|
|
"""
|
|
joints = np.multiply(vertical, horizontal)
|
|
tables = {}
|
|
for c in contours:
|
|
x, y, w, h = c
|
|
roi = joints[y : y + h, x : x + w]
|
|
try:
|
|
__, jc, __ = cv2.findContours(
|
|
roi.astype(np.uint8), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE
|
|
)
|
|
except ValueError:
|
|
# for opencv backward compatibility
|
|
jc, __ = cv2.findContours(
|
|
roi.astype(np.uint8), cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE
|
|
)
|
|
if len(jc) <= 4: # remove contours with less than 4 joints
|
|
continue
|
|
joint_coords = []
|
|
for j in jc:
|
|
jx, jy, jw, jh = cv2.boundingRect(j)
|
|
c1, c2 = x + (2 * jx + jw) // 2, y + (2 * jy + jh) // 2
|
|
joint_coords.append((c1, c2))
|
|
tables[(x, y + h, x + w, y)] = joint_coords
|
|
|
|
return tables
|