Create one class named "BoundingBoxFInder" and cover all the function in that class And return the complete code

import cv2
import pandas as pd
import json
from thefuzz import fuzz
from itertools import product
used_bounding_boxes = {}


def preprocess_entity(entity):
    try:
        token = entity.replace(",", "").strip()
        return token
    except:
        pass

def calculate_proximity_score(box_a, box_b):
    vertical_overlap = max(0, min(box_a["bottom"], box_b["bottom"]) - max(box_a["top"], box_b["top"]))
    vertical_distance = 0 if vertical_overlap > 0 else min(abs(box_a["top"] - box_b["bottom"]), abs(box_a["bottom"] - box_b["top"]))
    horizontal_overlap = max(0, min(box_a["right"], box_b["right"]) - max(box_a["left"], box_b["left"]))
    horizontal_distance = 0 if horizontal_overlap > 0 else abs(box_a["right"] - box_b["left"])
    return horizontal_distance + 2 * vertical_distance

def is_nearby(box_a, box_b, max_line_difference=5, max_distance=100):
    return calculate_proximity_score(box_a, box_b) <= max_distance + 2 * max_line_difference

def merge_boxes(boxes):
    min_left = min(box["left"] for box in boxes)
    max_right = max(box["right"] for box in boxes)
    min_top = min(box["top"] for box in boxes)
    max_bottom = max(box["bottom"] for box in boxes)
    return {"left": min_left, "right": max_right, "top": min_top, "bottom": max_bottom}

def find_potential_matches(dataframe, token, threshold=75):
    potential_matches = []
    for _, row in dataframe.iterrows():
        ocr_text = preprocess_entity(row["text"])
        score = fuzz.ratio(token, ocr_text)
        if score > threshold:
            potential_matches.append({
                "box": {"left": row["left"], "right": row["right"], "top": row["top"], "bottom": row["bottom"]},
                "score": score
            })
    return potential_matches

def find_best_sequence_heuristic(matches_list):
    if not matches_list or len(matches_list[0]) == 0:
        return []
    best_sequence = [min(matches_list[0], key=lambda match: match["score"])]
    for next_matches in matches_list[1:]:
        current_box = best_sequence[-1]["box"]
        next_best_match = min(next_matches, key=lambda match: calculate_proximity_score(current_box, match["box"]))
        best_sequence.append(next_best_match)
    return best_sequence


def process_single_token_entity(dataframe, entity, threshold=75):
    global used_bounding_boxes
    best_match = None
    best_score = threshold
    entity = preprocess_entity(entity)

    if entity not in used_bounding_boxes:
        used_bounding_boxes[entity] = []

    for _, row in dataframe.iterrows():
        ocr_text = preprocess_entity(row['text'])
        score = fuzz.ratio(entity, ocr_text)
        current_box = {'left': row['left'], 'right': row['right'], 'top': row['top'], 'bottom': row['bottom']}

        if score > best_score and current_box not in used_bounding_boxes[entity]:
            best_score = score
            best_match = current_box

    if best_match:
        used_bounding_boxes[entity].append(best_match)

    return best_match


def box_overlap(box1, box2):
    """Check if there’s any overlap in any coordinate between two boxes."""
    return box1["left"] == box2["left"] or box1["right"] == box2["right"] 

def all_boxes_unique(sequence_boxes, used_boxes):
    """Ensure no part of the boxes in sequence_boxes overlaps with any box in used_boxes."""
    for seq_box in sequence_boxes:
        for used_box in used_boxes:
            if box_overlap(seq_box, used_box):
                return False
    return True

def get_next_best_sequence(all_potential_matches, previous_matches, entity):
    """
    Try to find the next best sequence of matches that hasn’t used any part of the bounding boxes.
    """
    # Flatten the list of used boxes for easier comparison.
    used_boxes = [box for sequence in previous_matches.get(entity, []) for box in sequence]

    for sequence in product(*all_potential_matches):
        sequence_boxes = [match["box"] for match in sequence]
        
        if all_boxes_unique(sequence_boxes, used_boxes):
            return sequence  # Found a sequence where no box part has been used before

    return None  # No unique sequence found

