I'm looking to see if there are any better / faster ways of identifying table structures on a page without gridlines.
The text is extracted from the file and the coordinates of each block of text are stored in a dataframe. For the sake of this snippet, this has already been generated and has yielded the dataframe below. This is ordered top to bottom, left to right in reading order.
The bounding box (x,y,x1,y1) is represented below as (left,top,left1,top1). Middle is the mid-point between left and left1 and left_diff is the gap between current rows starting x position (left) and previous rows finishing x1 position (left1.shift()). Width is the left to left1 size.
top top1 left middle left1 left_diff width
0 78.0 126 54 62.0 70.0 NaN 16.0
1 78.0 123 71 94.0 118.0 1.0 47.0
2 78.0 126 125 136.0 147.0 7.0 22.0
3 78.0 123 147 215.0 283.0 0.0 136.0
4 167.0 199 54 130.0 206.0 -229.0 152.0
5 167.0 187 664 701.0 739.0 458.0 75.0
6 186.0 204 664 722.0 780.0 -75.0 116.0
7 202.0 220 664 751.0 838.0 -116.0 174.0
8 212.0 234 54 347.0 641.0 -784.0 587.0
9 212.0 237 664 737.0 811.0 23.0 147.0
10 232.0 254 54 347.0 641.0 -757.0 587.0
11 232.0 253 664 701.0 738.0 23.0 74.0
12 232.0 253 826 839.0 853.0 88.0 27.0
13 253.0 275 54 137.0 220.0 -799.0 166.0
14 268.0 286 664 717.0 770.0 444.0 106.0
15 285.0 310 54 347.0 641.0 -716.0 587.0
16 285.0 303 664 759.0 855.0 23.0 191.0
17 301.0 330 54 347.0 641.0 -801.0 587.0
18 301.0 319 664 684.0 704.0 23.0 40.0
19 301.0 319 826 839.0 853.0 122.0 27.0
20 328.0 350 54 347.0 641.0 -799.0 587.0
....... etc......
My method here is to group by an x coordinate (taking into account the text could be justified left, centred or to the right), search for ant other points which are close (within a tolerance of 5 pixels in this snippet). This gives me my columns.
Then, for each column identified, look to see where the rows are by looking for the points at which the gap between rows is over a certain a threshold. Here, we take the indexes of the points where the text should break and generate index pairs. By taking the max and min points, we can generate a bounding box around this cell.
Then, I look to see if there are other boxes located on the same x coordinate and store this in a table list.
Finally, form pairs from the tables and look at the index distance between each of the items in the table list. As the indexes should run sequentially, this should equal 1. If it doesn't, this indicates that the table doesn't continue.
import itertools
def pairwise(splits):
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
a, b = itertools.tee(splits, 2)
next(b, None)
return list(zip(a, b))
def space_sort(df):
groups = df.groupby('page')
pages = {i:j[['top','top1','left','middle','left1']] for i,j in groups}
cols = ['left','middle','left1']
boxes = {}
for page in pages:
rows = {}
c_df = pages[page]
min_x = min(c_df.left)
gaps = c_df.loc[df.left_diff>5]
# value count on left, middle and left1 values so we can deal with text justification.
counts = {'left':[], 'middle':[], 'left1':[]}
[counts[col].append(gaps[col].unique()) for col in cols if (gaps[col].value_counts()>2).any()]
if len(counts['left'])>0:
counts['left'][0] = np.insert(counts['left'][0], 0, int(min_x))
# search c_df for other points close to these x values.
for col in cols:
if len(counts[col])>0:
for x in counts[col][0]:
row_spaces = {}
matches = c_df.loc[np.isclose(c_df[col],x, atol=5)]
left_groups = df_coord.loc[matches.index.values].reset_index()
# find points where line diff > 5 indicating new row. Get indexes.
vert_gaps = left_groups.loc[(left_groups.top - left_groups.top1.shift())>5]
vert_indexes = vert_gaps.index.values
vert_indexes = np.insert(vert_indexes,0,0)
vert_indexes = np.append(vert_indexes,len(left_groups))
# form groups between rows.
pairs = pairwise(vert_indexes)
for start,end in pairs:
box = left_groups.loc[start:end-1]
coords = (page, min(box.top),min(box.left),max(box.top1),max(box.left1))
boxes[coords]=(list(left_groups.loc[start:end-1,('index')]))
# Find close boxes by seeing which align on the same x value (either top, centre or bottom)
table = []
for a, b in itertools.combinations(boxes, 2):
a_pg, a_top, a_left, a_top1, a_left1 = a
b_pg, b_top, b_left, b_top1, b_left1 = b
a_centre = (a_top+a_top1)//2
b_centre = (b_top+b_top1)//2
if (np.isclose(a_top, b_top, atol=5)) | (np.isclose(a_centre, b_centre, atol=5)) | (np.isclose(a_top1, b_top1, atol=5)):
table.append([boxes[a],boxes[b]])
# Table list contains two lists of indexes of rows which are close together.
# As ordered, the indexes should be sequential.
# If difference between one pair and next is 1, sequential. If not, reset rows
t = (pairwise(table))
row = 0
for i in t:
if (i[1][0][-1] - i[0][1][-1]) == 1:
for r in i:
row+=1
num = 1
for col in r:
print('indexes', col, 'row',row, 'col',num)
num+=1
else:
row = 0