Analyse the complete code and Fix this error
Exception has occurred: AttributeError
'BoundingBoxFinder' object has no attribute 'used_bounding_boxes'
  File "/home/ritik1s/Desktop/bbox_issues/temp_GPT/bbx_by_me.py", line 62, in process_single_token_entity
    if entity not in self.used_bounding_boxes:
                     ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ritik1s/Desktop/bbox_issues/temp_GPT/bbx_by_me.py", line 177, in process_data
    box = self.process_single_token_entity(dataframe, entity_value_no_comma)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/ritik1s/Desktop/bbox_issues/temp_GPT/bbx_by_me.py", line 201, in <module>
    bbox_finder.process_data("/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")
AttributeError: 'BoundingBoxFinder' object has no attribute 'used_bounding_boxes' in the given code

import cv2
import pandas as pd
import json
from thefuzz import fuzz
from itertools import product

class BoundingBoxFinder:
    def init(self):
        self.used_bounding_boxes = {}

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

    def calculate_proximity_score(self, 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(self, box_a, box_b, max_line_difference=5, max_distance=100):
        return self.calculate_proximity_score(box_a, box_b) <= max_distance + 2 * max_line_difference

    def merge_boxes(self, 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(self, dataframe, token, threshold=75):
        potential_matches = []
        for _, row in dataframe.iterrows():
            ocr_text = self.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(self, 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: self.calculate_proximity_score(current_box, match["box"]))
            best_sequence.append(next_best_match)
        return best_sequence

    def process_single_token_entity(self, dataframe, entity, threshold=75):
        best_match = None
        best_score = threshold
        entity = self.preprocess_entity(entity)

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

        for _, row in dataframe.iterrows():
            ocr_text = self.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 self.used_bounding_boxes[entity]:
                best_score = score
                best_match = current_box

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

        return best_match

    def box_overlap(self, 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(self, 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 self.box_overlap(seq_box, used_box):
                    return False
        return True

    def get_next_best_sequence(self, 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 self.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(self, dataframe, entity, threshold=75, max_distance=100, max_line_difference=3):
        tokens = entity.split()
        all_potential_matches = [self.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 self.used_bounding_boxes:
            self.used_bounding_boxes[entity] = []

        previous_matches = self.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 self.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(self.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 self.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 = self.merge_boxes(new_boxes_sequence)
            self.used_bounding_boxes[entity].append(new_boxes_sequence)
            return merged_box

        return None

    def draw_bounding_boxes(self, 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.jpg", image)

    def process_data(self, 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 = self.preprocess_entity(entity_value)
                    if " " in entity_value_no_comma:
                        box = self.process_multi_token_entity(dataframe, entity_value_no_comma)
                    else:
                        box = self.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 = self.preprocess_entity(entity_value)
                    if " " in entity_value_no_comma:
                        # Use the reversed_dataframe for multi-token entities
                        box = self.process_multi_token_entity(reversed_dataframe, entity_value_no_comma)
                    else:
                        # Use the reversed_dataframe for single-token entities
                        box = self.process_single_token_entity(reversed_dataframe, entity_value_no_comma)
                    if box:
                        bounding_boxes.append(box)
                        entity_names.append(entity_name)

        self.draw_bounding_boxes(image_path, bounding_boxes, entity_names)

# Example usage
if __name__ == "main":
    bbox_finder = BoundingBoxFinder()
    bbox_finder.process_data("/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")

bbox_finder.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")