CoolFace
Apppublic

mugdha99/Table-Structure-Recognition-Demo

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
postprocess.py888 linesDownload Raw Back to root
1"""2Copyright (C) 2021 Microsoft Corporation3"""4from collections import defaultdict5 6from fitz import Rect7 8 9def apply_threshold(objects, threshold):10    """11    Filter out objects below a certain score.12    """13    return [obj for obj in objects if obj['score'] >= threshold]14 15 16def apply_class_thresholds(bboxes, labels, scores, class_names, class_thresholds):17    """18    Filter out bounding boxes whose confidence is below the confidence threshold for19    its associated class label. 20    """21    # Apply class-specific thresholds22    indices_above_threshold = [idx for idx, (score, label) in enumerate(zip(scores, labels))23                                    if score >= class_thresholds[24                                        class_names[label]25                                    ]26                                ]27    bboxes = [bboxes[idx] for idx in indices_above_threshold]28    scores = [scores[idx] for idx in indices_above_threshold]29    labels = [labels[idx] for idx in indices_above_threshold]30 31    return bboxes, scores, labels32 33 34def iou(bbox1, bbox2):35    """36    Compute the intersection-over-union of two bounding boxes.37    """38    intersection = Rect(bbox1).intersect(bbox2)39    union = Rect(bbox1).include_rect(bbox2)40    41    union_area = union.get_area()  # getArea()42    if union_area > 0:43        return intersection.get_area() / union.get_area()  # .getArea()44    45    return 046 47 48def iob(bbox1, bbox2):49    """50    Compute the intersection area over box area, for bbox1.51    """52    intersection = Rect(bbox1).intersect(bbox2)53    54    bbox1_area = Rect(bbox1).get_area()  # .getArea()55    if bbox1_area > 0:56        return intersection.get_area() / bbox1_area  # getArea()57    58    return 059 60 61def objects_to_cells(table, objects_in_table, tokens_in_table, class_map, class_thresholds):62    """63    Process the bounding boxes produced by the table structure recognition model64    and the token/word/span bounding boxes into table cells.65 66    Also return a confidence score based on how well the text was able to be67    uniquely slotted into the cells detected by the table model.68    """69 70    table_structures = objects_to_table_structures(table, objects_in_table, tokens_in_table, class_map,71                                                   class_thresholds)72 73    # Check for a valid table74    if len(table_structures['columns']) < 1 or len(table_structures['rows']) < 1:75        cells = []#None76        confidence_score = 077    else:78        cells, confidence_score = table_structure_to_cells(table_structures, tokens_in_table, table['bbox'])79 80    return table_structures, cells, confidence_score81 82 83def objects_to_table_structures(table_object, objects_in_table, tokens_in_table, class_names, class_thresholds):84    """85    Process the bounding boxes produced by the table structure recognition model into86    a *consistent* set of table structures (rows, columns, supercells, headers).87    This entails resolving conflicts/overlaps, and ensuring the boxes meet certain alignment88    conditions (for example: rows should all have the same width, etc.).89    """90 91    page_num = table_object['page_num']92 93    table_structures = {}94 95    columns = [obj for obj in objects_in_table if class_names[obj['label']] == 'table column']96    rows = [obj for obj in objects_in_table if class_names[obj['label']] == 'table row']97    headers = [obj for obj in objects_in_table if class_names[obj['label']] == 'table column header']98    supercells = [obj for obj in objects_in_table if class_names[obj['label']] == 'table spanning cell']99    for obj in supercells:100        obj['subheader'] = False101    subheaders = [obj for obj in objects_in_table if class_names[obj['label']] == 'table projected row header']102    for obj in subheaders:103        obj['subheader'] = True104    supercells += subheaders105    for obj in rows:106        obj['header'] = False107        for header_obj in headers:108            if iob(obj['bbox'], header_obj['bbox']) >= 0.5:109                obj['header'] = True110 111    for row in rows:112        row['page'] = page_num113 114    for column in columns:115        column['page'] = page_num116 117    #Refine table structures118    rows = refine_rows(rows, tokens_in_table, class_thresholds['table row'])119    columns = refine_columns(columns, tokens_in_table, class_thresholds['table column'])120 121    # Shrink table bbox to just the total height of the rows122    # and the total width of the columns123    row_rect = Rect()124    for obj in rows:125        row_rect.include_rect(obj['bbox'])126    column_rect = Rect() 127    for obj in columns:128        column_rect.include_rect(obj['bbox'])129    table_object['row_column_bbox'] = [column_rect[0], row_rect[1], column_rect[2], row_rect[3]]130    table_object['bbox'] = table_object['row_column_bbox']131 132    # Process the rows and columns into a complete segmented table133    columns = align_columns(columns, table_object['row_column_bbox'])134    rows = align_rows(rows, table_object['row_column_bbox'])135 136    table_structures['rows'] = rows137    table_structures['columns'] = columns138    table_structures['headers'] = headers139    table_structures['supercells'] = supercells140 141    if len(rows) > 0 and len(columns) > 1:142        table_structures = refine_table_structures(table_object['bbox'], table_structures, tokens_in_table, class_thresholds)143 144    return table_structures145 146 147def refine_rows(rows, page_spans, score_threshold):148    """149    Apply operations to the detected rows, such as150    thresholding, NMS, and alignment.151    """152 153    rows = nms_by_containment(rows, page_spans, overlap_threshold=0.5)154    # remove_objects_without_content(page_spans, rows)  # TODO155    if len(rows) > 1:156        rows = sort_objects_top_to_bottom(rows)157 158    return rows159 160 161def refine_columns(columns, page_spans, score_threshold):162    """163    Apply operations to the detected columns, such as164    thresholding, NMS, and alignment.165    """166 167    columns = nms_by_containment(columns, page_spans, overlap_threshold=0.5)168    # remove_objects_without_content(page_spans, columns)  # TODO169    if len(columns) > 1:170        columns = sort_objects_left_to_right(columns)171 172    return columns173 174 175def nms_by_containment(container_objects, package_objects, overlap_threshold=0.5):176    """177    Non-maxima suppression (NMS) of objects based on shared containment of other objects.178    """179    container_objects = sort_objects_by_score(container_objects)180    num_objects = len(container_objects)181    suppression = [False for obj in container_objects]182 183    packages_by_container, _, _ = slot_into_containers(container_objects, package_objects, overlap_threshold=overlap_threshold,184                                                 unique_assignment=True, forced_assignment=False)185 186    for object2_num in range(1, num_objects):187        object2_packages = set(packages_by_container[object2_num])188        if len(object2_packages) == 0:189            suppression[object2_num] = True190        for object1_num in range(object2_num):191            if not suppression[object1_num]:192                object1_packages = set(packages_by_container[object1_num])193                if len(object2_packages.intersection(object1_packages)) > 0:194                    suppression[object2_num] = True195 196    final_objects = [obj for idx, obj in enumerate(container_objects) if not suppression[idx]]197    return final_objects198 199 200def slot_into_containers(container_objects, package_objects, overlap_threshold=0.5,201                         unique_assignment=True, forced_assignment=False):202    """203    Slot a collection of objects into the container they occupy most (the container which holds the largest fraction of the object).204    """205    best_match_scores = []206 207    container_assignments = [[] for container in container_objects]208    package_assignments = [[] for package in package_objects]209 210    if len(container_objects) == 0 or len(package_objects) == 0:211        return container_assignments, package_assignments, best_match_scores212 213    match_scores = defaultdict(dict)214    for package_num, package in enumerate(package_objects):215        match_scores = []216        package_rect = Rect(package['bbox'])217        package_area = package_rect.get_area()  # getArea()        218        for container_num, container in enumerate(container_objects):219            container_rect = Rect(container['bbox'])220            intersect_area = container_rect.intersect(package['bbox']).get_area()  # getArea()221            overlap_fraction = intersect_area / package_area222            match_scores.append({'container': container, 'container_num': container_num, 'score': overlap_fraction})223 224        sorted_match_scores = sort_objects_by_score(match_scores)225 226        best_match_score = sorted_match_scores[0]227        best_match_scores.append(best_match_score['score'])228        if forced_assignment or best_match_score['score'] >= overlap_threshold:229            container_assignments[best_match_score['container_num']].append(package_num)230            package_assignments[package_num].append(best_match_score['container_num'])231 232        if not unique_assignment: # slot package into all eligible slots233            for match_score in sorted_match_scores[1:]:234                if match_score['score'] >= overlap_threshold:235                    container_assignments[match_score['container_num']].append(package_num)236                    package_assignments[package_num].append(match_score['container_num'])237                else:238                    break239            240    return container_assignments, package_assignments, best_match_scores241 242 243def sort_objects_by_score(objects, reverse=True):244    """245    Put any set of objects in order from high score to low score.246    """247    if reverse:248        sign = -1249    else:250        sign = 1251    return sorted(objects, key=lambda k: sign*k['score'])252 253 254def remove_objects_without_content(page_spans, objects):255    """256    Remove any objects (these can be rows, columns, supercells, etc.) that don't257    have any text associated with them.258    """259    for obj in objects[:]:260        object_text, _ = extract_text_inside_bbox(page_spans, obj['bbox'])261        if len(object_text.strip()) == 0:262            objects.remove(obj)263            264            265def extract_text_inside_bbox(spans, bbox):266    """267    Extract the text inside a bounding box.268    """269    bbox_spans = get_bbox_span_subset(spans, bbox)270    bbox_text = extract_text_from_spans(bbox_spans, remove_integer_superscripts=True)271 272    return bbox_text, bbox_spans273 274 275def get_bbox_span_subset(spans, bbox, threshold=0.5):276    """277    Reduce the set of spans to those that fall within a bounding box.278 279    threshold: the fraction of the span that must overlap with the bbox.280    """281    span_subset = []282    for span in spans:283        if overlaps(span['bbox'], bbox, threshold):284            span_subset.append(span)285    return span_subset286 287 288def overlaps(bbox1, bbox2, threshold=0.5):289    """290    Test if more than "threshold" fraction of bbox1 overlaps with bbox2.291    """292    rect1 = Rect(list(bbox1))293    area1 = rect1.get_area()  # .getArea()294    if area1 == 0:295        return False296    return rect1.intersect(list(bbox2)).get_area()/area1 >= threshold  # getArea()297 298 299def extract_text_from_spans(spans, join_with_space=True, remove_integer_superscripts=True):300    """301    Convert a collection of page tokens/words/spans into a single text string.302    """303 304    if join_with_space:305        join_char = " "306    else:307        join_char = ""308    spans_copy = spans[:]309    310    if remove_integer_superscripts:311        for span in spans:312            flags = span['flags']313            if flags & 2**0: # superscript flag314                if is_int(span['text']):315                    spans_copy.remove(span)316                else:317                    span['superscript'] = True318 319    if len(spans_copy) == 0:320        return ""321    322    spans_copy.sort(key=lambda span: span['span_num'])323    spans_copy.sort(key=lambda span: span['line_num'])324    spans_copy.sort(key=lambda span: span['block_num'])325    326    # Force the span at the end of every line within a block to have exactly one space327    # unless the line ends with a space or ends with a non-space followed by a hyphen328    line_texts = []329    line_span_texts = [spans_copy[0]['text']]330    for span1, span2 in zip(spans_copy[:-1], spans_copy[1:]):331        if not span1['block_num'] == span2['block_num'] or not span1['line_num'] == span2['line_num']:332            line_text = join_char.join(line_span_texts).strip()333            if (len(line_text) > 0334                    and not line_text[-1] == ' '335                    and not (len(line_text) > 1 and line_text[-1] == "-" and not line_text[-2] == ' ')):336                if not join_with_space:337                    line_text += ' '338            line_texts.append(line_text)339            line_span_texts = [span2['text']]340        else:341            line_span_texts.append(span2['text'])342    line_text = join_char.join(line_span_texts)343    line_texts.append(line_text)344            345    return join_char.join(line_texts).strip()346 347 348def sort_objects_left_to_right(objs):349    """350    Put the objects in order from left to right.351    """352    return sorted(objs, key=lambda k: k['bbox'][0] + k['bbox'][2])353 354 355def sort_objects_top_to_bottom(objs):356    """357    Put the objects in order from top to bottom.358    """359    return sorted(objs, key=lambda k: k['bbox'][1] + k['bbox'][3])360 361 362def align_columns(columns, bbox):363    """364    For every column, align the top and bottom boundaries to the final365    table bounding box.366    """367    try:368        for column in columns:369            column['bbox'][1] = bbox[1]370            column['bbox'][3] = bbox[3]371    except Exception as err:372        print("Could not align columns: {}".format(err))373        pass374 375    return columns376 377 378def align_rows(rows, bbox):379    """380    For every row, align the left and right boundaries to the final381    table bounding box.382    """383    try:384        for row in rows:385            row['bbox'][0] = bbox[0]386            row['bbox'][2] = bbox[2]387    except Exception as err:388        print("Could not align rows: {}".format(err))389        pass390 391    return rows392 393 394def refine_table_structures(table_bbox, table_structures, page_spans, class_thresholds):395    """396    Apply operations to the detected table structure objects such as397    thresholding, NMS, and alignment.398    """399    rows = table_structures["rows"]400    columns = table_structures['columns']401 402    #columns = fill_column_gaps(columns, table_bbox)403    #rows = fill_row_gaps(rows, table_bbox)404 405    # Process the headers406    headers = table_structures['headers']407    headers = apply_threshold(headers, class_thresholds["table column header"])408    headers = nms(headers)409    headers = align_headers(headers, rows)410 411    # Process supercells412    supercells = [elem for elem in table_structures['supercells'] if not elem['subheader']]413    subheaders = [elem for elem in table_structures['supercells'] if elem['subheader']]414    supercells = apply_threshold(supercells, class_thresholds["table spanning cell"])415    subheaders = apply_threshold(subheaders, class_thresholds["table projected row header"])416    supercells += subheaders417    # Align before NMS for supercells because alignment brings them into agreement418    # with rows and columns first; if supercells still overlap after this operation,419    # the threshold for NMS can basically be lowered to just above 0420    supercells = align_supercells(supercells, rows, columns)421    supercells = nms_supercells(supercells)422 423    header_supercell_tree(supercells)424 425    table_structures['columns'] = columns426    table_structures['rows'] = rows427    table_structures['supercells'] = supercells428    table_structures['headers'] = headers429 430    return table_structures431 432 433def nms(objects, match_criteria="object2_overlap", match_threshold=0.05, keep_metric="score", keep_higher=True):434    """435    A customizable version of non-maxima suppression (NMS).436    437    Default behavior: If a lower-confidence object overlaps more than 5% of its area438    with a higher-confidence object, remove the lower-confidence object.439 440    objects: set of dicts; each object dict must have a 'bbox' and a 'score' field441    match_criteria: how to measure how much two objects "overlap"442    match_threshold: the cutoff for determining that overlap requires suppression of one object443    keep_metric: which metric to use to determine the object to keep444    keep_higher: if True, keep the object with the higher metric; otherwise, keep the lower445    """446    if len(objects) == 0:447        return []448 449    if keep_metric=="score":450        objects = sort_objects_by_score(objects, reverse=keep_higher)451    elif keep_metric=="area":452        objects = sort_objects_by_area(objects, reverse=keep_higher)453 454    num_objects = len(objects)455    suppression = [False for obj in objects]456 457    for object2_num in range(1, num_objects):458        object2_rect = Rect(objects[object2_num]['bbox'])459        object2_area = object2_rect.get_area()  # .getArea()460        for object1_num in range(object2_num):461            if not suppression[object1_num]:462                object1_rect = Rect(objects[object1_num]['bbox'])463                object1_area = object1_rect.get_area()  # .getArea()464                intersect_area = object1_rect.intersect(object2_rect).get_area()  # .getArea()465                try:466                    if match_criteria=="object1_overlap":467                        metric = intersect_area / object1_area468                    elif match_criteria=="object2_overlap":469                        metric = intersect_area / object2_area470                    elif match_criteria=="iou":471                        metric = intersect_area / (object1_area + object2_area - intersect_area)472                    if metric >= match_threshold:473                        suppression[object2_num] = True474                        break475                except Exception:476                    # Intended to recover from divide-by-zero477                    pass478 479    return [obj for idx, obj in enumerate(objects) if not suppression[idx]]480 481 482def align_headers(headers, rows):483    """484    Adjust the header boundary to be the convex hull of the rows it intersects485    at least 50% of the height of.486 487    For now, we are not supporting tables with multiple headers, so we need to488    eliminate anything besides the top-most header.489    """490    491    aligned_headers = []492 493    for row in rows:494        row['header'] = False495 496    header_row_nums = []497    for header in headers:498        for row_num, row in enumerate(rows):499            row_height = row['bbox'][3] - row['bbox'][1]500            min_row_overlap = max(row['bbox'][1], header['bbox'][1])501            max_row_overlap = min(row['bbox'][3], header['bbox'][3])502            overlap_height = max_row_overlap - min_row_overlap503            if overlap_height / row_height >= 0.5:504                header_row_nums.append(row_num)505 506    if len(header_row_nums) == 0:507        return aligned_headers508 509    header_rect = Rect()510    if header_row_nums[0] > 0:511        header_row_nums = list(range(header_row_nums[0]+1)) + header_row_nums512 513    last_row_num = -1514    for row_num in header_row_nums:515        if row_num == last_row_num + 1:516            row = rows[row_num]517            row['header'] = True518            header_rect = header_rect.include_rect(row['bbox'])519            last_row_num = row_num520        else:521            # Break as soon as a non-header row is encountered.522            # This ignores any subsequent rows in the table labeled as a header.523            # Having more than 1 header is not supported currently.524            break525 526    header = {'bbox': list(header_rect)}527    aligned_headers.append(header)528 529    return aligned_headers530 531 532def align_supercells(supercells, rows, columns):533    """534    For each supercell, align it to the rows it intersects 50% of the height of,535    and the columns it intersects 50% of the width of.536    Eliminate supercells for which there are no rows and columns it intersects 50% with.537    """538    aligned_supercells = []539 540    for supercell in supercells:541        supercell['header'] = False542        row_bbox_rect = None543        col_bbox_rect = None544        intersecting_header_rows = set()545        intersecting_data_rows = set()546        for row_num, row in enumerate(rows):547            row_height = row['bbox'][3] - row['bbox'][1]548            supercell_height = supercell['bbox'][3] - supercell['bbox'][1]549            min_row_overlap = max(row['bbox'][1], supercell['bbox'][1])550            max_row_overlap = min(row['bbox'][3], supercell['bbox'][3])551            overlap_height = max_row_overlap - min_row_overlap552            if 'span' in supercell:553                overlap_fraction = max(overlap_height/row_height,554                                       overlap_height/supercell_height)555            else:556                overlap_fraction = overlap_height / row_height557            if overlap_fraction >= 0.5:558                if 'header' in row and row['header']:559                    intersecting_header_rows.add(row_num)560                else:561                    intersecting_data_rows.add(row_num)562 563        # Supercell cannot span across the header boundary; eliminate whichever564        # group of rows is the smallest565        supercell['header'] = False566        if len(intersecting_data_rows) > 0 and len(intersecting_header_rows) > 0:567            if len(intersecting_data_rows) > len(intersecting_header_rows):568                intersecting_header_rows = set()569            else:570                intersecting_data_rows = set()571        if len(intersecting_header_rows) > 0:572            supercell['header'] = True573        elif 'span' in supercell:574            continue # Require span supercell to be in the header575        intersecting_rows = intersecting_data_rows.union(intersecting_header_rows)576        # Determine vertical span of aligned supercell577        for row_num in intersecting_rows:578            if row_bbox_rect is None:579                row_bbox_rect = Rect(rows[row_num]['bbox'])580            else:581                row_bbox_rect = row_bbox_rect.include_rect(rows[row_num]['bbox'])582        if row_bbox_rect is None:583            continue584 585        intersecting_cols = []586        for col_num, col in enumerate(columns):587            col_width = col['bbox'][2] - col['bbox'][0]588            supercell_width = supercell['bbox'][2] - supercell['bbox'][0]589            min_col_overlap = max(col['bbox'][0], supercell['bbox'][0])590            max_col_overlap = min(col['bbox'][2], supercell['bbox'][2])591            overlap_width = max_col_overlap - min_col_overlap592            if 'span' in supercell:593                overlap_fraction = max(overlap_width/col_width,594                                       overlap_width/supercell_width)595                # Multiply by 2 effectively lowers the threshold to 0.25596                if supercell['header']:597                    overlap_fraction = overlap_fraction * 2598            else:599                overlap_fraction = overlap_width / col_width600            if overlap_fraction >= 0.5:601                intersecting_cols.append(col_num)602                if col_bbox_rect is None:603                    col_bbox_rect = Rect(col['bbox'])604                else:605                    col_bbox_rect = col_bbox_rect.include_rect(col['bbox'])606        if col_bbox_rect is None:607            continue608 609        supercell_bbox = list(row_bbox_rect.intersect(col_bbox_rect))610        supercell['bbox'] = supercell_bbox611 612        # Only a true supercell if it joins across multiple rows or columns613        if (len(intersecting_rows) > 0 and len(intersecting_cols) > 0614                and (len(intersecting_rows) > 1 or len(intersecting_cols) > 1)):615            supercell['row_numbers'] = list(intersecting_rows)616            supercell['column_numbers'] = intersecting_cols617            aligned_supercells.append(supercell)618 619            # A span supercell in the header means there must be supercells above it in the header620            if 'span' in supercell and supercell['header'] and len(supercell['column_numbers']) > 1:621                for row_num in range(0, min(supercell['row_numbers'])):622                    new_supercell = {'row_numbers': [row_num], 'column_numbers': supercell['column_numbers'],623                                     'score': supercell['score'], 'propagated': True}624                    new_supercell_columns = [columns[idx] for idx in supercell['column_numbers']]625                    new_supercell_rows = [rows[idx] for idx in supercell['row_numbers']]626                    bbox = [min([column['bbox'][0] for column in new_supercell_columns]),627                            min([row['bbox'][1] for row in new_supercell_rows]),628                            max([column['bbox'][2] for column in new_supercell_columns]),629                            max([row['bbox'][3] for row in new_supercell_rows])]630                    new_supercell['bbox'] = bbox631                    aligned_supercells.append(new_supercell)632 633    return aligned_supercells634 635 636def nms_supercells(supercells):637    """638    A NMS scheme for supercells that first attempts to shrink supercells to639    resolve overlap.640    If two supercells overlap the same (sub)cell, shrink the lower confidence641    supercell to resolve the overlap. If shrunk supercell is empty, remove it.642    """643 644    supercells = sort_objects_by_score(supercells)645    num_supercells = len(supercells)646    suppression = [False for supercell in supercells]647 648    for supercell2_num in range(1, num_supercells):649        supercell2 = supercells[supercell2_num]650        for supercell1_num in range(supercell2_num):651            supercell1 = supercells[supercell1_num]652            remove_supercell_overlap(supercell1, supercell2)653        if ((len(supercell2['row_numbers']) < 2 and len(supercell2['column_numbers']) < 2)654                or len(supercell2['row_numbers']) == 0 or len(supercell2['column_numbers']) == 0):655            suppression[supercell2_num] = True656 657    return [obj for idx, obj in enumerate(supercells) if not suppression[idx]]658 659 660def header_supercell_tree(supercells):661    """662    Make sure no supercell in the header is below more than one supercell in any row above it.663    The cells in the header form a tree, but a supercell with more than one supercell in a row664    above it means that some cell has more than one parent, which is not allowed. Eliminate665    any supercell that would cause this to be violated.666    """667    header_supercells = [supercell for supercell in supercells if 'header' in supercell and supercell['header']]668    header_supercells = sort_objects_by_score(header_supercells)669    670    for header_supercell in header_supercells[:]:671        ancestors_by_row = defaultdict(int)672        min_row = min(header_supercell['row_numbers'])673        for header_supercell2 in header_supercells:674            max_row2 = max(header_supercell2['row_numbers'])675            if max_row2 < min_row:676                if (set(header_supercell['column_numbers']).issubset(677                    set(header_supercell2['column_numbers']))):678                    for row2 in header_supercell2['row_numbers']:679                        ancestors_by_row[row2] += 1680        for row in range(0, min_row):681            if not ancestors_by_row[row] == 1:682                supercells.remove(header_supercell)683                break684                685                686def table_structure_to_cells(table_structures, table_spans, table_bbox):687    """688    Assuming the row, column, supercell, and header bounding boxes have689    been refined into a set of consistent table structures, process these690    table structures into table cells. This is a universal representation691    format for the table, which can later be exported to Pandas or CSV formats.692    Classify the cells as header/access cells or data cells693    based on if they intersect with the header bounding box.694    """695    columns = table_structures['columns']696    rows = table_structures['rows']697    supercells = table_structures['supercells']698    cells = []699    subcells = []700 701    # Identify complete cells and subcells702    for column_num, column in enumerate(columns):703        for row_num, row in enumerate(rows):704            column_rect = Rect(list(column['bbox']))705            row_rect = Rect(list(row['bbox']))706            cell_rect = row_rect.intersect(column_rect)707            header = 'header' in row and row['header']708            cell = {'bbox': list(cell_rect), 'column_nums': [column_num], 'row_nums': [row_num],709                    'header': header}710 711            cell['subcell'] = False712            for supercell in supercells:713                supercell_rect = Rect(list(supercell['bbox']))714                if (supercell_rect.intersect(cell_rect).get_area()  # .getArea()715                        / cell_rect.get_area()) > 0.5:  # getArea()716                    cell['subcell'] = True717                    break718 719            if cell['subcell']:720                subcells.append(cell)721            else:722                #cell_text = extract_text_inside_bbox(table_spans, cell['bbox'])723                #cell['cell_text'] = cell_text724                cell['subheader'] = False725                cells.append(cell)726 727    for supercell in supercells:728        supercell_rect = Rect(list(supercell['bbox']))729        cell_columns = set()730        cell_rows = set()731        cell_rect = None732        header = True733        for subcell in subcells:734            subcell_rect = Rect(list(subcell['bbox']))735            subcell_rect_area = subcell_rect.get_area()  # .getArea()736            if (subcell_rect.intersect(supercell_rect).get_area()  # .getArea()737                    / subcell_rect_area) > 0.5:738                if cell_rect is None:739                    cell_rect = Rect(list(subcell['bbox']))740                else:741                    cell_rect.include_rect(Rect(list(subcell['bbox'])))742                cell_rows = cell_rows.union(set(subcell['row_nums']))743                cell_columns = cell_columns.union(set(subcell['column_nums']))744                # By convention here, all subcells must be classified745                # as header cells for a supercell to be classified as a header cell;746                # otherwise, this could lead to a non-rectangular header region747                header = header and 'header' in subcell and subcell['header']748        if len(cell_rows) > 0 and len(cell_columns) > 0:749            cell = {'bbox': list(cell_rect), 'column_nums': list(cell_columns), 'row_nums': list(cell_rows),750                    'header': header, 'subheader': supercell['subheader']}751            cells.append(cell)752 753    # Compute a confidence score based on how well the page tokens754    # slot into the cells reported by the model755    _, _, cell_match_scores = slot_into_containers(cells, table_spans)756    try:757        mean_match_score = sum(cell_match_scores) / len(cell_match_scores)758        min_match_score = min(cell_match_scores)759        confidence_score = (mean_match_score + min_match_score)/2760    except:761        confidence_score = 0762 763    # Dilate rows and columns before final extraction764    #dilated_columns = fill_column_gaps(columns, table_bbox)765    dilated_columns = columns766    #dilated_rows = fill_row_gaps(rows, table_bbox)767    dilated_rows = rows768    for cell in cells:769        column_rect = Rect()770        for column_num in cell['column_nums']:771            column_rect.include_rect(list(dilated_columns[column_num]['bbox']))772        row_rect = Rect()773        for row_num in cell['row_nums']:774            row_rect.include_rect(list(dilated_rows[row_num]['bbox']))775        cell_rect = column_rect.intersect(row_rect)776        cell['bbox'] = list(cell_rect)777 778    span_nums_by_cell, _, _ = slot_into_containers(cells, table_spans, overlap_threshold=0.001,779                                               unique_assignment=True, forced_assignment=False)780 781    for cell, cell_span_nums in zip(cells, span_nums_by_cell):782        cell_spans = [table_spans[num] for num in cell_span_nums]783        # TODO: Refine how text is extracted; should be character-based, not span-based;784        # but need to associate 785        # cell['cell_text'] = extract_text_from_spans(cell_spans, remove_integer_superscripts=False)  # TODO786        cell['spans'] = cell_spans787        788    # Adjust the row, column, and cell bounding boxes to reflect the extracted text789    num_rows = len(rows)790    rows = sort_objects_top_to_bottom(rows)791    num_columns = len(columns)792    columns = sort_objects_left_to_right(columns)793    min_y_values_by_row = defaultdict(list)794    max_y_values_by_row = defaultdict(list)795    min_x_values_by_column = defaultdict(list)796    max_x_values_by_column = defaultdict(list)797    for cell in cells:798        min_row = min(cell["row_nums"])799        max_row = max(cell["row_nums"])800        min_column = min(cell["column_nums"])801        max_column = max(cell["column_nums"])802        for span in cell['spans']:803            min_x_values_by_column[min_column].append(span['bbox'][0])804            min_y_values_by_row[min_row].append(span['bbox'][1])805            max_x_values_by_column[max_column].append(span['bbox'][2])806            max_y_values_by_row[max_row].append(span['bbox'][3])807    for row_num, row in enumerate(rows):808        if len(min_x_values_by_column[0]) > 0:809            row['bbox'][0] = min(min_x_values_by_column[0])810        if len(min_y_values_by_row[row_num]) > 0:811            row['bbox'][1] = min(min_y_values_by_row[row_num])812        if len(max_x_values_by_column[num_columns-1]) > 0:813            row['bbox'][2] = max(max_x_values_by_column[num_columns-1])814        if len(max_y_values_by_row[row_num]) > 0:815            row['bbox'][3] = max(max_y_values_by_row[row_num])816    for column_num, column in enumerate(columns):817        if len(min_x_values_by_column[column_num]) > 0:818            column['bbox'][0] = min(min_x_values_by_column[column_num])819        if len(min_y_values_by_row[0]) > 0:820            column['bbox'][1] = min(min_y_values_by_row[0])821        if len(max_x_values_by_column[column_num]) > 0:822            column['bbox'][2] = max(max_x_values_by_column[column_num])823        if len(max_y_values_by_row[num_rows-1]) > 0:824            column['bbox'][3] = max(max_y_values_by_row[num_rows-1])825    for cell in cells:826        row_rect = Rect()827        column_rect = Rect()828        for row_num in cell['row_nums']:829            row_rect.include_rect(list(rows[row_num]['bbox']))830        for column_num in cell['column_nums']:831            column_rect.include_rect(list(columns[column_num]['bbox']))832        cell_rect = row_rect.intersect(column_rect)833        if cell_rect.get_area() > 0:  # getArea()834            cell['bbox'] = list(cell_rect)835            pass836 837    return cells, confidence_score838 839 840def remove_supercell_overlap(supercell1, supercell2):841    """842    This function resolves overlap between supercells (supercells must be843    disjoint) by iteratively shrinking supercells by the fewest grid cells844    necessary to resolve the overlap.845    Example:846    If two supercells overlap at grid cell (R, C), and supercell #1 is less847    confident than supercell #2, we eliminate either row R from supercell #1848    or column C from supercell #1 by comparing the number of columns in row R849    versus the number of rows in column C. If the number of columns in row R850    is less than the number of rows in column C, we eliminate row R from851    supercell #1. This resolves the overlap by removing fewer grid cells from852    supercell #1 than if we eliminated column C from it.853    """854    common_rows = set(supercell1['row_numbers']).intersection(set(supercell2['row_numbers']))855    common_columns = set(supercell1['column_numbers']).intersection(set(supercell2['column_numbers']))856 857    # While the supercells have overlapping grid cells, continue shrinking the less-confident858    # supercell one row or one column at a time859    while len(common_rows) > 0 and len(common_columns) > 0:860        # Try to shrink the supercell as little as possible to remove the overlap;861        # if the supercell has fewer rows than columns, remove an overlapping column,862        # because this removes fewer grid cells from the supercell;863        # otherwise remove an overlapping row864        if len(supercell2['row_numbers']) < len(supercell2['column_numbers']):865            min_column = min(supercell2['column_numbers'])866            max_column = max(supercell2['column_numbers'])867            if max_column in common_columns:868                common_columns.remove(max_column)869                supercell2['column_numbers'].remove(max_column)870            elif min_column in common_columns:871                common_columns.remove(min_column)872                supercell2['column_numbers'].remove(min_column)873            else:874                supercell2['column_numbers'] = []875                common_columns = set()876        else:877            min_row = min(supercell2['row_numbers'])878            max_row = max(supercell2['row_numbers'])879            if max_row in common_rows:880                common_rows.remove(max_row)881                supercell2['row_numbers'].remove(max_row)882            elif min_row in common_rows:883                common_rows.remove(min_row)884                supercell2['row_numbers'].remove(min_row)885            else:886                supercell2['row_numbers'] = []887                common_rows = set()888