Skip to main content
4 of 4
Clarified question - dataframe columns explained in description
lawson
  • 141
  • 3

Determine table structure in pdf using whitespace between coordinates

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
lawson
  • 141
  • 3