CoolFace
Apppublic

rishabhv471/table-extraction-from-image

sourceHugging Faceupdated 2y agoView on Hugging Face
5likes
app.py570 linesDownload Raw Back to root
1import asyncio2import string3from collections import Counter4from itertools import count, tee5 6import cv27import matplotlib.pyplot as plt8import numpy as np9import pandas as pd10import streamlit as st11import torch12from PIL import Image13from transformers import (DetrImageProcessor,14                          TableTransformerForObjectDetection)15from vietocr.tool.config import Cfg16from vietocr.tool.predictor import Predictor17 18st.set_option('deprecation.showPyplotGlobalUse', False)19st.set_page_config(layout='wide')20st.title("Table Detection and Table Structure Recognition By VWITS")21st.write(22    "Implemented by MSFT team: https://github.com/microsoft/table-transformer")23 24# config = Cfg.load_config_from_name('vgg_transformer')25config = Cfg.load_config_from_name('vgg_seq2seq')26config['cnn']['pretrained'] = False27config['device'] = 'cpu'28config['predictor']['beamsearch'] = False29detector = Predictor(config)30 31table_detection_model = TableTransformerForObjectDetection.from_pretrained(32    "microsoft/table-transformer-detection")33 34table_recognition_model = TableTransformerForObjectDetection.from_pretrained(35    "microsoft/table-transformer-structure-recognition")36 37 38def PIL_to_cv(pil_img):39    return cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)40 41 42def cv_to_PIL(cv_img):43    return Image.fromarray(cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB))44 45 46async def pytess(cell_pil_img, threshold: float = 0.5):47    text, prob = detector.predict(cell_pil_img, return_prob=True)48    if prob < threshold:49        return ""50    return text.strip()51 52 53def sharpen_image(pil_img):54 55    img = PIL_to_cv(pil_img)56    sharpen_kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])57 58    sharpen = cv2.filter2D(img, -1, sharpen_kernel)59    pil_img = cv_to_PIL(sharpen)60    return pil_img61 62 63def uniquify(seq, suffs=count(1)):64    """Make all the items unique by adding a suffix (1, 2, etc).65    Credit: https://stackoverflow.com/questions/30650474/python-rename-duplicates-in-list-with-progressive-numbers-without-sorting-list66    `seq` is mutable sequence of strings.67    `suffs` is an optional alternative suffix iterable.68    """69    not_unique = [k for k, v in Counter(seq).items() if v > 1]70 71    suff_gens = dict(zip(not_unique, tee(suffs, len(not_unique))))72    for idx, s in enumerate(seq):73        try:74            suffix = str(next(suff_gens[s]))75        except KeyError:76            continue77        else:78            seq[idx] += suffix79 80    return seq81 82 83def binarizeBlur_image(pil_img):84    image = PIL_to_cv(pil_img)85    thresh = cv2.threshold(image, 150, 255, cv2.THRESH_BINARY_INV)[1]86 87    result = cv2.GaussianBlur(thresh, (5, 5), 0)88    result = 255 - result89    return cv_to_PIL(result)90 91 92def td_postprocess(pil_img):93    '''94    Removes gray background from tables95    '''96    img = PIL_to_cv(pil_img)97 98    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)99    mask = cv2.inRange(hsv, (0, 0, 100),100                       (255, 5, 255))  # (0, 0, 100), (255, 5, 255)101    nzmask = cv2.inRange(hsv, (0, 0, 5),102                         (255, 255, 255))  # (0, 0, 5), (255, 255, 255))103    nzmask = cv2.erode(nzmask, np.ones((3, 3)))  # (3,3)104    mask = mask & nzmask105 106    new_img = img.copy()107    new_img[np.where(mask)] = 255108 109    return cv_to_PIL(new_img)110 111 112# def super_res(pil_img):113#     # requires opencv-contrib-python installed without the opencv-python114#     sr = dnn_superres.DnnSuperResImpl_create()115#     image = PIL_to_cv(pil_img)116#     model_path = "./LapSRN_x8.pb"117#     model_name = model_path.split('/')[1].split('_')[0].lower()118#     model_scale = int(model_path.split('/')[1].split('_')[1].split('.')[0][1])119 120#     sr.readModel(model_path)121#     sr.setModel(model_name, model_scale)122#     final_img = sr.upsample(image)123#     final_img = cv_to_PIL(final_img)124 125#     return final_img126 127 128def table_detector(image, THRESHOLD_PROBA):129    '''130    Table detection using DEtect-object TRansformer pre-trained on 1 million tables131 132    '''133 134    feature_extractor = DetrImageProcessor(do_resize=True,135                                           size=800,136                                           max_size=800)137    encoding = feature_extractor(image, return_tensors="pt")138 139    with torch.no_grad():140        outputs = table_detection_model(**encoding)141 142    probas = outputs.logits.softmax(-1)[0, :, :-1]143    keep = probas.max(-1).values > THRESHOLD_PROBA144 145    target_sizes = torch.tensor(image.size[::-1]).unsqueeze(0)146    postprocessed_outputs = feature_extractor.post_process(147        outputs, target_sizes)148    bboxes_scaled = postprocessed_outputs[0]['boxes'][keep]149 150    return (probas[keep], bboxes_scaled)151 152 153def table_struct_recog(image, THRESHOLD_PROBA):154    '''155    Table structure recognition using DEtect-object TRansformer pre-trained on 1 million tables156    '''157 158    feature_extractor = DetrImageProcessor(do_resize=True,159                                           size=1000,160                                           max_size=1000)161    encoding = feature_extractor(image, return_tensors="pt")162 163    with torch.no_grad():164        outputs = table_recognition_model(**encoding)165 166    probas = outputs.logits.softmax(-1)[0, :, :-1]167    keep = probas.max(-1).values > THRESHOLD_PROBA168 169    target_sizes = torch.tensor(image.size[::-1]).unsqueeze(0)170    postprocessed_outputs = feature_extractor.post_process(171        outputs, target_sizes)172    bboxes_scaled = postprocessed_outputs[0]['boxes'][keep]173 174    return (probas[keep], bboxes_scaled)175 176 177class TableExtractionPipeline():178 179    colors = ["red", "blue", "green", "yellow", "orange", "violet"]180 181    # colors = ["red", "blue", "green", "red", "red", "red"]182 183    def add_padding(self,184                    pil_img,185                    top,186                    right,187                    bottom,188                    left,189                    color=(255, 255, 255)):190        '''191        Image padding as part of TSR pre-processing to prevent missing table edges192        '''193        width, height = pil_img.size194        new_width = width + right + left195        new_height = height + top + bottom196        result = Image.new(pil_img.mode, (new_width, new_height), color)197        result.paste(pil_img, (left, top))198        return result199 200    def plot_results_detection(self, c1, model, pil_img, prob, boxes,201                               delta_xmin, delta_ymin, delta_xmax, delta_ymax):202        '''203        crop_tables and plot_results_detection must have same co-ord shifts because 1 only plots the other one updates co-ordinates 204        '''205        # st.write('img_obj')206        # st.write(pil_img)207        plt.imshow(pil_img)208        ax = plt.gca()209 210        for p, (xmin, ymin, xmax, ymax) in zip(prob, boxes.tolist()):211            cl = p.argmax()212            xmin, ymin, xmax, ymax = xmin - delta_xmin, ymin - delta_ymin, xmax + delta_xmax, ymax + delta_ymax213            ax.add_patch(214                plt.Rectangle((xmin, ymin),215                              xmax - xmin,216                              ymax - ymin,217                              fill=False,218                              color='red',219                              linewidth=3))220            text = f'{model.config.id2label[cl.item()]}: {p[cl]:0.2f}'221            ax.text(xmin - 20,222                    ymin - 50,223                    text,224                    fontsize=10,225                    bbox=dict(facecolor='yellow', alpha=0.5))226        plt.axis('off')227        c1.pyplot()228 229    def crop_tables(self, pil_img, prob, boxes, delta_xmin, delta_ymin,230                    delta_xmax, delta_ymax):231        '''232        crop_tables and plot_results_detection must have same co-ord shifts because 1 only plots the other one updates co-ordinates 233        '''234        cropped_img_list = []235 236        for p, (xmin, ymin, xmax, ymax) in zip(prob, boxes.tolist()):237 238            xmin, ymin, xmax, ymax = xmin - delta_xmin, ymin - delta_ymin, xmax + delta_xmax, ymax + delta_ymax239            cropped_img = pil_img.crop((xmin, ymin, xmax, ymax))240            cropped_img_list.append(cropped_img)241 242        return cropped_img_list243 244    def generate_structure(self, c2, model, pil_img, prob, boxes,245                           expand_rowcol_bbox_top, expand_rowcol_bbox_bottom):246        '''247        Co-ordinates are adjusted here by 3 'pixels'248        To plot table pillow image and the TSR bounding boxes on the table249        '''250        # st.write('img_obj')251        # st.write(pil_img)252        plt.figure(figsize=(32, 20))253        plt.imshow(pil_img)254        ax = plt.gca()255        rows = {}256        cols = {}257        idx = 0258 259        for p, (xmin, ymin, xmax, ymax) in zip(prob, boxes.tolist()):260 261            xmin, ymin, xmax, ymax = xmin, ymin, xmax, ymax262            cl = p.argmax()263            class_text = model.config.id2label[cl.item()]264            text = f'{class_text}: {p[cl]:0.2f}'265            # or (class_text == 'table column')266            if (class_text267                    == 'table row') or (class_text268                                        == 'table projected row header') or (269                                            class_text == 'table column'):270                ax.add_patch(271                    plt.Rectangle((xmin, ymin),272                                  xmax - xmin,273                                  ymax - ymin,274                                  fill=False,275                                  color=self.colors[cl.item()],276                                  linewidth=2))277                ax.text(xmin - 10,278                        ymin - 10,279                        text,280                        fontsize=5,281                        bbox=dict(facecolor='yellow', alpha=0.5))282 283            if class_text == 'table row':284                rows['table row.' +285                     str(idx)] = (xmin, ymin - expand_rowcol_bbox_top, xmax,286                                  ymax + expand_rowcol_bbox_bottom)287            if class_text == 'table column':288                cols['table column.' +289                     str(idx)] = (xmin, ymin - expand_rowcol_bbox_top, xmax,290                                  ymax + expand_rowcol_bbox_bottom)291 292            idx += 1293 294        plt.axis('on')295        c2.pyplot()296        return rows, cols297 298    def sort_table_featuresv2(self, rows: dict, cols: dict):299        # Sometimes the header and first row overlap, and we need the header bbox not to have first row's bbox inside the headers bbox300        rows_ = {301            table_feature: (xmin, ymin, xmax, ymax)302            for table_feature, (303                xmin, ymin, xmax,304                ymax) in sorted(rows.items(), key=lambda tup: tup[1][1])305        }306        cols_ = {307            table_feature: (xmin, ymin, xmax, ymax)308            for table_feature, (309                xmin, ymin, xmax,310                ymax) in sorted(cols.items(), key=lambda tup: tup[1][0])311        }312 313        return rows_, cols_314 315    def individual_table_featuresv2(self, pil_img, rows: dict, cols: dict):316 317        for k, v in rows.items():318            xmin, ymin, xmax, ymax = v319            cropped_img = pil_img.crop((xmin, ymin, xmax, ymax))320            rows[k] = xmin, ymin, xmax, ymax, cropped_img321 322        for k, v in cols.items():323            xmin, ymin, xmax, ymax = v324            cropped_img = pil_img.crop((xmin, ymin, xmax, ymax))325            cols[k] = xmin, ymin, xmax, ymax, cropped_img326 327        return rows, cols328 329    def object_to_cellsv2(self, master_row: dict, cols: dict,330                          expand_rowcol_bbox_top, expand_rowcol_bbox_bottom,331                          padd_left):332        '''Removes redundant bbox for rows&columns and divides each row into cells from columns333        Args:334 335        Returns:336 337        338        '''339        cells_img = {}340        header_idx = 0341        row_idx = 0342        previous_xmax_col = 0343        new_cols = {}344        new_master_row = {}345        previous_ymin_row = 0346        new_cols = cols347        new_master_row = master_row348        ## Below 2 for loops remove redundant bounding boxes ###349        # for k_col, v_col in cols.items():350        #     xmin_col, _, xmax_col, _, col_img = v_col351        #     if (np.isclose(previous_xmax_col, xmax_col, atol=5)) or (xmin_col >= xmax_col):352        #         print('Found a column with double bbox')353        #         continue354        #     previous_xmax_col = xmax_col355        #     new_cols[k_col] = v_col356 357        # for k_row, v_row in master_row.items():358        #     _, ymin_row, _, ymax_row, row_img = v_row359        #     if (np.isclose(previous_ymin_row, ymin_row, atol=5)) or (ymin_row >= ymax_row):360        #         print('Found a row with double bbox')361        #         continue362        #     previous_ymin_row = ymin_row363        #     new_master_row[k_row] = v_row364        ######################################################365        for k_row, v_row in new_master_row.items():366 367            _, _, _, _, row_img = v_row368            xmax, ymax = row_img.size369            xa, ya, xb, yb = 0, 0, 0, ymax370            row_img_list = []371            # plt.imshow(row_img)372            # st.pyplot()373            for idx, kv in enumerate(new_cols.items()):374                k_col, v_col = kv375                xmin_col, _, xmax_col, _, col_img = v_col376                xmin_col, xmax_col = xmin_col - padd_left - 10, xmax_col - padd_left377                xa = xmin_col378                xb = xmax_col379                if idx == 0:380                    xa = 0381                if idx == len(new_cols) - 1:382                    xb = xmax383                xa, ya, xb, yb = xa, ya, xb, yb384 385                row_img_cropped = row_img.crop((xa, ya, xb, yb))386                row_img_list.append(row_img_cropped)387 388            cells_img[k_row + '.' + str(row_idx)] = row_img_list389            row_idx += 1390 391        return cells_img, len(new_cols), len(new_master_row) - 1392 393    def clean_dataframe(self, df):394        '''395        Remove irrelevant symbols that appear with tesseractOCR396        '''397        # df.columns = [col.replace('|', '') for col in df.columns]398 399        for col in df.columns:400 401            df[col] = df[col].str.replace("'", '', regex=True)402            df[col] = df[col].str.replace('"', '', regex=True)403            df[col] = df[col].str.replace(']', '', regex=True)404            df[col] = df[col].str.replace('[', '', regex=True)405            df[col] = df[col].str.replace('{', '', regex=True)406            df[col] = df[col].str.replace('}', '', regex=True)407        return df408 409    @st.cache410    def convert_df(self, df):411        return df.to_csv().encode('utf-8')412 413    def create_dataframe(self, c3, cell_ocr_res: list, max_cols: int,414                         max_rows: int):415        '''Create dataframe using list of cell values of the table, also checks for valid header of dataframe416        Args:417            cell_ocr_res: list of strings, each element representing a cell in a table418            max_cols, max_rows: number of columns and rows419        Returns:420            dataframe : final dataframe after all pre-processing 421        '''422 423        headers = cell_ocr_res[:max_cols]424        new_headers = uniquify(headers,425                               (f' {x!s}' for x in string.ascii_lowercase))426        counter = 0427 428        cells_list = cell_ocr_res[max_cols:]429        df = pd.DataFrame("", index=range(0, max_rows), columns=new_headers)430 431        cell_idx = 0432        for nrows in range(max_rows):433            for ncols in range(max_cols):434                df.iat[nrows, ncols] = str(cells_list[cell_idx])435                cell_idx += 1436 437        ## To check if there are duplicate headers if result of uniquify+col == col438        ## This check removes headers when all headers are empty or if median of header word count is less than 6439        for x, col in zip(string.ascii_lowercase, new_headers):440            if f' {x!s}' == col:441                counter += 1442        header_char_count = [len(col) for col in new_headers]443 444        # if (counter == len(new_headers)) or (statistics.median(header_char_count) < 6):445        #     st.write('woooot')446        #     df.columns = uniquify(df.iloc[0], (f' {x!s}' for x in string.ascii_lowercase))447        #     df = df.iloc[1:,:]448 449        df = self.clean_dataframe(df)450 451        c3.dataframe(df)452        csv = self.convert_df(df)453        c3.download_button("Download table",454                           csv,455                           "file.csv",456                           "text/csv",457                           key='download-csv')458 459        return df460 461    async def start_process(self, image_path: str, TD_THRESHOLD, TSR_THRESHOLD,462                            OCR_THRESHOLD, padd_top, padd_left, padd_bottom,463                            padd_right, delta_xmin, delta_ymin, delta_xmax,464                            delta_ymax, expand_rowcol_bbox_top,465                            expand_rowcol_bbox_bottom):466        '''467        Initiates process of generating pandas dataframes from raw pdf-page images468 469        '''470        image = Image.open(image_path).convert("RGB")471        probas, bboxes_scaled = table_detector(image,472                                               THRESHOLD_PROBA=TD_THRESHOLD)473 474        if bboxes_scaled.nelement() == 0:475            st.write('No table found in the pdf-page image')476            return ''477 478        # try:479        # st.write('Document: '+image_path.split('/')[-1])480        c1, c2, c3 = st.columns((1, 1, 1))481 482        self.plot_results_detection(c1, table_detection_model, image, probas,483                                    bboxes_scaled, delta_xmin, delta_ymin,484                                    delta_xmax, delta_ymax)485        cropped_img_list = self.crop_tables(image, probas, bboxes_scaled,486                                            delta_xmin, delta_ymin, delta_xmax,487                                            delta_ymax)488 489        for unpadded_table in cropped_img_list:490 491            table = self.add_padding(unpadded_table, padd_top, padd_right,492                                     padd_bottom, padd_left)493            # table = super_res(table)494            # table = binarizeBlur_image(table)495            # table = sharpen_image(table) # Test sharpen image next496            # table = td_postprocess(table)497 498            probas, bboxes_scaled = table_struct_recog(499                table, THRESHOLD_PROBA=TSR_THRESHOLD)500            rows, cols = self.generate_structure(c2, table_recognition_model,501                                                 table, probas, bboxes_scaled,502                                                 expand_rowcol_bbox_top,503                                                 expand_rowcol_bbox_bottom)504            # st.write(len(rows), len(cols))505            rows, cols = self.sort_table_featuresv2(rows, cols)506            master_row, cols = self.individual_table_featuresv2(507                table, rows, cols)508 509            cells_img, max_cols, max_rows = self.object_to_cellsv2(510                master_row, cols, expand_rowcol_bbox_top,511                expand_rowcol_bbox_bottom, padd_left)512 513            sequential_cell_img_list = []514            for k, img_list in cells_img.items():515                for img in img_list:516                    # img = super_res(img)517                    # img = sharpen_image(img) # Test sharpen image next518                    # img = binarizeBlur_image(img)519                    # img = self.add_padding(img, 10,10,10,10)520                    # plt.imshow(img)521                    # c3.pyplot()522                    sequential_cell_img_list.append(523                        pytess(cell_pil_img=img, threshold=OCR_THRESHOLD))524 525            cell_ocr_res = await asyncio.gather(*sequential_cell_img_list)526 527            self.create_dataframe(c3, cell_ocr_res, max_cols, max_rows)528            st.write(529                'Errors in OCR is due to either quality of the image or performance of the OCR'530            )531        # except:532        #     st.write('Either incorrectly identified table or no table, to debug remove try/except')533        # break534        # break535 536 537if __name__ == "__main__":538 539    img_name = st.file_uploader("Upload an image with table(s)")540    st1, st2, st3 = st.columns((1, 1, 1))541    TD_th = st1.slider('Table detection threshold', 0.0, 1.0, 0.8)542    TSR_th = st2.slider('Table structure recognition threshold', 0.0, 1.0, 0.8)543    OCR_th = st3.slider("Text Probs Threshold", 0.0, 1.0, 0.5)544 545    st1, st2, st3, st4 = st.columns((1, 1, 1, 1))546 547    padd_top = st1.slider('Padding top', 0, 200, 40)548    padd_left = st2.slider('Padding left', 0, 200, 40)549    padd_right = st3.slider('Padding right', 0, 200, 40)550    padd_bottom = st4.slider('Padding bottom', 0, 200, 40)551 552    te = TableExtractionPipeline()553    # for img in image_list:554    if img_name is not None:555        asyncio.run(556            te.start_process(img_name,557                             TD_THRESHOLD=TD_th,558                             TSR_THRESHOLD=TSR_th,559                             OCR_THRESHOLD=OCR_th,560                             padd_top=padd_top,561                             padd_left=padd_left,562                             padd_bottom=padd_bottom,563                             padd_right=padd_right,564                             delta_xmin=0,565                             delta_ymin=0,566                             delta_xmax=0,567                             delta_ymax=0,568                             expand_rowcol_bbox_top=0,569                             expand_rowcol_bbox_bottom=0))570