import torch import cv2 import numpy as np import os import argparse from pdf2image import convert_from_path import h5py from tqdm import tqdm import shutil from crnn_model import CRNN def decode_ctc_output(preds, int_to_char): texts = [] preds_idx = preds.argmax(2).cpu().numpy() for pred_sequence in preds_idx: decoded_sequence, last_char_idx = [], 0 for char_idx in pred_sequence: if char_idx != last_char_idx: if char_idx != 0: decoded_sequence.append(char_idx) last_char_idx = char_idx texts.append("".join([int_to_char.get(c, '') for c in decoded_sequence])) return texts PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DEVICE = torch.device("mps" if torch.backends.mps.is_available() else "cpu") MODEL_PATH = os.path.join(PROJECT_ROOT, "src/models/crnn_final/crnn_real_data_model.pth") DATA_FILE = os.path.join(PROJECT_ROOT, "src/data/real_line_dataset.h5") IMAGE_HEIGHT = 32 POPPLER_PATH = None def find_text_lines(image_data): gray = cv2.cvtColor(image_data, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU) kernel = np.ones((1, 40), np.uint8) connected = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) contours, _ = cv2.findContours(connected, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) line_images = [] bounding_boxes = [cv2.boundingRect(c) for c in contours] contours = [c for _, c in sorted(zip(bounding_boxes, contours), key=lambda b: b[0][1])] for contour in contours: x, y, w, h = cv2.boundingRect(contour) if w > 15 and h > 8: pad = 2 line_crop = binary[max(0, y - pad):y + h + pad, max(0, x - pad):x + w + pad] line_images.append(line_crop) return line_images def preprocess_line_for_model(line_image): inverted_image = cv2.bitwise_not(line_image) h, w = inverted_image.shape scale_factor = IMAGE_HEIGHT / h new_w = int(w * scale_factor) resized_image = cv2.resize(inverted_image, (new_w, IMAGE_HEIGHT), interpolation=cv2.INTER_AREA) normalized_image = (resized_image / 255.0).astype(np.float32) tensor = torch.from_numpy(normalized_image).unsqueeze(0).unsqueeze(0) return tensor.to(DEVICE) def main(): parser = argparse.ArgumentParser(description="Run the final trained CRNN model on a full PDF page.") parser.add_argument("file_path", type=str, help="Path to PDF, relative to project root.") parser.add_argument("--page", type=int, default=12, help="Page number to process.") args = parser.parse_args() abs_file_path = os.path.join(PROJECT_ROOT, args.file_path) print("Loading trained CRNN model and OFFICIAL character set from HDF5 file...") try: with h5py.File(DATA_FILE, 'r') as hf: char_list = [c.decode('utf-8') for c in hf['char_list'][:]] int_to_char = {i + 1: char for i, char in enumerate(char_list)} print(f"Character map loaded successfully with {len(char_list)} characters.") except FileNotFoundError: print(f"FATAL ERROR: Dataset file not found at '{DATA_FILE}'. Cannot determine character map.") return model = CRNN(num_chars=len(char_list)).to(DEVICE) try: model.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE)) except FileNotFoundError: print(f"FATAL ERROR: Model file not found at '{MODEL_PATH}'. Please train the model first.") return except RuntimeError as e: print( f"FATAL ERROR: Model and saved weights have a size mismatch. This indicates the dataset has changed since training.") print(f"Error details: {e}") return model.eval() print("Model loaded successfully.") try: pil_image = \ convert_from_path(abs_file_path, first_page=args.page, last_page=args.page, poppler_path=POPPLER_PATH)[0] image_data = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR) except Exception as e: print(f"PDF conversion failed: {e}"); return line_crops = find_text_lines(image_data) print(f"Detected {len(line_crops)} lines of text. Recognizing...") full_text = [] with torch.no_grad(): for line_image in tqdm(line_crops, desc="Recognizing lines"): line_tensor = preprocess_line_for_model(line_image) preds = model(line_tensor) decoded_text = decode_ctc_output(preds, int_to_char) full_text.append(decoded_text[0]) print("\n" + "=" * 50) print(f" FINAL RECOGNIZED TEXT - PAGE {args.page}") print("=" * 50) print("\n".join(full_text)) print("=" * 50) if __name__ == "__main__": main()