def process_multi_token_entity(dataframe, entity, threshold=75, max_distance=100, max_line_difference=3):
    global used_bounding_boxes
    
    tokens = entity.split()
    all_potential_matches = [find_potential_matches(dataframe, token, threshold) for token in tokens]
    
    if not all(matches for matches in all_potential_matches):
        return None
    
    if entity not in used_bounding_boxes:
        used_bounding_boxes[entity] = []
    
    previous_matches = used_bounding_boxes.get(entity, [])
    
    all_sequences = list(product(*all_potential_matches))  # Create all possible sequences
    valid_sequences = []  # List to hold sequences that meet the is_nearby condition
    
    for sequence in all_sequences:
        sequence_boxes = [match["box"] for match in sequence]
        sequence_is_valid = True
        for i in range(len(sequence_boxes) - 1):
            if not is_nearby(sequence_boxes[i], sequence_boxes[i + 1], max_line_difference, max_distance):
                sequence_is_valid = False
                break
        if sequence_is_valid:
            valid_sequences.append(sequence)
    
    # Sort valid sequences by their cumulative proximity score, to prioritize those with boxes closer together
    valid_sequences = sorted(valid_sequences, key=lambda seq: sum(calculate_proximity_score(seq[i]["box"], seq[i+1]["box"]) for i in range(len(seq) - 1)))
    
    next_best_sequence = None
    for sequence in valid_sequences:
        sequence_boxes = [match["box"] for match in sequence]
        if all_boxes_unique(sequence_boxes, [box for sublist in previous_matches for box in sublist]):
            next_best_sequence = sequence
            break
    
    if next_best_sequence:
        new_boxes_sequence = [match["box"] for match in next_best_sequence]
        merged_box = merge_boxes(new_boxes_sequence)
        used_bounding_boxes[entity].append(new_boxes_sequence)
        return merged_box
    
    return None


def draw_bounding_boxes(image_path, bounding_boxes, entity_names):
    image = cv2.imread(image_path)
    font = cv2.FONT_HERSHEY_SIMPLEX
    for box, name in zip(bounding_boxes, entity_names):
        if box:
            cv2.rectangle(image, (box["left"], box["top"]), (box["right"], box["bottom"]), (0, 255, 0), 2)
            cv2.putText(image, name, (box["left"], max(box["top"] - 10, 0)), font, 0.5, (0, 0, 255), 2)
    cv2.imwrite("annotated_image_using_dp1119.jpg", image)
    
def main(json_path, csv_path, image_path):
    with open(json_path, "r") as f:
        data = json.load(f)
    dataframe = pd.read_csv(csv_path)
    bounding_boxes = []
    entity_names = []

    # Existing processing for non-special sections
    special_sections = ["amounts_and_tax","Payment Details"]  # Define special handling cases here

    for section in ["invoice_details", "Payment Details", "amounts_and_tax"]:
        entities = data.get(section, {})
        
        # Check if the current section needs special handling
        if section not in special_sections:
            for entity_name, entity_value in entities.items():
                entity_value_no_comma = preprocess_entity(entity_value)
                if " " in entity_value_no_comma:
                    box = process_multi_token_entity(dataframe, entity_value_no_comma)
                else:
                    box = process_single_token_entity(dataframe, entity_value_no_comma)
                if box:
                    bounding_boxes.append(box)
                    entity_names.append(entity_name)
        else:
            # Special handling for "amounts_and_tax" section
            reversed_dataframe = dataframe.iloc[::-1].reset_index(drop=True) # Reverse the dataframe
            for entity_name, entity_value in entities.items():
                entity_value_no_comma = preprocess_entity(entity_value)
                if " " in entity_value_no_comma:
                    # Use the reversed_dataframe for multi-token entities
                    box = process_multi_token_entity(reversed_dataframe, entity_value_no_comma)
                else:
                    # Use the reversed_dataframe for single-token entities
                    box = process_single_token_entity(reversed_dataframe, entity_value_no_comma)
                if box:
                    bounding_boxes.append(box)
                    entity_names.append(entity_name)

    draw_bounding_boxes(image_path, bounding_boxes, entity_names)



main("/home/ritik1s/Desktop/bbox_issues/temp_GPT/row_skip.json", "/home/ritik1s/Desktop/bbox_issues/temp_GPT/check.csv", "/home/ritik1s/Desktop/bbox_issues/temp_GPT/check.jpeg")