diff --git a/.gitignore b/.gitignore index 394b9648..8fc7b143 100644 --- a/.gitignore +++ b/.gitignore @@ -167,4 +167,8 @@ cython_debug/ .idea/ *.sh -_testbed/cleaner/ + +.DS_Store +_testbed/media/** +_testbed/cleaner/** +_testbed/model/** diff --git a/_testbed/README.md b/_testbed/README.md new file mode 100644 index 00000000..702df030 --- /dev/null +++ b/_testbed/README.md @@ -0,0 +1,55 @@ +# PanelCleaner Testbed + +## Overview +The **PanelCleaner** testbed serves as a dedicated area for experimenting and testing new ideas with *PanelCleaner* using Jupyter Notebooks. Currently, it focuses on **OCR** technologies, primarily using **Tesseract** and **IDefics** models. The testbed also begins the development of an evaluation framework to support future experiments. This project utilizes the `nbdev` literate programming environment. + +## Installation +To get started with the notebooks, you'll need Jupyter Lab/Notebook or any Python IDE that supports Jupyter notebooks like *VSCode* or *Google Colab*. +The setup mostly shares the same requirements as PanelCleaner and its CLI, with a few additional dependencies. +Here’s how to set up your environment: +1. Activate a virtual environment. +2. Navigate to the `_testbed` directory: + ```bash + cd _testbed + ``` +3. Install the required dependencies: + ```bash + pip install -r requirements.txt + ``` +Note: Each notebook may require the installation of additional dependencies. + +## Google Colab Support +The notebooks are ready to use on Google Colab, allowing you to run them directly on the platform without any extra setup or local GPU rigs. +Instructions to use Google Colab are included in the notebooks (TBD). + +## Install Test Images +The test images are not included in the repository but can be downloaded from the following link: +- [Test images](https://drive.google.com/drive/folders/101_1_20240229) + +After downloading, place the test images in the [media](media) directory. If you want to use your own, each image should have a corresponding text file with the same name, but with the extension `.txt`, which contains the ground truth data, one line per box (as calculated by PanelCleaner). Optionally, you can also include a `.json` file with the same name, specifying the language of the page: +```json +{ + "lang": "Spanish" +} +``` +If no language file is found, English will be used by default. In the near future, language detection will be automated. + +## Introduction to nbdev +[nbdev](https://nbdev.fast.ai/) is a **literate programming** environment that allows you to develop a Python library in Jupyter Notebooks, integrating exploratory programming, code, tests, and documentation into a single cohesive workflow. Inspired by **Donald Knuth**'s concept of literate programming, this approach not only makes the development process more intuitive but also eases the maintenance and understanding of the codebase. + +## Notebooks (WIP) + +#### [helpers.ipynb](helpers.ipynb) +This notebook includes utility functions and helpers that support the experiments in other notebooks, streamlining repetitive tasks and data manipulation. + +#### [ocr_metric.ipynb](ocr_metric.ipynb) +This notebook focuses on defining and implementing metrics to evaluate the performance and accuracy of OCR engines, crucial for assessing the effectiveness of OCR technologies in various scenarios. It currently develops a basic metric for evaluating OCR models. In the near future, additional metrics will be added, such as precision and recall using Levenshtein distance (edit distance). More importantly, it will introduce a metric tailored to the unique characteristics of Comics/Manga OCR, a topic currently unexplored in technical literature. + +#### [experiments.ipynb](experiments.ipynb) +This notebook details the development of the evaluation framework used in other notebooks, with Tesseract as a case study to illustrate the evaluation process. It's a work in progress, and will be updated continuously. If you're only interested in visualizing the results of the experiments, go directly to `Test_tesseract.ipynb` or `Test_idefics.ipynb`, which are much shorter and more to the point. + +#### [test_tesseract.ipynb](test_tesseract.ipynb) +This notebook is dedicated to testing the Tesseract OCR engine, offering insights into its capabilities and limitations through hands-on experiments. + +#### [test_idefics.ipynb](test_idefics.ipynb) +Similar to `test_tesseract.ipynb`, this notebook focuses on the IDefics LVM model, evaluating its performance and accuracy under different conditions. Here you can compare the results of the Tesseract OCR engine with the IDefics LVM model to see how the two compare in terms of accuracy and performance. diff --git a/_testbed/experiments.ipynb b/_testbed/experiments.ipynb new file mode 100644 index 00000000..6c8d3c81 --- /dev/null +++ b/_testbed/experiments.ipynb @@ -0,0 +1,6810 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp experiments" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "# %reload_ext autoreload\n", + "# %autoreload 0\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# install (Colab)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# try: \n", + "# import fastcore as FC\n", + "# except ImportError: \n", + "# !pip install -q fastcore\n", + "# try:\n", + "# import rich\n", + "# except ImportError:\n", + "# !pip install -q rich\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note: we're using the `testbed` branch of PanelCleaner.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install -q git+https://github.com/civvic/PanelCleaner.git@testbed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Testing `Tesseract` OCR for Comics\n", + "> Accuracy Enhancements for OCR in `PanelCleaner`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Prologue" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "from __future__ import annotations\n", + "\n", + "import dataclasses\n", + "import difflib\n", + "import functools\n", + "import json\n", + "import shutil\n", + "from collections import defaultdict\n", + "from enum import Enum\n", + "from pathlib import Path\n", + "from typing import Any\n", + "from typing import Callable\n", + "from typing import cast\n", + "from typing import Mapping\n", + "from typing import Self\n", + "from typing import TypeAlias\n", + "\n", + "import fastcore.all as FC\n", + "import ipywidgets as W\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "import pcleaner.config as cfg\n", + "import pcleaner.ctd_interface as ctm\n", + "import pcleaner.image_ops as ops\n", + "import pcleaner.ocr.ocr as ocr\n", + "import pcleaner.structures as st\n", + "import torch\n", + "from IPython.display import clear_output\n", + "from IPython.display import display\n", + "from IPython.display import HTML\n", + "from ipywidgets.widgets.interaction import show_inline_matplotlib_plots\n", + "from loguru import logger\n", + "from pcleaner.ocr.ocr_tesseract import TesseractOcr\n", + "from PIL import Image\n", + "from PIL import ImageFilter\n", + "from rich.console import Console\n", + "from tqdm.notebook import tqdm\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "from helpers import *\n", + "from ocr_metric import *\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "import copy\n", + "\n", + "import fastcore.xtras # patch Path with some utils\n", + "import pcleaner.cli_utils as cli\n", + "import pcleaner.preprocessor as pp\n", + "import rich\n", + "from fastcore.test import * # type: ignore\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Helpers" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# pretty print by default\n", + "# %load_ext rich" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "#| exporti\n", + "console = Console(width=104, tab_size=4, force_jupyter=True)\n", + "cprint = console.print\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tesseract installation" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['tesseract 5.3.4',\n", + " ' leptonica-1.84.1',\n", + " ' libgif 5.2.1 : libjpeg 8d (libjpeg-turbo 3.0.0) : libpng 1.6.43 : libtiff 4.6.0 : zlib 1.2.11 : libwebp 1.4.0 : libopenjp2 2.5.2',\n", + " ' Found NEON',\n", + " ' Found libarchive 3.7.2 zlib/1.2.11 liblzma/5.4.6 bz2lib/1.0.8 liblz4/1.9.4 libzstd/1.5.6',\n", + " ' Found libcurl/8.4.0 SecureTransport (LibreSSL/3.3.6) zlib/1.2.11 nghttp2/1.51.0']" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "out = !tesseract --version\n", + "out\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Install jpn_vert tesserac lang\n", + "> It has much better results than the default `jpn` language model.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "Download from [tessdata_best](https://github.com/tesseract-ocr/tessdata_best), or from [here](https://groups.google.com/g/tesseract-ocr/c/FwjSZzoVgeg/m/u-zyFYQiBgAJ) trained for vertical Japanese text as found in manga.\n", + "\n", + "Note: I've not play much with this one, `managa-ocr` is surely a much better fit, but it can be educational to compare.\n", + "\n", + "I have copied models in my GDrive, and installed (in my Ubuntu, similar in Mac):\n", + "```bash\n", + "cd model\n", + "ln -s jpn_vert_tessdata_best.traineddata /usr/share/tesseract-ocr/5/tessdata/jpn_vert.traineddata\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Path('/opt/homebrew/share/tessdata'),\n", + " ['afr, amh, ara, asm, aze, aze_cyrl, bel, ben, bod, bos, bre, bul, cat, ceb, ces',\n", + " 'chi_sim, chi_sim_vert, chi_tra, chi_tra_vert, chr, cos, cym, dan, deu, div, dzo, ell, eng, enm, epo',\n", + " 'equ, est, eus, fao, fas, fil, fin, fra, frk, frm, fry, gla, gle, glg, grc',\n", + " 'guj, hat, heb, hin, hrv, hun, hye, iku, ind, isl, ita, ita_old, jav, jpn, jpn_vert',\n", + " 'kan, kat, kat_old, kaz, khm, kir, kmr, kor, kor_vert, lao, lat, lav, lit, ltz, mal',\n", + " 'mar, mkd, mlt, mon, mri, msa, mya, nep, nld, nor, oci, ori, osd, pan, pol',\n", + " 'por, pus, que, ron, rus, san, script/Arabic, script/Armenian, script/Bengali, script/Canadian_Aboriginal, script/Cherokee, script/Cyrillic, script/Devanagari, script/Ethiopic, script/Fraktur',\n", + " 'script/Georgian, script/Greek, script/Gujarati, script/Gurmukhi, script/HanS, script/HanS_vert, script/HanT, script/HanT_vert, script/Hangul, script/Hangul_vert, script/Hebrew, script/Japanese, script/Japanese_vert, script/Kannada, script/Khmer',\n", + " 'script/Lao, script/Latin, script/Malayalam, script/Myanmar, script/Oriya, script/Sinhala, script/Syriac, script/Tamil, script/Telugu, script/Thaana, script/Thai, script/Tibetan, script/Vietnamese, sin, slk',\n", + " 'slv, snd, snum, spa, spa_old, sqi, srp, srp_latn, sun, swa, swe, syr, tam, tat, tel',\n", + " 'tgk, tha, tir, ton, tur, uig, ukr, urd, uzb, uzb_cyrl, vie, yid, yor'])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "out = !tesseract --list-langs\n", + "tessdata = Path(out[0].split('\"')[1])\n", + "tessdata, [', '.join(sub) for sub in [out[i:i + 15] for i in range(1, len(out), 15)]]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
[\n",
+       "    Path('/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/eng_tessdata_best_410.traineddata'),\n",
+       "    Path('/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/jpn_vert_tessdata_best.traineddata'),\n",
+       "    Path('/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/jpn_tessdata_best.traineddata')\n",
+       "]\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1m[\u001b[0m\n", + " \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/eng_tessdata_best_410.traineddata'\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/jpn_vert_tessdata_best.traineddata'\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/jpn_tessdata_best.traineddata'\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m]\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "langs = tessdata.ls()\n", + "cprint([p.resolve() for p in langs if 'eng' in p.name] + [p.resolve() for p in langs if 'jpn' in p.name])\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## OCR results clean-up" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "def remove_multiple_whitespaces(text):\n", + " return ' '.join(text.split())\n", + "\n", + " \n", + "def postprocess_ocr(text):\n", + " \"Basic postprocessing for English Tesseract OCR results.\"\n", + " return ' '.join(remove_multiple_whitespaces(text).splitlines()).capitalize()\n", + "\n", + "def accuracy_ocr_naive(text, ground_truth):\n", + " return sum(1 for a, b in zip(text, ground_truth) if a == b) / len(text)\n", + "\n", + "\n", + "def accuracy_ocr_difflib(text, ground_truth):\n", + " \"\"\"\n", + " Calculates the OCR accuracy based on the similarity between the OCR text and the ground truth text,\n", + " using difflib's SequenceMatcher to account for differences in a manner similar to git diffs.\n", + "\n", + " :param text: The OCR-generated text.\n", + " :param ground_truth: The ground truth text.\n", + " :return: A float representing the similarity ratio between the OCR text and the ground truth, \n", + " where 1.0 is identical.\n", + " \"\"\"\n", + " # Initialize the SequenceMatcher with the OCR text and the ground truth\n", + " matcher = difflib.SequenceMatcher(None, text, ground_truth)\n", + " \n", + " # Get the similarity ratio\n", + " similarity_ratio = matcher.ratio()\n", + " \n", + " return similarity_ratio" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Ground truth" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "def ground_truth_path(page_data: st.PageData):\n", + " path = Path(page_data.original_path)\n", + " return path.with_stem(path.stem + '_gt').with_suffix('.txt')\n", + "\n", + "\n", + "def read_ground_truth(page_data: st.PageData):\n", + " gts_path = ground_truth_path(page_data)\n", + " if gts_path.exists():\n", + " gts = gts_path.read_text(encoding=\"utf-8\").splitlines()\n", + " else:\n", + " gts = [\"\" for _ in range(len(page_data.boxes))]\n", + " return gts\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Cropping" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "def dilate_by_fractional_pixel(image, dilation_fraction, filter_base_size=3):\n", + " \"\"\"\n", + " Dilates an image by a specified fractional pixel amount. The function calculates \n", + " the necessary scaling factor and filter size based on the desired dilation fraction.\n", + "\n", + " :param image: A PIL Image object (1-bit mode).\n", + " :param dilation_fraction: The desired fractional pixel amount for dilation (e.g., 0.2).\n", + " :param filter_base_size: The base size of the dilation filter to apply on the scaled image.\n", + " This size is adjusted based on the scaling factor to achieve the\n", + " desired dilation effect.\n", + " :return: A PIL Image object after dilation, converted back to grayscale.\n", + " \"\"\"\n", + " # Calculate the scale factor based on the desired dilation fraction\n", + " scale_factor = int(1 / dilation_fraction)\n", + " \n", + " # Adjust the filter size based on the scale factor\n", + " # This ensures the dilation effect is proportional to the desired fraction\n", + " filter_size = max(1, filter_base_size * scale_factor // 5)\n", + "\n", + " # Convert the image to grayscale for more nuanced intermediate values\n", + " image_gray = image.convert(\"L\")\n", + "\n", + " # Resize the image to a larger size using bicubic interpolation\n", + " larger_size = (int(image.width * scale_factor), int(image.height * scale_factor))\n", + " image_resized = image_gray.resize(larger_size, Image.BICUBIC)\n", + "\n", + " # Apply the dilation filter to the resized image\n", + " dilated_image = image_resized.filter(ImageFilter.MaxFilter(filter_size))\n", + "\n", + " # Resize the image back to its original size using bicubic interpolation\n", + " image_dilated_fractional_pixel = dilated_image.resize(image.size, Image.BICUBIC)\n", + "\n", + " return image_dilated_fractional_pixel\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "def extract_text(image, text_mask, box):\n", + " cropped_image = crop_box(box, image)\n", + " cropped_mask = crop_box(box, text_mask)\n", + " extracted = ops.extract_text(cropped_image, cropped_mask)\n", + " return cropped_image, cropped_mask, extracted\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Lang\n", + "> language name to a language code \n", + "> every one has language codes: tesseract, comic-text-detector, earthlings...\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "#| exporti\n", + "_lang2pcleaner = {'English': st.DetectedLang.ENG, 'Japanese': st.DetectedLang.JA, 'Spanish': st.DetectedLang.ENG,\n", + " 'French':st.DetectedLang.ENG}\n", + "# _lang2tesseract = {'English': 'eng', 'Japanese': 'jpn'}\n", + "_lang2tesseract = {'English': 'eng', 'Japanese': 'jpn_vert', 'Spanish': 'spa', 'French': 'fra'}\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def lang2pcleaner(lang: str):\n", + " return _lang2pcleaner[lang]\n", + "\n", + "def lang2tesseract(lang: str):\n", + " return _lang2tesseract[lang]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----\n", + "# Tesseract experiments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# PanelCleaner Configuration\n", + "> Adapt `PanelCleaner` `Config` current config to this notebook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "config = cfg.load_config()\n", + "config.cache_dir = Path(\".\")\n", + "\n", + "cache_dir = config.get_cleaner_cache_dir()\n", + "\n", + "profile = config.current_profile\n", + "preprocessor_conf = profile.preprocessor\n", + "# Modify the profile to OCR all boxes.\n", + "# Make sure OCR is enabled.\n", + "preprocessor_conf.ocr_enabled = True\n", + "# Make sure the max size is infinite, so no boxes are skipped in the OCR process.\n", + "preprocessor_conf.ocr_max_size = 10**10\n", + "# Make sure the sus box min size is infinite, so all boxes with \"unknown\" language are skipped.\n", + "preprocessor_conf.suspicious_box_min_size = 10**10\n", + "# Set the OCR blacklist pattern to match everything, so all text gets reported in the analytics.\n", + "preprocessor_conf.ocr_blacklist_pattern = \".*\"\n", + "\n", + "gpu = torch.cuda.is_available() or torch.backends.mps.is_available()\n", + "model_path = config.get_model_path(gpu)\n", + "device = (\"mps\" if torch.backends.mps.is_available() else \"cuda\") if model_path.suffix == \".pt\" else \"cpu\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Test images\n", + "> `IMAGE_PATHS` is a list of image file paths that are used as input for testing the OCR methods." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['00: Action_Comics_1960-01-00_(262).JPG',\n", + " '01: Adolf_Cap_01_008.jpg',\n", + " '02: Barnaby_v1-028.png',\n", + " '03: Barnaby_v1-029.png',\n", + " '04: Buck_Danny_-_12_-_Avions_Sans_Pilotes_-_013.jpg',\n", + " '05: Cannon-292.jpg',\n", + " '06: Contrato_con_Dios_028.jpg',\n", + " '07: Erase_una_vez_en_Francia_02_88.jpg',\n", + " '08: FOX_CHILLINTALES_T17_012.jpg',\n", + " '09: Furari_-_Jiro_Taniguchi_selma_056.jpg',\n", + " '10: Galactus_12.jpg',\n", + " '11: INOUE_KYOUMEN_002.png',\n", + " '12: MCCALL_ROBINHOOD_T31_010.jpg',\n", + " '13: MCCAY_LITTLENEMO_090.jpg',\n", + " '14: Mary_Perkins_On_Stage_v2006_1_-_P00068.jpg',\n", + " '15: PIKE_BOYLOVEGIRLS_T41_012.jpg',\n", + " '16: Sal_Buscema_Spaceknights_&_Superheroes_Ocular_Edition_1_1.png',\n", + " '17: Sal_Buscema_Spaceknights_&_Superheroes_Ocular_Edition_1_1_K.png',\n", + " '18: Sal_Buscema_Spaceknights_&_Superheroes_Ocular_Edition_1_2.png',\n", + " '19: Spirou_Et_Fantasio_Integrale_06_1958_1959_0025_0024.jpg',\n", + " '20: Strange_Tales_172005.jpg',\n", + " '21: Strange_Tales_172021.jpg',\n", + " '22: Tarzan_014-21.JPG',\n", + " '23: Tintin_21_Les_Bijoux_de_la_Castafiore_page_39.jpg',\n", + " '24: Transformers_-_Unicron_000-004.jpg',\n", + " '25: Transformers_-_Unicron_000-016.jpg',\n", + " '26: WARE_ACME_024.jpg',\n", + " '27: Yoko_Tsuno_T01_1972-10.jpg',\n", + " '28: Your_Name_Another_Side_Earthbound_T02_084.jpg',\n", + " '29: manga_0033.jpg',\n", + " '30: ronson-031.jpg',\n", + " '31: 哀心迷図のバベル 第01巻 - 22002_00_059.jpg']" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "media_path = Path(\"media/\")\n", + "\n", + "IMAGE_PATHS = sorted(\n", + " [_ for _ in media_path.glob(\"*\") if _.is_file() and _.suffix.lower() in [\".jpg\", \".png\", \".jpeg\"]])\n", + "\n", + "[f\"{i:02}: {_.name}\" for i,_ in enumerate(IMAGE_PATHS)]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Results helper\n", + "> Dataclass helper to store and display results\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "@dataclasses.dataclass\n", + "class ResultOCR:\n", + " block_idx: int\n", + " image: Image.Image | None\n", + " ocr: str\n", + " page_data: st.PageData\n", + " gts: list[str]\n", + " description: str = dataclasses.field(default='', kw_only=True)\n", + "\n", + " def __post_init__(self): \n", + " if self.image is None:\n", + " cache_path = self.cache_path()\n", + " if cache_path.exists():\n", + " self.image = Image.open(cache_path)\n", + "\n", + " @property\n", + " def acc(self):\n", + " self._acc = accuracy_ocr_difflib(self.ocr, self.gts[self.block_idx])\n", + " return self._acc\n", + " @property\n", + " def suffix(self): return f\"{self.block_idx}_{self.description}\"\n", + "\n", + " def diff_tagged(self):\n", + " _, html2 = get_text_diffs_html(self.gts[self.block_idx], self.ocr, False)\n", + " return f\"{html2}\"\n", + " \n", + " def cache_path(self, suffix: str | None = None):\n", + " suffix = self.suffix + (('_'+suffix) if suffix else '')\n", + " parent = Path(self.page_data.image_path).parent\n", + " img_name = Path(self.page_data.original_path).stem\n", + " box_image_path = parent / f\"{img_name}_{suffix}.png\"\n", + " return box_image_path\n", + " \n", + " def cache_image(self, image: Image.Image | None = None, suffix: str | None = None):\n", + " image = image or (self.image if not suffix else None)\n", + " box_image_path = self.cache_path(suffix)\n", + " if image and not box_image_path.exists():\n", + " image.save(box_image_path)\n", + " return box_image_path\n", + "\n", + "\n", + " def as_html(self):\n", + " acc_html = f\"
{self.acc:.2f}\"\n", + " box_image_path = self.cache_image()\n", + " html1 = get_columns_html([[box_image_path], [self.ocr + acc_html]])\n", + " html_str1, html_str2 = get_text_diffs_html(self.gts[self.block_idx], self.ocr)\n", + " html2 = f\"
{html_str1}
{html_str2}
\"\n", + " return html1 + '\\n
\\n' + html2\n", + "\n", + " def __repr__(self): \n", + " return f\"{type(self).__name__}#block {self.block_idx:02}: {self.acc:.2f}||{self.ocr}\"\n", + " \n", + " def display(self): display(HTML(self.as_html()))\n", + " \n", + " def _ipython_display_(self): self.display()\n", + "\n", + " def to_dict(self):\n", + " d = dataclasses.asdict(self)\n", + " d['image'] = d['page_data'] = d['gts'] = None\n", + " return d\n", + "\n", + " # @classmethod\n", + " # def from_dict(cls, d: dict, page_data: st.PageData, gts: list[str]):\n", + " # return cls(**(d | {'page_data':page_data, 'gts':gts}))\n", + "\n", + "\n", + "@dataclasses.dataclass\n", + "class ResultOCRExtracted(ResultOCR):\n", + "\n", + " def __repr__(self): return super().__repr__()\n", + " def as_html(self):\n", + " html_str1, html_str2 = get_text_diffs_html(self.gts[self.block_idx], self.ocr)\n", + " diff_html = f\"
{html_str1}
{html_str2}
\"\n", + " cropped_image_path = self.cache_image(None, \"cropped\")\n", + " cropped_mask_path = self.cache_image(None, \"mask\")\n", + " result_path = self.cache_image()\n", + " return '\\n
\\n'.join([\n", + " get_image_grid_html([cropped_image_path, cropped_mask_path, result_path], 1, 3), \n", + " acc_as_html(self.acc), \n", + " diff_html\n", + " ])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CropMethod\n", + "> Box cropping methods.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class CropMethod(Enum):\n", + " INITIAL_BOX = 'Initial box'\n", + " DEFAULT = 'Default'\n", + " DEFAULT_GREY_PAD = 'Default, grey pad'\n", + " PADDED_4 = 'Padded 4px'\n", + " PADDED_8 = 'Padded 8px'\n", + " EXTRACTED_INIT_BOX = 'Extracted, init box'\n", + " PADDED_4_EXTRACTED = 'Padded 4, extracted'\n", + " PADDED_8_EXTRACTED = 'Padded 8, extracted'\n", + " PADDED_8_DILATION_1 = 'Padded 8, dilation 1'\n", + " PAD_8_FRACT_0_5 = 'Pad 8, fract. 0.5'\n", + " PAD_8_FRACT_0_2 = 'Pad 8, fract. 0.2'\n", + "\n", + " @classmethod\n", + " def __display_names__(cls):\n", + " return dict(\n", + " zip([_.value for _ in cls], \n", + " cls))\n", + "\n", + "\n", + "CM = CropMethod\n", + "\n", + "_IMAGE_METHODS = [CM.INITIAL_BOX, CM.DEFAULT, CM.DEFAULT_GREY_PAD, \n", + " CM.PADDED_4, CM.PADDED_8]\n", + "_EXTRACTED_METHODS = [CM.EXTRACTED_INIT_BOX, CM.PADDED_4_EXTRACTED, \n", + " CM.PADDED_8_EXTRACTED, CM.PADDED_8_DILATION_1, \n", + " CM.PAD_8_FRACT_0_5, CM.PAD_8_FRACT_0_2]\n", + "\n", + "\n", + "def crop_by_image(method: CM, \n", + " box: st.Box, \n", + " base: Image.Image, \n", + " preproc: cfg.PreprocessorConfig,\n", + " ):\n", + " image = None\n", + " match method:\n", + " case CM.INITIAL_BOX :\n", + " image = crop_box(box, base)\n", + " case CM.DEFAULT:\n", + " padded2_4 = (\n", + " box.pad(preproc.box_padding_initial, base.size).right_pad(\n", + " preproc.box_right_padding_initial, base.size))\n", + " image = crop_box(padded2_4, base)\n", + " case CM.DEFAULT_GREY_PAD:\n", + " image = crop_box(box, base)\n", + " image = ops.pad_image(image, 8, fill_color=(128, 128, 128))\n", + " case CM.PADDED_4:\n", + " padded4 = box.pad(4, base.size)\n", + " image = crop_box(padded4, base)\n", + " case CM.PADDED_8:\n", + " padded4 = box.pad(8, base.size)\n", + " image = crop_box(padded4, base)\n", + " case _: pass\n", + " return image\n", + "\n", + "\n", + "def crop_by_extracted(method: CM, \n", + " box: st.Box, \n", + " base: Image.Image, \n", + " mask: Image.Image,\n", + " cropped_image_path: Path,\n", + " cropped_mask_path: Path,\n", + " dilated: dict[float, Image.Image]\n", + " ):\n", + " cropped_image, cropped_mask, image = None, None, None\n", + " if method in _EXTRACTED_METHODS:\n", + " if not cropped_image_path.exists() or not cropped_mask_path.exists():\n", + " match method:\n", + " case CM.EXTRACTED_INIT_BOX:\n", + " cropped_image, cropped_mask, image = extract_text(base, mask, box)\n", + " case CM.PADDED_4_EXTRACTED:\n", + " padded4 = box.pad(4, base.size)\n", + " cropped_image, cropped_mask, image = extract_text(base, mask, padded4)\n", + " case CM.PADDED_8_EXTRACTED:\n", + " padded8 = box.pad(8, base.size)\n", + " cropped_image, cropped_mask, image = extract_text(base, mask, padded8)\n", + " case CM.PADDED_8_DILATION_1:\n", + " padded8 = box.pad(8, base.size)\n", + " cropped_image, cropped_mask, image = extract_text(\n", + " base, dilated[1], padded8)\n", + " case CM.PAD_8_FRACT_0_5:\n", + " padded8 = box.pad(8, base.size)\n", + " cropped_image, cropped_mask, image = extract_text(\n", + " base, dilated[0.5], padded8)\n", + " case CM.PAD_8_FRACT_0_2:\n", + " padded8 = box.pad(8, base.size)\n", + " cropped_image, cropped_mask, image = extract_text(\n", + " base, dilated[0.2], padded8)\n", + " case _: pass\n", + "\n", + " return image, cropped_image, cropped_mask\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ResultSet\n", + "> tagged nested dict to store image results keyed by box, and crop method\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "SubjIdT: TypeAlias = int\n", + "ImgIdT = SubjIdT\n", + "BoxIdT: TypeAlias = int\n", + "\n", + "class ResultSet(dict[BoxIdT, dict[CropMethod, ResultOCR]]): ...\n", + "\n", + "class ResultSetDefault(defaultdict[BoxIdT, dict[CropMethod, ResultOCR]]): ...\n", + "\n", + "def results_to_dict(results: ResultSet) -> dict[BoxIdT, dict[str, str]]:\n", + " d = {}\n", + " for box, box_methods in results.items():\n", + " for method, result in box_methods.items():\n", + " if box not in d:\n", + " d[box] = {}\n", + " d[box][method.name] = result.ocr\n", + " return d\n", + "\n", + "def dict_to_results(\n", + " image_idx: ImgIdT, \n", + " results_dict: dict[BoxIdT, dict[str, str]],\n", + " result_factory: Callable\n", + " ) -> ResultSetDefault:\n", + " results = ResultSetDefault(dict[CropMethod, ResultOCR])\n", + " for box_idx, box_methods in results_dict.items():\n", + " box_idx = int(box_idx)\n", + " for method, ocr in box_methods.items():\n", + " m = CM[method]\n", + " results[box_idx][m] = result_factory(image_idx, box_idx, m, ocr)\n", + " return results\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ExperimentContext\n", + "> Utility class to maintain shared state across all experiments.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "# class ExperimentSubject(Protocol):\n", + "# @property\n", + "# def exp(self) -> 'ExperimentContext': ...\n", + "# @property\n", + "# def idx(self) -> SubjIdT: ...\n", + "# def setup(self,\n", + "# exp: 'ExperimentContext',\n", + "# idx: Any,\n", + "# *args, **kwargs\n", + "# ): ...\n", + "\n", + "\n", + "# class ExperimentContext(Protocol):\n", + "# def subject_factory(self) -> Callable[..., ExperimentSubject]: ...\n", + "# def normalize_idx(self, idx: Any) -> SubjIdT: ...\n", + "# def experiment_subject(self, idx: Any, /, \n", + "# create: bool = False, *args, **kwargs) -> ExperimentSubject | None: \n", + "# \"\"\"Get or create an `ExperimentSubject` for the given identifier. \n", + "# Returns `None` if `idx` is out of domain range.\n", + "# \"\"\"\n", + "# ...\n" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class ExperimentSubject:\n", + " exp: ExperimentContext\n", + " idx: SubjIdT\n", + "\n", + " def setup(self, exp: ExperimentContext, idx: Any, *args, **kwargs): \n", + " self.exp = exp\n", + " self.idx = cast(SubjIdT, exp.normalize_idx(idx))\n", + " return self\n", + "\n", + " def __new__(cls,\n", + " exp: ExperimentContext,\n", + " idx: Any,\n", + " *args, **kwargs):\n", + " self = exp.experiment_subject(idx)\n", + " if self is None:\n", + " self = super().__new__(cls)\n", + " self = exp.experiment_subject(idx, new_subject=self, *args, **kwargs)\n", + " if self is None:\n", + " raise ValueError(f\"Can't create new subject with idx: {idx}: out of range\")\n", + " return self\n", + "\n", + "\n", + "class ExperimentContext:\n", + " \"Class to maintain shared state across all file-based experiments within the experiment domain.\"\n", + "\n", + " subject_cls: Callable[..., ExperimentSubject]\n", + " def subject_factory(self) -> Callable[..., ExperimentSubject]: return type(self).subject_cls\n", + "\n", + " def normalize_idx(self, idx: int | str | Path) -> SubjIdT | None:\n", + " nidx = None\n", + " if isinstance(idx, int) and idx < len(self._paths):\n", + " nidx = idx\n", + " elif isinstance(idx, str):\n", + " try:\n", + " nidx = [_.name for _ in self._paths].index(idx)\n", + " except Exception:\n", + " pass\n", + " elif isinstance(idx, Path):\n", + " idx = idx.resolve()\n", + " if idx in self._paths:\n", + " nidx = self._paths.index(idx)\n", + " return nidx\n", + " \n", + " def path_from_idx(self, idx: int | str | Path):\n", + " _idx = self.normalize_idx(idx)\n", + " if _idx is None:\n", + " raise ValueError(f\"{_idx} not found in context.\")\n", + " path = Path(self._paths[_idx])\n", + " if not path.exists():\n", + " raise ValueError(f\"{path} not found in context.\")\n", + " return path\n", + " \n", + " @property\n", + " def count(self): return len(self._paths)\n", + " @property\n", + " def cache_dir(self): return Path(\".cache/\")\n", + " @functools.lru_cache()\n", + " def _cache_dir(self, idx: SubjIdT):\n", + " # create one folder for each image to cache and save results\n", + " path = self.path_from_idx(idx)\n", + " cache_dir = self.cache_dir / path.stem\n", + " cache_dir.mkdir(parents=True, exist_ok=True)\n", + " return cache_dir\n", + " def subject_cache_dir(self, idx: int | str | Path):\n", + " return self._cache_dir(idx)\n", + "\n", + " def empty_cache(self, idx: SubjIdT | None = None):\n", + " cache_dir = self.cache_dir\n", + " if idx is None:\n", + " shutil.rmtree(cache_dir, ignore_errors=True)\n", + " cache_dir.mkdir(parents=True, exist_ok=True)\n", + " else:\n", + " path = Path(self._paths[idx])\n", + " cache_dir = cache_dir / path.stem\n", + " for p in cache_dir.glob(\"*\"):\n", + " p.unlink(missing_ok=True)\n", + " if not any(cache_dir.iterdir()):\n", + " cache_dir.rmdir()\n", + "\n", + " def empty_cache_warn(self, idx: SubjIdT | None=None, *, warn: bool=True, out: W.Output | None=None):\n", + " def on_confirm_clicked(b):\n", + " try:\n", + " self.empty_cache(idx)\n", + " print(\"Cache cleared successfully.\")\n", + " except Exception as e:\n", + " print(f\"Failed to clear cache: {e}\")\n", + " finally:\n", + " for widget in confirmation_box.children:\n", + " widget.close()\n", + "\n", + " def on_cancel_clicked(b):\n", + " print(\"Cache clear cancelled.\")\n", + " for widget in confirmation_box.children:\n", + " widget.close()\n", + "\n", + " if out is None:\n", + " out = W.Output()\n", + " with out:\n", + " if FC.IN_NOTEBOOK:\n", + " confirm_button = W.Button(description=\"Confirm\")\n", + " cancel_button = W.Button(description=\"Cancel\")\n", + " confirm_button.on_click(on_confirm_clicked)\n", + " cancel_button.on_click(on_cancel_clicked)\n", + " label = W.Label('Are you sure you want to clear the cache? This action cannot be undone.')\n", + " confirmation_box = W.VBox([label, W.HBox([confirm_button, cancel_button])])\n", + " display(confirmation_box)\n", + " else:\n", + " on_confirm_clicked(None)\n", + "\n", + " def experiment_subject(self, idx: SubjIdT | str | Path, /, \n", + " new_subject: ExperimentSubject | None = None, *args, **kwargs) -> ExperimentSubject | None:\n", + " \"Cached subject. If provided, `new_subject` replaces value at the index.\"\n", + " if (nidx := self.normalize_idx(idx)) is None:\n", + " return None\n", + " if new_subject is None:\n", + " subject = self._subjects.get(nidx)\n", + " else:\n", + " new_subject.setup(self, nidx, *args, **kwargs)\n", + " self._subjects[nidx] = subject = new_subject\n", + " return subject\n", + "\n", + " def reset(self):\n", + " self._subjects.clear()\n", + " self._cache_dir.cache_clear()\n", + " \n", + " def __init__(self, paths: list[Path], root: Path | None = None):\n", + " self._root = (root or Path('.')).resolve()\n", + " self._paths = [p.resolve().relative_to(self._root) for p in paths]\n", + " self._subjects: dict[SubjIdT, ExperimentSubject] = {}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`ExperimentSubject`s are singletons" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "exp = ExperimentContext([Path('a'), Path('b')])\n", + "subj = exp.experiment_subject(5)\n", + "test_eq(subj, None)\n", + "\n", + "_ = exp.experiment_subject(1)\n", + "test_is(_, None)\n", + "\n", + "subj1 = ExperimentSubject(exp, 1)\n", + "_ = exp.experiment_subject(1)\n", + "test_eq(_ is not None, True)\n", + "test_is(_, subj1)\n", + "test_is(subj1, ExperimentSubject(exp, 1))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can't create `ExperimentSubject`s beyond `ExperimentContext` domain." + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "test_fail(lambda:ExperimentSubject(exp, 2), 'out of range')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ImageContext\n", + "> A utility class to maintain image state for a `OCRExperimentContext`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "ImgSpecT: TypeAlias = ImgIdT | str | Path\n", + "\n", + "class ImageContext(ExperimentSubject):\n", + " \"\"\"\n", + " A utility class to maintain image state for a ExperimentContext.\n", + " This class encapsulates state necessary for conducting OCR experiments.\n", + "\n", + " Attributes:\n", + " json_data (dict): JSON data loaded from cached files.\n", + " page_data (st.PageData): PanelClaner page data.\n", + " base_image (Image.Image): The base image loaded from the page data.\n", + " mask (Image.Image): The mask image used for text detection.\n", + " gts (list[str]): Ground truth data for the text in the images.\n", + " ocr_model (str): Name or identifier of the OCR model used.\n", + " mocr (ocr.OCRModel): OCR model configured for the experiment.\n", + " mask_dilated1 (Image.Image): Image mask dilated by 1 pixel.\n", + " mask_dilated05 (Image.Image): Image mask dilated by 0.5 pixels.\n", + " mask_dilated02 (Image.Image): Image mask dilated by 0.2 pixels.\n", + "\n", + " Methods:\n", + " init(config: cfg.Config, img_path: Path, cache_dir: Path, ocr_model: str):\n", + " Initializes the experiment context. It also handles the generation of text boxes \n", + " if they are not already present.\n", + "\n", + " setup_ground_truth():\n", + " Loads or initializes ground truth data for the experiment based on the page data.\n", + "\n", + " setup_crop_masks():\n", + " Prepares various dilated versions of the mask image to be used in different cropping \n", + " strategies during the experiments.\n", + " \"\"\"\n", + " exp: ExperimentContext\n", + " idx: ImgIdT\n", + " base_image: Image.Image\n", + " mask: Image.Image\n", + " json_data: dict | None\n", + " page_data: st.PageData\n", + " # ocr_model: str\n", + " # mocr: ocr.OCRModel\n", + " # postprocess_ocr: Callable[..., str]\n", + " _page_lang: str\n", + " _gts: list[str]\n", + " _mask_dilated1: Image.Image | None\n", + " _mask_dilated05: Image.Image | None\n", + " _mask_dilated02: Image.Image | None\n", + " \n", + "\n", + " # # this methods will be set downstream, declared here to make the type checker happy\n", + " # def result(self: Self, \n", + " # box_idx: int, method: CropMethod, ocr: bool = True, reset: bool=False) -> ResultOCR: ...\n", + " # def summary_box(self: Self, box_idx: int): ...\n", + "\n", + " def to_dict(self):\n", + " return {\n", + " 'image_idx': self.idx,\n", + " 'page_lang': self.page_lang,\n", + " }\n", + " \n", + " @property\n", + " def image_idx(self): return self.idx\n", + " @property\n", + " def cache_dir(self): \n", + " return self.exp.subject_cache_dir(self.idx)\n", + " cache_dir_image = cache_dir\n", + " \n", + " @property\n", + " def image_info(self): \n", + " img = self.base_image\n", + " w, h = img.size\n", + " print_size_in = size(w, h, 'in', 300)\n", + " print_size_cm = size(w, h, 'cm', 300)\n", + " required_dpi = dpi(w, h, 'Modern Age')\n", + " return (w, h), print_size_in, print_size_cm, required_dpi\n", + "\n", + " @property\n", + " def original_image_path(self): return Path(self.page_data.original_path)\n", + " @property\n", + " def image_path(self): return Path(self.page_data.image_path)\n", + " @property\n", + " def image_name(self): return self.original_image_path.name\n", + " @property\n", + " def image_size(self): return self.base_image.size\n", + " @property\n", + " def image_dim(self):return size(*self.image_size)\n", + " @property\n", + " def image_dpi(self): return dpi(*self.image_size)\n", + " @property\n", + " def image_print(self):\n", + " return self.image_size, self.image_dim, self.image_dpi\n", + " @property\n", + " def image_name_rich(self):\n", + " siz, dim, res = self.image_print\n", + " return f\"{self.image_name} - {siz[0]}x{siz[1]} px: {dim[0]:.2f}x{dim[1]:.2f}\\\" @ {res:.2f} dpi\"\n", + " \n", + " def setup_page_lang(self, page_lang: str | None = None):\n", + " path = Path(self.page_data.original_path).with_suffix('.json')\n", + " metadata = json.load(open(path)) if path.exists() else {}\n", + " if 'lang' in metadata and (page_lang == metadata['lang'] or page_lang is None):\n", + " self._page_lang = metadata['lang']\n", + " return\n", + " self._page_lang = metadata['lang'] = page_lang or 'English'\n", + " json.dump(metadata, open(path, 'w'), indent=2)\n", + " @property\n", + " def page_lang(self):\n", + " if self._page_lang == None:\n", + " self.setup_page_lang()\n", + " return self._page_lang\n", + " \n", + " @property\n", + " def boxes(self): return self.page_data.boxes\n", + " \n", + " def setup_ground_truth(self):\n", + " self._gts = read_ground_truth(self.page_data)\n", + " @property\n", + " def gts(self): \n", + " if self._gts is None:\n", + " self.setup_ground_truth()\n", + " return self._gts\n", + " \n", + " @functools.lru_cache(typed=True)\n", + " def dilated_mask(self, fraction: float):\n", + " return dilate_by_fractional_pixel(self.mask, fraction)\n", + " \n", + " def mask_dilated1(self): \n", + " if self._mask_dilated1 is None:\n", + " self._mask_dilated1 = self.mask.filter(ImageFilter.MaxFilter(3))\n", + " return self._mask_dilated1\n", + " \n", + " def mask_dilated05(self): \n", + " if self._mask_dilated05 is None:\n", + " self._mask_dilated05 = self.dilated_mask(0.5)\n", + " return self._mask_dilated05\n", + " \n", + " def mask_dilated02(self): \n", + " if self._mask_dilated02 is None:\n", + " self._mask_dilated02 = self.dilated_mask(0.2)\n", + " return self._mask_dilated02\n", + " \n", + " def dilated(self):\n", + " return {1: self.mask_dilated1(),\n", + " 0.5: self.mask_dilated05(),\n", + " 0.2: self.mask_dilated02(),}\n", + "\n", + " def __new__(cls,\n", + " exp: ExperimentContext,\n", + " idx: ImgSpecT,\n", + " *args, **kwargs) -> Self:\n", + " return super().__new__(cls, exp, idx, *args, **kwargs) # type: ignore\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# OCRExperimentContext" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class OCRExperimentContext(ExperimentContext):\n", + " \"\"\"\n", + " A utility class to maintain shared state across all experiments within OCR domain.\n", + " This class encapsulates state necessary for conducting PanelCleaner OCR experiments.\n", + " \"\"\"\n", + "\n", + " config: cfg.Config\n", + " image_paths: list[Path]\n", + " # OCR engine -> Image index -> Box index -> Crop method -> Result\n", + " _results: dict[str, dict[ImgIdT, ResultSet]]\n", + "\n", + " \n", + " engines = {\n", + " 'Tesseract': cfg.OCREngine.TESSERACT, \n", + " 'Idefics': None, \n", + " 'manga-ocr': cfg.OCREngine.MANGAOCR}\n", + "\n", + " # subject_cls: ImageContext\n", + " # def subject_factory(self) -> Callable[..., ExperimentSubject]: return type(self).subject_cls\n", + "\n", + " @classmethod\n", + " def get_config(cls, cache_dir: Path | None = None) -> cfg.Config:\n", + " config = cfg.load_config()\n", + " config.cache_dir = cache_dir or Path(\".\")\n", + " profile = config.current_profile\n", + " preprocessor_conf = profile.preprocessor\n", + " # Modify the profile to OCR all boxes.\n", + " # Make sure OCR is enabled.\n", + " preprocessor_conf.ocr_enabled = True\n", + " # Make sure the max size is infinite, so no boxes are skipped in the OCR process.\n", + " preprocessor_conf.ocr_max_size = 10**10\n", + " # Make sure the sus box min size is infinite, so all boxes with \"unknown\" language are skipped.\n", + " preprocessor_conf.suspicious_box_min_size = 10**10\n", + " # Set the OCR blacklist pattern to match everything, so all text gets reported in the analytics.\n", + " preprocessor_conf.ocr_blacklist_pattern = \".*\"\n", + " return config\n", + "\n", + " def to_dict(self):\n", + " return {\n", + " 'image_paths': list(map(str, self.image_paths)),\n", + " 'cache_dir': str(self.config.cache_dir)\n", + " }\n", + " def to_json(self):\n", + " return json.dumps(self.to_dict(), indent=2)\n", + " @classmethod\n", + " def from_json_data(cls, d: dict):\n", + " return cls(cls.get_config(Path(d['cache_dir'])), d['image_paths'])\n", + " @classmethod\n", + " def from_json_path(cls, path: Path):\n", + " return cls.from_json_data(json.loads(path.read_text()))\n", + "\n", + " \n", + " @functools.lru_cache()\n", + " def mocr(self, ocr_model: str, lang: str):\n", + " engine = self.engines[ocr_model]\n", + " ocr_processor = ocr.get_ocr_processor(True, engine)\n", + " proc = ocr_processor[lang2pcleaner(lang)]\n", + " if isinstance(proc, TesseractOcr):\n", + " proc.lang = lang2tesseract(lang)\n", + " return proc\n", + "\n", + " def ocr_box(self, result: ResultOCR, ocr_model: str, lang: str): \n", + " assert result.image is not None\n", + " text = self.mocr(ocr_model, lang)(result.image)\n", + " result.ocr = postprocess_ocr(text)\n", + " return result\n", + "\n", + " @property\n", + " def cache_dir(self): return self.config.get_cleaner_cache_dir()\n", + " image_cache_dir = ExperimentContext.subject_cache_dir\n", + "\n", + " @functools.lru_cache()\n", + " def _load_page_data(self, image_idx: int):\n", + " config = self.config\n", + " cache_dir = self.image_cache_dir(image_idx)\n", + " img_path = self.path_from_idx(image_idx)\n", + " image_name = img_path.stem\n", + " # read cached json\n", + " jsons = [_ for _ in cache_dir.glob(\"*#raw.json\") if image_name in _.stem]\n", + " assert len(jsons) <= 1\n", + " # generate text boxes if needed\n", + " if not jsons:\n", + " pfl = config.current_profile\n", + " gpu = torch.cuda.is_available() or torch.backends.mps.is_available()\n", + " model_path = config.get_model_path(gpu)\n", + " ctm.model2annotations(pfl.general, pfl.text_detector, model_path, [img_path], cache_dir)\n", + " # we don't need unique names for this tests, strip uuids\n", + " for p in cache_dir.glob(f\"*{image_name}*\"):\n", + " p.rename(strip_uuid(p))\n", + " jsons = [_ for _ in cache_dir.glob(\"*#raw.json\") if image_name in _.stem]\n", + "\n", + " # adapt paths to be relative to this notebook\n", + " this_path = self._root\n", + " json_file_path = jsons[0]\n", + " json_data = json.loads(json_file_path.read_text(encoding=\"utf-8\"))\n", + " json_data[\"image_path\"] = str(strip_uuid(json_data[\"image_path\"]).relative_to(this_path))\n", + " json_data[\"mask_path\"] = str(strip_uuid(json_data[\"mask_path\"]).relative_to(this_path))\n", + " json.dump(json_data, open(json_file_path, \"w\"), indent=2)\n", + " else:\n", + " json_file_path = jsons[0]\n", + " json_data = json.loads(json_file_path.read_text(encoding=\"utf-8\"))\n", + "\n", + " page_data = st.PageData(\n", + " json_data[\"image_path\"], json_data[\"mask_path\"], \n", + " json_data[\"original_path\"], json_data[\"scale\"], \n", + " [st.Box(*data[\"xyxy\"]) for data in json_data[\"blk_list\"]], \n", + " [], [], [])\n", + " # Merge boxes that have mutually overlapping centers.\n", + " page_data.resolve_total_overlaps()\n", + " return json_data, page_data\n", + "\n", + " def page_data(self, image_idx: int):\n", + " _, page_data = self._load_page_data(image_idx)\n", + " return page_data\n", + " def json_data(self, image_idx: int):\n", + " json_data, _ = self._load_page_data(image_idx)\n", + " return json_data\n", + "\n", + " def experiment_image(self, image_idx: ImgIdT | str | Path) -> ImageContext | None:\n", + " \"Cached image context.\"\n", + " return cast(ImageContext, self.experiment_subject(image_idx))\n", + "\n", + " def update_results(self, ocr_model: str, img_idx: ImgIdT, results: ResultSetDefault):\n", + " self._results[ocr_model][img_idx] = cast(ResultSet, results)\n", + " \n", + " \n", + " def _result_from(self, image_idx: ImgIdT, box_idx: BoxIdT, method: CropMethod, ocr: str | None = None):\n", + " img_ctx = ImageContext(self, image_idx)\n", + " extracted = method in _EXTRACTED_METHODS\n", + " result_cls = ResultOCRExtracted if extracted else ResultOCR\n", + " result = result_cls(int(box_idx), None, '', img_ctx.page_data, \n", + " img_ctx.gts, description=f\"{method.value}\")\n", + " if ocr is not None:\n", + " result.ocr = ocr\n", + " return result\n", + " \n", + " def result(self, \n", + " ocr_model: str,\n", + " image_idx: ImgIdT, box_idx: BoxIdT, method: CropMethod, \n", + " ocr: bool=True, \n", + " rebuild: bool=False) -> ResultOCR | None:\n", + " img_ctx = ImageContext(self, image_idx)\n", + " result = self._results[ocr_model][image_idx][box_idx].get(method)\n", + " if not rebuild and result is not None:\n", + " return result\n", + " \n", + " result = self._result_from(image_idx, box_idx, method)\n", + " image, cropped_image, cropped_mask = result.image, None, None\n", + " base_image = img_ctx.base_image\n", + " box = img_ctx.boxes[box_idx]\n", + " if image is None and method in _IMAGE_METHODS:\n", + " image = crop_by_image(\n", + " method, box, base_image, self.config.current_profile.preprocessor)\n", + "\n", + " if image is None and method in _EXTRACTED_METHODS:\n", + " mask = img_ctx.mask\n", + " cropped_image_path = result.cache_image(cropped_image, \"cropped\")\n", + " cropped_mask_path = result.cache_image(cropped_mask, \"mask\")\n", + " if not cropped_image_path.exists() or not cropped_mask_path.exists():\n", + " image, cropped_image, cropped_mask = crop_by_extracted(\n", + " method, box, base_image, mask, \n", + " cropped_image_path, cropped_mask_path, img_ctx.dilated())\n", + " \n", + " assert image is not None\n", + " if result.image is None:\n", + " result.image = image\n", + " result.cache_image()\n", + " if cropped_image is not None:\n", + " result.cache_image(cropped_image, \"cropped\")\n", + " if cropped_mask is not None:\n", + " result.cache_image(cropped_mask, \"mask\")\n", + " \n", + " if ocr:\n", + " result = self.ocr_box(result, ocr_model, img_ctx.page_lang)\n", + " self._results[ocr_model][image_idx][box_idx][method] = result\n", + " return result\n", + "\n", + " def results(self, ocr_model: str | None = None, img_idx: ImgIdT | None = None):\n", + " if ocr_model is None: return self._results\n", + " if img_idx is None: return self._results[ocr_model]\n", + " return self._results[ocr_model][img_idx]\n", + " def model_results(self, ocr_model: str):\n", + " return cast(dict[ImgIdT, ResultSet], self.results(ocr_model))\n", + " def image_results(self, ocr_model: str, img_idx: ImgIdT):\n", + " return cast(ResultSet, self.results(ocr_model, img_idx))\n", + " def box_results(self, ocr_model: str, img_idx: ImgIdT, box_idx: BoxIdT):\n", + " return cast(ResultSet, self.results(ocr_model, img_idx))[box_idx]\n", + " def method_results(self, ocr_model: str, img_idx: ImgIdT, method: CropMethod):\n", + " image_results = self.image_results(ocr_model, img_idx)\n", + " return {i: box_results.get(method) for i,box_results in image_results.items()}\n", + "\n", + " def _reset_results(self):\n", + " results = defaultdict(lambda: defaultdict(lambda: ResultSetDefault(dict)))\n", + " self._results = cast(dict[str, dict[ImgIdT, ResultSet]], results)\n", + " def reset_results(self, \n", + " ocr_model: str | None = None, \n", + " image_idx: int | None = None, \n", + " box_idx: int | None = None, \n", + " method: CropMethod | None = None):\n", + " if ocr_model is None and image_idx is None and box_idx is None and method is None:\n", + " self._reset_results()\n", + " return\n", + " results = self._results\n", + " models = tuple(results.keys()) if ocr_model is None else [ocr_model] if ocr_model in results else []\n", + " for ocr_model in models:\n", + " img_nodes = results[ocr_model]\n", + " imgs = tuple(img_nodes.keys()) if image_idx is None else [image_idx] if image_idx in img_nodes else []\n", + " for img_idx in imgs:\n", + " box_nodes = img_nodes[img_idx]\n", + " boxes = tuple(box_nodes.keys()) if box_idx is None else [box_idx] if box_idx in box_nodes else []\n", + " for box_idx in boxes:\n", + " if method is None:\n", + " del box_nodes[box_idx]\n", + " else:\n", + " methods = box_nodes[box_idx]\n", + " if method in methods:\n", + " del methods[method]\n", + " if not box_nodes[box_idx]:\n", + " del box_nodes[box_idx]\n", + " if not img_nodes[img_idx]:\n", + " del img_nodes[img_idx]\n", + " if not results[ocr_model]:\n", + " del results[ocr_model]\n", + " def reset(self):\n", + " super().reset()\n", + " self.reset_results()\n", + " self._load_page_data.cache_clear()\n", + " self.mocr.cache_clear()\n", + "\n", + " def __init__(self, \n", + " config: cfg.Config | None, \n", + " image_paths: list[Path]\n", + " ):\n", + " super().__init__(list(map(lambda p: p.resolve(), image_paths)))\n", + " self.config = config or type(self).get_config()\n", + " self.image_paths = self._paths\n", + " self._reset_results()\n", + " self._images = self._subjects\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "#| exporti\n", + "\n", + "@FC.patch_to(ImageContext)\n", + "def setup(self, exp: OCRExperimentContext, image_idx: ImgSpecT, page_lang: str | None = None):\n", + " super(type(self), self).setup(exp, image_idx)\n", + " self._mask_dilated1 = self._mask_dilated05 = self._mask_dilated02 = None\n", + " # if ocr_model not in exp.engines:\n", + " # raise ValueError(f\"OCR model {ocr_model} not supported.\")\n", + " # self.ocr_model = ocr_model\n", + " # self.idx = exp.normalize_idx(image_idx)\n", + " self.json_data, self.page_data = exp._load_page_data(self.idx)\n", + " self.setup_page_lang(page_lang)\n", + " self.mask = Image.open(self.page_data.mask_path)\n", + " self.base_image = Image.open(self.page_data.image_path)\n", + " self.setup_ground_truth()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "tirar = OCRExperimentContext(None, [])\n", + "test_eq(rr := tirar._results, {})\n", + "test_eq(rr['Tesseract'][0][0], {})\n", + "test_eq(rr, {'Tesseract': {0: {0: {}}}})\n", + "test_eq(rr['Tesseract'][0][0].get(CM.INITIAL_BOX), None)\n", + "rr['Tesseract'][0][0][CM.INITIAL_BOX] = 'a' # type: ignore\n", + "test_eq(rr, {'Tesseract': {0: {0: {CM.INITIAL_BOX: 'a'}}}})\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ContextVisor\n" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class ContextVisor:\n", + " ctx: Any\n", + " # control_names: list[str]\n", + " values: dict[str, Any]\n", + "\n", + " _css = ''\n", + "\n", + " _ctxs: dict[str, ContextVisor]\n", + " _hdlrs: dict[str, ContextVisor]\n", + "\n", + " @property\n", + " def w(self) -> W.DOMWidget:\n", + " if getattr(self, '_w', None) is None:\n", + " self._w = self.setup_ui()\n", + " return self._w\n", + " @property\n", + " def out(self) -> W.Output:\n", + " if getattr(self, '_out', None) is None:\n", + " self._out = W.Output()\n", + " self._out.clear_output(wait=True)\n", + " return self._out # type: ignore\n", + " @property\n", + " def controls(self) -> dict[str, W.ValueWidget | W.fixed]:\n", + " if getattr(self, '_controls', None) is None:\n", + " self._controls = self.setup_controls()\n", + " return self._controls\n", + " @property\n", + " def all_controls(self) -> dict[str, W.ValueWidget | W.fixed]:\n", + " if getattr(self, '_all_controls', None) is None:\n", + " controls = {}\n", + " for visor in self._ctxs.values():\n", + " controls.update(visor.all_controls)\n", + " controls.update(self.controls)\n", + " self._all_controls = controls\n", + " return self._all_controls\n", + " \n", + " @property\n", + " def all_values(self):\n", + " return {**{k:v.values for k,v in (self._ctxs | {'self': self}).items()}, **self.values}\n", + " \n", + " @property\n", + " def comps(self): return self._ctxs\n", + " def comp(self, k: str) -> ContextVisor | None:\n", + " return self._ctxs.get(k)\n", + " def handler(self, k: str) -> ContextVisor | None:\n", + " return self._hdlrs.get(k)\n", + " \n", + " @property\n", + " def styler(self) -> W.Output | None:\n", + " if (stl := self.setup_style()) is None: \n", + " return None\n", + " if getattr(self, '_style', None) is None:\n", + " self._style = W.Output(layout={'height': '0px'})\n", + " with self._style:\n", + " display(stl)\n", + " return self._style\n", + " def setup_style(self):\n", + " return HTML(f\"\") if self._css else None\n", + " \n", + " def update_output(self, **kwargs): \n", + " cprint(kwargs)\n", + " \n", + " def setup_controls(self) -> dict[str, W.ValueWidget | W.fixed]:\n", + " return {k: W.Label(value=k) for k,v in self.values.items()}\n", + " \n", + " def hide(self):\n", + " self.w.layout.visibility = 'hidden'\n", + " def show(self):\n", + " self.w.layout.visibility = 'visible'\n", + "\n", + " def setup_ui(self):\n", + " comps = []\n", + " for visor in self._ctxs.values():\n", + " comps.append(visor.w)\n", + " return W.HBox([*comps, *self.controls.values()])\n", + "\n", + " def setup_display(self): \n", + " if getattr(self, '_w', None) is None:\n", + " self._w = self.setup_ui()\n", + " \n", + "\n", + " def _output(self, **kwargs):\n", + " collator = defaultdict(dict)\n", + " show_inline_matplotlib_plots()\n", + " with self.out:\n", + " clear_output(wait=True)\n", + " for k,v in kwargs.items():\n", + " if (comp := self.handler(k)) is not None:\n", + " collator[comp][k] = v\n", + " else:\n", + " assert 0\n", + " # self.update_output(**{k: v})\n", + " for comp, kw in collator.items():\n", + " comp.update_output(**kw)\n", + " show_inline_matplotlib_plots()\n", + " def interactive_output(self):\n", + " controls = self.all_controls\n", + " controls2names = {v:k for k,v in controls.items()}\n", + " def observer(change):\n", + " control_name = controls2names[change['owner']]\n", + " kwargs = {control_name: change['new']}\n", + " updated = self._update(**kwargs)\n", + " self._output(**updated)\n", + " for w in controls.values():\n", + " w.observe(observer, 'value')\n", + " def display(self, **kwargs): \n", + " if getattr(self, '_w', None) is None:\n", + " self.setup_display()\n", + " self.interactive_output()\n", + " self._update(**(self.values | kwargs))\n", + " all_values= {}\n", + " for comp in list(self.comps.values()) + [self]: all_values.update(comp.values)\n", + " self._hdlrs = {k:self._hdlrs.get(k, self) for k in all_values}\n", + " self._output(**all_values)\n", + " display(self.styler, self.w, self.out) if self.styler else display(self.w, self.out)\n", + " else:\n", + " self.update(**kwargs)\n", + " def _ipython_display_(self): self.display()\n", + "\n", + " def _update(self, update_value: bool=True, **kwargs):\n", + " updated = {}\n", + " for visor in self.comps.values():\n", + " updated.update(visor._update(update_value=update_value, **kwargs))\n", + " values = self.values\n", + " my_vals = _pops_(kwargs, self.values.keys())\n", + " for k,v in my_vals.items():\n", + " if v is not None and v != values[k]:\n", + " if update_value: values[k] = v\n", + " updated[k] = v\n", + " return updated\n", + " def update(self, **kwargs):\n", + " updated = self._update(update_value=False, **kwargs)\n", + " controls = self.all_controls\n", + " for k in updated:\n", + " controls[k].value = updated[k]\n", + " # self._output(**updated)\n", + " \n", + " def __init__(self, \n", + " ctx: Any, \n", + " values: dict[str, Any], \n", + " out: W.Output | None = None,\n", + " ctxs: dict[str, ContextVisor] | None = None,\n", + " hdlrs: dict[str, ContextVisor] | None = None,\n", + " ):\n", + " self._ctxs = ctxs or {}\n", + " self._hdlrs = hdlrs or {}\n", + " self.ctx = ctx\n", + " self._out = out\n", + " self.values = values\n", + " \n" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "82c802235bf84f6eb36d87cd72607440", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(Label(value='a'),))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4f0809d6276b4bea9f0f992b337817a8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cleanupwidgets('test_visor')\n", + "\n", + "test_visor = ContextVisor(None, {'a': 1})\n", + "test_visor\n" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(test_visor.values, {'a': 1})\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CONTEXT\n", + "> `CONTEXT` is an `OCRExperimentContext` object that contains the configuration and the list of image paths.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can get the configuration with `OCRExperimentContext.get_config()`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Current Configuration:\n", + "\n", + "Locale: System default\n", + "Default Profile: Built-in\n", + "Saved Profiles:\n", + "- victess: /Users/vic/dev/repo/DL-mac/cleaned/victess.conf\n", + "- vicmang: /Users/vic/dev/repo/DL-mac/cleaned/vicmang.conf\n", + "\n", + "Profile Editor: cursor\n", + "Cache Directory: .\n", + "Default Torch Model Path: /Users/vic/Library/Caches/pcleaner/model/comictextdetector.pt\n", + "Default CV2 Model Path: /Users/vic/Library/Caches/pcleaner/model/comictextdetector.pt.onnx\n", + "GUI Theme: System default\n", + "\n", + "--------------------\n", + "\n", + "Config file located at: /Users/vic/Library/Application Support/pcleaner/pcleanerconfig.ini\n", + "System default cache directory: /Users/vic/Library/Caches/pcleaner\n" + ] + }, + { + "data": { + "text/html": [ + "
      cache_dir: Path('cleaner')\n",
+       "     model_path: Path('/Users/vic/Library/Caches/pcleaner/model/comictextdetector.pt')\n",
+       "         device: 'mps'\n",
+       "
\n" + ], + "text/plain": [ + " cache_dir: \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'cleaner'\u001b[0m\u001b[1m)\u001b[0m\n", + " model_path: \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'/Users/vic/Library/Caches/pcleaner/model/comictextdetector.pt'\u001b[0m\u001b[1m)\u001b[0m\n", + " device: \u001b[32m'mps'\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "CONFIG = OCRExperimentContext.get_config()\n", + "\n", + "gpu = torch.cuda.is_available() or torch.backends.mps.is_available()\n", + "model_path = CONFIG.get_model_path(gpu)\n", + "device = (\"mps\" if torch.backends.mps.is_available() else \"cuda\") if model_path.suffix == \".pt\" else \"cpu\"\n", + "\n", + "CONFIG.show()\n", + "cprint(\n", + " f\"{'cache_dir':>15}: {repr(cache_dir)}\\n\"\n", + " f\"{'model_path':>15}: {repr(model_path)}\\n\"\n", + " f\"{'device':>15}: {repr(device)}\")\n", + "\n", + "test_eq(CONFIG.cache_dir, Path(\".\"))\n", + "test_eq(CONFIG.current_profile.preprocessor.ocr_enabled, True)\n", + "test_eq(CONFIG.current_profile.preprocessor.ocr_max_size, 10**10)\n", + "test_eq(CONFIG.current_profile.preprocessor.suspicious_box_min_size, 10**10)\n", + "test_eq(CONFIG.current_profile.preprocessor.ocr_blacklist_pattern, \".*\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "CONTEXT = OCRExperimentContext(CONFIG, IMAGE_PATHS)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ImageSelector" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class ImageSelector(ContextVisor):\n", + " ctx: OCRExperimentContext\n", + "\n", + " @property\n", + " def image_ctx(self):\n", + " return ImageContext(self.ctx, self.values['image_idx'])\n", + " \n", + " def setup_controls(self):\n", + " paths = self.ctx.image_paths\n", + " w = W.Dropdown(\n", + " options={_.stem:i for i,_ in enumerate(paths)}, \n", + " value=self.values['image_idx'],\n", + " layout={'width': 'fit-content'},\n", + " style={'description_width': 'initial'})\n", + " return {'image_idx': w}\n", + "\n", + " def update(self, image_idx: ImgSpecT | None = None, **kwargs):\n", + " if image_idx is None: return\n", + " idx = self.ctx.normalize_idx(image_idx)\n", + " if idx is None: return\n", + " super().update(image_idx=idx, **kwargs)\n", + "\n", + "\n", + " def __init__(self, \n", + " ctx: OCRExperimentContext, /, \n", + " image_idx: ImgSpecT = 0, *, \n", + " out: W.Output | None=None):\n", + " idx = ctx.normalize_idx(image_idx)\n", + " assert idx is not None, f\"Image {image_idx} not found in experiment context\"\n", + " super().__init__(ctx, {'image_idx': idx}, out)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c033b2c76629478f8d7702e1f3f8666a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(Dropdown(index=2, layout=Layout(width='fit-content'), options={'Action_Comics_1960-01-00_(262)'…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d78cb5cf72c34bcab9274cfd77f585a5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cleanupwidgets('image_selector')\n", + "\n", + "image_selector = ImageSelector(CONTEXT, 2)\n", + "image_selector\n" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "image_selector.update(13)\n", + "test_eq(image_selector.values['image_idx'], 13)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# OCRContextVisor" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "class OCRContextVisor(ContextVisor):\n", + " ctx: OCRExperimentContext\n", + " \n", + " def update_output(self, /, image_idx: ImgIdT, **kwargs):\n", + " img_path = self.ctx.path_from_idx(image_idx)\n", + " display_image_grid([img_path], 1, 1)\n", + "\n", + " def update(self, image_idx: ImgSpecT | None = None, **kwargs):\n", + " if image_idx is None: return\n", + " idx = self.ctx.normalize_idx(image_idx)\n", + " if idx is None: return\n", + " super().update(image_idx=idx, **kwargs)\n", + " \n", + " def __init__(self, \n", + " ctx: OCRExperimentContext, /, \n", + " image_idx: ImgSpecT = 0, *, \n", + " out: W.Output | None=None):\n", + " super().__init__(ctx, {}, out, \n", + " ctxs={'image_idx': ImageSelector(ctx, image_idx, out=self.out)})\n" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3b40f82e57564fbcae7913d7a76fbc32", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(Dropdown(index=2, layout=Layout(width='fit-content'), options={'Action_Comics_19…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "174fbbc4b6ff42ac8013984946e787e4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cleanupwidgets('ctx_visor')\n", + "\n", + "# ContextVisor(CONTEXT)\n", + "# ContextVisor(CONTEXT).display(3)\n", + "ctx_visor = OCRContextVisor(CONTEXT, 2)\n", + "ctx_visor\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "ctx_visor.update('Mary_Perkins_On_Stage_v2006_1_-_P00068.jpg')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Base image\n", + "> Change `BASE_IMAGE_IDX` to select a different base image to use in the examples below." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [], + "source": [ + "BASE_IMAGE_IDX: ImgIdT = cast(ImgIdT, CONTEXT.normalize_idx(\"Strange_Tales_172005.jpg\"))\n", + "# BASE_IMAGE_IDX = CONTEXT.normalize_idx(\"0033\")\n", + "# BASE_IMAGE_IDX = CONTEXT.normalize_idx(\"INOUE_KYOUMEN_002\")\n", + "# BASE_IMAGE_IDX = CONTEXT.normalize_idx(\"Action_Comics_1960-01-00_(262)\")\n", + "\n", + "assert BASE_IMAGE_IDX is not None\n", + "img_path = Path(CONTEXT.image_paths[BASE_IMAGE_IDX])\n", + "assert img_path.exists()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Empty cache\n", + "> Clear the image cache used profusely throughout the examples below." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "You will be warned before the cache is emptied." + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [], + "source": [ + "# CONTEXT.empty_cache_warn()" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [], + "source": [ + "# CONTEXT.empty_cache_warn(BASE_IMAGE_IDX)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ImageContext of base image\n", + "> Creates the `ImageContext` for the base image.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If `PanelCleaner` page data is already cached, it is loaded from the cache.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [], + "source": [ + "CONTEXT.reset()" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "page_lang = 'English'\n", + "# page_lang = 'Japanese'\n", + "# page_lang = 'Spanish'\n", + "# page_lang = 'French'\n", + "\n", + "IMAGE_CONTEXT = ImageContext(CONTEXT, BASE_IMAGE_IDX, page_lang=page_lang)\n", + "test_eq(IMAGE_CONTEXT.page_data is not None, True)\n", + "# cprint(IMAGE_CONTEXT.page_data.boxes)\n", + "RenderJSON(IMAGE_CONTEXT.json_data, 360, 2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [], + "source": [ + "test_is(IMAGE_CONTEXT, ImageContext(CONTEXT, BASE_IMAGE_IDX))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Visualize image" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Strange_Tales_172005.jpg - 1275x1888 px: 4.25x6.29\" @ 188.32 dpi
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "page_data = IMAGE_CONTEXT.page_data\n", + "display_image_grid([page_data.image_path, page_data.mask_path], 1, 2, caption=IMAGE_CONTEXT.image_name_rich)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "_, out_path = page_boxes(page_data)\n", + "display_image_grid([out_path], 1, 1)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ModelSelector\n" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class OCRModel(Enum):\n", + " TESSERACT = 0\n", + " IDEFICS = 1\n", + " @staticmethod\n", + " def __display_names__() -> dict[str, OCRModel]:\n", + " return dict(\n", + " zip(\"Tesseract, Idefics\".split(', '), \n", + " OCRModel))\n", + "\n", + "\n", + "class ModelSelector(ContextVisor):\n", + " ctx: OCRExperimentContext\n", + " \n", + " def setup_controls(self):\n", + " options = self.models\n", + " w = W.Dropdown(\n", + " options=options, \n", + " value=self.values['model'],\n", + " layout={'width': 'fit-content'},\n", + " style={'description_width': 'initial'})\n", + " return {'model': w}\n", + "\n", + " def setup_ui(self):\n", + " ctls = self.controls\n", + " model_grp = W.HBox([ctls['model']])\n", + " model_grp.add_class('model_grp')\n", + " comps = []\n", + " for visor in self.comps.values():\n", + " comps.append(visor.setup_ui())\n", + " ui = W.HBox([*comps, model_grp])\n", + " return ui\n", + "\n", + " def __init__(self, \n", + " exp_ctx: OCRExperimentContext,\n", + " ocr_model: OCRModel | None=OCRModel.TESSERACT,\n", + " ocr_models: dict[str, OCRModel] | None = None,\n", + " out: W.Output | None = None\n", + " ):\n", + " self.models: dict[str, OCRModel] = ocr_models or OCRModel.__display_names__()\n", + " super().__init__(exp_ctx, \n", + " {'model': ocr_model or OCRModel.TESSERACT}, \n", + " out=out or self.out)#, ctxs=[exp_visor])\n" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "413f66f1d43a4d849e43a79ca9b16502", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(Dropdown(layout=Layout(width='fit-content'), options={'Tesseract': 30}: {w} x {h} pixels\\n\"\n", + " f\"{'PIL Info DPI':>30}: {repr(img.info.get('dpi', None))}\\n\"\n", + " f\"{'Print Size 300 DPI':>30}: {print_size_in[0]:.3f} x {print_size_in[1]:.3f} in\"\n", + " f\" / {print_size_cm[0]:.3f} x {print_size_cm[1]:.3f} cm\\n\"\n", + " f\"Required DPI Modern Age format: {required_dpi:.3f} dpi \"\n", + " f\"({format[0]:.3f} x {format[1]:.3f} in)\")\n", + "\n", + "\n", + " def display_content(self, image_ctx: ImageContext, display_option: DisplayOptions):\n", + " page_data = image_ctx.page_data\n", + " if display_option in (DisplayOptions.ALL, DisplayOptions.PAGE_DATA):\n", + " self.image_info(image_ctx)\n", + " RenderJSON(image_ctx.json_data, 350, 2).display()\n", + " if display_option in (DisplayOptions.ALL, DisplayOptions.GROUND_TRUTH):\n", + " cprint(image_ctx.gts)\n", + " if display_option == DisplayOptions.IMAGE:\n", + " display_image_grid([page_data.image_path], 1, 1)\n", + " if display_option == DisplayOptions.MASK:\n", + " display_image_grid([page_data.mask_path], 1, 1)\n", + " if display_option in (DisplayOptions.ALL, DisplayOptions.IMAGE_MASK):\n", + " display_image_grid([page_data.image_path, page_data.mask_path], 1, 2)\n", + " if display_option in (DisplayOptions.ALL, DisplayOptions.BOXES):\n", + " _, out_path = page_boxes(page_data)\n", + " display_image_grid([out_path], 1, 1)\n", + "\n", + "\n", + " def setup_controls(self):\n", + " options = self.display_options or {**DisplayOptions.__display_names__()}\n", + " display_option_wdgt = W.Dropdown(\n", + " options=options, \n", + " value=self.values['display_option'],\n", + " layout={'width': '120px'},\n", + " style={'description_width': 'initial'})\n", + " return {'display_option': display_option_wdgt}\n", + "\n", + "\n", + " def setup_ui(self):\n", + " ctls = self.controls\n", + " display_option_grp = W.HBox([ctls['display_option']])\n", + " display_option_grp.add_class('display_option_grp')\n", + " comps = []\n", + " for visor in self.comps.values():\n", + " comps.append(visor.setup_ui())\n", + " ui = W.HBox([*comps, display_option_grp])\n", + " return ui\n", + "\n", + "\n", + " def __init__(self, \n", + " exp_ctx: OCRExperimentContext,\n", + " display_option: DisplayOptions | None=DisplayOptions.BOXES,\n", + " display_options: Mapping[str, DisplayOptions] | None = None,\n", + " out: W.Output | None = None\n", + " ):\n", + " self.display_options = display_options\n", + " super().__init__(exp_ctx, \n", + " {'display_option': display_option or DisplayOptions.BOXES}, \n", + " out=out or self.out)#, ctxs=[exp_visor])\n" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "51b272e199e94fe69465b30306499d8a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(Dropdown(index=2, layout=Layout(width='120px'), options={'Boxes': Width x Height: 1275 x 1888 pixels\n", + " PIL Info DPI: None\n", + " Print Size 300 DPI: 4.250 x 6.293 in / 10.795 x 15.985 cm\n", + "Required DPI Modern Age format: 188.324 dpi (6.625 x 10.250 in)\n", + "\n" + ], + "text/plain": [ + " Width x Height: \u001b[1;36m1275\u001b[0m x \u001b[1;36m1888\u001b[0m pixels\n", + " PIL Info DPI: \u001b[3;35mNone\u001b[0m\n", + " Print Size \u001b[1;36m300\u001b[0m DPI: \u001b[1;36m4.250\u001b[0m x \u001b[1;36m6.293\u001b[0m in \u001b[35m/\u001b[0m \u001b[1;36m10.795\u001b[0m x \u001b[1;36m15.985\u001b[0m cm\n", + "Required DPI Modern Age format: \u001b[1;36m188.324\u001b[0m dpi \u001b[1m(\u001b[0m\u001b[1;36m6.625\u001b[0m x \u001b[1;36m10.250\u001b[0m in\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "img = IMAGE_CONTEXT.base_image\n", + "w, h = img.size\n", + "\n", + "print_size_in = size(w, h, 'in', 300)\n", + "print_size_cm = size(w, h, 'cm', 300)\n", + "required_dpi = dpi(w, h, 'Modern Age')\n", + "format = PRINT_FORMATS['Modern Age']\n", + "cprint( f\"{'Width x Height':>30}: {w} x {h} pixels\\n\"\n", + " f\"{'PIL Info DPI':>30}: {repr(img.info.get('dpi', None))}\\n\"\n", + " f\"{'Print Size 300 DPI':>30}: {print_size_in[0]:.3f} x {print_size_in[1]:.3f} in\"\n", + " f\" / {print_size_cm[0]:.3f} x {print_size_cm[1]:.3f} cm\\n\"\n", + " f\"Required DPI Modern Age format: {required_dpi:.3f} dpi ({format[0]:.3f} x {format[1]:.3f} in)\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((1275, 1888),\n", + " (4.25, 6.293333333333333),\n", + " (10.795, 15.985066666666667),\n", + " 188.32397606994937)" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
                Width x Height: 804 x 1241 pixels\n",
+       "                  PIL Info DPI: None\n",
+       "            Print Size 300 DPI: 2.680 x 4.137 in / 6.807 x 10.507 cm\n",
+       "Required DPI Modern Age format: 121.216 dpi (6.625 x 10.250 in)\n",
+       "
\n" + ], + "text/plain": [ + " Width x Height: \u001b[1;36m804\u001b[0m x \u001b[1;36m1241\u001b[0m pixels\n", + " PIL Info DPI: \u001b[3;35mNone\u001b[0m\n", + " Print Size \u001b[1;36m300\u001b[0m DPI: \u001b[1;36m2.680\u001b[0m x \u001b[1;36m4.137\u001b[0m in \u001b[35m/\u001b[0m \u001b[1;36m6.807\u001b[0m x \u001b[1;36m10.507\u001b[0m cm\n", + "Required DPI Modern Age format: \u001b[1;36m121.216\u001b[0m dpi \u001b[1m(\u001b[0m\u001b[1;36m6.625\u001b[0m x \u001b[1;36m10.250\u001b[0m in\u001b[1m)\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display(IMAGE_CONTEXT.image_info)\n", + "img_visor.image_info()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Balloons and Captions Ground Truth\n", + "> The ground truth for the balloons and captions is read from a `.txt` file.\n", + "\n", + "The file is named `.gt.txt` and contains one entry per line, corresponding to each balloon or caption in the order found in PanelClenaer page data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.',\n", + " 'The house and the old man are alike in many ways; tall, proud, patient, contented always to wait until their master comes home--',\n", + " 'And one in need of some help, it would appear.',\n", + " 'Bambu-- we have a guest.',\n", + " '--and tonight, he comes most urgently, slamming open the oaken front doors!',\n", + " 'Tell me, master-- how may Bambu serve?',\n", + " 'Some blankets to keep her warm, Bambu-- and perhaps some dry clothes',\n", + " \"The echo of the old man's footsteps fades down the hall as...\",\n", + " 'How curious the whims of fate. Had I not chanced to stroll along the river tonight--',\n", + " 'As quickly as I can, master',\n", + " '--the girl would most surely be dead by now.',\n", + " 'Ghede has been generous. the Death God has given the girl a second chance at--',\n", + " \"Easy, girl-- there's nothing to scream about anymore.\",\n", + " \"You're among friends now, you're safe!\",\n", + " 'Continued after next page']" + ] + }, + "execution_count": 63, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "IMAGE_CONTEXT.gts\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Experiment\n", + "> Use `ExperimentOCR` to perform OCR on the page balloons given a `CropMethod` and a model (i.e., `'Tesseract'` or `'Idefics'`)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "metadata": {}, + "outputs": [], + "source": [ + "#| exporti\n", + "\n", + "def trimmed_mean(data, trim_percent):\n", + " sorted_data = np.sort(data)\n", + " n = len(data)\n", + " trim_count = int(trim_percent * n)\n", + " trimmed_data = sorted_data[trim_count:-trim_count]\n", + " return np.mean(trimmed_data)\n", + "\n", + "def mad_based_outlier(points, threshold=3.5):\n", + " median = np.median(points)\n", + " diff = np.abs(points - median)\n", + " mad = np.median(diff)\n", + " modified_z_score = 0.6745 * diff / mad\n", + " return points[modified_z_score < threshold]\n", + "\n", + "def iqr_outlier_removal(data):\n", + " q1 = np.percentile(data, 25)\n", + " q3 = np.percentile(data, 75)\n", + " iqr = q3 - q1\n", + " lower_bound = q1 - 1.5 * iqr\n", + " upper_bound = q3 + 1.5 * iqr\n", + " return data[(data >= lower_bound) & (data <= upper_bound)]\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "@dataclasses.dataclass\n", + "class Experiment:\n", + " ctx: ExperimentContext\n", + "\n", + "\n", + "@dataclasses.dataclass\n", + "class ExperimentOCR(Experiment):\n", + " ctx: ImageContext\n", + " ocr_model: str\n", + "\n", + " @property\n", + " def img_ctx(self): return self.ctx\n", + " @property\n", + " def ctxs(self):\n", + " img_ctx = self.img_ctx\n", + " return cast(OCRExperimentContext, img_ctx.exp), img_ctx\n", + "\n", + " @classmethod\n", + " def file_path_of(cls, page_data: st.PageData, ocr_model: str):\n", + " return f\"{Path(page_data.original_path).stem}_{ocr_model}.json\"\n", + " \n", + " def file_path(self):\n", + " img_ctx = self.img_ctx\n", + " return type(self).file_path_of(img_ctx.page_data, self.ocr_model)\n", + " \n", + " def to_dict(self):\n", + " \"JSON serializable dict of the experiment\"\n", + " img_ctx = self.img_ctx\n", + " img_idx = img_ctx.image_idx\n", + " results = results_to_dict(self.results())\n", + " return {\n", + " 'image_name': img_ctx.image_name,\n", + " 'ocr_model': self.ocr_model, \n", + " 'results': results,\n", + " }\n", + "\n", + " def to_json(self, out_dir: Path | None = None):\n", + " img_ctx = self.img_ctx\n", + " fp = (out_dir or img_ctx.cache_dir_image) / self.file_path()\n", + " data = self.to_dict()\n", + " with open(fp, 'w') as f:\n", + " json.dump(data, f, indent=2)\n", + " return fp, data\n", + "\n", + " @classmethod\n", + " def from_json(cls, experiment: OCRExperimentContext, json_path: Path) -> Self:\n", + " try:\n", + " with open(json_path, 'r') as f:\n", + " data = json.load(f)\n", + " except Exception as e:\n", + " logger.error(f\"Error loading {json_path}: {e}\")\n", + " raise e\n", + " ocr_model = data['ocr_model']\n", + " img_ctx = ImageContext(experiment, data['image_name'])\n", + " results: ResultSetDefault = dict_to_results(\n", + " img_ctx.image_idx, \n", + " data['results'], \n", + " result_factory=experiment._result_from)\n", + " experiment.update_results(ocr_model, img_ctx.image_idx, results)\n", + " return cls(img_ctx, ocr_model)\n", + "\n", + " @classmethod\n", + " def from_image(cls, \n", + " ctx: OCRExperimentContext, \n", + " ocr_model: str, \n", + " image_idx: ImgSpecT):\n", + " idx = cast(ImgIdT, ctx.normalize_idx(image_idx))\n", + " img_ctx = ImageContext(ctx, idx)\n", + " if img_ctx is None:\n", + " raise ValueError(f\"Image {image_idx} not found in experiment context\")\n", + " fp = img_ctx.cache_dir / cls.file_path_of(img_ctx.page_data, ocr_model)\n", + " if fp.exists(): \n", + " return cast(Self, cls.from_json(cast(OCRExperimentContext, img_ctx.exp), fp))\n", + " return cls(img_ctx, ocr_model)\n", + "\n", + " @classmethod\n", + " def from_method(cls, \n", + " ctx: OCRExperimentContext, \n", + " ocr_model: str, \n", + " image_idx: ImgIdT | str | Path, \n", + " method: CropMethod):\n", + " experiment = cls.from_image(ctx, ocr_model, image_idx)\n", + " if experiment is None:\n", + " return None\n", + " return experiment.method_experiment(method)\n", + "\n", + " @classmethod\n", + " def saved_experiment(cls, \n", + " ctx: OCRExperimentContext, ocr_model: str, image_idx: ImgIdT | str | Path):\n", + " idx = ctx.normalize_idx(image_idx)\n", + " if idx is None: \n", + " logger.warning(f\"Image {image_idx} not found in experiment context\")\n", + " return None\n", + " return cls.from_image(ctx, ocr_model, idx)\n", + "\n", + " @classmethod\n", + " def saved_experiments(cls, ctx: OCRExperimentContext, ocr_model: str) -> list[Self]:\n", + " return [exp for i in range(len(ctx.image_paths))\n", + " if (exp := cls.from_image(ctx, ocr_model, i)) is not None]\n", + " \n", + "\n", + " def result(self, box_idx: BoxIdT, method: CropMethod, ocr: bool=True, rebuild: bool=False):\n", + " ctx, img_ctx = self.ctxs\n", + " return ctx.result(self.ocr_model, img_ctx.image_idx, box_idx, method, ocr, rebuild)\n", + "\n", + " def results(self):\n", + " ctx, img_ctx = self.ctxs\n", + " return cast(ResultSet, ctx.results(self.ocr_model, img_ctx.image_idx))\n", + "\n", + " def has_run(self):\n", + " \"at least one method has run\"\n", + " img_ctx = self.img_ctx\n", + " return len(self.results()) == len(img_ctx.page_data.boxes)\n", + " \n", + " def best_results(self):\n", + " img_ctx = self.img_ctx\n", + " results = self.results()\n", + " if len(results) < len(img_ctx.page_data.boxes): # at least one method has run\n", + " return None\n", + " best = []\n", + " for box_idx in results:\n", + " methods = results[box_idx]\n", + " best_method = max(methods, key=lambda m: methods[m].acc) # type: ignore\n", + " best.append((best_method, methods[best_method]))\n", + " return best\n", + "\n", + " def save_results_as_ground_truth(self, overwrite=False):\n", + " img_ctx = self.img_ctx\n", + " gts_path = ground_truth_path(img_ctx.page_data)\n", + " if overwrite or not gts_path.exists():\n", + " best_results = self.best_results()\n", + " if best_results:\n", + " tt = [r.ocr for m,r in best_results]\n", + " gts_path.write_text('\\n'.join(tt), encoding=\"utf-8\")\n", + " img_ctx.setup_ground_truth()\n", + " logger.info(f\"Ground truth data saved successfully to {gts_path}\")\n", + " return True\n", + " else:\n", + " logger.info(\"No best results available to save.\")\n", + " return False\n", + " else:\n", + " return False\n", + "\n", + " @property\n", + " def experiments(self):\n", + " if not hasattr(self, '_experiments'):\n", + " self._experiments = {}\n", + " return self._experiments\n", + " def method_experiment(self, method: CropMethod) -> ExperimentOCRMethod:\n", + " if method not in self.experiments:\n", + " self.experiments[method] = ExperimentOCRMethod(self, method)\n", + " return self.experiments[method]\n", + " \n", + "\n", + " def to_dataframe(self):\n", + " \"Dataframe with crop methods as columns and box ids as rows\"\n", + " methods = list(CropMethod.__members__.values())\n", + " experiments = [self.method_experiment(m) for m in methods]\n", + " accuracies = [[result.acc for result in exp.results()] for exp in experiments]\n", + " # transpose accuracies\n", + " accuracies = list(zip(*accuracies))\n", + " return pd.DataFrame(accuracies, columns=CropMethod.__display_names__())\n", + "\n", + " def plot_accuracies(self, \n", + " methods: list[CropMethod] | None = None, \n", + " ):\n", + " \"Plots a horizontal bar chart of the accuracies for a list of method experiments.\"\n", + " methods = methods or list(CropMethod.__members__.values())\n", + " experiments = [self.method_experiment(m) for m in methods]\n", + " if not experiments: return\n", + "\n", + " ctx, img_ctx = self.ctxs\n", + " page_data = img_ctx.page_data\n", + " model = self.ocr_model\n", + " accuracies = [[result.acc for result in exp.results()] for exp in experiments]\n", + " accuracies = [np.mean(a) for a in accuracies]\n", + " # accuracies = [np.mean([result.acc for result in exp.results()]) for exp in experiments]\n", + "\n", + " _, ax = plt.subplots(figsize=(10, 5))\n", + " \n", + " # Normalize the accuracies for color mapping\n", + " norm = plt.Normalize(min(accuracies), max(accuracies))\n", + " # Color map from red to green\n", + " cmap = plt.get_cmap('RdYlGn')\n", + " colors = cmap(norm(accuracies))\n", + "\n", + " ax.barh([m.value for m in methods], accuracies, color=colors)\n", + "\n", + " ax.set_xscale('log') # Set the x-axis to a logarithmic scale\n", + " ax.set_xlabel('Average Accuracy (log scale)', fontsize=12, fontweight='bold')\n", + "\n", + " ax.set_ylabel('Method', fontsize=12, fontweight='bold')\n", + " ax.set_yticks(range(len(methods)))\n", + " ax.set_yticklabels([f'{method.value} ({acc:.2f})' \n", + " for method, acc in zip(methods, accuracies)], fontsize=12)\n", + " max_acc_index = np.argmax(accuracies)\n", + " ax.get_yticklabels()[max_acc_index].set(color='blue', fontweight='bold')\n", + "\n", + " title_text = (f\"{page_data.original_path} - OCR model: {model}\")\n", + " ax.set_title(title_text, fontsize=12, fontweight='bold')\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + "\n", + " def summary_box(self, box_idx: int):\n", + " results: list[tuple[CropMethod, ResultOCR]] = []\n", + " pb = tqdm(CropMethod.__members__.values(), leave=False, desc=f\"Box #{box_idx+1}\")\n", + " for m in pb:\n", + " r = cast(ResultOCR, self.result(box_idx, m))\n", + " results.append((m, r))\n", + " methods, images, ocrs, accs = zip(\n", + " *map(\n", + " lambda t: (t[0].value, t[1].cache_image(), t[1].diff_tagged(), acc_as_html(t[1].acc)), \n", + " results))\n", + " display_columns([methods, images, accs, ocrs], \n", + " headers=[\"Method\", f\"Box #{box_idx+1}\", \"Accuracy\", \"OCR\"])\n", + "\n", + "\n", + " def summary_method(self, method: CropMethod):\n", + " results = self.method_experiment(method).results()\n", + " methods, images, ocrs, accs = zip(\n", + " *map(\n", + " lambda r: (r.block_idx+1, r.cache_image(), r.diff_tagged(), acc_as_html(r.acc)), \n", + " results))\n", + " display_columns([methods, images, accs, ocrs], \n", + " headers=[\"Box #\", \"Box\", \"Accuracy\", f\"{method.value} OCR\"])\n", + "\n", + "\n", + " def display(self):\n", + " out = []\n", + " for method in CropMethod:\n", + " out.append(f\"---------- {method.value} ----------\")\n", + " results = self.method_experiment(method).results()\n", + " out.extend(results)\n", + " out.append('\\n')\n", + " cprint(*out, soft_wrap=True)\n", + "\n", + "\n", + " def reset(self, box_idx: int | None = None, method: CropMethod | None = None):\n", + " ctx, img_ctx = self.ctxs\n", + " ctx.reset_results(None, img_ctx.image_idx, box_idx, method)\n", + "\n", + " def perform_methods(self, \n", + " methods: CropMethod | list[CropMethod] | None = None, \n", + " box_idxs: BoxIdT | list[BoxIdT] | None = None,\n", + " rebuild: bool = False,\n", + " plot_acc: bool = False\n", + " ):\n", + " if methods is None:\n", + " methods = [*CropMethod.__members__.values()]\n", + " elif isinstance(methods, CropMethod):\n", + " methods = [methods]\n", + " if rebuild:\n", + " _methods = tqdm(methods, desc=\"Methods\")\n", + " else:\n", + " _methods = methods\n", + " for method in _methods:\n", + " method_exp = self.method_experiment(method)\n", + " if method_exp: \n", + " if rebuild:\n", + " method_exp(box_idxs, rebuild=rebuild)\n", + " if plot_acc:\n", + " self.plot_accuracies()\n", + "\n", + " def __call__(self, \n", + " box_idxs: BoxIdT | list[BoxIdT] | None = None,\n", + " methods: CropMethod | list[CropMethod] | None = None, \n", + " save: bool = True,\n", + " display=False, \n", + " rebuild: bool=False, \n", + " save_as_ground_truth=False):\n", + " self.perform_methods(methods, box_idxs, rebuild=rebuild)\n", + " if save_as_ground_truth:\n", + " self.save_results_as_ground_truth(overwrite=True)\n", + " if save:\n", + " self.to_json()\n", + " if display:\n", + " self.display()\n", + " \n", + "\n", + "@dataclasses.dataclass\n", + "class ExperimentOCRMethod:\n", + " ctx: ExperimentOCR\n", + " method: CropMethod\n", + "\n", + " @property\n", + " def exp_ctx(self): return self.ctx\n", + " @property\n", + " def img_ctx(self): return self.ctx.ctx\n", + " @property\n", + " def ctxs(self):\n", + " img_ctx = self.img_ctx\n", + " return cast(OCRExperimentContext, img_ctx.exp), img_ctx, self.ctx\n", + " \n", + " def result(self, box_idx: BoxIdT, ocr: bool=True, rebuild: bool=False) -> ResultOCR | None:\n", + " ctx, img_ctx, exp_ctx = self.ctxs\n", + " return ctx.result(exp_ctx.ocr_model, img_ctx.image_idx, box_idx, self.method, ocr, rebuild)\n", + "\n", + " def results(self, \n", + " box_idxs: BoxIdT | list[BoxIdT] | None = None, \n", + " ocr: bool=True, rebuild: bool=False) -> list[ResultOCR]:\n", + " ctx, img_ctx, exp_ctx = self.ctxs\n", + " if box_idxs is None:\n", + " box_idxs = list(range(len(img_ctx.boxes)))\n", + " elif isinstance(box_idxs, int):\n", + " box_idxs = [box_idxs]\n", + " model = exp_ctx.ocr_model\n", + " results = ctx.method_results(model, img_ctx.image_idx, self.method)\n", + " results = {i:results[i] if i in results else None for i in box_idxs}\n", + " pb = rebuild or not results or any(r is None for r in results.values())\n", + " if pb and len(results) > 2:\n", + " progress_bar = tqdm(list(results.keys()), desc=f\"{self.method.value} - {model}\")\n", + " else:\n", + " progress_bar = list(results.keys())\n", + " results = []\n", + " for i in progress_bar:\n", + " results.append(self.result(i, ocr, rebuild=rebuild))\n", + " return results\n", + "\n", + "\n", + " def get_results_html(self, \n", + " box_idxs: BoxIdT | list[BoxIdT] | None = None,\n", + " max_image_width: int | None = None): \n", + " _, img_ctx, exp_ctx = self.ctxs\n", + " results: list[ResultOCR] = self.results(box_idxs)\n", + " accs = np.array([r.acc for r in results])\n", + " mean_accuracy = np.mean(accs)\n", + " mean_trimmed = trimmed_mean(accs, 0.1)\n", + " # filtered_data = mad_based_outlier(accs)\n", + " # mean_mad = np.mean(filtered_data)\n", + " # filtered_data = iqr_outlier_removal(accs)\n", + " # mean_iqr = np.mean(filtered_data)\n", + " \n", + " descriptions, images, ocrs, accs = zip(*map(\n", + " lambda r: (\n", + " r.block_idx+1, \n", + " r.cache_image(), \n", + " r.diff_tagged(), \n", + " acc_as_html(r.acc)\n", + " ), results))\n", + " non_breakin_space = u'\\u00A0'\n", + " tmpl = \"{}\"\n", + " padded_s = lambda s,n: tmpl.format(s.rjust(n))\n", + " acc_fmt = f\"{mean_accuracy:.2f}/{mean_trimmed:.2f}\"\n", + " w, h = img_ctx.base_image.size\n", + " dim, _dpi = size(w, h), dpi(w, h)\n", + " dim_fmt = f\"{w}x{h} px: {dim[0]:.2f} x {dim[1]:.2f} in @ {_dpi:.2f} dpi\"\n", + " return '\\n
\\n'.join([\n", + " (\"
\"\n", + " f\"{padded_s('Page', 24)}: {img_ctx.page_data.original_path}
\"\n", + " f\"{padded_s('Size', 24)}: {dim_fmt}
\"\n", + " f\"{padded_s('Model', 24)}: {exp_ctx.ocr_model}
\"\n", + " f\"{padded_s('Crop Method', 24)}: {self.method.value}
\"\n", + " f\"{padded_s('Accuracy Mean/Trimmed', 24)}: {acc_fmt}\"\n", + " \"
\"), \n", + " get_columns_html(\n", + " [descriptions, images, accs, ocrs], \n", + " max_image_width, \n", + " headers=[\"Box #\", \"Image\", \"Accuracy\", \"OCR\"]),\n", + " ])\n", + "\n", + " def display(self, \n", + " box_idxs: BoxIdT | list[BoxIdT] | None = None, max_image_width: int | None = None):\n", + " display(HTML(self.get_results_html(box_idxs, max_image_width)))\n", + "\n", + "\n", + " def summary(self):\n", + " results = self.results()\n", + " methods, images, ocrs, accs = zip(\n", + " *map(\n", + " lambda r: (r.block_idx+1, r.cache_image(), r.diff_tagged(), acc_as_html(r.acc)), \n", + " results))\n", + " display_columns([methods, images, accs, ocrs], \n", + " headers=[\"Box #\", \"Box\", \"Accuracy\", f\"{self.method.value} OCR\"])\n", + "\n", + "\n", + " def reset(self):\n", + " _, _, exp_ctx = self.ctxs\n", + " exp_ctx.reset(method=self.method)\n", + " \n", + " def __call__(self, box_idxs: BoxIdT | list[BoxIdT] | None = None, display=False, rebuild=False):\n", + " if isinstance(box_idxs, int):\n", + " result = self.result(cast(BoxIdT, box_idxs), rebuild=rebuild)\n", + " if result is not None and display:\n", + " result.display()\n", + " else:\n", + " results = self.results(box_idxs, rebuild=rebuild)\n", + " if results and display:\n", + " self.display(box_idxs)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Box id\n", + "> change `BOX_IDX` to use any box to test crop methods" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [], + "source": [ + "BOX_IDX = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Crop methods testing" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [], + "source": [ + "CONTEXT.reset()\n", + "test_eq(CONTEXT.results(), {})\n", + "\n", + "image_experiment = ExperimentOCR(IMAGE_CONTEXT, 'Tesseract')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Single box results\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### comics_text_detector initial boxes + padding" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Initial box" + ] + }, + { + "cell_type": "code", + "execution_count": 68, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[32m2024-05-10 20:25:26.385\u001b[0m | \u001b[1mINFO \u001b[0m | \u001b[36mpcleaner.ocr.ocr_mangaocr\u001b[0m:\u001b[36m__new__\u001b[0m:\u001b[36m15\u001b[0m - \u001b[1mCreating the MangaOcr instance\u001b[0m\n" + ] + }, + { + "data": { + "text/html": [ + "
Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3
0.90
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.⎕⎕

Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "method = CropMethod.INITIAL_BOX\n", + "\n", + "result = image_experiment.result(BOX_IDX, method, ocr=False)\n", + "assert result is not None\n", + "\n", + "image = result.image\n", + "assert image is not None\n", + "text = CONTEXT.mocr('Tesseract', page_lang)(image)\n", + "result.ocr = postprocess_ocr(text)\n", + "result\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### PanelCleaner default pad" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Eneowered by great gnarled cypress jrfes, the ancient manor ! alone on the eit of mew rce: eans, kept tipy by a white-haired ao han known only as
0.85
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orl⎕⎕⎕eans, kept tidy by a white-haired old man known only as Bambu.

Eneowered by great gnarled cypress jrfes, the ancient manor !⎕⎕⎕⎕⎕ alone on the eit⎕⎕⎕⎕⎕⎕ of mew rce: eans, kept tipy by a white-haired ao⎕⎕ han known only as⎕⎕⎕⎕⎕⎕⎕
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "method = CropMethod.DEFAULT\n", + "\n", + "result = image_experiment.result(BOX_IDX, method, ocr=False)\n", + "assert result is not None\n", + "\n", + "CONTEXT.ocr_box(result, 'Tesseract', page_lang)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### PanelCleaner default pad, grey pad" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Enbowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as
0.95
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.

Enbowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as⎕⎕⎕⎕⎕⎕⎕
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "method = CropMethod.DEFAULT_GREY_PAD\n", + "result = image_experiment.result(BOX_IDX, method)\n", + "result\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### padded, 4px" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Enbonered by great gnarled cypress jrfes, the ancient manor stands alone on the eit of mew rce: eans, kept tipy by a white-haired ao lo man known only as
0.88
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orl⎕⎕⎕eans, kept tidy by a white-haired old man known only as Bambu.

Enbonered by great gnarled cypress jrfes, the ancient manor stands alone on the eit⎕⎕⎕⎕⎕⎕ of mew rce: eans, kept tipy by a white-haired aolo man known only as⎕⎕⎕⎕⎕⎕⎕
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(BOX_IDX, CropMethod.PADDED_4)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### padded, 8px" + ] + }, + { + "cell_type": "code", + "execution_count": 72, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Enbonered by great gnarled cypress trees, the ancient manor stands alone on the pag hile of new orleans, kept tipy by a white-haired ao lo man known omy as
0.88
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.

Enbonered by great gnarled cypress trees, the ancient manor stands alone on the pag hile of new orleans, kept tipy by a white-haired aolo man known omy as⎕⎕⎕⎕⎕⎕⎕
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(BOX_IDX, CropMethod.PADDED_8)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Extracted text" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Extracted text, initial box\n", + "\n", + "Unfortunately, the `comic-text_detector` does not remove letter holes from the text mask, despite using OpenCV. This oversight likely impacts the accuracy of the OCR results." + ] + }, + { + "cell_type": "code", + "execution_count": 73, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
0.92
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu⎕⎕.

Fhbonered by great snarled cypress trees, the ancient nmanor stands alone on the outskirts of new orleans. kept tipy by a white-haire old man known only as bambi] .
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "method = CropMethod.EXTRACTED_INIT_BOX\n", + "# results[method] = IMAGE_CONTEXT.result(BOX_IDX, method)\n", + "# image = results[method].image\n", + "# assert image is not None\n", + "# results[method].ocr = postprocess_ocr(IMAGE_CONTEXT.mocr(image))\n", + "# display_extracted_result(None, None, results[method], IMAGE_CONTEXT.gts[BOX_IDX])\n", + "image_experiment.result(BOX_IDX, method)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### padded 4, extracted" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
0.91
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.

Enbonsred by great shale cypress trees, the anci⎕⎕⎕ manor stands alone on the [tskirts of new orleans, kept tidy by a white-haired old man known only as b8ambl .
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(BOX_IDX, CropMethod.PADDED_4_EXTRACTED)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### padded 8, extracted" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
0.94
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu⎕⎕.

Enbonered by great snarled cypress trees, the ancient nmanor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as 8ambli .
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(BOX_IDX, CropMethod.PADDED_8_EXTRACTED)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### padded 8, dilation 1, extracted" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
0.61
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.

O⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕utskirts of new orleans, kept tipy by a white-haired old man known only as sams .
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(BOX_IDX, CropMethod.PADDED_8_DILATION_1)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### padded 8, dilation 0.5, extracted" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
0.94
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.

Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as b8ambl .
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(BOX_IDX, CropMethod.PAD_8_FRACT_0_5)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### padded 8, dilation 0.2, extracted" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
0.94
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.

Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as b8ambl .
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(BOX_IDX, CropMethod.PAD_8_FRACT_0_2)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Summary\n", + "> Use `ImageContext.summary_box` to display the results of the crop methods for OCR of a given box index.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2b5b75333766431bb85d2ae72ee47b50", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Box #1: 0%| | 0/11 [00:00MethodBox #1AccuracyOCRInitial box
0.90
Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3
Default
0.85
Eneowered by great gnarled cypress jrfes, the ancient manor !⎕⎕⎕⎕⎕ alone on the eit⎕⎕⎕⎕⎕⎕ of mew rce: eans, kept tipy by a white-haired ao⎕⎕ han known only as⎕⎕⎕⎕⎕⎕⎕
Default, grey pad
0.95
Enbowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as⎕⎕⎕⎕⎕⎕⎕
Padded 4px
0.88
Enbonered by great gnarled cypress jrfes, the ancient manor stands alone on the eit⎕⎕⎕⎕⎕⎕ of mew rce: eans, kept tipy by a white-haired aolo man known only as⎕⎕⎕⎕⎕⎕⎕
Padded 8px
0.88
Enbonered by great gnarled cypress trees, the ancient manor stands alone on the pag hile of new orleans, kept tipy by a white-haired aolo man known omy as⎕⎕⎕⎕⎕⎕⎕
Extracted, init box
0.92
Fhbonered by great snarled cypress trees, the ancient nmanor stands alone on the outskirts of new orleans. kept tipy by a white-haire old man known only as bambi] .
Padded 4, extracted
0.91
Enbonsred by great shale cypress trees, the anci⎕⎕⎕ manor stands alone on the [tskirts of new orleans, kept tidy by a white-haired old man known only as b8ambl .
Padded 8, extracted
0.94
Enbonered by great snarled cypress trees, the ancient nmanor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as 8ambli .
Padded 8, dilation 1
0.61
O⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕utskirts of new orleans, kept tipy by a white-haired old man known only as sams .
Pad 8, fract. 0.5
0.94
Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as b8ambl .
Pad 8, fract. 0.2
0.94
Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as b8ambl .
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# methods, images, ocrs, accs = zip(\n", + "# *map(lambda t: (t[0].value, t[1].cache_image(), t[1].diff_tagged(), acc_as_html(t[1].acc)), \n", + "# IMAGE_CONTEXT.results[BOX_IDX].items()))\n", + "# display_columns([methods, images, accs, ocrs], headers=[\"Method\", \"Box\", \"Accuracy\", \"OCR\"])\n", + "\n", + "image_experiment.summary_box(BOX_IDX)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Show result for any box # and any method" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Tonight, he comes host slamming open
0.61
\n", + "
\n", + "
--and tonight, he comes most urgently, slamming open the oaken front doors!

T⎕⎕⎕⎕⎕⎕onight, he comes host⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ slamming open⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(4, CropMethod.PADDED_8)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
0.67
\n", + "
\n", + "
Bambu-- we have a guest.

⎕⎕⎕⎕⎕⎕ we have a i=s7t.
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(3, CropMethod.EXTRACTED_INIT_BOX)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ResultVisor" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class ResultVisor(ContextVisor):\n", + " ctx: ExperimentOCR\n", + " control_names: list[str] = ['all_boxes', 'box_idx', 'all_methods', 'method']\n", + "\n", + " _css = \"\"\"\n", + " .box_grp {\n", + " background-color: aliceblue;\n", + " }\n", + " .method_grp {\n", + " background-color: #ededed;\n", + " }\n", + " \"\"\"\n", + " \n", + " def best_results(self): \n", + " ll = self.ctx.best_results()\n", + " if ll:\n", + " cprint([(m.value, f\"{r.acc:.3f}\", r.ocr) for m,r in ll])\n", + "\n", + " def pd_to_html(self):\n", + " df = self.ctx.to_dataframe()\n", + " # set float precision\n", + " df = df.round(3)\n", + " # display floats with 3 decimal digits\n", + " df = df.applymap(lambda x: f\"{x:.3f}\")\n", + " # highlight max value in each row\n", + " stl = df.style.highlight_max(axis=0)\n", + " display(HTML(stl.to_html()))\n", + "\n", + " def update_output(self, **kwargs):\n", + " all_boxes: bool = self.values['all_boxes']\n", + " box_idx: int = self.values['box_idx']\n", + " all_methods: bool = self.values['all_methods']\n", + " method: CropMethod = self.values['method']\n", + "\n", + " # cprint(f\"all_boxes: {all_boxes}, box_idx: {box_idx}, all_methods: {all_methods}, method: {method}\")\n", + "\n", + " if all_boxes and all_methods:\n", + " self.ctx.plot_accuracies()\n", + " elif all_boxes:\n", + " self.ctx.summary_method(method)\n", + " elif all_methods:\n", + " self.ctx.summary_box(box_idx)\n", + " else:\n", + " result = self.ctx.result(box_idx, method)\n", + " if result is not None:\n", + " result.display()\n", + "\n", + " def setup_controls(self):\n", + " _, img_ctx = self.ctx.ctxs\n", + " values = self.values\n", + " box_wdgt = W.BoundedIntText(\n", + " value=values['box_idx'], min=0, max=len(img_ctx.boxes)-1, step=1,\n", + " disabled=values['all_boxes'],\n", + " layout={'width': '50px'},\n", + " style={'description_width': 'initial'})\n", + " methods_wdgt = W.Dropdown(\n", + " options=CropMethod.__display_names__(), \n", + " value=values['method'],\n", + " layout={'width': '150px'},\n", + " style={'description_width': 'initial'})\n", + " all_boxes_wdgt = W.Checkbox(label='All', value=values['all_boxes'], \n", + " description=\"all\", \n", + " layout={'width': 'initial'},\n", + " style={'description_width': '0px'})\n", + " all_methods_wdgt = W.Checkbox(label='All', value=values['all_methods'], \n", + " description=\"all\", \n", + " layout={'width': 'initial'},\n", + " style={'description_width': '0px'})\n", + " return {'all_boxes': all_boxes_wdgt, 'box_idx': box_wdgt, \n", + " 'all_methods': all_methods_wdgt, 'method': methods_wdgt}\n", + " \n", + " def setup_ui(self):\n", + " ctls = self.controls\n", + " _, img_ctx = self.ctx.ctxs\n", + " box_label = W.Label(\n", + " value=f\"Box # (of {len(img_ctx.boxes)}):\", \n", + " layout={'width': 'initial', 'padding': '0px 0px 0px 10px'})\n", + " method_label = W.Label(value='Method:', layout={'width': 'initial', 'padding': '0px 0px 0px 10px'})\n", + "\n", + " box_grp = W.HBox([box_label, ctls['all_boxes'], ctls['box_idx']])\n", + " box_grp.add_class('box_grp')\n", + " method_grp = W.HBox([method_label, ctls['all_methods'], ctls['method']])\n", + " method_grp.add_class('method_grp')\n", + " \n", + " return W.HBox([box_grp, method_grp])\n", + "\n", + " def __init__(self, \n", + " ctx: OCRExperimentContext | ExperimentOCR,\n", + " img_idx: int | str | Path | None = None,\n", + " all_boxes: bool = False,\n", + " box_idx: int = 0,\n", + " all_methods: bool = False,\n", + " method: CropMethod=CropMethod.INITIAL_BOX,\n", + " out: W.Output | None = None,\n", + " ):\n", + " if isinstance(ctx, OCRExperimentContext):\n", + " assert img_idx is not None, \"img_idx must be provided if ctx is an ExperimentContext\"\n", + " exp = ExperimentOCR.from_image(ctx, 'Tesseract', img_idx)\n", + " if not exp:\n", + " raise ValueError(f\"Image {img_idx} not found in experiment context\")\n", + " ctx = exp\n", + " else:\n", + " if not isinstance(ctx, ExperimentOCR):\n", + " raise ValueError(\"ctx must be an ExperimentOCR or OCRExperimentContext\")\n", + " \n", + " super().__init__(ctx, {'all_boxes': all_boxes, 'box_idx': box_idx, \n", + " 'all_methods': all_methods, 'method': method}, out=out or self.out)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "400781b58e3e43cb868c0c278fd3ecd2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output(layout=Layout(height='0px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2f2917080eb943acb8b665e1fa607c13", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(Label(value='Box # (of 15):', layout=Layout(padding='0px 0px 0px 10px', width='i…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "81027eadfa3d40b6a81efcd991f2379a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cleanupwidgets('result_visor')\n", + "\n", + "result_visor = ResultVisor(image_experiment)\n", + "result_visor\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# ExperimentVisor\n" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class ExperimentVisor(ContextVisor):\n", + " ctx: ExperimentOCR\n", + "\n", + " def update_output(self, \n", + " image_idx: int | None = None,\n", + " **kwargs):\n", + " exp_ctx, img_ctx = self.ctx.ctxs\n", + " if image_idx is not None and image_idx != img_ctx.image_idx:\n", + " ctx = ImageContext(exp_ctx, image_idx)\n", + " assert ctx is not None\n", + " self.ctx.ctx = ctx\n", + " result_visor = self.comp('result_visor')\n", + " if result_visor is not None:\n", + " result_visor.update_output(**kwargs)\n", + "\n", + " def __init__(self, \n", + " ctx: OCRExperimentContext | ExperimentOCR,\n", + " img_idx: int | str | Path | None = None,\n", + " all_boxes: bool = False,\n", + " box_idx: int = 0,\n", + " all_methods: bool = False,\n", + " method: CropMethod=CropMethod.INITIAL_BOX,\n", + " out: W.Output | None = None,\n", + " ):\n", + " if isinstance(ctx, OCRExperimentContext):\n", + " assert img_idx is not None, \"img_idx must be provided if ctx is an ExperimentContext\"\n", + " exp = ExperimentOCR.from_image(ctx, 'Tesseract', img_idx)\n", + " if not exp:\n", + " raise ValueError(f\"Image {img_idx} not found in experiment context\")\n", + " ctx = exp\n", + " else:\n", + " if not issubclass(type(ctx), ExperimentOCR):\n", + " raise ValueError(\"ctx must be an ExperimentOCR or OCRExperimentContext\")\n", + " \n", + " exp_ctx, img_ctx = ctx.ctxs\n", + " out = out or self.out\n", + " image_selector = ImageSelector(exp_ctx, image_idx=img_ctx.image_idx, out=out)\n", + " result_visor = ResultVisor(ctx, out=out,\n", + " all_boxes=all_boxes, box_idx=box_idx, all_methods=all_methods, method=method)\n", + "\n", + " super().__init__(ctx, {}, out=out, \n", + " ctxs={'image_selector': image_selector, 'result_visor': result_visor},\n", + " hdlrs={'display_option': result_visor}\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1e40a9f23a674b48abd50720e6523c2c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(Dropdown(index=20, layout=Layout(width='fit-content'), options={'Action_Comics_1…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5af1d96d5d9942fd8e895787ff50bbe5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "cleanupwidgets('exp_visor')\n", + "\n", + "exp_visor = ExperimentVisor(image_experiment)\n", + "exp_visor\n" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": {}, + "outputs": [], + "source": [ + "exp_visor.update(box_idx=1)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": {}, + "outputs": [], + "source": [ + "exp_visor.update(image_idx=0)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Method experiment\n", + "> perform method on one or more boxes" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "metadata": {}, + "outputs": [], + "source": [ + "CONTEXT.reset()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "metadata": {}, + "outputs": [], + "source": [ + "image_experiment = ExperimentOCR(IMAGE_CONTEXT, 'Tesseract')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Visualize summary of crop methods on a given box\n" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "151bc83d198e447d8932fe48aa7ef2e6", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Box #2: 0%| | 0/11 [00:00MethodBox #2AccuracyOCRInitial box
0.93
The house and the old man are alike in many ways; tall, prolid, patient, contented always 0 wait until. their. master cones mome ~~
Default
0.96
The house and the old man are alike in many ways; tall, proud, patient, contentel always to wait until their. master cones home ~~
Default, grey pad
0.96
The house and the old man are alike in many ways; tall, prolid, patient, contented always to wait until their.⎕* master cones home⎕~-
Padded 4px
0.93
The house and the oldman are alike in many ways; tall, proud, patient, contented a ways 0 wait until their. aster comes home ~~ | }
Padded 8px
0.99
The house and the old man are alike in many ways; tall, proud, patient, contented always to wait until their. master comes home⎕⎕
Extracted, init box
0.93
Ee house and the old man por alike in many ways; tall, proud, patient, contented always t° wait until their. master cones home ~~
Padded 4, extracted
0.98
The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master cones home--
Padded 8, extracted
0.98
The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master comes home--
Padded 8, dilation 1
0.88
The house and the old man are alike in many ways, tall, proud, ⎕⎕⎕⎕⎕nt, contented live⎕⎕⎕⎕ walt gtie their, master comes home-=
Pad 8, fract. 0.5
0.97
The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until. their. master comes home ~~
Pad 8, fract. 0.2
0.97
The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master comes home ~~
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.summary_box(1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Results for any crop method" + ] + }, + { + "cell_type": "code", + "execution_count": 92, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Box #BoxAccuracyPadded 4px OCR
1
0.88
Enbonered by great gnarled cypress jrfes, the ancient manor stands alone on the eit⎕⎕⎕⎕⎕⎕ of mew rce: eans, kept tipy by a white-haired aolo man known only as⎕⎕⎕⎕⎕⎕⎕
2
0.93
The house and the oldman are alike in many ways; tall, proud, patient, contented a ways 0 wait until their. aster comes home ~~ | }
3
0.69
F and one in ee⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ would appear.
4
0.77
" bambli- we have a gliest.
5
0.55
P⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ comes⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ slamming open the caken⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕
6
0.57
Tel oe ⎕⎕⎕⎕er-- 5 ow a =⎕⎕⎕⎕ 7⎕⎕⎕⎕⎕
7
0.38
W⎕⎕e⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ and perhaps c oe /⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕
8
0.75
The⎕⎕⎕⎕⎕⎕⎕⎕ the old man⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕s fades down the hall sra⎕⎕⎕⎕
9
0.92
How curious the a⎕whims of fate. - had i not chanced to stroll along the river yl⎕tonight~-
10
0.79
A⎕⎕⎕ulckly as t can, masrer.
11
0.92
<the girl wolld⎕- most surely be dead by now.
12
0.88
Ghede has been generous. the oeath gop has given -⎕the girl. a second chance ye,⎕alem
13
0.67
Soe⎕⎕ ⎕⎕⎕⎕⎕⎕⎕⎕⎕ereke othing to scream ay⎕⎕⎕ anymore.
14
0.94
"you're among friends now. you're safe!
15
1.00
Continued after next page
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.summary_method(CropMethod.PADDED_4)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can use method experiment directly" + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3
0.90
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.⎕⎕

Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "method_experiment = cast(ExperimentOCRMethod, \n", + " ExperimentOCR.from_method(CONTEXT, 'Tesseract', IMAGE_CONTEXT.image_idx, CropMethod.INITIAL_BOX))\n", + "method_experiment(BOX_IDX, display=True)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "for all boxes" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Page: media/Strange_Tales_172005.jpg
Size: 1275x1888 px: 4.25 x 6.29 in @ 188.32 dpi
Model: Tesseract
Crop Method: Initial box
Accuracy Mean/Trimmed: 0.79/0.80
\n", + "
\n", + "
Box #ImageAccuracyOCR
1
0.90
Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3
2
0.93
The house and the old man are alike in many ways; tall, prolid, patient, contented always 0 wait until. their. master cones mome ~~
3
0.70
“and one in ee⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ would appear.
4
0.62
Re bambli-~ we have a⎕⎕⎕⎕⎕⎕⎕
5
0.70
T⎕⎕⎕⎕⎕⎕onight, he comes noost⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ slamming open the caken⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕
6
0.82
Tell me naster. how may bambli serve 7
7
0.56
£7⎕⎕ »⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ and perhaps some dry clothes...⎕7⎕/
8
0.81
The⎕⎕⎕⎕⎕⎕⎕⎕ the old man'⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕s fades down the hall as...⎕7
9
0.85
How curious the 4⎕fate.⎕whims of h⎕⎕⎕⎕⎕⎕ad t not chanced to stroll along the river yl⎕tonight ==
10
0.80
Fas oulckly as t ca, master.
11
0.91
<the girl would -⎕most slirely be dead by now.
12
0.47
A⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕th⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ the girl. a second chance ge⎕a ee yg adil
13
0.84
Ah⎕⎕⎕ girl--there's othing to scream nt⎕⎕t anymore.
14
0.93
You're among friends now. you're sale
15
1.00
Continued after next page
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "method_experiment(display=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "or selected boxes" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "metadata": {}, + "outputs": [], + "source": [ + "CONTEXT.reset()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Page: media/Strange_Tales_172005.jpg
Size: 1275x1888 px: 4.25 x 6.29 in @ 188.32 dpi
Model: Tesseract
Crop Method: Initial box
Accuracy Mean/Trimmed: 0.80/nan
\n", + "
\n", + "
Box #ImageAccuracyOCR
1
0.90
Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3
2
0.93
The house and the old man are alike in many ways; tall, prolid, patient, contented always 0 wait until. their. master cones mome ~~
7
0.56
£7⎕⎕ »⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ and perhaps some dry clothes...⎕7⎕/
10
0.80
Fas oulckly as t ca, master.
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "method_experiment = cast(ExperimentOCRMethod, \n", + " ExperimentOCR.from_method(CONTEXT, 'Tesseract', IMAGE_CONTEXT.image_idx, CropMethod.INITIAL_BOX))\n", + "method_experiment([0, 1, 6, 9], display=True)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Full page results\n", + "> all methods on all boxes" + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "metadata": {}, + "outputs": [], + "source": [ + "CONTEXT.reset()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "metadata": {}, + "outputs": [], + "source": [ + "image_experiment = ExperimentOCR(IMAGE_CONTEXT, 'Tesseract')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Page: media/Strange_Tales_172005.jpg
Size: 1275x1888 px: 4.25 x 6.29 in @ 188.32 dpi
Model: Tesseract
Crop Method: Initial box
Accuracy Mean/Trimmed: 0.79/0.80
\n", + "
\n", + "
Box #ImageAccuracyOCR
1
0.90
Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3
2
0.93
The house and the old man are alike in many ways; tall, prolid, patient, contented always 0 wait until. their. master cones mome ~~
3
0.70
“and one in ee⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ would appear.
4
0.62
Re bambli-~ we have a⎕⎕⎕⎕⎕⎕⎕
5
0.70
T⎕⎕⎕⎕⎕⎕onight, he comes noost⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ slamming open the caken⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕
6
0.82
Tell me naster. how may bambli serve 7
7
0.56
£7⎕⎕ »⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ and perhaps some dry clothes...⎕7⎕/
8
0.81
The⎕⎕⎕⎕⎕⎕⎕⎕ the old man'⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕s fades down the hall as...⎕7
9
0.85
How curious the 4⎕fate.⎕whims of h⎕⎕⎕⎕⎕⎕ad t not chanced to stroll along the river yl⎕tonight ==
10
0.80
Fas oulckly as t ca, master.
11
0.91
<the girl would -⎕most slirely be dead by now.
12
0.47
A⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕th⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕ the girl. a second chance ge⎕a ee yg adil
13
0.84
Ah⎕⎕⎕ girl--there's othing to scream nt⎕⎕t anymore.
14
0.93
You're among friends now. you're sale
15
1.00
Continued after next page
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "method = CropMethod.INITIAL_BOX\n", + "# method = CropMethod.DEFAULT\n", + "# method = CropMethod.PADDED_4\n", + "# method = CropMethod.PADDED_8\n", + "# method = CropMethod.EXTRACTED_INIT_BOX\n", + "# method = CropMethod.PAD_8_FRACT_0_5\n", + "# method = CropMethod.PAD_8_FRACT_0_2\n", + "\n", + "# image_experiment.method_experiment(CropMethod.INITIAL_BOX).results()\n", + "initial_box_exp = image_experiment.method_experiment(CropMethod.INITIAL_BOX)\n", + "initial_box_exp(display=True)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Other method" + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[ResultOCR#block 00: 0.85||Eneowered by great gnarled cypress jrfes, the ancient manor ! alone on the eit of mew rce: eans, kept tipy by a white-haired ao han known only as,\n", + " ResultOCR#block 01: 0.96||The house and the old man are alike in many ways; tall, proud, patient, conten tel always to wait until their. master cones home ~~,\n", + " ResultOCR#block 02: 0.74||And one in ee would appear.,\n", + " ResultOCR#block 03: 0.41||Rir guest.,\n", + " ResultOCR#block 04: 0.59||=~and tonight, he comes host sane oo,\n", + " ResultOCR#block 05: 0.78||Tell me masts - how may bambli . serve 7 _,\n", + " ResultOCR#block 06: 0.48||R warm, bambli-~ and perhaps,\n", + " ResultOCR#block 07: 0.76||The the old mans fades down the hall s.00,\n", + " ResultOCR#block 08: 0.92||How curious the a whims of fate . had t not chanced to stroll along the river tonight~~ >,\n", + " ResultOCR#block 09: 0.50||Aulckly “master as t can,,\n", + " ResultOCR#block 10: 0.94||" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.perform_methods(plot_acc=True)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
---------- Initial box ----------\n",
+       "ResultOCR#block 00: 0.90||Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3\n",
+       "ResultOCR#block 01: 0.93||The house and the old man are alike in many ways; tall, prolid, patient, contented always 0 wait until. their. master cones mome ~~\n",
+       "ResultOCR#block 02: 0.70||“and one in ee would appear.\n",
+       "ResultOCR#block 03: 0.62||Re bambli-~ we have a\n",
+       "ResultOCR#block 04: 0.70||Tonight, he comes noost slamming open the caken\n",
+       "ResultOCR#block 05: 0.82||Tell me naster. how may bambli serve 7\n",
+       "ResultOCR#block 06: 0.56||£7 » and perhaps some dry clothes... 7 /\n",
+       "ResultOCR#block 07: 0.81||The the old man's fades down the hall as... 7\n",
+       "ResultOCR#block 08: 0.85||How curious the 4 fate. whims of had t not chanced to stroll along the river yl tonight ==\n",
+       "ResultOCR#block 09: 0.80||Fas oulckly as t ca, master.\n",
+       "ResultOCR#block 10: 0.91||<the girl would - most slirely be dead by now.\n",
+       "ResultOCR#block 11: 0.47||Ath the girl. a second chance ge a ee yg adil\n",
+       "ResultOCR#block 12: 0.84||Ah girl--there's othing to scream ntt anymore .\n",
+       "ResultOCR#block 13: 0.93||You're among friends now. you're sale\n",
+       "ResultOCR#block 14: 1.00||Continued after next page\n",
+       "\n",
+       " ---------- Default ----------\n",
+       "ResultOCR#block 00: 0.85||Eneowered by great gnarled cypress jrfes, the ancient manor ! alone on the eit of mew rce: eans, kept tipy by a white-haired ao han known only as\n",
+       "ResultOCR#block 01: 0.96||The house and the old man are alike in many ways; tall, proud, patient, conten tel always to wait until their. master cones home ~~\n",
+       "ResultOCR#block 02: 0.74||And one in ee would appear.\n",
+       "ResultOCR#block 03: 0.41||Rir guest.\n",
+       "ResultOCR#block 04: 0.59||=~and tonight, he comes host sane oo\n",
+       "ResultOCR#block 05: 0.78||Tell me masts - how may bambli . serve 7 _\n",
+       "ResultOCR#block 06: 0.48||R warm, bambli-~ and perhaps\n",
+       "ResultOCR#block 07: 0.76||The the old mans fades down the hall s.00\n",
+       "ResultOCR#block 08: 0.92||How curious the a whims of fate . had t not chanced to stroll along the river tonight~~ >\n",
+       "ResultOCR#block 09: 0.50||Aulckly “master as t can,\n",
+       "ResultOCR#block 10: 0.94||<the girl would - most surely be dead by now.\n",
+       "ResultOCR#block 11: 0.50||Ath - the girl. a second chance ee oo tr tt\n",
+       "ResultOCR#block 12: 0.84||Oe girl--there's othing to scream nt anymore. 4\n",
+       "ResultOCR#block 13: 0.96||You're among friends now. youre safe!\n",
+       "ResultOCR#block 14: 1.00||Continued after next page\n",
+       "\n",
+       " ---------- Default, grey pad ----------\n",
+       "ResultOCR#block 00: 0.95||Enbowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as\n",
+       "ResultOCR#block 01: 0.96||The house and the old man are alike in many ways; tall, prolid, patient, contented always to wait until their. * master cones home ~-\n",
+       "ResultOCR#block 02: 0.94||“and one in > need of some help, it would appear .\n",
+       "ResultOCR#block 03: 0.88||\" bambl-- we have a guest.\n",
+       "ResultOCR#block 04: 0.72||~~and tonight, he comes urgently, slanming open\n",
+       "ResultOCR#block 05: 0.86||Tell me, master: how may bambli serve 7\n",
+       "ResultOCR#block 06: 0.90||Some blankets to keep her. warm, bambli-- and perhaps some dry \\ clothes--7 /.\n",
+       "ResultOCR#block 07: 0.06||As.\n",
+       "ResultOCR#block 08: 0.91||How curious the d 7e . whims of fa; had i not chanced to stroll along the river tonight--\n",
+       "ResultOCR#block 09: 0.55||Ickl as t can\n",
+       "ResultOCR#block 10: 0.00||\n",
+       "ResultOCR#block 11: 0.85||Ghede has been generous. the death god has gen + the girl. a second chance te oe ato\" pd ate\n",
+       "ResultOCR#block 12: 0.95||Easy, girl--there's | nothing to scream about anyaore.\n",
+       "ResultOCR#block 13: 0.97||You're among friends now. you're safe!\n",
+       "ResultOCR#block 14: 0.54||“continued a\n",
+       "\n",
+       " ---------- Padded 4px ----------\n",
+       "ResultOCR#block 00: 0.88||Enbonered by great gnarled cypress jrfes, the ancient manor stands alone on the eit of mew rce: eans, kept tipy by a white-haired ao lo man known only as\n",
+       "ResultOCR#block 01: 0.93||The house and the oldman are alike in many ways; tall, proud, patient, contented a ways 0 wait until their. aster comes home ~~ | }\n",
+       "ResultOCR#block 02: 0.69||F and one in ee would appear.\n",
+       "ResultOCR#block 03: 0.77||\" bambli-— we have a gliest.\n",
+       "ResultOCR#block 04: 0.55||P comes slamming open the caken\n",
+       "ResultOCR#block 05: 0.57||Tel oe er-- 5 ow a = 7\n",
+       "ResultOCR#block 06: 0.38||We and perhaps c oe /\n",
+       "ResultOCR#block 07: 0.75||The the old mans fades down the hall sra\n",
+       "ResultOCR#block 08: 0.92||How curious the a whims of fate . - had i not chanced to stroll along the river yl tonight~-\n",
+       "ResultOCR#block 09: 0.79||Aulckly as t can, ‘masrer.\n",
+       "ResultOCR#block 10: 0.92||<the girl wolld - most surely be dead by now.\n",
+       "ResultOCR#block 11: 0.88||Ghede has been generous. the oeath gop has given - the girl. a second chance ye, alem\n",
+       "ResultOCR#block 12: 0.67||Soe er eke othing to scream ay anymore.\n",
+       "ResultOCR#block 13: 0.94||\"you're among friends now. you're safe!\n",
+       "ResultOCR#block 14: 1.00||Continued after next page\n",
+       "\n",
+       " ---------- Padded 8px ----------\n",
+       "ResultOCR#block 00: 0.88||Enbonered by great gnarled cypress trees, the ancient manor stands alone on the pag hile of new orleans, kept tipy by a white-haired ao lo man known omy as\n",
+       "ResultOCR#block 01: 0.99||The house and the old man are alike in many ways; tall, proud, patient, contented always to wait until their. master comes home\n",
+       "ResultOCR#block 02: 0.67||7 and one in ee would appear,\n",
+       "ResultOCR#block 03: 0.76||Zf mbl == we have a guest.\n",
+       "ResultOCR#block 04: 0.61||Tonight, he comes host slamming open\n",
+       "ResultOCR#block 05: 0.75||Yy i tell me master - how may bambli serve 7 _,\n",
+       "ResultOCR#block 06: 0.89||Some. blankets to keep he arm , bambli-= and perhaps some dry clothes. 2s\n",
+       "ResultOCR#block 07: 0.72||The the old mans fades down the hal. srl see\n",
+       "ResultOCR#block 08: 0.88||* how curious the p whims of fate . - had i not chanced . to stroll along _ the river 3 tonight-~\n",
+       "ResultOCR#block 09: 0.65||Tiie as t can, \\ master ,\n",
+       "ResultOCR#block 10: 0.86||The girl wolld - most slirely be - dead by now.\n",
+       "ResultOCR#block 11: 0.62||Ghede has been generous. : the crn son ue;\n",
+       "ResultOCR#block 12: 0.62||Soe er eke othing to scream hbolt anhore hr\n",
+       "ResultOCR#block 13: 0.92||” you're among friends now. you're safe!\n",
+       "ResultOCR#block 14: 0.94||“continued after next page\n",
+       "\n",
+       " ---------- Extracted, init box ----------\n",
+       "ResultOCRExtracted#block 00: 0.92||Fhbonered by great snarled cypress trees, the ancient nmanor stands alone on the outskirts of new orleans. kept tipy by a whi te-haire old man known only as bambi] .\n",
+       "ResultOCRExtracted#block 01: 0.93||Ee house and the old man por alike in many ways; tall, proud, patient, contented always t° wait until their. master cones home ~~\n",
+       "ResultOCRExtracted#block 02: 0.73||And one in fee would appear.\n",
+       "ResultOCRExtracted#block 03: 0.67||— we have a i=s7t.\n",
+       "ResultOCRExtracted#block 04: 0.74||~and tonight, he comes urgently, slamming open\n",
+       "ResultOCRExtracted#block 05: 0.85||Tell me master how may bambli serve 7\n",
+       "ResultOCRExtracted#block 06: 0.93||Some blankets to keep her warm, banbli-- and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.77||The the old man's fades down the hall s.,00\n",
+       "ResultOCRExtracted#block 08: 0.77||Hin ef fare” had i not chanced to stroll along the river. tonigmt=~\n",
+       "ResultOCRExtracted#block 09: 0.85||Aulckly as t can, master,\n",
+       "ResultOCRExtracted#block 10: 1.00||--the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.51||Ath the girl a second chance ro\n",
+       "ResultOCRExtracted#block 12: 0.56||Cas ee, othing to scream pls aa .\n",
+       "ResultOCRExtracted#block 13: 0.95||You're among friends now. you're sale!\n",
+       "ResultOCRExtracted#block 14: 0.91||Continued af ext page\n",
+       "\n",
+       " ---------- Padded 4, extracted ----------\n",
+       "ResultOCRExtracted#block 00: 0.91||Enbonsred by great shale cypress trees, the anci manor stands alone on the [tskirts of new orleans, kept tidy by a whi te- haired old man known only as b8ambl .\n",
+       "ResultOCRExtracted#block 01: 0.98||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master cones home --\n",
+       "ResultOCRExtracted#block 02: 0.73||And one in fee would appear.\n",
+       "ResultOCRExtracted#block 03: 0.83||Bambli we have a gliest.\n",
+       "ResultOCRExtracted#block 04: 0.74||=~and tonight, he comes urgently, slamming open\n",
+       "ResultOCRExtracted#block 05: 0.84||Tell me master. how may bambli serve 7\n",
+       "ResultOCRExtracted#block 06: 0.53||Warm, bambli-- and perhaps. som\n",
+       "ResultOCRExtracted#block 07: 0.75||The the old mans fades down the hall sra\n",
+       "ResultOCRExtracted#block 08: 0.76||We sea had i not chanced to stroll along the river. tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.76||Alley as t can, master,\n",
+       "ResultOCRExtracted#block 10: 0.98||~-the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.92||Chepe has been generous. the peath god has given the girl a second chance amr\n",
+       "ResultOCRExtracted#block 12: 0.73||Cas gr theres othing to scream pissy tore .\n",
+       "ResultOCRExtracted#block 13: 0.95||You're among eriends now. you're safe!\n",
+       "ResultOCRExtracted#block 14: 0.91||Continued af ext page\n",
+       "\n",
+       " ---------- Padded 8, extracted ----------\n",
+       "ResultOCRExtracted#block 00: 0.94||Enbonered by great snarled cypress trees, the ancient nmanor stands alone on the outskirts of new orleans, kept tipy by a whi te-haired old man known only as 8ambli .\n",
+       "ResultOCRExtracted#block 01: 0.98||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master comes home --\n",
+       "ResultOCRExtracted#block 02: 0.70||And one in fee wolld appear.\n",
+       "ResultOCRExtracted#block 03: 0.86||Bambl ~~ we have a guest.\n",
+       "ResultOCRExtracted#block 04: 0.64||=~and tonight, he comes slamming open urgently,\n",
+       "ResultOCRExtracted#block 05: 0.82||Tell me master... how may bambli serve't\n",
+       "ResultOCRExtracted#block 06: 0.91||Some blankets to keep her warm, banbli-~ and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.75||The the old mans fades down the hall sire\n",
+       "ResultOCRExtracted#block 08: 0.96||How curious the whims of fate . had i not chanced to stroll along the river tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.77||Allckry as t can, master.\n",
+       "ResultOCRExtracted#block 10: 0.98||~-the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.94||Ghede has been generous. the peath god has given the girl a second chance po\n",
+       "ResultOCRExtracted#block 12: 0.74||Cas gr theres othing to scream pps hore .\n",
+       "ResultOCRExtracted#block 13: 0.42||You're safe § r\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       " ---------- Padded 8, dilation 1 ----------\n",
+       "ResultOCRExtracted#block 00: 0.61||Outskirts of new orleans, kept tipy by a white-haired old man known only as sams .\n",
+       "ResultOCRExtracted#block 01: 0.88||The house and the old man are alike in many ways, tall, proud, nt, contented live walt gtie their, master comes home -=\n",
+       "ResultOCRExtracted#block 02: 0.97||And one in need of some help, it wolld appear .\n",
+       "ResultOCRExtracted#block 03: 0.78||Bambli ~~ we have a gliest.\n",
+       "ResultOCRExtracted#block 04: 0.79||=and tonight, he comes most slamming open the front\n",
+       "ResultOCRExtracted#block 05: 0.86||Tell me, master: how may bambli serve 7\n",
+       "ResultOCRExtracted#block 06: 0.85||Gone blankets to keep her. warm, bambli-~ and perhaps some dry\n",
+       "ResultOCRExtracted#block 07: 0.73||The old man's footsteps the hall as.re\n",
+       "ResultOCRExtracted#block 08: 0.94||How curious the whims of fate . had i not chanced to stroll along the r/| ver tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.68||Aulckly as t can,\n",
+       "ResultOCRExtracted#block 10: 0.95||~<the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.75||Ee lh boe ene. the death gop has the girl. a second chance\n",
+       "ResultOCRExtracted#block 12: 0.92||Easy, girl--there's nothing to scream abolit anymo!\n",
+       "ResultOCRExtracted#block 13: 0.97||You're among friends now. you're safe!\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       " ---------- Pad 8, fract. 0.5 ----------\n",
+       "ResultOCRExtracted#block 00: 0.94||Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a whi te-haired old man known only as b8ambl .\n",
+       "ResultOCRExtracted#block 01: 0.97||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until. their. master comes home ~~\n",
+       "ResultOCRExtracted#block 02: 0.78||And one in eee pe would appear.\n",
+       "ResultOCRExtracted#block 03: 0.86||Bambl ~~ we have a guest.\n",
+       "ResultOCRExtracted#block 04: 0.64||=~and tonight, he comes slamming open urgently,\n",
+       "ResultOCRExtracted#block 05: 0.82||Tell me master... how may bambli serve'7\n",
+       "ResultOCRExtracted#block 06: 0.94||Some blankets to keep her. warm, bambli-~ and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.73||The the old mans fades donn the hall sire\n",
+       "ResultOCRExtracted#block 08: 0.96||How curious the whims of fate . had i not chanced to stroll along the river tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.81||Aulckry as t can, master.\n",
+       "ResultOCRExtracted#block 10: 0.98||~-the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.92||Ghede hag been generous. the peath god has given the girl a second chance po\n",
+       "ResultOCRExtracted#block 12: 0.76||Cas srl theres othing to scream seoit hore .\n",
+       "ResultOCRExtracted#block 13: 0.42||You're safe 4 ’\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       " ---------- Pad 8, fract. 0.2 ----------\n",
+       "ResultOCRExtracted#block 00: 0.94||Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a whi te-haired old man known only as b8ambl .\n",
+       "ResultOCRExtracted#block 01: 0.97||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master comes home ~~\n",
+       "ResultOCRExtracted#block 02: 0.77||And one in eet sve would appear.\n",
+       "ResultOCRExtracted#block 03: 0.86||Bambl ~~ we have a guest.\n",
+       "ResultOCRExtracted#block 04: 0.64||=~and tonight, he comes slamming open urgently,\n",
+       "ResultOCRExtracted#block 05: 0.82||Tell me master... how may bambli serve'7\n",
+       "ResultOCRExtracted#block 06: 0.94||Some blankets to keep her, warm, bambli-~ and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.75||The the old mans fades down the hall sere\n",
+       "ResultOCRExtracted#block 08: 0.96||How curious the whims of fate . had i not chanced to stroll along the river tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.81||Aulckry as t can, master.\n",
+       "ResultOCRExtracted#block 10: 0.95||~<the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.94||Ghede has been generous. the ceath god has given the girl a second chance po\n",
+       "ResultOCRExtracted#block 12: 0.67||Yi renee othing to scream seat anhore .\n",
+       "ResultOCRExtracted#block 13: 0.93||Youre among eriends now. you're safe!\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       "\n",
+       "
\n" + ], + "text/plain": [ + "---------- Initial box ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.90\u001b[0m||Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski\u001b[1m)\u001b[0m \u001b[1;36m2\u001b[0m of mew ce eans, kept tidy by a white-haired old man known only as bambs, \u001b[1;36m3\u001b[0m\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.93\u001b[0m||The house and the old man are alike in many ways; tall, prolid, patient, contented always \u001b[1;36m0\u001b[0m wait until. their. master cones mome ~~\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.70\u001b[0m||“and one in ee would appear.\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.62\u001b[0m||Re bambli-~ we have a\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.70\u001b[0m||Tonight, he comes noost slamming open the caken\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.82\u001b[0m||Tell me naster. how may bambli serve \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.56\u001b[0m||£\u001b[1;36m7\u001b[0m » and perhaps some dry clothes\u001b[33m...\u001b[0m \u001b[1;36m7\u001b[0m \u001b[35m/\u001b[0m\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.81\u001b[0m||The the old man's fades down the hall as\u001b[33m...\u001b[0m \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.85\u001b[0m||How curious the \u001b[1;36m4\u001b[0m fate. whims of had t not chanced to stroll along the river yl tonight ==\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.80\u001b[0m||Fas oulckly as t ca, master.\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.91\u001b[0m||\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.50\u001b[0m||Aulckly “master as t can,\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.94\u001b[0m|| need of some help, it would appear .\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.88\u001b[0m||\" bambl-- we have a guest.\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.72\u001b[0m||~~and tonight, he comes urgently, slanming open\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.86\u001b[0m||Tell me, master: how may bambli serve \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.90\u001b[0m||Some blankets to keep her. warm, bambli-- and perhaps some dry \\ clothes-\u001b[1;36m-7\u001b[0m \u001b[35m/\u001b[0m\u001b[95m.\u001b[0m\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.06\u001b[0m||As.\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.91\u001b[0m||How curious the d 7e . whims of fa; had i not chanced to stroll along the river tonight--\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.55\u001b[0m||Ickl as t can\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.00\u001b[0m||\n", + "ResultOCR#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.85\u001b[0m||Ghede has been generous. the death god has gen + the girl. a second chance te oe ato\" pd ate\n", + "ResultOCR#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.95\u001b[0m||Easy, girl--there's | nothing to scream about anyaore.\n", + "ResultOCR#block \u001b[1;36m13\u001b[0m: \u001b[1;36m0.97\u001b[0m||You're among friends now. you're safe!\n", + "ResultOCR#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.54\u001b[0m||“continued a\n", + "\n", + " ---------- Padded 4px ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.88\u001b[0m||Enbonered by great gnarled cypress jrfes, the ancient manor stands alone on the eit of mew rce: eans, kept tipy by a white-haired ao lo man known only as\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.93\u001b[0m||The house and the oldman are alike in many ways; tall, proud, patient, contented a ways \u001b[1;36m0\u001b[0m wait until their. aster comes home ~~ | \u001b[1m}\u001b[0m\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.69\u001b[0m||F and one in ee would appear.\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.77\u001b[0m||\" bambli-— we have a gliest.\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.55\u001b[0m||P comes slamming open the caken\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.57\u001b[0m||Tel oe er-- \u001b[1;36m5\u001b[0m ow a = \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.38\u001b[0m||We and perhaps c oe \u001b[35m/\u001b[0m\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.75\u001b[0m||The the old mans fades down the hall sra\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.92\u001b[0m||How curious the a whims of fate . - had i not chanced to stroll along the river yl tonight~-\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.79\u001b[0m||Aulckly as t can, ‘masrer.\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.92\u001b[0m||" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.plot_accuracies()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 103, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "RenderJSON(image_experiment.to_dict(), 400, 2)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "best results" + ] + }, + { + "cell_type": "code", + "execution_count": 104, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
[\n",
+       "    (\n",
+       "        'Default, grey pad',\n",
+       "        '0.953',\n",
+       "        'Enbowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of \n",
+       "new orleans, kept tipy by a white-haired old man known only as'\n",
+       "    ),\n",
+       "    (\n",
+       "        'Padded 8px',\n",
+       "        '0.988',\n",
+       "        'The house and the old man are alike in many ways; tall, proud, patient, contented always to \n",
+       "wait until their. master comes home'\n",
+       "    ),\n",
+       "    ('Padded 8, dilation 1', '0.968', 'And one in need of some help, it wolld appear .'),\n",
+       "    ('Default, grey pad', '0.880', '\" bambl-- we have a guest.'),\n",
+       "    ('Padded 8, dilation 1', '0.794', '=and tonight, he comes most slamming open the front'),\n",
+       "    ('Default, grey pad', '0.857', 'Tell me, master: how may bambli serve 7'),\n",
+       "    (\n",
+       "        'Pad 8, fract. 0.5',\n",
+       "        '0.935',\n",
+       "        'Some blankets to keep her. warm, bambli-~ and perhaps. some dry clothes'\n",
+       "    ),\n",
+       "    ('Initial box', '0.811', \"The the old man's fades down the hall as... 7\"),\n",
+       "    (\n",
+       "        'Padded 8, extracted',\n",
+       "        '0.959',\n",
+       "        'How curious the whims of fate . had i not chanced to stroll along the river tonight-~'\n",
+       "    ),\n",
+       "    ('Extracted, init box', '0.846', 'Aulckly as t can, master,'),\n",
+       "    ('Extracted, init box', '1.000', '--the girl would most surely be dead by now.'),\n",
+       "    (\n",
+       "        'Padded 8, extracted',\n",
+       "        '0.935',\n",
+       "        'Ghede has been generous. the peath god has given the girl a second chance po'\n",
+       "    ),\n",
+       "    ('Default, grey pad', '0.953', \"Easy, girl--there's | nothing to scream about anyaore.\"),\n",
+       "    ('Default, grey pad', '0.974', \"You're among friends now. you're safe!\"),\n",
+       "    ('Initial box', '1.000', 'Continued after next page')\n",
+       "]\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1m[\u001b[0m\n", + " \u001b[1m(\u001b[0m\n", + " \u001b[32m'Default, grey pad'\u001b[0m,\n", + " \u001b[32m'0.953'\u001b[0m,\n", + " \u001b[32m'Enbowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of \u001b[0m\n", + "\u001b[32mnew orleans, kept tipy by a white-haired old man known only as'\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\n", + " \u001b[32m'Padded 8px'\u001b[0m,\n", + " \u001b[32m'0.988'\u001b[0m,\n", + " \u001b[32m'The house and the old man are alike in many ways; tall, proud, patient, contented always to \u001b[0m\n", + "\u001b[32mwait until their. master comes home'\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\u001b[32m'Padded 8, dilation 1'\u001b[0m, \u001b[32m'0.968'\u001b[0m, \u001b[32m'And one in need of some help, it wolld appear .'\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\u001b[32m'Default, grey pad'\u001b[0m, \u001b[32m'0.880'\u001b[0m, \u001b[32m'\" bambl-- we have a guest.'\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\u001b[32m'Padded 8, dilation 1'\u001b[0m, \u001b[32m'0.794'\u001b[0m, \u001b[32m'=and tonight, he comes most slamming open the front'\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\u001b[32m'Default, grey pad'\u001b[0m, \u001b[32m'0.857'\u001b[0m, \u001b[32m'Tell me, master: how may bambli serve 7'\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\n", + " \u001b[32m'Pad 8, fract. 0.5'\u001b[0m,\n", + " \u001b[32m'0.935'\u001b[0m,\n", + " \u001b[32m'Some blankets to keep her. warm, bambli-~ and perhaps. some dry clothes'\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\u001b[32m'Initial box'\u001b[0m, \u001b[32m'0.811'\u001b[0m, \u001b[32m\"The the old man's fades down the hall as... 7\"\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\n", + " \u001b[32m'Padded 8, extracted'\u001b[0m,\n", + " \u001b[32m'0.959'\u001b[0m,\n", + " \u001b[32m'How curious the whims of fate . had i not chanced to stroll along the river tonight-~'\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\u001b[32m'Extracted, init box'\u001b[0m, \u001b[32m'0.846'\u001b[0m, \u001b[32m'Aulckly as t can, master,'\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\u001b[32m'Extracted, init box'\u001b[0m, \u001b[32m'1.000'\u001b[0m, \u001b[32m'--the girl would most surely be dead by now.'\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\n", + " \u001b[32m'Padded 8, extracted'\u001b[0m,\n", + " \u001b[32m'0.935'\u001b[0m,\n", + " \u001b[32m'Ghede has been generous. the peath god has given the girl a second chance po'\u001b[0m\n", + " \u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\u001b[32m'Default, grey pad'\u001b[0m, \u001b[32m'0.953'\u001b[0m, \u001b[32m\"Easy, girl--there's | nothing to scream about anyaore.\"\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\u001b[32m'Default, grey pad'\u001b[0m, \u001b[32m'0.974'\u001b[0m, \u001b[32m\"You're among friends now. you're safe!\"\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1m(\u001b[0m\u001b[32m'Initial box'\u001b[0m, \u001b[32m'1.000'\u001b[0m, \u001b[32m'Continued after next page'\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m]\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ll = image_experiment.best_results()\n", + "if ll:\n", + " cprint([(m.value, f\"{r.acc:.3f}\", r.ocr) for m,r in ll])\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perfom experiments given a list of `CropMethod`s\n" + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "metadata": {}, + "outputs": [], + "source": [ + "# methods = [*CropMethod.__members__.values()]\n", + "methods = [CropMethod.INITIAL_BOX, CropMethod.DEFAULT]\n", + "image_experiment.perform_methods(methods)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Plot the perfomance of the experiments" + ] + }, + { + "cell_type": "code", + "execution_count": 106, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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lagPAP9A7qMs7n/Jb9CggVU+adIkq7xkyZJGkgkODrbKmjRpkuVfwiWZGjVqGGOMef/9962yzp07e7SlcePG1rrLjXSnpaWZ0aNHm/r165uIiAivIzM//vijVX/69OlGkhk7dmyG12Dq1Knmxhtv9LqPiIgI88cff1h1Bw8enOVf+N1HuteuXZth/V9//WX69+9vqlatao18uj9CQkKsuu6jgh07drTKt23bZpW3bt3aGGPMjh07PEY/3Eeunn/++QwjIStWrLjseybJzJ07N8MxXE5Wo0k5Pf6ctts10hQUFGQefPBB8+6775ply5aZkydPZvu43GVnpDs5Odn63Pj5+Zldu3ZlWf/w4cMeI1UvvfSSx/rKlStb69xPe01LS/N4Df7880+zbt066+fSpUt77OfDDz+01rVp08YqnzZtmpk9e7Y5ePCgOXfunPn5559N3bp1rbruo+5Z8dYH3N/T//u///Oo7/75X7hwoUlKSjJ+fn5Gco40ur9nEydOtOraMdLtPiL89NNPe63z1FNPWXXatm1rjDFm5MiRVlmtWrV8ei737+BLH8WKFTMfffSRT/tx/9w1bNjQKp8wYYJVftNNN1nlgwYNssr/85//GGM8+4T7d44xxnTo0MFa52qTq19LMtu2bbPqLlu2zCp3jVKfO3fOBAQEXPaz+/jjjxtjsh7pzqnMvpvs+l6pXbu29Xvk8ccfNxMnTjSrVq0yZ86csers2rXLp+cePXp0hmOoWLGi18ui1q5da9q3b28iIyO9jt4/8sgjxhhjvv76a6usQoUKHr83LnW5332AN8xeDgBeVKxY0VoODQ21lsuVK2ctu0ZuXH8Zl7yPkl3KNULgPnJ66WhIdHS0fvjhB5/a+vTTT1sj5plxjSxK0pw5cyTJ6+hmz5491bNnTx0/flw//PCDFi9erGnTpiktLU0JCQn66quv1K9fP5/a5a5evXoeP1+4cEEtW7bU1q1bM90ms1sQuU/oFhYWlqG+++taunRp+fv7Wz97m/jKl/fs0v1eqSs5fpfstvvtt99WXFycvvvuO3344YfW+sDAQD3xxBMaM2aMT/u7EjNnztSff/4pSerQoYPH5+xS27dv1x133GGNtA4bNkwvvfSSR50SJUro999/lySPszlcI46S8/gKFy7scZaDe91L65coUcJa7tGjh0e9unXr6t1331XTpk0lSevWrcu0/dkRHR2d4WfX5//48eM6efKkLly4IMk5Ku8+Up+d25BNnTrVGuVzad68uddbvXlr2969e73WcS931Xcf/d69e7dSUlJyPNotOb9nczI5Vna+y6WLnzv3z9el74/7a+6ql9n3+aXbSrLOmric3D67xhd2fa98/PHH6tWrl7Zu3erx+yo0NFTDhg3TM888c0XfxXXq1LHuUuCyfv163XLLLTp//nym+3L9bnR9L0lS9erVPX5vALmB08sBwIvMTtm83Kmc7v9h/+GHH7zOeOw6dbxo0aJW3UOHDnnsx3WKpi9mzJhhLc+bN08pKSkyxni9rU5iYqKWL1+uhg0bqmzZsh7rEhISrOXixYurQ4cO+vDDDz1uExYfH28tZ+dervny5fP4edu2bVbgLFGihLZt26b09HSPNmQmMDAwyzYUK1bMWj5y5IgVViTPU15d3N+z1q1be33PXDNb55YrOf6ctjs6Olrffvutjh07ppUrV+rDDz9UgwYNlJaWprFjx+rHH3/MtePLzFtvvWUtP/vss5nWW7t2rZo2baqDBw8qMDBQn3zySYbALUmNGjWylt1POXW/tVWdOnUUGBio8uXLq3jx4pKcs6C7f8bc6zds2FCSPPpNZi79T35OXfp5d/+5ePHiKlSokBUCTp8+7dFPvPXp3HTHHXdYy0uXLtXRo0c91h8+fFjLli2zfm7Tpo0kqWXLltYfxc6ePat3333X6/4zC5+DBw9WamqqFi1apHz58ikhIUFPPvmkxyUYvsiN7/JL3x/3Wehd9TL7Pvf2XV6kSBHr+cPDw63v7Esfn376aZZttINd3yt16tTRli1bdOjQIS1btkzjx49XlSpVdO7cOQ0YMEBHjhzxeO6qVat6fW5jjHWJlrtLf8dIzlP0XYG7W7duiouLkzHGa18sWbKktfzbb79l+fnnPubICUI3AOSijh07WsuPPfaYfv75Z6WkpCg+Pl5ffvmlunTpohEjRkiSbrvtNuuX96JFi7R48WKdOXNG06ZN83mUW/L8z2N4eLjS09M1adIkj3sRuyxevFjJycnWdYMu6enpKlu2rJ588kl98803OnnypNLS0rR161atXbvWqnfDDTdYy+7X4v73v//1aeTGW5v9/f2VP39+nT592uO68pyqVKmSNRJ1/Phxvf7669Z1eR999FGG+k2aNLGC+vLlyzV69GjFx8crJSVFO3bs0MiRI7Mckc2J3Dj+7Lb7zTff1MyZM5WQkKBGjRrp3nvv9bjdl/u1u5dz9uxZxcXFKS4uzuN9P336tFV+qeXLl1vBuGHDhmrSpInXfc+ePVutWrXSyZMnVaBAAS1dulQPPPCA17q9evWywuiECRO0adMmHTlyxOOaywcffNBa7tOnj7U8cOBAxcXFaeXKldbZHxEREbrvvvskOV+PBg0aaPr06Tpy5IhSUlK0adMmPfXUU9Y+mjdv7tEeh8Mhh8ORrdFnSdY9qC/9/IeGhqpp06bKly+fNbpujNHAgQN18uRJbdu2TaNGjfL5eXr16pUhwGQ1yi1J7du3t655TklJUefOnbVlyxalpaVpy5Yt6ty5s1JTUyVJ9evXV/v27SU5r5t/+eWXrf28/PLLGjVqlI4ePaq0tDTt2bNHY8eOtep7ExgYqDvvvFOvv/66VfbUU095nF1klzZt2igoKEiS83vziy++UFJSkubPn29dax0UFKTWrVtLklq1amVt+8ILL+jYsWM6ePCgR190CQkJsf44kZiYaF0/nJaWpkOHDmnatGlq0qSJx3dvZlx9LreCoF3fKy+++KLmz5+v9PR0NWvWTPfee6+1H2OM/vjjD1WsWFE1atSQJO3YsUMDBgyw+svevXs1YcIE1apVy+c/Srt/z4aEhCg0NFRbtmzRO++84/W4XX+U2717tx5++GEdPHhQSUlJWr9+vSZNmmTVvZLffbiO2XjqOgDkKZndpst9NnL3cvdrll2Sk5NNixYtsrwezX1W3sxmL3efcfdy13Q/8sgjGbbPly+fKV26dIZ9dOzY0Uiet0syJuM1sN4ejRs39rjOzf1WOO4P121YvL0+Lunp6aZGjRoZtnW/Rtd9u8xm787sWsfMZi93n0nY/RZTCxcuNEFBQVkef05kdt1kbh1/dtrdsmXLTOuEh4ebI0eO+Hxc7tc0+vp6tWrVylo3Z86cTPft3m982ffw4cMzrXfnnXd6XOd55swZU6dOHa91/fz8rOt5jfHsW5l9Ri+dwdzbe51Zuft76t4v3R/ut9lav36915mV3bft1atXlu9bTh06dMjUrFkzy9ejZs2a1t0Q3D377LNZbhcbG2vVzWz28tTUVFOpUiVr3ahRo7Js76W3knJxf83dyzO7Rvdydxhw3XLRmMxnL3f/Lnf/jtq7d2+m77vr4fre/rtnL7fje6VChQqZ1itdurQ5d+6cMcY5e3lERESWz+36HZPZ++yybt06ay4E94f796z7dr7MXm7M5X/3Ad4w0g0AuSg4OFgrVqzQe++9p8aNGysiIkJBQUEqXbq0mjVrpmHDhnmcrv32229r2LBhKlOmjIKCglSjRg3NnDnTminVF2PGjFH//v0VFRWlkJAQNW7cWCtWrPC4plKSkpKStHTpUtWvXz/DdYYBAQH65JNP1KdPH9WsWVPFixdXQECAwsLCFBsbqyFDhmj58uUe17nVq1dPEyZMUKVKlawRIV/5+/tr0aJFuvvuu1WoUCFFRESoc+fO+uabb7K1n8x07NhR8+fPV2xsrIKCglS2bFkNHTrUY+Zr99NB27dvr59//lk9evRQ2bJlFRgYqAIFCqhatWrq0aOHZs2alSvtcsmt489Ou3v16qW77rpL0dHRCgsLk7+/vyIjI9W5c2d9++23ioyMzNVjdPfrr79q+fLlkpzX0rqfEXKlXnjhBc2bN0/NmjVT/vz5FRISopo1a2rUqFGaN2+exyngYWFhWrNmjV566SWr3xYsWFBt2rTRqlWr1LVrV6tuyZIl9c4776ht27aKiYlRaGioQkJCVKVKFfXv319bt25VlSpVLts+91GwzD4n/fr108SJE1W5cmUFBQWpYsWKGj9+vJ5//nmrToMGDfT111+rcePGCg4OVokSJfT00097jAK79+ncVLp0aW3YsEHjxo1T8+bNVbhwYQUEBKhw4cJq1qyZ3nvvPW3YsMG6G4K70aNHa/369erdu7cqVKig0NBQhYWFqVKlSurevbvefPPNyz5/YGCgR73XX3/9b7neuX///lqxYoXatWunokWLyt/fX0WKFFHbtm21fPlyj7Meypcvr2+//Va33367QkNDVahQIT3wwAOZng5frlw5/fLLLxo4cKCqV69ujcSWL19e7du318SJE71eIvR3sON75YknnlDr1q1VunRphYSEKDAwUGXKlFHPnj21du1ahYSESHKeLbF161Y9+uijqlixooKDg5U/f35VqlRJXbp00dSpUxUVFeXTcTRu3Fhz5sxRrVq1FBISoujoaA0fPlyDBg3yWr9t27bavHmzevfurZiYGAUFBSk8PFyxsbEeZzJcye8+XL8cxvzvnioAgGva7Nmzdd9992nkyJEet9q6FiUm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A491P/30k+666y4VLlxYISEhqlOnjmbPnm2tHzJkiEqXLi1Jev755+VwOKzTv3fv3q3evXurUqVKypcvn0qVKqX27dtr27ZtHs/hOvV9//79HuWrV6+Ww+HQ6tWrM227w+FQUlKSpk2bZp2i3aJFiyyPd+LEiSpZsqRuv/12j/L58+fr1ltvtQK3JEVERKhTp05atGiR0tPTs9zvpRITEzVnzhw1b95cFStWzNa27h544AH9/vvvWrVqVY73AQAAAABXitB9Bdq2bSt/f3+tXbvWKlu1apWaNGmiU6dO6f3339eCBQtUu3Zt3XfffZo6daokqV+/fpo3b54k6YknntAPP/yg+fPnS3KOgBcpUkRvvPGGli5dqvHjxysgIEANGzbUzp07c6XdP/zwg0JDQ9W2bVv98MMP+uGHHzRhwoQst1myZImaNWsmP7+LXebcuXPas2ePatWqlaF+rVq1dO7cOe3duzdbbfvss8+UlJSkfv36eV3/6aefKjQ0VMHBwapXr56mTJnitV69evWUP39+LVmyJNPnSklJUUJCgscDAAAAAHITp5dfgbCwMBUtWlRHjhyxyh599FHdcMMN+uabbxQQ4Hx5W7durbi4OL344ovq0aOHSpcubY0Aly1b1uN06WbNmqlZs2bWz+fPn1e7du10ww036IMPPtDYsWOvuN2NGjWSn5+fihUrdtlTtSXp+PHj2rt3rx566CGP8pMnT8oYo8KFC2fYxlUWHx+frbZNnjxZBQsWVOfOnTOsu//++9WuXTuVKVNGx48f1+TJk9WnTx/t3btXQ4cO9ajr7++v2NhYff/995k+14gRI/Tqq69mq30AAAAAkB2MdF8hY4y1vHv3bu3YsUPdunWTJKWnp1uPtm3b6ujRo5cdrU5PT9fw4cNVvXp1BQUFKSAgQEFBQdq1a5d+++03W48lM64/KhQvXtzr+qyumc7O9dS//vqr1q9fr27duikkJCTD+pkzZ+r+++/XzTffrM6dO+vLL7/UnXfeqTfeeEN//fVXhvrFixfX4cOHM32+F154QadPn7Yehw4d8rmtAAAAAOALQvcVSEpKUnx8vKKioiRJx44dk+ScPTswMNDj8eijj0qS4uListznM888o3//+9+6++67tWjRIq1fv14bN25UbGyszp07Z+8BZcL1vJcG4UKFCsnhcHgdzT5x4oQkeR0Fz8zkyZMlKdNTy73p3r270tPT9dNPP2VYFxISkuVrFhwcrIiICI8HAAAAAOQmTi+/AkuWLNH58+etSciKFi0qyTmC2qlTJ6/bXO4WWjNmzFCPHj00fPhwj/K4uDgVLFjQ+tkVgFNSUjLUy22u43IFaZfQ0FBVrFgxwyRvkrRt2zaFhoaqfPnyPj1Hamqqpk+frnr16ql27do+t811poH7teYuJ06csNoOAAAAAFcDoTuHDh48qAEDBqhAgQJ6+OGHJTkDdaVKlbRly5YModlXDodDwcHBHmVLlizR4cOHPWbzds12vnXrVo8gv3DhQp+eJzg42OeR8+joaIWGhmrPnj0Z1nXs2FFvv/22Dh06pDJlykhyzkA+b9483XXXXdZ17ZezcOFCxcXF6bXXXvOpvsv06dMVGBioevXqZVi3d+9e1ahRI1v7AwAAAIDcROj2wX//+1/r2uzjx4/r22+/1ZQpU+Tv76/58+erWLFiVt0PPvhAd9xxh1q3bq1evXqpVKlSOnHihH777Tdt2rRJc+bMyfK57rzzTk2dOlVVq1ZVrVq19PPPP2vUqFHWLcZc6tevrypVqmjAgAFKT09XoUKFNH/+fH333Xc+HVPNmjW1evVqLVq0SJGRkQoPD890FD4oKEiNGzfWjz/+mGHdgAEDNH36dLVr106vvfaagoOD9cYbbyg5OVlDhgzxqOv6o8Hu3bsz7Gfy5MkKDQ3V/fff77UNo0aN0vbt29WyZUuVLl3amkht+fLlGjJkSIYR7fj4eO3atUtPPPGELy8HAAAAANiC0O2D3r17S3KGz4IFC6patWp6/vnn1a9fP4/ALUm33HKLNmzYoNdff139+/fXyZMnVaRIEVWvXl333nvvZZ/rnXfeUWBgoEaMGKEzZ86obt26mjdvnl5++WWPev7+/lq0aJEef/xxPfLIIwoODlbXrl01btw4tWvXzqfneeyxx9S1a1edPXtWzZs3z/Le3t26ddNDDz2ko0ePKjIy0iovVqyYvv32Ww0YMEA9e/ZUenq6GjdurNWrV6tq1aoe+8jsnt2HDh3S8uXL1b17dxUoUMBrnapVq2rhwoVasmSJTp48qdDQUNWuXVv/+c9/1LVr1wz1FyxYoMDAQJ9ecwAAAACwi8O4T78NZCI5OVlly5bVs88+q+eff/5qN+eybr75ZpUtW1YzZ870eZuEhAQVKFBAH6qC8snfxtb57n6TO/dmBwAAAJC7XPnh9OnTWU7KzOzl8ElISIheffVVjR07VklJSVe7OVlau3atNm7cmOHe3QAAAADwd+P0cvjsoYce0qlTp7R3717VrFnzajcnU/Hx8frkk098njkdAAAAAOzC6eXA/3B6OQAAAABfcXo5AAAAAABXGaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsElAdirfeuutPtd1OBxauXJlthsEAAAAAMC1Iluhe/Xq1XI4HJetZ4zxqR4AAAAAANeybIVuyRmoAQAAAADA5WXrmu59+/ZZj59++kmlSpVSo0aNtGzZMu3YsUPLli1Tw4YNVbx4cf344492tRkAAAAAgDwhWyPd0dHR1vLQoUN15MgRrVu3TmXKlJEkVa5cWVWqVFFMTIwmTJigKVOm5G5rAQAAAADIQ3I8e/kXX3whSQoNDfUoDwkJkSQtWrQo560CAAAAAOAakOPQfe7cOUlS3759tX37diUmJmr79u168MEHJUnJycm500IAAAAAAPKobE+k5tK0aVOtWLFCixcv1uLFiz3WORwONW3a9IobBwAAAABAXpbjke4xY8aoQIECMsZkeERERGjMmDG52U4AAAAAAPKcHIfuGjVqaNOmTXrggQdUsmRJBQQEqGTJkurRo4c2bdqkG264ITfbCQAAAABAnpPj08slqVy5cpo2bVputQUAAAAAgGvKFYVuSTp69KiWLl2qY8eOqUSJEmrdurWioqJyo23AVXHv6U2KiIi42s0AAAAAcA24otA9YcIEPfvss0pNTbXKgoKCNGrUKD3++ONX3DgAAAAAAPKyHF/TvWrVKj3xxBNKTU31mEQtJSVFTz31lL755pvcbCcAAAAAAHlOjkP32LFjZYyRn5+fOnTooKeeekodOnRQQIBz8Pytt97KtUYCAAAAAJAX5fj08vXr18vhcGj27Nnq2LGjVT5//nx17txZ69evz5UGAgAAAACQV+V4pPvUqVOSpNatW3uUu352rQcAAAAA4HqV49BdqFAhSdLy5cs9ylesWOGxHgAAAACA61WOTy9v1KiRFi1apPvuu0/t27dXdHS0Dhw4oMWLF8vhcKhhw4a52U4AAAAAAPKcHIfu/v37a/HixUpPT9f8+fOtcmOMHA6H+vfvnxvtAwAAAAAgz8rx6eW33HKL3n77bQUGBnrcMiwoKEhjx47VrbfempvtBAAAAAAgz3EYY8yV7ODw4cNaunSpjh07phIlSqhNmzYqVapUbrUP+NskJCSoQIECOn36tCIiIq52cwAAAAD8g/maH644dAPXCkI3AAAAAF/5mh9yfE23JJ05c0Zffvml9u/fr+Tk5AzrX3nllSvZPQAAAAAAeVqOR7o3bdqkO+64Q3FxcZnWOX/+fI4bBvzdGOkGAAAA4CvbR7r79++vv/76K9P1Docjp7sGAAAAAOCakOPQ/csvv8jhcKh58+bq3LmzwsLCCNoAAAAAALjJcegODw9XUlKSPv/8cxUqVCg32wQAAAAAwDUhx/fp7t69uyRp3759udYYAAAAAACuJdmaSG3t2rXWclJSkvr27auAgAANHDhQ1apVU2BgoEf9Zs2a5V5LAZsxkRoAAAAAX9lyn24/Pz+fr9t2OBxKT0/3ddfAVUfoBgAAAOAr22Yvz+EdxgAAAAAAuO5kK3T37NnTrnYAAAAAAHDNyVbonjJlil3tAAAAAADgmpPj2cv79Omjvn37el33ySefaPr06TluFAAAAAAA14JsTaTmzjWp2vnz572u8/PzYyI15ClMpAYAAADAV77mhxyPdGfm7NmzkphwDQAAAACAbF3TvWDBAi1YsMCjrE+fPh4/79q1S5KUP3/+K2waAAAAAAB5W7ZC9y+//KKpU6da9+o2xmjatGkZ6jkcDlWvXj13WggAAAAAQB6V7ft0S86w7R68L1WsWDGNHDnyyloGAAAAAEAel63Q3atXL7Vo0ULGGN16661yOBxatWqVtd7hcKhIkSKqWLGigoODc72xAAAAAADkJdkK3dHR0YqOjpYkNWvWTA6HQ82bN7elYQAAAAAA5HU5Or1cklavXm0tnzhxQvHx8apUqVJutAkAAAAAgGvCFd0ybNOmTWrYsKGKFSumatWqSZI6deqkW2+9VevXr8+VBgIAAAAAkFflOHTv3r1bLVq00E8//SRjjDWhWpkyZbRmzRrNmTMn1xoJAAAAAEBelOPQPXToUJ05c0aBgYEe5f/3f/8nY4zWrFlzxY0DAAAAACAvy3HoXrlypRwOh7766iuP8po1a0qSDh06dGUtAwAAAAAgj8tx6D5+/LgkqWnTph7lrvt3nzx58gqaBQAAAABA3pfj0B0RESFJ+vPPPz3KXfftLlSo0BU0CwAAAACAvC/HobtevXqSpEceecQqe/PNN9WjRw85HA7Vr1//ylsHAAAAAEAeluPQ/eijj8oYo6VLl1qnlL/wwgvWaeWPPvpo7rQQAAAAAIA8Ksehu0OHDho4cKB1uzD324YNGjRId9xxR641EgAAAACAvMhhXEk5h3766SctWLBAx44dU4kSJXT33Xdbp54DeUlCQoIKFCgg9aknBQVc7eYAAAAAuISZ+OPVboLFlR9Onz5tzXnmTbaSRWpqaoayWrVqqVatWl7rBQUFZWf3AAAAAABcU7IVukNDQ32u63A4lJ6enu0GAQAAAABwrchW6L7CM9EBAAAAALiuZHsiNYfDYc1WDgAAAAAAMpft0O0a7S5fvrxGjx6tkydP6sKFCxke58+fz/XGAgAAAACQl2QrdK9fv17333+/AgMDtXfvXj333HMqU6aM/vWvf+nXX3+1q40AAAAAAORJ2Qrd9evX14wZM3Tw4EENGTJEJUuW1JkzZzRp0iTVqlVLLVu21MqVK+1qKwAAAAAAeUq2Ty+XpOLFi+uVV17RgQMHNHPmTBUuXFjGGK1evVrjx4/P7TYCAAAAAJAnZWv2cncJCQn6+OOPNX78eJ04cUKS83rvUqVK5VrjAAAAAADIy7Idurdv365x48ZpxowZSkpKkjFGQUFB6tKli5588knVr1/fjnYCAAAAAJDnZCt033bbbVq1apUk56h2ZGSkHnnkET388MMqXry4LQ0EAAAAACCvylbo/uabb6zl8uXLq2PHjjp37pzefvttr/WHDx9+RY0DAAAAACAvcxjXjbd94OfnJ4fD4fPOuVc38pKEhAQVKFBA6lNPCsrxdAcAAAAAbGIm/ni1m2Bx5YfTp08rIiIi03rZTha+ZvTshHMAAAAAAK5F2QrdgwcPtqsdAAAAAABccwjdAAAAAADYxO9qNwAAAAAAgGsVoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwyVUP3UOGSA6H89GrV87306LFxf3s33+xfOpU53MMGSKdOuW5zf79F7dp0SJnz7t6dfbaHxNzsf4/VVqaVKWKs41PPOG5LjVVGj1aqlVLCg2VChSQmjeXFizwff/u71Vmj6lTPbdZsUJq3VoqVEgKDpYqVZIGDZISEjzr/fGHFBLi3Mcnn+Tk6AEAAAAg91z10G23qVOlV191Pi4N3fDu3Xel3393httBgy6WX7gg3X239Nxz0rZtUnKyM/SuXessHzUq99oQHn5x+f33nYF7+XLne5iaKu3eLY0cKTVtKiUmXqxburTUt69zedAg6cyZ3GsTAAAAAGTXNRO6V6+WjHE+YmJ82yYm5uI2q1fb17a8JDX1Ynju0EEqVeriukmTpK++ci7ffLN0+LD0889S8eLOshdflHbsuPxzuL9XrseOHRdH/0uUkO6807l84oT07LPOOiEh0nffSadPXzyrYNs25x9U3P3rX85/jx6VPv44u68AAAAAAOSef2zodj/tfNIk6ZVXpOhoKV8+qV495+nG7i49vdx12veaNRfrlCvnWSez08t//VXq0sV5inWhQlJAgFSwoHNU9eOPnQEwNxw4IN1zj/MU7fBwqVMnz1PjXT7/XLrtNqlwYSkoSIqKku67T9q06WKdPXuc+3E4nEH5xAlneVycs77D4TyGvXuzbtPnn0vHjjmXu3f3XDd58sXloUOd+61bV3r0UWdZero0ZUp2XoGL3nrr4uv6+OPOUXbJGbLPnnUut2ghNWkiRURI/ftf3HbKFM/3pEYN5+nvkjRhQs7aAwAAAAC54R8but0NGuQMeQcPSufOOcPmnXd6D6i5Ydcuae5c5ynWp05J5887R1e//9556vIbb+TO8zRp4gy5CQnO06Dnz3eOIMfHX6wzYIAzmK9cKZ086bze+uhRafZsqVEj6YsvnPUqVJA++si5fOSI9PDDzuUHH3TWl5zry5fPuk1Lljj/dTikZs0ulqemSr/8cvHnmjW9L//4o69Hf1F8/MXrr/PluzhSLV0M3JdyD9knTjj/6ODO9UeUnTszrgMAAACAv0ueCN3p6Rev573/fmdZaqr02WeZb9OihTOYNW9+sWzfPt9OQa9Z0xk+Dx92Xrd87py0bp0zEErSmDG5M9pdp47055/OPx40auQs++MP50RlkrRxo/O5JOco9TffOAP6e+85y9LSnKH63Dnnz126XAysc+dKHTteDOWPP+4M75fjCs0xMc6Rc5f4eOf74FKw4MVl93quUfLsmDDh4jH06iUVKXJxXZ06F5dXr3a+DwkJ0jvveO7jr788f65b9+JyZn8ISElJUUJCgscDAAAAAHJTngjd/fpJt9/uDHf/938Xy+0a6S5Z0hnU7rjDGQDz5ZNuuuniqGt8vHT8+JU/z5gxzuuXo6M9r0tevtz5r/uM4L17S7fc4jwN/fHHpdhYZ3lcnDOIurz1llS7tnPZFbjr1bsY3i/HNSperFjW9dz/6OC+nN1Z2VNSLp4C7ucnPf205/oqVaQ+fZzLycnOswMKFMg4u3lQkOfP7u13HdOlRowYoQIFCliPMmXKZK/xAAAAAHAZeSJ0V6t2cTks7OJycrI9z9e1q/N09q1bpaQk76ParpHZKxEd7X3ZFejdR43d10ueI/Xu9YKDMwbXJ5/MGEqzq0gR57XtLu4zwZ8+fXG5RIns7XfmTOdov+ScuK1ixYx1Jk2S3nzTGcCDgpwzlD/6qPOacpeyZbP3vJL0wgsv6PTp09bj0KFD2d8JAAAAAGQhT4TuwMCLy9kdSc1u/VOnpMWLncvBwc6JvNLSnMG7cOHs7etyDhzwvuyaDdw9wLqvlzxH+d3r/fmnNHCgZ93nnst8tPdSkZHOfy89XTsoyPNU7//+9+Lytm0Xlxs29O15XN566+Lys896r+Pv7zyGHTucI+OHDjn/kOA6ptq1M47Mu7e/ZEnv+w0ODlZERITHAwAAAAByU54I3VfC/frgLVsufy12QMDFoO7n5zyd+9w5afDgizOC55aBA52j1AcPOvfv0qqV89+77rpYNnWqcyb2M2ecp2Nv2eIsL1rUeeq75LyPdrduzn36+TlnfHc4nCPn3bo511+OKzTv35/xvuau+19L0r//7ZywbfNmaeJEZ1lAgPM0eBf3GegvPR1ccp5G7wrvDRs6Tx335quvpFWrnK//mTPOSeXuvvvie/niixm3cZ/Z3XW9PAAAAAD83a750O0KpJIzqPn5ZT2JWv78UuvWzuVz55zXTkdESO+/7zl5WG746SfnKGx09MXJvkqXds5YLkkNGly8NdbJk87J4cLDpccec5YFBDjbFRrq/PnVV52TrUnOQP/qqxdHj1etkl577fJtatfO+a8x0rffeq578EHnde6Sc12pUs4Jy1ynww8fLlWt6vvxu19n7jpmb5Ytk2691fkHlPBw5+3TXPcDHzDAOYHcpVz3Xa9c2fsp6wAAAADwd7jmQ/ejjzpDaqlSzsDti+nTpZ49nacs58vnnMRt9WrPWbpzw/ffO+/NHR7uDPt33+0Ms+6j82+9Jc2a5ZxErWBBZ9AuWdI5E/m6dVLnzs5633wjDRvmXK5b92LAHj5cuvFG5/LQoRdDeWbuuefi6e0zZ3qu8/NzTs725pvOGd5DQpztbtbMebuz557z/dh//fXihHHlyjlnWs9M06bOUfCiRZ3HX6SIM/wvWSKNGuV931u3Opdd9xAHAAAAgKvBYUxu3PwK15LRo50BOiRE2rv34nXeecVjjzlPwS9Z0nnP9fz5fdsuISFBBQoUkPrUk4ICLr8BAAAAgL+VmZjJ/YCvAld+OH36dJbzQ13zI93Ivqeecp6WnZzsHCnPSw4fliZPdi6PHOl74AYAAAAAOzCchwwCA6WdO692K3KmVCn7biUHAAAAANnFSDcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGATQjcAAAAAADYhdAMAAAAAYBNCNwAAAAAANiF0AwAAAABgE0I3AAAAAAA2IXQDAAAAAGCTgKvdAOCf5vRb3ygiIuJqNwMAAADANYCRbgAAAAAAbELoBgAAAADAJoRuAAAAAABsQugGAAAAAMAmhG4AAAAAAGxC6AYAAAAAwCaEbgAAAAAAbELoBgAAAADAJoRuAAAAAABsQugGAAAAAMAmhG4AAAAAAGxC6AYAAAAAwCaEbgAAAAAAbELoBgAAAADAJoRuAAAAAABsQugGAAAAAMAmAVe7AcA/hTFGkpSQkHCVWwIAAADgn86VG1w5IjOEbuB/EhMTJUllypS5yi0BAAAAkFckJiaqQIECma53mMvFcuA6ceHCBR05ckTh4eFyOBxXuznZVr9+fW3cuPFqNwPIMfow8ir6LvIa+izygrzQT40xSkxMVFRUlPz8Mr9ym5Fu4H/8/PxUunTpq92MHPP391dERMTVbgaQY/Rh5FX0XeQ19FnkBXmln2Y1wu3CRGrANeKxxx672k0Argh9GHkVfRd5DX0WecG11E85vRwAAAAAAJsw0g0AAAAAgE0I3QAAAAAA2ITQDQAAAACATQjdAAAAAADYhNAN4Ip17NhRhQoV0j333HO1mwLkCH0YeRH9FnkNfRZ5RW73VUI3gCv25JNP6pNPPrnazQByjD6MvIh+i7yGPou8Irf7KqEbwBW75ZZbFB4efrWbAeQYfRh5Ef0WeQ19FnlFbvdVQjdwhWJiYuRwODI8HnvssUy3GTJkSIb6JUuWzPW2rV27Vu3bt1dUVJQcDoe++OKLDHUmTJigcuXKKSQkRPXq1dO3336b6+3AP1tO+rAkHT58WN27d1eRIkWUL18+1a5dWz///HOuto0+jMvJbv/NaX/PLvouvMlu/0tPT9fLL7+scuXKKTQ0VOXLl9drr72mCxcu5Gq7fOmvEn32epGT78nExET1799f0dHRCg0N1U033aSNGzfmetvy6ncroRu4Qhs3btTRo0etx4oVKyRJXbp0yXK7G264wWO7bdu2ZVr3+++/V1paWobyHTt26M8//8x0u6SkJMXGxmrcuHFe18+aNUv9+/fXSy+9pM2bN+vmm2/WHXfcoYMHD1p16tWrpxo1amR4HDlyJMvjQ96Rkz588uRJNWnSRIGBgfrqq6+0fft2jRkzRgULFvRanz4Mu2S3/2a3Pn0XuSm7/W/kyJF6//33NW7cOP3222968803NWrUKL333nuZPkdO+uzl+qtEn72e5OT/Bf369dOKFSs0ffp0bdu2Ta1atdJtt92mw4cPe61/3X23GgC56qmnnjIVKlQwFy5cyLTO4MGDTWxsrE/7O3/+vImNjTX33HOPSU9Pt8p37txpSpYsaUaOHOnTfiSZ+fPne5Q1aNDAPPLIIx5lVatWNYMGDfJpn+5WrVplOnfunO3t8M/jSx9+/vnnTdOmTX3aH30Yfydf+q+v9fNC36Xf5m2X66/t2rUzffr08Sjr1KmT6d69u9f6udFnvfVXY+iz17PL9dOzZ88af39/s3jxYo/y2NhY89JLL2Wonxe+W43J3b7KSDeQi1JTUzVjxgz16dNHDocjy7q7du1SVFSUypUrp65du2rv3r1e6/n5+enLL7/U5s2b1aNHD124cEF79uzRrbfeqrvuuksDBw7McVt//vlntWrVyqO8VatWWrduXY72ibzP1z68cOFC3XjjjerSpYuKFy+uOnXq6MMPP/Ralz6Mv0t2voN9qU/fhZ186a9NmzbVypUr9fvvv0uStmzZou+++05t27b1Wp8+i9zmSz9NT0/X+fPnFRIS4lEeGhqq7777LkP967Kf5kp0B2CMMWbWrFnG39/fHD58OMt6X375pZk7d67ZunWrWbFihWnevLkpUaKEiYuLy3SbAwcOmOjoaHPfffeZsmXLmh49evg8kmNMxr8GHj582Egy33//vUe9119/3VSuXNnn/RpjTKtWrUzRokVNaGioKVWqlNmwYUO2tsc/h699ODg42AQHB5sXXnjBbNq0ybz//vsmJCTETJs2LdNt6MOwm6/9N7v1/6l9l36bt/nS/y5cuGAGDRpkHA6HCQgIMA6HwwwfPvyy+76SPntpfzWGPns98/V7snHjxqZ58+bm8OHDJj093UyfPt04HI4s+8c/9bvVmNzvqwFXK+wD16LJkyfrjjvuUFRUVJb17rjjDmu5Zs2aaty4sSpUqKBp06bpmWee8bpN2bJl9cknn6h58+YqX768Jk+e7NNIzuVcug9jTLb3u2zZsituB/4ZfO3DFy5c0I033qjhw4dLkurUqaNff/1VEydOVI8ePbxuQx+G3Xztv9mt/0/tu/TbvM2X/jdr1izNmDFDn376qW644Qb98ssv6t+/v6KiotSzZ89Mt6PPIrf4+j05ffp09enTR6VKlZK/v7/q1q2r+++/X5s2bcp0m39qP5Vyv69yejmQSw4cOKCvv/5a/fr1y/a2YWFhqlmzpnbt2pVpnWPHjumhhx5S+/btdfbsWT399NNX0lwVLVpU/v7+GSarOH78uEqUKHFF+0belJ0+HBkZqerVq3uUVatWzWOikkvRh2Gn7H4HZ6c+fRe5zdf+99xzz2nQoEHq2rWratasqQceeEBPP/20RowYkeV29Fnkhux8T1aoUEFr1qzRmTNndOjQIW3YsEFpaWkqV65cpttcT/2U0A3kkilTpqh48eJq165dtrdNSUnRb7/9psjISK/r4+Li1LJlS1WrVk3z5s3TN998o9mzZ2vAgAE5bm9QUJDq1atnzUjpsmLFCt1000053i/yruz04SZNmmjnzp0eZb///ruio6O91qcPw27Z/Q72tT59F3bwtf+dPXtWfn6e/1339/fP8pZh9Fnklpz83zYsLEyRkZE6efKkli1bpg4dOnitd9310ys6OR2AMcY5C2PZsmXN888/n2Hde++9Z2699VaPsmeffdasXr3a7N271/z444/mzjvvNOHh4Wb//v1e912vXj3Ttm1bk5KSYpVv3brVFClSxIwdOzbTdiUmJprNmzebzZs3G0lm7NixZvPmzebAgQPGGGM+++wzExgYaCZPnmy2b99u+vfvb8LCwry2A9e27PbhDRs2mICAAPP666+bXbt2mZkzZ5p8+fKZGTNmeN03fRh2ym7/zar+pfXou8ht2emvPXv2NKVKlTKLFy82+/btM/PmzTNFixY1AwcOzHTfOemzl+uvxtBnrzfZ/V5dunSp+eqrr8zevXvN8uXLTWxsrGnQoIFJTU31uu/r7buV0A3kgmXLlhlJZufOnRnWDR482ERHR3uU3XfffSYyMtIEBgaaqKgo06lTJ/Prr79muv/ly5ebc+fOZSjfvHmzOXjwYKbbrVq1ykjK8OjZs6dVZ/z48SY6OtoEBQWZunXrmjVr1lz+gHHNyW4fNsaYRYsWmRo1apjg4GBTtWpVM2nSpEz3Tx+GnbLbf7Oqfyn6LnJbdvprQkKCeeqpp0zZsmVNSEiIKV++vHnppZc8gsqlctJnfemvxtBnryfZ/V6dNWuWKV++vAkKCjIlS5Y0jz32mDl16lSm+7/evlsdxhhj71g6AAAAAADXJ67pBgAAAADAJoRuAAAAAABsQugGAAAAAMAmhG4AAAAAAGxC6AYAAAAAwCaEbgAAAAAAbELoBgAAAADAJoRuAAAAAABsQugGAAAAAMAmhG4AADLRt29fORwO6/Hqq69e7SblWbyW9tu1a5eCg4PlcDj0+eefW+UtWrSwXvf9+/dfvQZeZa7XICYmJkfbG2NUs2ZNORwO9enTJ3cbB+CaRugGAMCLs2fPas6cOR5l06ZNkzHmKrUo7+K1/Hs888wzSk1NVc2aNdWpU6er3ZxrjsPh0CuvvCJJmjp1qjZu3HiVWwQgryB0AwDgxeeff67ExERJzv9sS9K+ffu0du3aq9ksSc4Qm5f8k19LX/3TX/MtW7Zo8eLFkqR//etf1uuM3NWpUyeVKFFCxhiNGDHiajcHQB5B6AYAwIupU6day4888ojX8oULF1qnrD700EMe23///ffWunvuuccqP378uJ599llVrVpVoaGhCgsLU/369fXBBx94jPzu37/f2r5FixZavHixbrzxRoWEhOjRRx+VJE2aNEktW7ZU6dKlFRYWpqCgIJUuXVpdu3bV1q1bMxzTwoULVbt2bYWEhKhcuXJ644039PHHH1vPM2TIEI/6W7ZsUbdu3VS6dGkFBQWpcOHCatOmjVauXJnrr6W73377TX379lW5cuUUHBysAgUKqHbt2powYYJHvfXr16tr164e7WvYsKHmzp3r9TV0FxMTY61zWb16tVXWq1cvTZkyRTVq1FBQUJDefPNNSdLrr7+um2++WVFRUQoNDVVISIjKly+vvn37ej11O6tjuXDhgipVqiSHw6F8+fLpxIkTHtvWqlVLDodDwcHB+uuvv7J8jV2vTUBAgLp27ZplXRdjjD766CM1adJEBQoUUFBQkKKjo9WnTx/t3r07Q/3s9h9vFi9erObNm6tQoUIKCAhQkSJFVLt2bfXt21cnT570qPvZZ5/p9ttvV9GiRRUUFKSSJUuqdevW+u9//ytJSk5OVu/evVW7dm0VK1ZMQUFBCgsLU61atfTKK68oKSnJp9fhzJkzevXVV1WrVi2FhYUpNDRUNWvW1BtvvKHU1FSPuv7+/rrvvvus1+Pw4cM+PQeA65wBAAAeDhw4YPz8/IwkExUVZRISEkxoaKiRZPLnz2/OnDljjDEmPT3dlCpVykgyBQsWNMnJydY+Hn74YSPJSDLLli0zxhizZ88eExkZaZVf+ujatau1/b59+6zyQoUKWe2RZHr27GmMMaZDhw6Z7it//vzm999/t/Y3b94843A4MtQrU6aMtTx48GCr/oIFC0xgYKDXfTscDjNx4sRcfS1dlixZYoKDg70+b4cOHax6kyZN8nhN3B9PPfVUhtewefPmHs8THR1trXNZtWqVVVa0aFGPfbpem9jY2Exf88jISBMfH5+tY5kwYYJVNnLkSGvbrVu3eu0XmSldurSRZOrWrZthXfPmza197du3zxhjzIULF0yXLl2y7D8//vijtY/s9h9vfvrpJxMQEJDpc+7atcuq+8ADD2Rab/78+cYYY06ePJlpHUmmVatWHs/vKo+OjrbK4uPjTfXq1TPdR7NmzUxKSorHfubNm2et/+ijjy773gAAI90AAFxi2rRpunDhgiSpS5cuCg8PV9u2bSU5R8VcI6n+/v7WhEqnTp3SokWLJEmpqamaPXu2JKlcuXK6/fbbJUlPPfWUjh49qoCAAM2ZM0dnz57VsWPH1KVLF0nOkb0lS5ZkaM/JkyfVpUsXHTp0SAkJCXrxxRclSY8++qh++uknxcXFKS0tTfHx8Xr55Zetdr7//vuSnCOaTz/9tDWS/uKLL+rUqVP69ttvvY4Gnjt3Tv369VNaWppiYmK0ceNGpaSkaOfOnapSpYqMMXrmmWcUFxeXa6+ldHHkMiUlRZLUp08f7d+/X4mJifruu++s7Y4cOaInn3zS2u+LL76oo0eP6tSpU1q+fLkaN2582XZdTlxcnPr3769jx44pPj5ePXv2lCQNGTJEW7du1YkTJ5SWlqZjx46pd+/ekqSjR49q5syZ2TqWXr16qVixYpKkiRMnWsc0Y8YMqy3uZwd488cff+iPP/6QJMXGxvp0fHPnzrWus4+OjtbPP/+sU6dO6fnnn5fkfG/69u0rKfv9JzNr1qxRenq6JGnWrFlKTU3V8ePHtW7dOr3yyivKnz+/JGnevHmaPn26JCksLEwzZszQqVOndPToUU2bNk2lSpWSJIWGhmrmzJnas2ePEhMTlZqaqt27d6t27dqSpOXLl2vbtm1Ztmnw4MHavn27JGncuHFKSEjQqVOn9OSTT0qS1q5dqw8//NBjm7p161rLP/74o8/HD+A6dlUjPwAA/0AVK1a0RrLWrVtnjDFm9uzZVlmLFi2suu4jue3btzfGGDN37lyr7uuvv26MMebcuXNZjvK5Ho8//rgxxnOUNiIiIsOIsDHGbNmyxXTt2tWUKVPGBAUFZdhXmzZtjDHG7Nixw2MENz093drH888/n2GkcsWKFZdtpyQzd+7cXH0tv/76a6u8QoUKHu1099FHH3nd/lJXMtJdsWJFc/78+Qz7XLt2rWnfvr2JjIz0eibAI488kq1jMcaYIUOGWHUXLFhgLly4YI0gV6tWLdPtXDZs2GBtP3DgwAzrvY10d+vWzSp75513rLppaWmmSJEi1rrdu3dnu/9k5osvvvAYQR46dKiZPXu2xxkZxhjTvXt3q96QIUOy3OfkyZNN06ZNM5wN4np89tlnVl1XmftIt+tMlawed955p8dzJiUlWevatWuXZfsAwBhjAnxO5wAAXAe+/fZb63rWQoUKKSQkRL/88otKlSqlgIAApaena82aNdq/f79iYmJUtmxZtW7dWl999ZWWLl2quLg4a5QuICDAGgmPj4+3Rvmy4m30uEqVKgoLC/MoO3DggG666aYsRxrPnTuXYZ+lS5eWv7+/9bO32ycdO3bssu3MrK3usvta/vnnn9a21atX92inO/d6NWvW9Kmt5pKZ0i/3XtSpU0d+fp4nBK5fv1633HKLzp8/n+l2rtfc12ORpMcee0wjR47UuXPnNG7cOEVEROjQoUOSpIcffjjLduaU+3scHR1tLQcEBKh06dKKj4+36rm/dr70n8x06NBBzz77rCZOnKi1a9d6TKRXt25dLVq0SFFRUT6/v2PGjNGAAQOyfE7X+5EZX/r6pf2cSeoAZBenlwMA4MZ9cq+TJ0+qbt26qlOnjpo0aWIFNWOMpk2bZtV78MEHJUlpaWkaP368vvzyS0nSXXfdpZIlS0qSihQpooAA59+6w8PDlZKSImNMhsenn36aoU358uXLUPbFF19YgfvWW2/V4cOHZYzRwoULM9R1nb4sOU/Ndp3CLDlnEb9UiRIlrOXWrVt7beeFCxcuGwiz+1q6XivJOQGZezvduddzTarlTUhIiLXsPvv4mTNnPIKdN95e888++8wK3N26dVNcXJyMMXr33XezbGNWxyJJRYsWVa9evSRJX3/9tXUP89DQUOu09qxERkZay5ebcM3F/T0+cOCAtXz+/HnrVHVXvez2n6yMHj1aJ06c0MaNGzV79mw99thjkqRNmzbptddek+T7++t+Cv4777yjs2fPyhiTrduluV4Hh8OhI0eOeO3r69at89jm+PHj1rJ7WwEgM4RuAAD+x9v9pDPjfp/p9u3bW8Fn2LBhSktLkySPGc1DQkLUpk0bSVJiYqJ1jW9aWpoOHTqkadOmqUmTJj7fRssV4CVZszbv2bNHw4YNy1C3UqVK1ojk8ePH9frrr1vXFn/00UcZ6jdp0sQKWsuXL9fo0aMVHx+vlJQU7dixQyNHjlTFihWzbF9OXssmTZqoePHikqTdu3fr4Ycf1sGDB5WUlKT169dr0qRJkqQ77rjDCtSrVq3SK6+8omPHjikhIUGrVq3SrFmzJDkDlaver7/+qn379un8+fN66aWXshytzoz7ax4SEqLQ0FBt2bJF77zzToa6vh6LyzPPPCM/Pz8ZY7R69WpJUteuXVWwYMHLtqt06dLWdc6//PKLT8dy1113WctvvfWWfvnlFyUkJOjf//63NcpdvXp1VahQIdv9JzNr1qzR8OHD9euvvyomJkZ333237r77bmv9wYMHJckjNI8aNUqfffaZEhISdPz4cX366afW/bHd34/8+fPL4XBowYIFXudFyEzHjh0lOf/407NnT/32229KS0vTn3/+qblz56pNmzbWmSsumzZtspYbNWrk83MBuI79XeexAwDwT/fJJ59Y12rWqVMnw3r32colmdWrV1vrXnzxRY/rQGNiYjJcE7x3797LXkO6atUqY0zW1yO79pUvX74M21euXNnrdpnNPu3eHvfrZxcuXOj1OnH3hx2vZW7OXm6MMQ8++KBV7u/vb/Lly2cCAgI8js3F/Zpu1wzx7tatW+f1Od1fc/ftfD0Wl86dO3vUWb9+fZavsTvXcfr7+5sTJ054rMts9vJOnTpl+t7my5fPfP/999Y+stt/vJk+fXqW/em9996z6vbo0SPTeq7Zy994440M6/z8/EyFChWsn6dMmWLt01V26ezlN9xwQ5btct+HMcY8+eST1nMdPHjQ5/cIwPWLkW4AAP7H/ZRx17XY7vz9/T1O93U/ffrBBx/0uNazX79+Ga4JLleunH755RcNHDhQ1atXt0ZLy5cvr/bt22vixIkeMyNnpVy5cvryyy/VqFEj5cuXT5GRkRowYIDXU50l54je/PnzFRsbq6CgIJUtW1ZDhw7V448/btUpWrSotdy+fXv9/PPP6tGjh8qWLavAwEAVKFBA1apVU48ePazR5Mzk9LVs27atNm/erN69eysmJkZBQUEKDw9XbGysWrVqZdV/8MEHtW7dOt13330qVaqUAgMDVbBgQTVo0EBNmza16o0dO1YPP/ywIiMjFRQUpPr16+ubb77xOCXbV40bN9acOXNUq1YthYSEKDo6WsOHD9egQYO81vf1WFzcr0+uU6eOGjRo4HPbXPduP3/+/GXfG8l5OvWcOXP0/vvvq1GjRgoPD1dAQIDKlCmjnj17avPmzbrpppus+tntP97Uq1dP/fr1U82aNVW4cGH5+/srPDxcjRo10qRJkzz2NW3aNH366adq2bKlChcurICAABUvXly33367dZbFgAED9NprrykmJkbBwcGKjY3V/PnzPd7/yylcuLDWr1+voUOHqk6dOgoLC1NwcLCio6N1++23a8yYMbrjjjus+ufPn7fuTHDXXXepTJkyPj8XgOuXw5hLZhYBAADXnMTERG3YsEHNmjVTYGCgJGn79u1q166d9u/fLz8/P23fvl1VqlS5yi29fs2fP986tfrjjz+2bkXmqzvvvFNLlixRbGysNm/enKsTftF/nObMmaN7771XDodD69evV/369a92kwDkAYRuAACuA/v371e5cuUUGBio4sWLKzk52bp2V5JeffVVvfLKK1exhdevF154QbNnz9a+fftkjFHVqlW1bds2j2uWfbFr1y7VqFFDqampmjt3rjp37pxrbaT/OK/7jo2N1bZt29S7d299/PHHV7tJAPIIbhkGAMB1oGDBgurevbt++OEH/fnnn0pNTVVUVJQaNmyoRx55xOvpzvh7HD16VHv37lV4eLiaNm2q8ePHZztwS84J81JSUmxoIf1Hcp6Sv3Xr1qvdDAB5ECPdAAAAAADYhInUAAAAAACwCaEbAAAAAACbELoBAAAAALAJoRsAAAAAAJsQugEAAAAAsAmhGwAAAAAAmxC6AQAAAACwCaEbAAAAAACbELoBAAAAALDJ/wPycqSMHEO5YQAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# image_experiment.plot_accuracies(exps, IMAGE_CONTEXT)\n", + "image_experiment.plot_accuracies(methods)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Save the results to a file\n" + ] + }, + { + "cell_type": "code", + "execution_count": 107, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
cleaner/Strange_Tales_172005/Strange_Tales_172005_Tesseract.json\n",
+       "
\n" + ], + "text/plain": [ + "cleaner/Strange_Tales_172005/Strange_Tales_172005_Tesseract.json\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fp, json_results = image_experiment.to_json()\n", + "cprint(fp)\n", + "RenderJSON(json_results, 300, 2)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Load the results from a file\n" + ] + }, + { + "cell_type": "code", + "execution_count": 108, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
---------- Initial box ----------\n",
+       "ResultOCR#block 00: 0.90||Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3\n",
+       "ResultOCR#block 01: 0.93||The house and the old man are alike in many ways; tall, prolid, patient, contented always 0 wait until. their. master cones mome ~~\n",
+       "ResultOCR#block 02: 0.70||“and one in ee would appear.\n",
+       "ResultOCR#block 03: 0.62||Re bambli-~ we have a\n",
+       "ResultOCR#block 04: 0.70||Tonight, he comes noost slamming open the caken\n",
+       "ResultOCR#block 05: 0.82||Tell me naster. how may bambli serve 7\n",
+       "ResultOCR#block 06: 0.56||£7 » and perhaps some dry clothes... 7 /\n",
+       "ResultOCR#block 07: 0.81||The the old man's fades down the hall as... 7\n",
+       "ResultOCR#block 08: 0.85||How curious the 4 fate. whims of had t not chanced to stroll along the river yl tonight ==\n",
+       "ResultOCR#block 09: 0.80||Fas oulckly as t ca, master.\n",
+       "ResultOCR#block 10: 0.91||<the girl would - most slirely be dead by now.\n",
+       "ResultOCR#block 11: 0.47||Ath the girl. a second chance ge a ee yg adil\n",
+       "ResultOCR#block 12: 0.84||Ah girl--there's othing to scream ntt anymore .\n",
+       "ResultOCR#block 13: 0.93||You're among friends now. you're sale\n",
+       "ResultOCR#block 14: 1.00||Continued after next page\n",
+       "\n",
+       " ---------- Default ----------\n",
+       "ResultOCR#block 00: 0.85||Eneowered by great gnarled cypress jrfes, the ancient manor ! alone on the eit of mew rce: eans, kept tipy by a white-haired ao han known only as\n",
+       "ResultOCR#block 01: 0.96||The house and the old man are alike in many ways; tall, proud, patient, conten tel always to wait until their. master cones home ~~\n",
+       "ResultOCR#block 02: 0.74||And one in ee would appear.\n",
+       "ResultOCR#block 03: 0.41||Rir guest.\n",
+       "ResultOCR#block 04: 0.59||=~and tonight, he comes host sane oo\n",
+       "ResultOCR#block 05: 0.78||Tell me masts - how may bambli . serve 7 _\n",
+       "ResultOCR#block 06: 0.48||R warm, bambli-~ and perhaps\n",
+       "ResultOCR#block 07: 0.76||The the old mans fades down the hall s.00\n",
+       "ResultOCR#block 08: 0.92||How curious the a whims of fate . had t not chanced to stroll along the river tonight~~ >\n",
+       "ResultOCR#block 09: 0.50||Aulckly “master as t can,\n",
+       "ResultOCR#block 10: 0.94||<the girl would - most surely be dead by now.\n",
+       "ResultOCR#block 11: 0.50||Ath - the girl. a second chance ee oo tr tt\n",
+       "ResultOCR#block 12: 0.84||Oe girl--there's othing to scream nt anymore. 4\n",
+       "ResultOCR#block 13: 0.96||You're among friends now. youre safe!\n",
+       "ResultOCR#block 14: 1.00||Continued after next page\n",
+       "\n",
+       " ---------- Default, grey pad ----------\n",
+       "ResultOCR#block 00: 0.95||Enbowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as\n",
+       "ResultOCR#block 01: 0.96||The house and the old man are alike in many ways; tall, prolid, patient, contented always to wait until their. * master cones home ~-\n",
+       "ResultOCR#block 02: 0.94||“and one in > need of some help, it would appear .\n",
+       "ResultOCR#block 03: 0.88||\" bambl-- we have a guest.\n",
+       "ResultOCR#block 04: 0.72||~~and tonight, he comes urgently, slanming open\n",
+       "ResultOCR#block 05: 0.86||Tell me, master: how may bambli serve 7\n",
+       "ResultOCR#block 06: 0.90||Some blankets to keep her. warm, bambli-- and perhaps some dry \\ clothes--7 /.\n",
+       "ResultOCR#block 07: 0.06||As.\n",
+       "ResultOCR#block 08: 0.91||How curious the d 7e . whims of fa; had i not chanced to stroll along the river tonight--\n",
+       "ResultOCR#block 09: 0.55||Ickl as t can\n",
+       "ResultOCR#block 10: 0.00||\n",
+       "ResultOCR#block 11: 0.85||Ghede has been generous. the death god has gen + the girl. a second chance te oe ato\" pd ate\n",
+       "ResultOCR#block 12: 0.95||Easy, girl--there's | nothing to scream about anyaore.\n",
+       "ResultOCR#block 13: 0.97||You're among friends now. you're safe!\n",
+       "ResultOCR#block 14: 0.54||“continued a\n",
+       "\n",
+       " ---------- Padded 4px ----------\n",
+       "ResultOCR#block 00: 0.88||Enbonered by great gnarled cypress jrfes, the ancient manor stands alone on the eit of mew rce: eans, kept tipy by a white-haired ao lo man known only as\n",
+       "ResultOCR#block 01: 0.93||The house and the oldman are alike in many ways; tall, proud, patient, contented a ways 0 wait until their. aster comes home ~~ | }\n",
+       "ResultOCR#block 02: 0.69||F and one in ee would appear.\n",
+       "ResultOCR#block 03: 0.77||\" bambli-— we have a gliest.\n",
+       "ResultOCR#block 04: 0.55||P comes slamming open the caken\n",
+       "ResultOCR#block 05: 0.57||Tel oe er-- 5 ow a = 7\n",
+       "ResultOCR#block 06: 0.38||We and perhaps c oe /\n",
+       "ResultOCR#block 07: 0.75||The the old mans fades down the hall sra\n",
+       "ResultOCR#block 08: 0.92||How curious the a whims of fate . - had i not chanced to stroll along the river yl tonight~-\n",
+       "ResultOCR#block 09: 0.79||Aulckly as t can, ‘masrer.\n",
+       "ResultOCR#block 10: 0.92||<the girl wolld - most surely be dead by now.\n",
+       "ResultOCR#block 11: 0.88||Ghede has been generous. the oeath gop has given - the girl. a second chance ye, alem\n",
+       "ResultOCR#block 12: 0.67||Soe er eke othing to scream ay anymore.\n",
+       "ResultOCR#block 13: 0.94||\"you're among friends now. you're safe!\n",
+       "ResultOCR#block 14: 1.00||Continued after next page\n",
+       "\n",
+       " ---------- Padded 8px ----------\n",
+       "ResultOCR#block 00: 0.88||Enbonered by great gnarled cypress trees, the ancient manor stands alone on the pag hile of new orleans, kept tipy by a white-haired ao lo man known omy as\n",
+       "ResultOCR#block 01: 0.99||The house and the old man are alike in many ways; tall, proud, patient, contented always to wait until their. master comes home\n",
+       "ResultOCR#block 02: 0.67||7 and one in ee would appear,\n",
+       "ResultOCR#block 03: 0.76||Zf mbl == we have a guest.\n",
+       "ResultOCR#block 04: 0.61||Tonight, he comes host slamming open\n",
+       "ResultOCR#block 05: 0.75||Yy i tell me master - how may bambli serve 7 _,\n",
+       "ResultOCR#block 06: 0.89||Some. blankets to keep he arm , bambli-= and perhaps some dry clothes. 2s\n",
+       "ResultOCR#block 07: 0.72||The the old mans fades down the hal. srl see\n",
+       "ResultOCR#block 08: 0.88||* how curious the p whims of fate . - had i not chanced . to stroll along _ the river 3 tonight-~\n",
+       "ResultOCR#block 09: 0.65||Tiie as t can, \\ master ,\n",
+       "ResultOCR#block 10: 0.86||The girl wolld - most slirely be - dead by now.\n",
+       "ResultOCR#block 11: 0.62||Ghede has been generous. : the crn son ue;\n",
+       "ResultOCR#block 12: 0.62||Soe er eke othing to scream hbolt anhore hr\n",
+       "ResultOCR#block 13: 0.92||” you're among friends now. you're safe!\n",
+       "ResultOCR#block 14: 0.94||“continued after next page\n",
+       "\n",
+       " ---------- Extracted, init box ----------\n",
+       "ResultOCRExtracted#block 00: 0.92||Fhbonered by great snarled cypress trees, the ancient nmanor stands alone on the outskirts of new orleans. kept tipy by a whi te-haire old man known only as bambi] .\n",
+       "ResultOCRExtracted#block 01: 0.93||Ee house and the old man por alike in many ways; tall, proud, patient, contented always t° wait until their. master cones home ~~\n",
+       "ResultOCRExtracted#block 02: 0.73||And one in fee would appear.\n",
+       "ResultOCRExtracted#block 03: 0.67||— we have a i=s7t.\n",
+       "ResultOCRExtracted#block 04: 0.74||~and tonight, he comes urgently, slamming open\n",
+       "ResultOCRExtracted#block 05: 0.85||Tell me master how may bambli serve 7\n",
+       "ResultOCRExtracted#block 06: 0.93||Some blankets to keep her warm, banbli-- and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.77||The the old man's fades down the hall s.,00\n",
+       "ResultOCRExtracted#block 08: 0.77||Hin ef fare” had i not chanced to stroll along the river. tonigmt=~\n",
+       "ResultOCRExtracted#block 09: 0.85||Aulckly as t can, master,\n",
+       "ResultOCRExtracted#block 10: 1.00||--the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.51||Ath the girl a second chance ro\n",
+       "ResultOCRExtracted#block 12: 0.56||Cas ee, othing to scream pls aa .\n",
+       "ResultOCRExtracted#block 13: 0.95||You're among friends now. you're sale!\n",
+       "ResultOCRExtracted#block 14: 0.91||Continued af ext page\n",
+       "\n",
+       " ---------- Padded 4, extracted ----------\n",
+       "ResultOCRExtracted#block 00: 0.91||Enbonsred by great shale cypress trees, the anci manor stands alone on the [tskirts of new orleans, kept tidy by a whi te- haired old man known only as b8ambl .\n",
+       "ResultOCRExtracted#block 01: 0.98||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master cones home --\n",
+       "ResultOCRExtracted#block 02: 0.73||And one in fee would appear.\n",
+       "ResultOCRExtracted#block 03: 0.83||Bambli we have a gliest.\n",
+       "ResultOCRExtracted#block 04: 0.74||=~and tonight, he comes urgently, slamming open\n",
+       "ResultOCRExtracted#block 05: 0.84||Tell me master. how may bambli serve 7\n",
+       "ResultOCRExtracted#block 06: 0.53||Warm, bambli-- and perhaps. som\n",
+       "ResultOCRExtracted#block 07: 0.75||The the old mans fades down the hall sra\n",
+       "ResultOCRExtracted#block 08: 0.76||We sea had i not chanced to stroll along the river. tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.76||Alley as t can, master,\n",
+       "ResultOCRExtracted#block 10: 0.98||~-the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.92||Chepe has been generous. the peath god has given the girl a second chance amr\n",
+       "ResultOCRExtracted#block 12: 0.73||Cas gr theres othing to scream pissy tore .\n",
+       "ResultOCRExtracted#block 13: 0.95||You're among eriends now. you're safe!\n",
+       "ResultOCRExtracted#block 14: 0.91||Continued af ext page\n",
+       "\n",
+       " ---------- Padded 8, extracted ----------\n",
+       "ResultOCRExtracted#block 00: 0.94||Enbonered by great snarled cypress trees, the ancient nmanor stands alone on the outskirts of new orleans, kept tipy by a whi te-haired old man known only as 8ambli .\n",
+       "ResultOCRExtracted#block 01: 0.98||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master comes home --\n",
+       "ResultOCRExtracted#block 02: 0.70||And one in fee wolld appear.\n",
+       "ResultOCRExtracted#block 03: 0.86||Bambl ~~ we have a guest.\n",
+       "ResultOCRExtracted#block 04: 0.64||=~and tonight, he comes slamming open urgently,\n",
+       "ResultOCRExtracted#block 05: 0.82||Tell me master... how may bambli serve't\n",
+       "ResultOCRExtracted#block 06: 0.91||Some blankets to keep her warm, banbli-~ and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.75||The the old mans fades down the hall sire\n",
+       "ResultOCRExtracted#block 08: 0.96||How curious the whims of fate . had i not chanced to stroll along the river tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.77||Allckry as t can, master.\n",
+       "ResultOCRExtracted#block 10: 0.98||~-the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.94||Ghede has been generous. the peath god has given the girl a second chance po\n",
+       "ResultOCRExtracted#block 12: 0.74||Cas gr theres othing to scream pps hore .\n",
+       "ResultOCRExtracted#block 13: 0.42||You're safe § r\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       " ---------- Padded 8, dilation 1 ----------\n",
+       "ResultOCRExtracted#block 00: 0.61||Outskirts of new orleans, kept tipy by a white-haired old man known only as sams .\n",
+       "ResultOCRExtracted#block 01: 0.88||The house and the old man are alike in many ways, tall, proud, nt, contented live walt gtie their, master comes home -=\n",
+       "ResultOCRExtracted#block 02: 0.97||And one in need of some help, it wolld appear .\n",
+       "ResultOCRExtracted#block 03: 0.78||Bambli ~~ we have a gliest.\n",
+       "ResultOCRExtracted#block 04: 0.79||=and tonight, he comes most slamming open the front\n",
+       "ResultOCRExtracted#block 05: 0.86||Tell me, master: how may bambli serve 7\n",
+       "ResultOCRExtracted#block 06: 0.85||Gone blankets to keep her. warm, bambli-~ and perhaps some dry\n",
+       "ResultOCRExtracted#block 07: 0.73||The old man's footsteps the hall as.re\n",
+       "ResultOCRExtracted#block 08: 0.94||How curious the whims of fate . had i not chanced to stroll along the r/| ver tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.68||Aulckly as t can,\n",
+       "ResultOCRExtracted#block 10: 0.95||~<the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.75||Ee lh boe ene. the death gop has the girl. a second chance\n",
+       "ResultOCRExtracted#block 12: 0.92||Easy, girl--there's nothing to scream abolit anymo!\n",
+       "ResultOCRExtracted#block 13: 0.97||You're among friends now. you're safe!\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       " ---------- Pad 8, fract. 0.5 ----------\n",
+       "ResultOCRExtracted#block 00: 0.94||Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a whi te-haired old man known only as b8ambl .\n",
+       "ResultOCRExtracted#block 01: 0.97||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until. their. master comes home ~~\n",
+       "ResultOCRExtracted#block 02: 0.78||And one in eee pe would appear.\n",
+       "ResultOCRExtracted#block 03: 0.86||Bambl ~~ we have a guest.\n",
+       "ResultOCRExtracted#block 04: 0.64||=~and tonight, he comes slamming open urgently,\n",
+       "ResultOCRExtracted#block 05: 0.82||Tell me master... how may bambli serve'7\n",
+       "ResultOCRExtracted#block 06: 0.94||Some blankets to keep her. warm, bambli-~ and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.73||The the old mans fades donn the hall sire\n",
+       "ResultOCRExtracted#block 08: 0.96||How curious the whims of fate . had i not chanced to stroll along the river tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.81||Aulckry as t can, master.\n",
+       "ResultOCRExtracted#block 10: 0.98||~-the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.92||Ghede hag been generous. the peath god has given the girl a second chance po\n",
+       "ResultOCRExtracted#block 12: 0.76||Cas srl theres othing to scream seoit hore .\n",
+       "ResultOCRExtracted#block 13: 0.42||You're safe 4 ’\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       " ---------- Pad 8, fract. 0.2 ----------\n",
+       "ResultOCRExtracted#block 00: 0.94||Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a whi te-haired old man known only as b8ambl .\n",
+       "ResultOCRExtracted#block 01: 0.97||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master comes home ~~\n",
+       "ResultOCRExtracted#block 02: 0.77||And one in eet sve would appear.\n",
+       "ResultOCRExtracted#block 03: 0.86||Bambl ~~ we have a guest.\n",
+       "ResultOCRExtracted#block 04: 0.64||=~and tonight, he comes slamming open urgently,\n",
+       "ResultOCRExtracted#block 05: 0.82||Tell me master... how may bambli serve'7\n",
+       "ResultOCRExtracted#block 06: 0.94||Some blankets to keep her, warm, bambli-~ and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.75||The the old mans fades down the hall sere\n",
+       "ResultOCRExtracted#block 08: 0.96||How curious the whims of fate . had i not chanced to stroll along the river tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.81||Aulckry as t can, master.\n",
+       "ResultOCRExtracted#block 10: 0.95||~<the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.94||Ghede has been generous. the ceath god has given the girl a second chance po\n",
+       "ResultOCRExtracted#block 12: 0.67||Yi renee othing to scream seat anhore .\n",
+       "ResultOCRExtracted#block 13: 0.93||Youre among eriends now. you're safe!\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       "\n",
+       "
\n" + ], + "text/plain": [ + "---------- Initial box ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.90\u001b[0m||Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski\u001b[1m)\u001b[0m \u001b[1;36m2\u001b[0m of mew ce eans, kept tidy by a white-haired old man known only as bambs, \u001b[1;36m3\u001b[0m\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.93\u001b[0m||The house and the old man are alike in many ways; tall, prolid, patient, contented always \u001b[1;36m0\u001b[0m wait until. their. master cones mome ~~\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.70\u001b[0m||“and one in ee would appear.\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.62\u001b[0m||Re bambli-~ we have a\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.70\u001b[0m||Tonight, he comes noost slamming open the caken\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.82\u001b[0m||Tell me naster. how may bambli serve \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.56\u001b[0m||£\u001b[1;36m7\u001b[0m » and perhaps some dry clothes\u001b[33m...\u001b[0m \u001b[1;36m7\u001b[0m \u001b[35m/\u001b[0m\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.81\u001b[0m||The the old man's fades down the hall as\u001b[33m...\u001b[0m \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.85\u001b[0m||How curious the \u001b[1;36m4\u001b[0m fate. whims of had t not chanced to stroll along the river yl tonight ==\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.80\u001b[0m||Fas oulckly as t ca, master.\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.91\u001b[0m||\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.50\u001b[0m||Aulckly “master as t can,\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.94\u001b[0m|| need of some help, it would appear .\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.88\u001b[0m||\" bambl-- we have a guest.\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.72\u001b[0m||~~and tonight, he comes urgently, slanming open\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.86\u001b[0m||Tell me, master: how may bambli serve \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.90\u001b[0m||Some blankets to keep her. warm, bambli-- and perhaps some dry \\ clothes-\u001b[1;36m-7\u001b[0m \u001b[35m/\u001b[0m\u001b[95m.\u001b[0m\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.06\u001b[0m||As.\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.91\u001b[0m||How curious the d 7e . whims of fa; had i not chanced to stroll along the river tonight--\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.55\u001b[0m||Ickl as t can\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.00\u001b[0m||\n", + "ResultOCR#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.85\u001b[0m||Ghede has been generous. the death god has gen + the girl. a second chance te oe ato\" pd ate\n", + "ResultOCR#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.95\u001b[0m||Easy, girl--there's | nothing to scream about anyaore.\n", + "ResultOCR#block \u001b[1;36m13\u001b[0m: \u001b[1;36m0.97\u001b[0m||You're among friends now. you're safe!\n", + "ResultOCR#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.54\u001b[0m||“continued a\n", + "\n", + " ---------- Padded 4px ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.88\u001b[0m||Enbonered by great gnarled cypress jrfes, the ancient manor stands alone on the eit of mew rce: eans, kept tipy by a white-haired ao lo man known only as\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.93\u001b[0m||The house and the oldman are alike in many ways; tall, proud, patient, contented a ways \u001b[1;36m0\u001b[0m wait until their. aster comes home ~~ | \u001b[1m}\u001b[0m\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.69\u001b[0m||F and one in ee would appear.\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.77\u001b[0m||\" bambli-— we have a gliest.\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.55\u001b[0m||P comes slamming open the caken\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.57\u001b[0m||Tel oe er-- \u001b[1;36m5\u001b[0m ow a = \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.38\u001b[0m||We and perhaps c oe \u001b[35m/\u001b[0m\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.75\u001b[0m||The the old mans fades down the hall sra\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.92\u001b[0m||How curious the a whims of fate . - had i not chanced to stroll along the river yl tonight~-\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.79\u001b[0m||Aulckly as t can, ‘masrer.\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.92\u001b[0m|| Page: media/Strange_Tales_172005.jpg
Size: 1275x1888 px: 4.25 x 6.29 in @ 188.32 dpi
Model: Tesseract
Crop Method: Pad 8, fract. 0.2
Accuracy Mean/Trimmed: 0.85/0.86
\n", + "
\n", + "
Box #ImageAccuracyOCR
1
0.94
Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as b8ambl .
2
0.97
The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master comes home ~~
3
0.77
And one in eet⎕⎕⎕ sve⎕⎕⎕⎕⎕⎕⎕⎕⎕ would appear.
4
0.86
Bambl ~~ we have a guest.
5
0.64
=~and tonight, he comes ⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕slamming open urg⎕⎕⎕⎕en⎕⎕⎕⎕⎕tly,⎕⎕⎕⎕
6
0.82
Tell me master... how may bambli serve'7
7
0.94
Some blankets to keep her, warm, bambli-~ and perhaps. some dry clothes
8
0.75
The⎕⎕⎕⎕⎕⎕⎕⎕ the old man⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕s fades down the hall sere
9
0.96
How curious the whims of fate. had i not chanced to stroll along the river tonight-~
10
0.81
A⎕⎕⎕ulckry as t can, master.
11
0.95
~<the girl would most surely be dead by now.
12
0.94
Ghede has been generous. the ceath god has given the girl a second chance po⎕⎕
13
0.67
Yi⎕⎕⎕ ⎕⎕⎕⎕⎕⎕⎕⎕⎕⎕renee othing to scream sea⎕⎕⎕t anhore.
14
0.93
Youre among eriends now. you're safe!
15
0.83
| continued af⎕⎕⎕ ext page
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "saved_exp = ExperimentOCR.saved_experiment(CONTEXT, 'Tesseract', IMAGE_CONTEXT.image_idx)\n", + "# saved_exp = ExperimentOCR.saved_experiment(IMAGE_CONTEXT, 'Tesseract', 'Action_Comics_1960-01-00_(262).JPG')\n", + "if saved_exp:\n", + " saved_exp.method_experiment(CropMethod.PAD_8_FRACT_0_2).display()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Perform experiments for selected boxes y methods" + ] + }, + { + "cell_type": "code", + "execution_count": 110, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
---------- Initial box ----------\n",
+       "ResultOCR#block 00: 0.90||Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski) 2 of mew ce eans, kept tidy by a white-haired old man known only as bambs, 3\n",
+       "ResultOCR#block 01: 0.93||The house and the old man are alike in many ways; tall, prolid, patient, contented always 0 wait until. their. master cones mome ~~\n",
+       "ResultOCR#block 02: 0.70||“and one in ee would appear.\n",
+       "ResultOCR#block 03: 0.62||Re bambli-~ we have a\n",
+       "ResultOCR#block 04: 0.70||Tonight, he comes noost slamming open the caken\n",
+       "ResultOCR#block 05: 0.82||Tell me naster. how may bambli serve 7\n",
+       "ResultOCR#block 06: 0.56||£7 » and perhaps some dry clothes... 7 /\n",
+       "ResultOCR#block 07: 0.81||The the old man's fades down the hall as... 7\n",
+       "ResultOCR#block 08: 0.85||How curious the 4 fate. whims of had t not chanced to stroll along the river yl tonight ==\n",
+       "ResultOCR#block 09: 0.80||Fas oulckly as t ca, master.\n",
+       "ResultOCR#block 10: 0.91||<the girl would - most slirely be dead by now.\n",
+       "ResultOCR#block 11: 0.47||Ath the girl. a second chance ge a ee yg adil\n",
+       "ResultOCR#block 12: 0.84||Ah girl--there's othing to scream ntt anymore .\n",
+       "ResultOCR#block 13: 0.93||You're among friends now. you're sale\n",
+       "ResultOCR#block 14: 1.00||Continued after next page\n",
+       "\n",
+       " ---------- Default ----------\n",
+       "ResultOCR#block 00: 0.85||Eneowered by great gnarled cypress jrfes, the ancient manor ! alone on the eit of mew rce: eans, kept tipy by a white-haired ao han known only as\n",
+       "ResultOCR#block 01: 0.96||The house and the old man are alike in many ways; tall, proud, patient, conten tel always to wait until their. master cones home ~~\n",
+       "ResultOCR#block 02: 0.74||And one in ee would appear.\n",
+       "ResultOCR#block 03: 0.41||Rir guest.\n",
+       "ResultOCR#block 04: 0.59||=~and tonight, he comes host sane oo\n",
+       "ResultOCR#block 05: 0.78||Tell me masts - how may bambli . serve 7 _\n",
+       "ResultOCR#block 06: 0.48||R warm, bambli-~ and perhaps\n",
+       "ResultOCR#block 07: 0.76||The the old mans fades down the hall s.00\n",
+       "ResultOCR#block 08: 0.92||How curious the a whims of fate . had t not chanced to stroll along the river tonight~~ >\n",
+       "ResultOCR#block 09: 0.50||Aulckly “master as t can,\n",
+       "ResultOCR#block 10: 0.94||<the girl would - most surely be dead by now.\n",
+       "ResultOCR#block 11: 0.50||Ath - the girl. a second chance ee oo tr tt\n",
+       "ResultOCR#block 12: 0.84||Oe girl--there's othing to scream nt anymore. 4\n",
+       "ResultOCR#block 13: 0.96||You're among friends now. youre safe!\n",
+       "ResultOCR#block 14: 1.00||Continued after next page\n",
+       "\n",
+       " ---------- Default, grey pad ----------\n",
+       "ResultOCR#block 00: 0.95||Enbowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a white-haired old man known only as\n",
+       "ResultOCR#block 01: 0.96||The house and the old man are alike in many ways; tall, prolid, patient, contented always to wait until their. * master cones home ~-\n",
+       "ResultOCR#block 02: 0.94||“and one in > need of some help, it would appear .\n",
+       "ResultOCR#block 03: 0.88||\" bambl-- we have a guest.\n",
+       "ResultOCR#block 04: 0.72||~~and tonight, he comes urgently, slanming open\n",
+       "ResultOCR#block 05: 0.86||Tell me, master: how may bambli serve 7\n",
+       "ResultOCR#block 06: 0.90||Some blankets to keep her. warm, bambli-- and perhaps some dry \\ clothes--7 /.\n",
+       "ResultOCR#block 07: 0.06||As.\n",
+       "ResultOCR#block 08: 0.91||How curious the d 7e . whims of fa; had i not chanced to stroll along the river tonight--\n",
+       "ResultOCR#block 09: 0.55||Ickl as t can\n",
+       "ResultOCR#block 10: 0.00||\n",
+       "ResultOCR#block 11: 0.85||Ghede has been generous. the death god has gen + the girl. a second chance te oe ato\" pd ate\n",
+       "ResultOCR#block 12: 0.95||Easy, girl--there's | nothing to scream about anyaore.\n",
+       "ResultOCR#block 13: 0.97||You're among friends now. you're safe!\n",
+       "ResultOCR#block 14: 0.54||“continued a\n",
+       "\n",
+       " ---------- Padded 4px ----------\n",
+       "ResultOCR#block 00: 0.88||Enbonered by great gnarled cypress jrfes, the ancient manor stands alone on the eit of mew rce: eans, kept tipy by a white-haired ao lo man known only as\n",
+       "ResultOCR#block 01: 0.93||The house and the oldman are alike in many ways; tall, proud, patient, contented a ways 0 wait until their. aster comes home ~~ | }\n",
+       "ResultOCR#block 02: 0.69||F and one in ee would appear.\n",
+       "ResultOCR#block 03: 0.77||\" bambli-— we have a gliest.\n",
+       "ResultOCR#block 04: 0.55||P comes slamming open the caken\n",
+       "ResultOCR#block 05: 0.57||Tel oe er-- 5 ow a = 7\n",
+       "ResultOCR#block 06: 0.38||We and perhaps c oe /\n",
+       "ResultOCR#block 07: 0.75||The the old mans fades down the hall sra\n",
+       "ResultOCR#block 08: 0.92||How curious the a whims of fate . - had i not chanced to stroll along the river yl tonight~-\n",
+       "ResultOCR#block 09: 0.79||Aulckly as t can, ‘masrer.\n",
+       "ResultOCR#block 10: 0.92||<the girl wolld - most surely be dead by now.\n",
+       "ResultOCR#block 11: 0.88||Ghede has been generous. the oeath gop has given - the girl. a second chance ye, alem\n",
+       "ResultOCR#block 12: 0.67||Soe er eke othing to scream ay anymore.\n",
+       "ResultOCR#block 13: 0.94||\"you're among friends now. you're safe!\n",
+       "ResultOCR#block 14: 1.00||Continued after next page\n",
+       "\n",
+       " ---------- Padded 8px ----------\n",
+       "ResultOCR#block 00: 0.88||Enbonered by great gnarled cypress trees, the ancient manor stands alone on the pag hile of new orleans, kept tipy by a white-haired ao lo man known omy as\n",
+       "ResultOCR#block 01: 0.99||The house and the old man are alike in many ways; tall, proud, patient, contented always to wait until their. master comes home\n",
+       "ResultOCR#block 02: 0.67||7 and one in ee would appear,\n",
+       "ResultOCR#block 03: 0.76||Zf mbl == we have a guest.\n",
+       "ResultOCR#block 04: 0.61||Tonight, he comes host slamming open\n",
+       "ResultOCR#block 05: 0.75||Yy i tell me master - how may bambli serve 7 _,\n",
+       "ResultOCR#block 06: 0.89||Some. blankets to keep he arm , bambli-= and perhaps some dry clothes. 2s\n",
+       "ResultOCR#block 07: 0.72||The the old mans fades down the hal. srl see\n",
+       "ResultOCR#block 08: 0.88||* how curious the p whims of fate . - had i not chanced . to stroll along _ the river 3 tonight-~\n",
+       "ResultOCR#block 09: 0.65||Tiie as t can, \\ master ,\n",
+       "ResultOCR#block 10: 0.86||The girl wolld - most slirely be - dead by now.\n",
+       "ResultOCR#block 11: 0.62||Ghede has been generous. : the crn son ue;\n",
+       "ResultOCR#block 12: 0.62||Soe er eke othing to scream hbolt anhore hr\n",
+       "ResultOCR#block 13: 0.92||” you're among friends now. you're safe!\n",
+       "ResultOCR#block 14: 0.94||“continued after next page\n",
+       "\n",
+       " ---------- Extracted, init box ----------\n",
+       "ResultOCRExtracted#block 00: 0.92||Fhbonered by great snarled cypress trees, the ancient nmanor stands alone on the outskirts of new orleans. kept tipy by a whi te-haire old man known only as bambi] .\n",
+       "ResultOCRExtracted#block 01: 0.93||Ee house and the old man por alike in many ways; tall, proud, patient, contented always t° wait until their. master cones home ~~\n",
+       "ResultOCRExtracted#block 02: 0.73||And one in fee would appear.\n",
+       "ResultOCRExtracted#block 03: 0.67||— we have a i=s7t.\n",
+       "ResultOCRExtracted#block 04: 0.74||~and tonight, he comes urgently, slamming open\n",
+       "ResultOCRExtracted#block 05: 0.85||Tell me master how may bambli serve 7\n",
+       "ResultOCRExtracted#block 06: 0.93||Some blankets to keep her warm, banbli-- and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.77||The the old man's fades down the hall s.,00\n",
+       "ResultOCRExtracted#block 08: 0.77||Hin ef fare” had i not chanced to stroll along the river. tonigmt=~\n",
+       "ResultOCRExtracted#block 09: 0.85||Aulckly as t can, master,\n",
+       "ResultOCRExtracted#block 10: 1.00||--the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.51||Ath the girl a second chance ro\n",
+       "ResultOCRExtracted#block 12: 0.56||Cas ee, othing to scream pls aa .\n",
+       "ResultOCRExtracted#block 13: 0.95||You're among friends now. you're sale!\n",
+       "ResultOCRExtracted#block 14: 0.91||Continued af ext page\n",
+       "\n",
+       " ---------- Padded 4, extracted ----------\n",
+       "ResultOCRExtracted#block 00: 0.91||Enbonsred by great shale cypress trees, the anci manor stands alone on the [tskirts of new orleans, kept tidy by a whi te- haired old man known only as b8ambl .\n",
+       "ResultOCRExtracted#block 01: 0.98||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master cones home --\n",
+       "ResultOCRExtracted#block 02: 0.73||And one in fee would appear.\n",
+       "ResultOCRExtracted#block 03: 0.83||Bambli we have a gliest.\n",
+       "ResultOCRExtracted#block 04: 0.74||=~and tonight, he comes urgently, slamming open\n",
+       "ResultOCRExtracted#block 05: 0.84||Tell me master. how may bambli serve 7\n",
+       "ResultOCRExtracted#block 06: 0.53||Warm, bambli-- and perhaps. som\n",
+       "ResultOCRExtracted#block 07: 0.75||The the old mans fades down the hall sra\n",
+       "ResultOCRExtracted#block 08: 0.76||We sea had i not chanced to stroll along the river. tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.76||Alley as t can, master,\n",
+       "ResultOCRExtracted#block 10: 0.98||~-the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.92||Chepe has been generous. the peath god has given the girl a second chance amr\n",
+       "ResultOCRExtracted#block 12: 0.73||Cas gr theres othing to scream pissy tore .\n",
+       "ResultOCRExtracted#block 13: 0.95||You're among eriends now. you're safe!\n",
+       "ResultOCRExtracted#block 14: 0.91||Continued af ext page\n",
+       "\n",
+       " ---------- Padded 8, extracted ----------\n",
+       "ResultOCRExtracted#block 00: 0.94||Enbonered by great snarled cypress trees, the ancient nmanor stands alone on the outskirts of new orleans, kept tipy by a whi te-haired old man known only as 8ambli .\n",
+       "ResultOCRExtracted#block 01: 0.98||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master comes home --\n",
+       "ResultOCRExtracted#block 02: 0.70||And one in fee wolld appear.\n",
+       "ResultOCRExtracted#block 03: 0.86||Bambl ~~ we have a guest.\n",
+       "ResultOCRExtracted#block 04: 0.64||=~and tonight, he comes slamming open urgently,\n",
+       "ResultOCRExtracted#block 05: 0.82||Tell me master... how may bambli serve't\n",
+       "ResultOCRExtracted#block 06: 0.91||Some blankets to keep her warm, banbli-~ and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.75||The the old mans fades down the hall sire\n",
+       "ResultOCRExtracted#block 08: 0.96||How curious the whims of fate . had i not chanced to stroll along the river tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.77||Allckry as t can, master.\n",
+       "ResultOCRExtracted#block 10: 0.98||~-the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.94||Ghede has been generous. the peath god has given the girl a second chance po\n",
+       "ResultOCRExtracted#block 12: 0.74||Cas gr theres othing to scream pps hore .\n",
+       "ResultOCRExtracted#block 13: 0.42||You're safe § r\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       " ---------- Padded 8, dilation 1 ----------\n",
+       "ResultOCRExtracted#block 00: 0.61||Outskirts of new orleans, kept tipy by a white-haired old man known only as sams .\n",
+       "ResultOCRExtracted#block 01: 0.88||The house and the old man are alike in many ways, tall, proud, nt, contented live walt gtie their, master comes home -=\n",
+       "ResultOCRExtracted#block 02: 0.97||And one in need of some help, it wolld appear .\n",
+       "ResultOCRExtracted#block 03: 0.78||Bambli ~~ we have a gliest.\n",
+       "ResultOCRExtracted#block 04: 0.79||=and tonight, he comes most slamming open the front\n",
+       "ResultOCRExtracted#block 05: 0.86||Tell me, master: how may bambli serve 7\n",
+       "ResultOCRExtracted#block 06: 0.85||Gone blankets to keep her. warm, bambli-~ and perhaps some dry\n",
+       "ResultOCRExtracted#block 07: 0.73||The old man's footsteps the hall as.re\n",
+       "ResultOCRExtracted#block 08: 0.94||How curious the whims of fate . had i not chanced to stroll along the r/| ver tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.68||Aulckly as t can,\n",
+       "ResultOCRExtracted#block 10: 0.95||~<the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.75||Ee lh boe ene. the death gop has the girl. a second chance\n",
+       "ResultOCRExtracted#block 12: 0.92||Easy, girl--there's nothing to scream abolit anymo!\n",
+       "ResultOCRExtracted#block 13: 0.97||You're among friends now. you're safe!\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       " ---------- Pad 8, fract. 0.5 ----------\n",
+       "ResultOCRExtracted#block 00: 0.94||Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a whi te-haired old man known only as b8ambl .\n",
+       "ResultOCRExtracted#block 01: 0.97||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until. their. master comes home ~~\n",
+       "ResultOCRExtracted#block 02: 0.78||And one in eee pe would appear.\n",
+       "ResultOCRExtracted#block 03: 0.86||Bambl ~~ we have a guest.\n",
+       "ResultOCRExtracted#block 04: 0.64||=~and tonight, he comes slamming open urgently,\n",
+       "ResultOCRExtracted#block 05: 0.82||Tell me master... how may bambli serve'7\n",
+       "ResultOCRExtracted#block 06: 0.94||Some blankets to keep her. warm, bambli-~ and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.73||The the old mans fades donn the hall sire\n",
+       "ResultOCRExtracted#block 08: 0.96||How curious the whims of fate . had i not chanced to stroll along the river tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.81||Aulckry as t can, master.\n",
+       "ResultOCRExtracted#block 10: 0.98||~-the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.92||Ghede hag been generous. the peath god has given the girl a second chance po\n",
+       "ResultOCRExtracted#block 12: 0.76||Cas srl theres othing to scream seoit hore .\n",
+       "ResultOCRExtracted#block 13: 0.42||You're safe 4 ’\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       " ---------- Pad 8, fract. 0.2 ----------\n",
+       "ResultOCRExtracted#block 00: 0.94||Enbonered by great snarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tipy by a whi te-haired old man known only as b8ambl .\n",
+       "ResultOCRExtracted#block 01: 0.97||The house and the old man are alike in many ways; tall, proud, patient, contented always t° wait until their. master comes home ~~\n",
+       "ResultOCRExtracted#block 02: 0.77||And one in eet sve would appear.\n",
+       "ResultOCRExtracted#block 03: 0.86||Bambl ~~ we have a guest.\n",
+       "ResultOCRExtracted#block 04: 0.64||=~and tonight, he comes slamming open urgently,\n",
+       "ResultOCRExtracted#block 05: 0.82||Tell me master... how may bambli serve'7\n",
+       "ResultOCRExtracted#block 06: 0.94||Some blankets to keep her, warm, bambli-~ and perhaps. some dry clothes\n",
+       "ResultOCRExtracted#block 07: 0.75||The the old mans fades down the hall sere\n",
+       "ResultOCRExtracted#block 08: 0.96||How curious the whims of fate . had i not chanced to stroll along the river tonight-~\n",
+       "ResultOCRExtracted#block 09: 0.81||Aulckry as t can, master.\n",
+       "ResultOCRExtracted#block 10: 0.95||~<the girl would most surely be dead by now.\n",
+       "ResultOCRExtracted#block 11: 0.94||Ghede has been generous. the ceath god has given the girl a second chance po\n",
+       "ResultOCRExtracted#block 12: 0.67||Yi renee othing to scream seat anhore .\n",
+       "ResultOCRExtracted#block 13: 0.93||Youre among eriends now. you're safe!\n",
+       "ResultOCRExtracted#block 14: 0.83||| continued af ext page\n",
+       "\n",
+       "\n",
+       "
\n" + ], + "text/plain": [ + "---------- Initial box ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.90\u001b[0m||Eneonered by great gnarled cypress jrfes, the ancient manor stands alone on the outski\u001b[1m)\u001b[0m \u001b[1;36m2\u001b[0m of mew ce eans, kept tidy by a white-haired old man known only as bambs, \u001b[1;36m3\u001b[0m\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.93\u001b[0m||The house and the old man are alike in many ways; tall, prolid, patient, contented always \u001b[1;36m0\u001b[0m wait until. their. master cones mome ~~\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.70\u001b[0m||“and one in ee would appear.\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.62\u001b[0m||Re bambli-~ we have a\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.70\u001b[0m||Tonight, he comes noost slamming open the caken\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.82\u001b[0m||Tell me naster. how may bambli serve \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.56\u001b[0m||£\u001b[1;36m7\u001b[0m » and perhaps some dry clothes\u001b[33m...\u001b[0m \u001b[1;36m7\u001b[0m \u001b[35m/\u001b[0m\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.81\u001b[0m||The the old man's fades down the hall as\u001b[33m...\u001b[0m \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.85\u001b[0m||How curious the \u001b[1;36m4\u001b[0m fate. whims of had t not chanced to stroll along the river yl tonight ==\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.80\u001b[0m||Fas oulckly as t ca, master.\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.91\u001b[0m||\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.50\u001b[0m||Aulckly “master as t can,\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.94\u001b[0m|| need of some help, it would appear .\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.88\u001b[0m||\" bambl-- we have a guest.\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.72\u001b[0m||~~and tonight, he comes urgently, slanming open\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.86\u001b[0m||Tell me, master: how may bambli serve \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.90\u001b[0m||Some blankets to keep her. warm, bambli-- and perhaps some dry \\ clothes-\u001b[1;36m-7\u001b[0m \u001b[35m/\u001b[0m\u001b[95m.\u001b[0m\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.06\u001b[0m||As.\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.91\u001b[0m||How curious the d 7e . whims of fa; had i not chanced to stroll along the river tonight--\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.55\u001b[0m||Ickl as t can\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.00\u001b[0m||\n", + "ResultOCR#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.85\u001b[0m||Ghede has been generous. the death god has gen + the girl. a second chance te oe ato\" pd ate\n", + "ResultOCR#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.95\u001b[0m||Easy, girl--there's | nothing to scream about anyaore.\n", + "ResultOCR#block \u001b[1;36m13\u001b[0m: \u001b[1;36m0.97\u001b[0m||You're among friends now. you're safe!\n", + "ResultOCR#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.54\u001b[0m||“continued a\n", + "\n", + " ---------- Padded 4px ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.88\u001b[0m||Enbonered by great gnarled cypress jrfes, the ancient manor stands alone on the eit of mew rce: eans, kept tipy by a white-haired ao lo man known only as\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.93\u001b[0m||The house and the oldman are alike in many ways; tall, proud, patient, contented a ways \u001b[1;36m0\u001b[0m wait until their. aster comes home ~~ | \u001b[1m}\u001b[0m\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.69\u001b[0m||F and one in ee would appear.\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.77\u001b[0m||\" bambli-— we have a gliest.\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.55\u001b[0m||P comes slamming open the caken\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.57\u001b[0m||Tel oe er-- \u001b[1;36m5\u001b[0m ow a = \u001b[1;36m7\u001b[0m\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.38\u001b[0m||We and perhaps c oe \u001b[35m/\u001b[0m\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.75\u001b[0m||The the old mans fades down the hall sra\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.92\u001b[0m||How curious the a whims of fate . - had i not chanced to stroll along the river yl tonight~-\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.79\u001b[0m||Aulckly as t can, ‘masrer.\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.92\u001b[0m||---------- Initial box ----------\n", + "ResultOCR#block 00: 0.90||Suddenly.\n", + "ResultOCR#block 01: 0.81||>gasp!z everything's w- whirling around me!t can't stand sr rea\n", + "ResultOCR#block 02: 0.81||Clark!i'm falling' “help! help! —\n", + "ResultOCR#block 03: 0.67||I-i'm bs \"passing ohhh\n", + "ResultOCR#block 04: 0.92||Action comics\n", + "ResultOCR#block 05: 1.00||Then, seconds later...\n", + "ResultOCR#block 06: 0.89||Great caesars ghost! { this /s black magic! we've been transportel to the weirdest world, tit ever saw!\n", + "ResultOCR#block 07: 0.91||...|t certainly isn't our earth, perry. look at the size of those bees.\n", + "ResultOCR#block 08: 0.88||Watch out, clark!\n", + "ResultOCR#block 09: 0.73||Owwww.\n", + "ResultOCR#block 10: 0.85||Yet the bee's stinger went ws. right through my uniform and penetrated my skin! that means. the fabric of pe ay costume has become dj} satie fued 1,\n", + "ResultOCR#block 11: 0.87||Hurry. let's beat it before we get stung, foo = aii\n", + "ResultOCR#block 12: 0.86||Ggreat guns!...2g, .pa/n i feel n pain! as superman, i should be > invulnerable! 1 have unbreakabl ‘skin! under my clark kent clothes, im wearing an woestructisle superman uniform ! =\n", + "ResultOCR#block 13: 1.00||Abruptly...\n", + "ResultOCR#block 14: 0.77||Great caesar's ghost. he's spinning a web of g/ant, sr strands --\n", + "ResultOCR#block 15: 0.89||I-i feel the heat of the sun...the pain of the bee-sting ... the heavy weight of my pack! every human discomfort... good grief! i've lost all my super-powers! i've become an ordinary mortal in this world. /\n", + "ResultOCR#block 16: 0.84||Enormous spider- like creature is going berserk, as if the sight of us excited him (into mad spinning get back! that 2\n", + "\n", + " ---------- Default ----------\n", + "ResultOCR#block 00: 1.00||Suddenly...\n", + "ResultOCR#block 01: 0.80||Gasp! everything's w- whi rns atound me!i can't stand we a\n", + "ResultOCR#block 02: 0.87||Clark!i'm falling! help! help\n", + "ResultOCR#block 03: 0.45||I-i'm se ou\n", + "ResultOCR#block 04: 0.92||Action comics\n", + "ResultOCR#block 05: 0.95||Then, seconds later.\n", + "ResultOCR#block 06: 0.92||Great caesar's ghost! this /s black magic! we've been transported, to the weirdest world, i ever saw!\n", + "ResultOCR#block 07: 0.93||...|t certainly isn't our earth, perry look at the size of those bees!\n", + "ResultOCR#block 08: 0.88||Watch out, clark)\n", + "ResultOCR#block 09: 0.67||Owwww.,\n", + "ResultOCR#block 10: 0.97||Yet the bee's stinger went = right through my uniform ando penetrated my skin! that mean the fabric of my superman costume has become ordinary, cloth! £\n", + "ResultOCR#block 11: 0.89||Hurry. let's beat it before we get stung, tod yi\n", + "ResultOCR#block 12: 0.88||Ggreat guns!...2gasp/z...pain i feel n fan! as superman, i should be invulnerable! 1 have unbreakable skin! under my clark kent clothes, i'm wearing an /noestruct/ele superman uniform !\n", + "ResultOCR#block 13: 1.00||Abruptly...\n", + "ResultOCR#block 14: 0.76||Great caesars ° ghost! he's spinning & web of g/a, silk strands\n", + "ResultOCR#block 15: 0.89||I-i feel the heat of the sum...the pain of the bee-sting... the heavy weight of my { pack! every human discomfort... good grief! i've lost all my super -powersx ve become an ordinary mortal in this world! j\n", + "ResultOCR#block 16: 0.70||Like creature is going berserk, as if the sight of us excited him into mad spinning, get back! that \\ enormous spider-\n", + "\n", + " ---------- Default, grey pad ----------\n", + "ResultOCR#block 00: 0.78||Sudden.\n", + "ResultOCR#block 01: 0.82||5gasp!z everything's | w- whirling around] me!t can't stand ue a\n", + "ResultOCR#block 02: 0.57||Ark!i'm falling\n", + "ResultOCR#block 03: 0.85||I-t'm eq \"passing out... ohhh.\n", + "ResultOCR#block 04: 0.92||Action comics\n", + "ResultOCR#block 05: 0.00||\n", + "ResultOCR#block 06: 0.88||‘great caesar's ghost! || this /§ black magic! we've been transportel to the weirdest world, i ever saw.\n", + "ResultOCR#block 07: 0.87||\"...it certainly isn't our | earth, perry look at the| \\s|ze of those bees.\n", + "ResultOCR#block 08: 0.88||Watch out, clark!\n", + "ResultOCR#block 09: 0.73||Owwww.\n", + "ResultOCR#block 10: 0.90||Yet the bee's stinger went ~ right through my uniform and penetrated my skin! that means. the fabric of sera ry costume vis become din a s clots! a\n", + "ResultOCR#block 11: 0.80||Hurry. let's ) beat it before] we get stung, k 00! __j\n", + "ResultOCR#block 12: 0.86||Ggreat guns!...3gasp/=... rain t feel fan! as superman, i should be invulnerable® 1 have unbreakabli skin! under my clark kent clothes, ia wearing an /noestryct/iele supe iperman uniform !\n", + "ResultOCR#block 13: 0.90||Abruptly.\n", + "ResultOCR#block 14: 0.92||Great caesar's | ghost! he's spinning k web of giant, silk strands -- as tough as steel! ne]\n", + "ResultOCR#block 15: 0.89||\\i-t feel the heat of the sun...the pain of the bee-sting... the heavy weight of my & pack! every human discomfort... good grief! i've lost all my super-powers i've become an ordinary mortal in this world. /.\n", + "ResultOCR#block 16: 0.94||(get back! that enormous spider- like creature 1 § going berserk, as if the sight of us excited him into mad spinning]\n", + "\n", + " ---------- Padded 4px ----------\n", + "ResultOCR#block 00: 0.90||Suddenly.\n", + "ResultOCR#block 01: 0.83||Fgasp/= everything's whirling around me!i can't stand ea\n", + "ResultOCR#block 02: 0.68||Ie lottie eto clark!i'm falling help! help!\n", + "ResultOCR#block 03: 0.55||I-i'm yr a ohh hh.\n", + "ResultOCR#block 04: 0.92||Action comics\n", + "ResultOCR#block 05: 0.74||Then, seconds\n", + "ResultOCR#block 06: 0.62||Great caesar's ghost! \\ this /§ black magic! i ever saw!\n", + "ResultOCR#block 07: 0.87||T certainly isn't our earth, perry! look at the \\s|ze of those bees.\n", + "ResultOCR#block 08: 0.76||#3 watch out, clark!\n", + "ResultOCR#block 09: 0.73||Owwww.\n", + "ResultOCR#block 10: 0.95||\"yet the bee's stinger went ~~ right through my uniform and enetrated my skin! that means. the fabric of my superman costume has become ordinary, clot! &\n", + "ResultOCR#block 11: 0.86||Hurry! let's beat it b=fore we get stung, ¢ tool: puma\n", + "ResultOCR#block 12: 0.86||‘ggreat guns!...3gasp/5... pain i feel pain? as superman, i should be invilnerable | t ne unbreakasle gkin! under my clark kent clothes, tih wearing an indestructible superman uniform\n", + "ResultOCR#block 13: 1.00||Abruptly...\n", + "ResultOCR#block 14: 0.91||Great caesars ghost. he's spinning a web of g/ant, | silk strands as tough as steel!\n", + "ResultOCR#block 15: 0.89||I-i feel the heat of the sun...the pain of the bee-sting... the heavy weight of my { pack! every human discomfort... good grief! i've lost all \\my super-powersx ve become an ordinary mortal in this world.\n", + "ResultOCR#block 16: 0.81||(get back! that enormous spider- as if the sight of us excited him [into mad spinning\n", + "\n", + " ---------- Padded 8px ----------\n", + "ResultOCR#block 00: 0.00||\n", + "ResultOCR#block 01: 0.76||Ega5p/% everything s\\ w- whirling around me! t can't stand ue... slee\n", + "ResultOCR#block 02: 0.41||\\ help! help’\n", + "ResultOCR#block 03: 0.86||-i'm passing qut... ohh hh.\n", + "ResultOCR#block 04: 0.92||Action comics\n", + "ResultOCR#block 05: 0.00||\n", + "ResultOCR#block 06: 0.18||I ever saw/\n", + "ResultOCR#block 07: 0.86||It certainly isn't our earth, perry! poor a the size of thos!\n", + "ResultOCR#block 08: 0.62||“watch out” ial ae clark!\n", + "ResultOCR#block 09: 0.73||Owwww.\n", + "ResultOCR#block 10: 0.98||Yet the bee's stinger went right through my uniform and \\penetrated my skin! that means. the fabric of my superman costume has become ordinar! cloth!\n", + "ResultOCR#block 11: 0.93||Hurry. let's beat it before we get stung, too!\n", + "ResultOCR#block 12: 0.88||Ggreat guns!...2gasp/z...pain i feel fan! as superman, i should be invulnerable! 1 have unbreakabl skin! under my clark kent clothes, i'm wearing an /noestruct/ele $ superman uniform ! &\n", + "ResultOCR#block 13: 0.00||\n", + "ResultOCR#block 14: 0.91||Great caesai ghost. he's spinning a web of g/ant, silk strands -- as tough as steel!\n", + "ResultOCR#block 15: 0.88||I-i feel the heat of the sun...the pain of the bee-sting... the heavy weight of my ! every human discomfort... good grief! i've lost all \\my super-powers! ‘ve become an ordinary mortal in this world. x\n", + "ResultOCR#block 16: 0.60||As if the sight of us excited him {into mad spinning\n", + "\n", + " ---------- Extracted, init box ----------\n", + "ResultOCRExtracted#block 00: 0.90||Suddenly.\n", + "ResultOCRExtracted#block 01: 0.83||Fgasp!z everything § w- whirling around me!i can't stand ue.\n", + "ResultOCRExtracted#block 02: 0.89||Clark!i'm falling! help! help!\n", + "ResultOCRExtracted#block 03: 0.71||I-i'm passing ohhh?\n", + "ResultOCRExtracted#block 04: 0.92||Action comics\n", + "ResultOCRExtracted#block 05: 0.98||Then, seconds later..\n", + "ResultOCRExtracted#block 06: 0.91||Great caesar's ghost! this /s black magic! we've been transportel to the weirdest world tit ever saw.\n", + "ResultOCRExtracted#block 07: 0.90||...it certainly isnt our earth, perry. look at the size of those bees.\n", + "ResultOCRExtracted#block 08: 0.88||Watch out, clark!\n", + "ResultOCRExtracted#block 09: 0.62||Oowwwww.\n", + "ResultOCRExtracted#block 10: 0.99||Yet the bee's stinger went right through my uniform and penetrated my skin! that means. the fabric of my superman costume has become ordinary cloth!\n", + "ResultOCRExtracted#block 11: 0.93||Hurry. let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block 12: 0.88||Ggreat guns!...2g/ pain t feel pain? as superman, i should be invulnerable! 1 have unbreakabl skin! under my clark kent clothes, i'm wearing an /noestruct/ble superman uniform *\n", + "ResultOCRExtracted#block 13: 0.96||Abruptly. ..\n", + "ResultOCRExtracted#block 14: 0.80||Great caesar's ghost. he's spinning a web of g/ant, silk strands --\n", + "ResultOCRExtracted#block 15: 0.89||I-i feel the heat of the sun. the pain of the bee-sting... the heavy weight of my pack! every human discomfort... good grief! t've lost all my super-powers. ve become an ordinary mortal im this world.\n", + "ResultOCRExtracted#block 16: 0.97||Get back. that enormous spider- like creature is going berserk, as if the sight of us excited him into mad spinning\n", + "\n", + " ---------- Padded 4, extracted ----------\n", + "ResultOCRExtracted#block 00: 1.00||Suddenly...\n", + "ResultOCRExtracted#block 01: 0.83||Sgasp/z everything s w- whirling around me!i can't stand ue.\n", + "ResultOCRExtracted#block 02: 0.87||Clark! i'm falling’ help! help!\n", + "ResultOCRExtracted#block 03: 0.78||I-i'm passing ohhhh.\n", + "ResultOCRExtracted#block 04: 0.92||Action comics\n", + "ResultOCRExtracted#block 05: 0.98||Then, seconds later..\n", + "ResultOCRExtracted#block 06: 0.93||Great caesar's ghost! this as black magic! we've been transported to the weirdest world i ever saw!\n", + "ResultOCRExtracted#block 07: 0.90||...it certainly isnt our earth, perry. look at the size of those bees.\n", + "ResultOCRExtracted#block 08: 0.88||Watch out, clark!\n", + "ResultOCRExtracted#block 09: 0.67||Owwww.,\n", + "ResultOCRExtracted#block 10: 0.99||Yet the bee's stinger went right through my uniform and penetrated my skin! that means. the fabric of my superman costume has become ordinary cloth!\n", + "ResultOCRExtracted#block 11: 0.96||Hurry! let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block 12: 0.89||Ggreat guns!...2gasp/:... pain t feel fan! as superman, i should be invulnerable! 1 have unbreakabl skin! under my clark kent clothes, i'm wearing an /noestruct/ble superman uniform !\n", + "ResultOCRExtracted#block 13: 1.00||Abruptly...\n", + "ResultOCRExtracted#block 14: 0.94||Great caesar's ghost! he's spinning a web of g/ant, \"silk strands -- as tough as steel!\n", + "ResultOCRExtracted#block 15: 0.89||I-i feel the heat of the sun., the pain of the bee-sting... the heavy weight of my pack! every human discomfort... good grief! t've lost all my super-powers. ve become an ordinary mortal in this world.\n", + "ResultOCRExtracted#block 16: 0.97||Get back! that enormous spider- like creature is going berserk, 25 if the sight of us excited him into mad spinning\n", + "\n", + " ---------- Padded 8, extracted ----------\n", + "ResultOCRExtracted#block 00: 0.91||Suddemly...\n", + "ResultOCRExtracted#block 01: 0.85||Sgasp/z everything s w- whirling around me!i can't stand up.\n", + "ResultOCRExtracted#block 02: 0.84||Clark! i'm falling’ _ help! help!\n", + "ResultOCRExtracted#block 03: 0.73||I-i'm passing ohhha.\n", + "ResultOCRExtracted#block 04: 0.92||Action comics\n", + "ResultOCRExtracted#block 05: 1.00||Then, seconds later...\n", + "ResultOCRExtracted#block 06: 0.90||Great caesar's ghost! f this /§ black magic! : we've been transported to the weirdest world i ever saw.\n", + "ResultOCRExtracted#block 07: 0.90||...it certainly isnt our earth, perry. look at the size of those bees.\n", + "ResultOCRExtracted#block 08: 0.88||Watch out, clark!\n", + "ResultOCRExtracted#block 09: 0.67||Owwww.,\n", + "ResultOCRExtracted#block 10: 0.99||Yet the bee's stinger went right through my uniform and penetrated my skin! that means. the fabric of my superman costume has become ordinary cloth!\n", + "ResultOCRExtracted#block 11: 0.96||Hurry! let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block 12: 0.89||Ggreat guns!...2gasp/:... pain t feel fan! as superman, i should be invulnerable! 1 have unbreakabl skin! under my clark kent clothes, i'm wearing an /noestruct/ble superman uniform !\n", + "ResultOCRExtracted#block 13: 0.96||Abruptly. ..\n", + "ResultOCRExtracted#block 14: 0.93||Great caesar's ghost! he's spinning _ a web of g/ant, \"silk strands -- as tough as steel!\n", + "ResultOCRExtracted#block 15: 0.89||I-t feel the heat of the sun., the pain of the bee-sting... the heavy weight of my pack! every human discomfort... good grief! t've lost all my super-powers. ve become an ordinary mortal in this world.\n", + "ResultOCRExtracted#block 16: 0.95||Get back! that enormous spider- like creature is going berserk, 25 if the sight of us excited him into mad spinning g [\n", + "\n", + " ---------- Padded 8, dilation 1 ----------\n", + "ResultOCRExtracted#block 00: 1.00||Suddenly...\n", + "ResultOCRExtracted#block 01: 0.83||Gasp! everything s w-whirling around wel] i can't stand\n", + "ResultOCRExtracted#block 02: 0.86||Clark!i'm falling! . help! help!\n", + "ResultOCRExtracted#block 03: 0.73||I-i'm passing ohhha.\n", + "ResultOCRExtracted#block 04: 0.92||Action comics\n", + "ResultOCRExtracted#block 05: 0.95||Then, seconds later.\n", + "ResultOCRExtracted#block 06: 0.91||Great caesar's ghost! e this /5 black magic! we've been transported to the weirdest world i ever saw/\n", + "ResultOCRExtracted#block 07: 0.91||...|it certainly isnt our earth, perry” look at the size of those bees!\n", + "ResultOCRExtracted#block 08: 0.88||Watch out, clark!\n", + "ResultOCRExtracted#block 09: 0.73||Owwww.\n", + "ResultOCRExtracted#block 10: 0.94||Yet the bee's stinger we right through my tform and penetrated my skin! that means. the fabric of my superman othe has become ordinary clot?\n", + "ResultOCRExtracted#block 11: 0.96||Hurry! let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block 12: 0.82||Fain! as superman, i should be invulnerable! 1 have unbreakabl skin! under my clark kent clothes, i'm wearing an indestructible superman uniform !\n", + "ResultOCRExtracted#block 13: 0.00||\n", + "ResultOCRExtracted#block 14: 0.67||Great caesars ghost! he's spinning _ a web of g/ant,\n", + "ResultOCRExtracted#block 15: 0.86||I-i feel the heat of the sun. the pain of the bee-sting... the heavy weight of my pack! every human discomfort... good grief! i've lost all my super-powers. ve ie an ordinary mortal in this world.\n", + "ResultOCRExtracted#block 16: 0.96||Get back! that enormous spider- like creature is going berserk, as if the sight of us excited him into mad spinning’ g [\n", + "\n", + " ---------- Pad 8, fract. 0.5 ----------\n", + "ResultOCRExtracted#block 00: 0.91||Suddemly...\n", + "ResultOCRExtracted#block 01: 0.87||Gasp/z everything s w- whirling around me!i can't stand up.\n", + "ResultOCRExtracted#block 02: 0.84||Clark! i'm falling’ _ help! help!\n", + "ResultOCRExtracted#block 03: 0.78||I-i'm passing ohhhh.\n", + "ResultOCRExtracted#block 04: 0.92||Action comics\n", + "ResultOCRExtracted#block 05: 1.00||Then, seconds later...\n", + "ResultOCRExtracted#block 06: 0.90||Great caesar's ghost! f this /§ black magic! : we've been transported to the weirdest world i ever saw.\n", + "ResultOCRExtracted#block 07: 0.92||...it certainly isnt our earth, perry! look at the size of those bees.\n", + "ResultOCRExtracted#block 08: 0.88||Watch out, clark!\n", + "ResultOCRExtracted#block 09: 0.67||Owwww.,\n", + "ResultOCRExtracted#block 10: 0.99||Yet the bee's stinger went right through my uniform and penetrated my skin! that means. the fabric of my superman costume has become ordinary cloth!\n", + "ResultOCRExtracted#block 11: 0.96||Hurry! let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block 12: 0.90||Ggreat guns!...2gasp/:...pain i feel fan! as superman, i should be invulnerable! 1 have unbreakabl skin! under my clark kent clothes, i'm wearing an /nodestruct/ible superman uniform !\n", + "ResultOCRExtracted#block 13: 0.96||Abruptly. ..\n", + "ResultOCRExtracted#block 14: 0.93||Great caesar's ghost! he's spinning _ a web of g/ant, \"silk strands -- as tough as steel!\n", + "ResultOCRExtracted#block 15: 0.89||I-i feel the heat of the sun. the pain of the bee-sting... the heavy weight of my pack! every human discomfort... good grief! i've lost all my super-powers. ve become an ordinary mortal in this world.\n", + "ResultOCRExtracted#block 16: 0.95||Get back! that enormous spider- like creature is going berserk, 25 if the sight of us excited him into mad spinning g [\n", + "\n", + " ---------- Pad 8, fract. 0.2 ----------\n", + "ResultOCRExtracted#block 00: 1.00||Suddenly...\n", + "ResultOCRExtracted#block 01: 0.87||Gasp/z everything s w- whirling around me!i can't stand up.\n", + "ResultOCRExtracted#block 02: 0.84||Clark! i'm falling’ _ help! help!\n", + "ResultOCRExtracted#block 03: 0.73||I-i'm passing ohhha.\n", + "ResultOCRExtracted#block 04: 0.92||Action comics\n", + "ResultOCRExtracted#block 05: 1.00||Then, seconds later...\n", + "ResultOCRExtracted#block 06: 0.90||Great caesar's ghost! f this /§ black magic! : we've been transported to the weirdest world i ever saw!\n", + "ResultOCRExtracted#block 07: 0.92||...it certainly isnt our earth, perry! look at the size of those bees.\n", + "ResultOCRExtracted#block 08: 0.88||Watch out, clark!\n", + "ResultOCRExtracted#block 09: 0.67||Owwww.,\n", + "ResultOCRExtracted#block 10: 0.99||Yet the bee's stinger went right through my uniform and penetrated my skin! that means. the fabric of my superman costume has become ordinary cloth!\n", + "ResultOCRExtracted#block 11: 0.96||Hurry! let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block 12: 0.91||Ggreat guns!...2gasp/:... pain i feel fan! as superman, i should be invulnerable! 1 have unbreakabl skin! under my clark kent clothes, i'm wearing an indestructible superman uniform !\n", + "ResultOCRExtracted#block 13: 0.96||Abruptly....\n", + "ResultOCRExtracted#block 14: 0.93||Great caesar's ghost! he's spinning _ a web of g/ant, \"silk strands -- as tough as steel!\n", + "ResultOCRExtracted#block 15: 0.89||I-i feel the heat of the sun., the pain of the bee-sting... the heavy weight of my pack! every human discomfort... good grief! i've lost all my super-powers. ve become an ordinary mortal in this world.\n", + "ResultOCRExtracted#block 16: 0.96||Get back! that enormous spider- like creature is going berserk, as if the sight of us excited him into mad spinning gs [\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "---------- Initial box ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.90\u001b[0m||Suddenly.\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.81\u001b[0m||>gasp!z everything's w- whirling around me!t can't stand sr rea\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.81\u001b[0m||Clark!i'm falling' “help! help! —\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.67\u001b[0m||I-i'm bs \"passing ohhh\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m1.00\u001b[0m||Then, seconds later\u001b[33m...\u001b[0m\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.89\u001b[0m||Great caesars ghost! \u001b[1m{\u001b[0m this \u001b[35m/\u001b[0m\u001b[95ms\u001b[0m black magic! we've been transportel to the weirdest world, tit ever saw!\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.91\u001b[0m||\u001b[33m...\u001b[0m|t certainly isn't our earth, perry. look at the size of those bees.\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.88\u001b[0m||Watch out, clark!\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.73\u001b[0m||Owwww.\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.85\u001b[0m||Yet the bee's stinger went ws. right through my uniform and penetrated my skin! that means. the fabric of pe ay costume has become dj\u001b[1m}\u001b[0m satie fued \u001b[1;36m1\u001b[0m,\n", + "ResultOCR#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.87\u001b[0m||Hurry. let's beat it before we get stung, foo = aii\n", + "ResultOCR#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.86\u001b[0m||Ggreat guns!\u001b[33m...\u001b[0m2g, .pa/n i feel n pain! as superman, i should be > invulnerable! \u001b[1;36m1\u001b[0m have unbreakabl ‘skin! under my clark kent clothes, im wearing an woestructisle superman uniform ! =\n", + "ResultOCR#block \u001b[1;36m13\u001b[0m: \u001b[1;36m1.00\u001b[0m||Abruptly\u001b[33m...\u001b[0m\n", + "ResultOCR#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.77\u001b[0m||Great caesar's ghost. he's spinning a web of g/ant, sr strands --\n", + "ResultOCR#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.89\u001b[0m||I-i feel the heat of the sun\u001b[33m...\u001b[0mthe pain of the bee-sting \u001b[33m...\u001b[0m the heavy weight of my pack! every human discomfort\u001b[33m...\u001b[0m good grief! i've lost all my super-powers! i've become an ordinary mortal in this world. \u001b[35m/\u001b[0m\n", + "ResultOCR#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.84\u001b[0m||Enormous spider- like creature is going berserk, as if the sight of us excited him \u001b[1m(\u001b[0minto mad spinning get back! that \u001b[1;36m2\u001b[0m\n", + "\n", + " ---------- Default ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m1.00\u001b[0m||Suddenly\u001b[33m...\u001b[0m\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.80\u001b[0m||Gasp! everything's w- whi rns atound me!i can't stand we a\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.87\u001b[0m||Clark!i'm falling! help! help\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.45\u001b[0m||I-i'm se ou\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.95\u001b[0m||Then, seconds later.\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.92\u001b[0m||Great caesar's ghost! this \u001b[35m/\u001b[0m\u001b[95ms\u001b[0m black magic! we've been transported, to the weirdest world, i ever saw!\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.93\u001b[0m||\u001b[33m...\u001b[0m|t certainly isn't our earth, perry look at the size of those bees!\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.88\u001b[0m||Watch out, clark\u001b[1m)\u001b[0m\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.67\u001b[0m||Owwww.,\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.97\u001b[0m||Yet the bee's stinger went = right through my uniform ando penetrated my skin! that mean the fabric of my superman costume has become ordinary, cloth! £\n", + "ResultOCR#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.89\u001b[0m||Hurry. let's beat it before we get stung, tod yi\n", + "ResultOCR#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.88\u001b[0m||Ggreat guns!\u001b[33m...\u001b[0m2gasp/z\u001b[33m...\u001b[0mpain i feel n fan! as superman, i should be invulnerable! \u001b[1;36m1\u001b[0m have unbreakable skin! under my clark kent clothes, i'm wearing an \u001b[35m/noestruct/\u001b[0m\u001b[95mele\u001b[0m superman uniform !\n", + "ResultOCR#block \u001b[1;36m13\u001b[0m: \u001b[1;36m1.00\u001b[0m||Abruptly\u001b[33m...\u001b[0m\n", + "ResultOCR#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.76\u001b[0m||Great caesars ° ghost! he's spinning & web of g/a, silk strands\n", + "ResultOCR#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.89\u001b[0m||I-i feel the heat of the sum\u001b[33m...\u001b[0mthe pain of the bee-sting\u001b[33m...\u001b[0m the heavy weight of my \u001b[1m{\u001b[0m pack! every human discomfort\u001b[33m...\u001b[0m good grief! i've lost all my super -powersx ve become an ordinary mortal in this world! j\n", + "ResultOCR#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.70\u001b[0m||Like creature is going berserk, as if the sight of us excited him into mad spinning, get back! that \\ enormous spider-\n", + "\n", + " ---------- Default, grey pad ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.78\u001b[0m||Sudden.\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.82\u001b[0m||5gasp!z everything's | w- whirling around\u001b[1m]\u001b[0m me!t can't stand ue a\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.57\u001b[0m||Ark!i'm falling\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.85\u001b[0m||I-t'm eq \"passing out\u001b[33m...\u001b[0m ohhh.\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.00\u001b[0m||\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.88\u001b[0m||‘great caesar's ghost! || this \u001b[35m/\u001b[0m§ black magic! we've been transportel to the weirdest world, i ever saw.\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.87\u001b[0m||\"\u001b[33m...\u001b[0mit certainly isn't our | earth, perry look at the| \\s|ze of those bees.\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.88\u001b[0m||Watch out, clark!\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.73\u001b[0m||Owwww.\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.90\u001b[0m||Yet the bee's stinger went ~ right through my uniform and penetrated my skin! that means. the fabric of sera ry costume vis become din a s clots! a\n", + "ResultOCR#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.80\u001b[0m||Hurry. let's \u001b[1m)\u001b[0m beat it before\u001b[1m]\u001b[0m we get stung, k \u001b[1;36m00\u001b[0m! __j\n", + "ResultOCR#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.86\u001b[0m||Ggreat guns!\u001b[33m...\u001b[0m3gasp/=\u001b[33m...\u001b[0m rain t feel fan! as superman, i should be invulnerable® \u001b[1;36m1\u001b[0m have unbreakabli skin! under my clark kent clothes, ia wearing an \u001b[35m/noestryct/\u001b[0m\u001b[95miele\u001b[0m supe iperman uniform !\n", + "ResultOCR#block \u001b[1;36m13\u001b[0m: \u001b[1;36m0.90\u001b[0m||Abruptly.\n", + "ResultOCR#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.92\u001b[0m||Great caesar's | ghost! he's spinning k web of giant, silk strands -- as tough as steel! ne\u001b[1m]\u001b[0m\n", + "ResultOCR#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.89\u001b[0m||\\i-t feel the heat of the sun\u001b[33m...\u001b[0mthe pain of the bee-sting\u001b[33m...\u001b[0m the heavy weight of my & pack! every human discomfort\u001b[33m...\u001b[0m good grief! i've lost all my super-powers i've become an ordinary mortal in this world. \u001b[35m/\u001b[0m\u001b[95m.\u001b[0m\n", + "ResultOCR#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.94\u001b[0m||\u001b[1m(\u001b[0mget back! that enormous spider- like creature \u001b[1;36m1\u001b[0m § going berserk, as if the sight of us excited him into mad spinning\u001b[1m]\u001b[0m\n", + "\n", + " ---------- Padded 4px ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.90\u001b[0m||Suddenly.\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.83\u001b[0m||Fgasp/= everything's whirling around me!i can't stand ea\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.68\u001b[0m||Ie lottie eto clark!i'm falling help! help!\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.55\u001b[0m||I-i'm yr a ohh hh.\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.74\u001b[0m||Then, seconds\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.62\u001b[0m||Great caesar's ghost! \\ this \u001b[35m/\u001b[0m§ black magic! i ever saw!\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.87\u001b[0m||T certainly isn't our earth, perry! look at the \\s|ze of those bees.\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.76\u001b[0m||#\u001b[1;36m3\u001b[0m watch out, clark!\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.73\u001b[0m||Owwww.\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.95\u001b[0m||\"yet the bee's stinger went ~~ right through my uniform and enetrated my skin! that means. the fabric of my superman costume has become ordinary, clot! &\n", + "ResultOCR#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.86\u001b[0m||Hurry! let's beat it \u001b[33mb\u001b[0m=\u001b[35mfore\u001b[0m we get stung, ¢ tool: puma\n", + "ResultOCR#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.86\u001b[0m||‘ggreat guns!\u001b[33m...\u001b[0m3gasp/\u001b[1;36m5\u001b[0m\u001b[33m...\u001b[0m pain i feel pain? as superman, i should be invilnerable | t ne unbreakasle gkin! under my clark kent clothes, tih wearing an indestructible superman uniform\n", + "ResultOCR#block \u001b[1;36m13\u001b[0m: \u001b[1;36m1.00\u001b[0m||Abruptly\u001b[33m...\u001b[0m\n", + "ResultOCR#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.91\u001b[0m||Great caesars ghost. he's spinning a web of g/ant, | silk strands as tough as steel!\n", + "ResultOCR#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.89\u001b[0m||I-i feel the heat of the sun\u001b[33m...\u001b[0mthe pain of the bee-sting\u001b[33m...\u001b[0m the heavy weight of my \u001b[1m{\u001b[0m pack! every human discomfort\u001b[33m...\u001b[0m good grief! i've lost all \\my super-powersx ve become an ordinary mortal in this world.\n", + "ResultOCR#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.81\u001b[0m||\u001b[1m(\u001b[0mget back! that enormous spider- as if the sight of us excited him \u001b[1m[\u001b[0minto mad spinning\n", + "\n", + " ---------- Padded 8px ----------\n", + "ResultOCR#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.00\u001b[0m||\n", + "ResultOCR#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.76\u001b[0m||Ega5p/% everything s\\ w- whirling around me! t can't stand ue\u001b[33m...\u001b[0m slee\n", + "ResultOCR#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.41\u001b[0m||\\ help! help’\n", + "ResultOCR#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.86\u001b[0m||-i'm passing qut\u001b[33m...\u001b[0m ohh hh.\n", + "ResultOCR#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCR#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.00\u001b[0m||\n", + "ResultOCR#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.18\u001b[0m||I ever saw/\n", + "ResultOCR#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.86\u001b[0m||It certainly isn't our earth, perry! poor a the size of thos!\n", + "ResultOCR#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.62\u001b[0m||“watch out” ial ae clark!\n", + "ResultOCR#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.73\u001b[0m||Owwww.\n", + "ResultOCR#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.98\u001b[0m||Yet the bee's stinger went right through my uniform and \\penetrated my skin! that means. the fabric of my superman costume has become ordinar! cloth!\n", + "ResultOCR#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.93\u001b[0m||Hurry. let's beat it before we get stung, too!\n", + "ResultOCR#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.88\u001b[0m||Ggreat guns!\u001b[33m...\u001b[0m2gasp/z\u001b[33m...\u001b[0mpain i feel fan! as superman, i should be invulnerable! \u001b[1;36m1\u001b[0m have unbreakabl skin! under my clark kent clothes, i'm wearing an \u001b[35m/noestruct/\u001b[0m\u001b[95mele\u001b[0m $ superman uniform ! &\n", + "ResultOCR#block \u001b[1;36m13\u001b[0m: \u001b[1;36m0.00\u001b[0m||\n", + "ResultOCR#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.91\u001b[0m||Great caesai ghost. he's spinning a web of g/ant, silk strands -- as tough as steel!\n", + "ResultOCR#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.88\u001b[0m||I-i feel the heat of the sun\u001b[33m...\u001b[0mthe pain of the bee-sting\u001b[33m...\u001b[0m the heavy weight of my ! every human discomfort\u001b[33m...\u001b[0m good grief! i've lost all \\my super-powers! ‘ve become an ordinary mortal in this world. x\n", + "ResultOCR#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.60\u001b[0m||As if the sight of us excited him \u001b[1m{\u001b[0minto mad spinning\n", + "\n", + " ---------- Extracted, init box ----------\n", + "ResultOCRExtracted#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.90\u001b[0m||Suddenly.\n", + "ResultOCRExtracted#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.83\u001b[0m||Fgasp!z everything § w- whirling around me!i can't stand ue.\n", + "ResultOCRExtracted#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.89\u001b[0m||Clark!i'm falling! help! help!\n", + "ResultOCRExtracted#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.71\u001b[0m||I-i'm passing ohhh?\n", + "ResultOCRExtracted#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCRExtracted#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.98\u001b[0m||Then, seconds later..\n", + "ResultOCRExtracted#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.91\u001b[0m||Great caesar's ghost! this \u001b[35m/\u001b[0m\u001b[95ms\u001b[0m black magic! we've been transportel to the weirdest world tit ever saw.\n", + "ResultOCRExtracted#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.90\u001b[0m||\u001b[33m...\u001b[0mit certainly isnt our earth, perry. look at the size of those bees.\n", + "ResultOCRExtracted#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.88\u001b[0m||Watch out, clark!\n", + "ResultOCRExtracted#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.62\u001b[0m||Oowwwww.\n", + "ResultOCRExtracted#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.99\u001b[0m||Yet the bee's stinger went right through my uniform and penetrated my skin! that means. the fabric of my superman costume has become ordinary cloth!\n", + "ResultOCRExtracted#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.93\u001b[0m||Hurry. let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.88\u001b[0m||Ggreat guns!\u001b[33m...\u001b[0m2g/ pain t feel pain? as superman, i should be invulnerable! \u001b[1;36m1\u001b[0m have unbreakabl skin! under my clark kent clothes, i'm wearing an \u001b[35m/noestruct/\u001b[0m\u001b[95mble\u001b[0m superman uniform *\n", + "ResultOCRExtracted#block \u001b[1;36m13\u001b[0m: \u001b[1;36m0.96\u001b[0m||Abruptly. ..\n", + "ResultOCRExtracted#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.80\u001b[0m||Great caesar's ghost. he's spinning a web of g/ant, silk strands --\n", + "ResultOCRExtracted#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.89\u001b[0m||I-i feel the heat of the sun. the pain of the bee-sting\u001b[33m...\u001b[0m the heavy weight of my pack! every human discomfort\u001b[33m...\u001b[0m good grief! t've lost all my super-powers. ve become an ordinary mortal im this world.\n", + "ResultOCRExtracted#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.97\u001b[0m||Get back. that enormous spider- like creature is going berserk, as if the sight of us excited him into mad spinning\n", + "\n", + " ---------- Padded \u001b[1;36m4\u001b[0m, extracted ----------\n", + "ResultOCRExtracted#block \u001b[1;36m00\u001b[0m: \u001b[1;36m1.00\u001b[0m||Suddenly\u001b[33m...\u001b[0m\n", + "ResultOCRExtracted#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.83\u001b[0m||Sgasp/z everything s w- whirling around me!i can't stand ue.\n", + "ResultOCRExtracted#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.87\u001b[0m||Clark! i'm falling’ help! help!\n", + "ResultOCRExtracted#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.78\u001b[0m||I-i'm passing ohhhh.\n", + "ResultOCRExtracted#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCRExtracted#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.98\u001b[0m||Then, seconds later..\n", + "ResultOCRExtracted#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.93\u001b[0m||Great caesar's ghost! this as black magic! we've been transported to the weirdest world i ever saw!\n", + "ResultOCRExtracted#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.90\u001b[0m||\u001b[33m...\u001b[0mit certainly isnt our earth, perry. look at the size of those bees.\n", + "ResultOCRExtracted#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.88\u001b[0m||Watch out, clark!\n", + "ResultOCRExtracted#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.67\u001b[0m||Owwww.,\n", + "ResultOCRExtracted#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.99\u001b[0m||Yet the bee's stinger went right through my uniform and penetrated my skin! that means. the fabric of my superman costume has become ordinary cloth!\n", + "ResultOCRExtracted#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.96\u001b[0m||Hurry! let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.89\u001b[0m||Ggreat guns!\u001b[33m...\u001b[0m2gasp/:\u001b[33m...\u001b[0m pain t feel fan! as superman, i should be invulnerable! \u001b[1;36m1\u001b[0m have unbreakabl skin! under my clark kent clothes, i'm wearing an \u001b[35m/noestruct/\u001b[0m\u001b[95mble\u001b[0m superman uniform !\n", + "ResultOCRExtracted#block \u001b[1;36m13\u001b[0m: \u001b[1;36m1.00\u001b[0m||Abruptly\u001b[33m...\u001b[0m\n", + "ResultOCRExtracted#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.94\u001b[0m||Great caesar's ghost! he's spinning a web of g/ant, \"silk strands -- as tough as steel!\n", + "ResultOCRExtracted#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.89\u001b[0m||I-i feel the heat of the sun., the pain of the bee-sting\u001b[33m...\u001b[0m the heavy weight of my pack! every human discomfort\u001b[33m...\u001b[0m good grief! t've lost all my super-powers. ve become an ordinary mortal in this world.\n", + "ResultOCRExtracted#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.97\u001b[0m||Get back! that enormous spider- like creature is going berserk, \u001b[1;36m25\u001b[0m if the sight of us excited him into mad spinning\n", + "\n", + " ---------- Padded \u001b[1;36m8\u001b[0m, extracted ----------\n", + "ResultOCRExtracted#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.91\u001b[0m||Suddemly\u001b[33m...\u001b[0m\n", + "ResultOCRExtracted#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.85\u001b[0m||Sgasp/z everything s w- whirling around me!i can't stand up.\n", + "ResultOCRExtracted#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.84\u001b[0m||Clark! i'm falling’ _ help! help!\n", + "ResultOCRExtracted#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.73\u001b[0m||I-i'm passing ohhha.\n", + "ResultOCRExtracted#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCRExtracted#block \u001b[1;36m05\u001b[0m: \u001b[1;36m1.00\u001b[0m||Then, seconds later\u001b[33m...\u001b[0m\n", + "ResultOCRExtracted#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.90\u001b[0m||Great caesar's ghost! f this \u001b[35m/\u001b[0m§ black magic! : we've been transported to the weirdest world i ever saw.\n", + "ResultOCRExtracted#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.90\u001b[0m||\u001b[33m...\u001b[0mit certainly isnt our earth, perry. look at the size of those bees.\n", + "ResultOCRExtracted#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.88\u001b[0m||Watch out, clark!\n", + "ResultOCRExtracted#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.67\u001b[0m||Owwww.,\n", + "ResultOCRExtracted#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.99\u001b[0m||Yet the bee's stinger went right through my uniform and penetrated my skin! that means. the fabric of my superman costume has become ordinary cloth!\n", + "ResultOCRExtracted#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.96\u001b[0m||Hurry! let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.89\u001b[0m||Ggreat guns!\u001b[33m...\u001b[0m2gasp/:\u001b[33m...\u001b[0m pain t feel fan! as superman, i should be invulnerable! \u001b[1;36m1\u001b[0m have unbreakabl skin! under my clark kent clothes, i'm wearing an \u001b[35m/noestruct/\u001b[0m\u001b[95mble\u001b[0m superman uniform !\n", + "ResultOCRExtracted#block \u001b[1;36m13\u001b[0m: \u001b[1;36m0.96\u001b[0m||Abruptly. ..\n", + "ResultOCRExtracted#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.93\u001b[0m||Great caesar's ghost! he's spinning _ a web of g/ant, \"silk strands -- as tough as steel!\n", + "ResultOCRExtracted#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.89\u001b[0m||I-t feel the heat of the sun., the pain of the bee-sting\u001b[33m...\u001b[0m the heavy weight of my pack! every human discomfort\u001b[33m...\u001b[0m good grief! t've lost all my super-powers. ve become an ordinary mortal in this world.\n", + "ResultOCRExtracted#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.95\u001b[0m||Get back! that enormous spider- like creature is going berserk, \u001b[1;36m25\u001b[0m if the sight of us excited him into mad spinning g \u001b[1m[\u001b[0m\n", + "\n", + " ---------- Padded \u001b[1;36m8\u001b[0m, dilation \u001b[1;36m1\u001b[0m ----------\n", + "ResultOCRExtracted#block \u001b[1;36m00\u001b[0m: \u001b[1;36m1.00\u001b[0m||Suddenly\u001b[33m...\u001b[0m\n", + "ResultOCRExtracted#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.83\u001b[0m||Gasp! everything s w-whirling around wel\u001b[1m]\u001b[0m i can't stand\n", + "ResultOCRExtracted#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.86\u001b[0m||Clark!i'm falling! . help! help!\n", + "ResultOCRExtracted#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.73\u001b[0m||I-i'm passing ohhha.\n", + "ResultOCRExtracted#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCRExtracted#block \u001b[1;36m05\u001b[0m: \u001b[1;36m0.95\u001b[0m||Then, seconds later.\n", + "ResultOCRExtracted#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.91\u001b[0m||Great caesar's ghost! e this \u001b[35m/\u001b[0m\u001b[95m5\u001b[0m black magic! we've been transported to the weirdest world i ever saw/\n", + "ResultOCRExtracted#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.91\u001b[0m||\u001b[33m...\u001b[0m|it certainly isnt our earth, perry” look at the size of those bees!\n", + "ResultOCRExtracted#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.88\u001b[0m||Watch out, clark!\n", + "ResultOCRExtracted#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.73\u001b[0m||Owwww.\n", + "ResultOCRExtracted#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.94\u001b[0m||Yet the bee's stinger we right through my tform and penetrated my skin! that means. the fabric of my superman othe has become ordinary clot?\n", + "ResultOCRExtracted#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.96\u001b[0m||Hurry! let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.82\u001b[0m||Fain! as superman, i should be invulnerable! \u001b[1;36m1\u001b[0m have unbreakabl skin! under my clark kent clothes, i'm wearing an indestructible superman uniform !\n", + "ResultOCRExtracted#block \u001b[1;36m13\u001b[0m: \u001b[1;36m0.00\u001b[0m||\n", + "ResultOCRExtracted#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.67\u001b[0m||Great caesars ghost! he's spinning _ a web of g/ant,\n", + "ResultOCRExtracted#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.86\u001b[0m||I-i feel the heat of the sun. the pain of the bee-sting\u001b[33m...\u001b[0m the heavy weight of my pack! every human discomfort\u001b[33m...\u001b[0m good grief! i've lost all my super-powers. ve ie an ordinary mortal in this world.\n", + "ResultOCRExtracted#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.96\u001b[0m||Get back! that enormous spider- like creature is going berserk, as if the sight of us excited him into mad spinning’ g \u001b[1m[\u001b[0m\n", + "\n", + " ---------- Pad \u001b[1;36m8\u001b[0m, fract. \u001b[1;36m0.5\u001b[0m ----------\n", + "ResultOCRExtracted#block \u001b[1;36m00\u001b[0m: \u001b[1;36m0.91\u001b[0m||Suddemly\u001b[33m...\u001b[0m\n", + "ResultOCRExtracted#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.87\u001b[0m||Gasp/z everything s w- whirling around me!i can't stand up.\n", + "ResultOCRExtracted#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.84\u001b[0m||Clark! i'm falling’ _ help! help!\n", + "ResultOCRExtracted#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.78\u001b[0m||I-i'm passing ohhhh.\n", + "ResultOCRExtracted#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCRExtracted#block \u001b[1;36m05\u001b[0m: \u001b[1;36m1.00\u001b[0m||Then, seconds later\u001b[33m...\u001b[0m\n", + "ResultOCRExtracted#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.90\u001b[0m||Great caesar's ghost! f this \u001b[35m/\u001b[0m§ black magic! : we've been transported to the weirdest world i ever saw.\n", + "ResultOCRExtracted#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.92\u001b[0m||\u001b[33m...\u001b[0mit certainly isnt our earth, perry! look at the size of those bees.\n", + "ResultOCRExtracted#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.88\u001b[0m||Watch out, clark!\n", + "ResultOCRExtracted#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.67\u001b[0m||Owwww.,\n", + "ResultOCRExtracted#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.99\u001b[0m||Yet the bee's stinger went right through my uniform and penetrated my skin! that means. the fabric of my superman costume has become ordinary cloth!\n", + "ResultOCRExtracted#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.96\u001b[0m||Hurry! let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.90\u001b[0m||Ggreat guns!\u001b[33m...\u001b[0m2gasp/:\u001b[33m...\u001b[0mpain i feel fan! as superman, i should be invulnerable! \u001b[1;36m1\u001b[0m have unbreakabl skin! under my clark kent clothes, i'm wearing an \u001b[35m/nodestruct/\u001b[0m\u001b[95mible\u001b[0m superman uniform !\n", + "ResultOCRExtracted#block \u001b[1;36m13\u001b[0m: \u001b[1;36m0.96\u001b[0m||Abruptly. ..\n", + "ResultOCRExtracted#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.93\u001b[0m||Great caesar's ghost! he's spinning _ a web of g/ant, \"silk strands -- as tough as steel!\n", + "ResultOCRExtracted#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.89\u001b[0m||I-i feel the heat of the sun. the pain of the bee-sting\u001b[33m...\u001b[0m the heavy weight of my pack! every human discomfort\u001b[33m...\u001b[0m good grief! i've lost all my super-powers. ve become an ordinary mortal in this world.\n", + "ResultOCRExtracted#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.95\u001b[0m||Get back! that enormous spider- like creature is going berserk, \u001b[1;36m25\u001b[0m if the sight of us excited him into mad spinning g \u001b[1m[\u001b[0m\n", + "\n", + " ---------- Pad \u001b[1;36m8\u001b[0m, fract. \u001b[1;36m0.2\u001b[0m ----------\n", + "ResultOCRExtracted#block \u001b[1;36m00\u001b[0m: \u001b[1;36m1.00\u001b[0m||Suddenly\u001b[33m...\u001b[0m\n", + "ResultOCRExtracted#block \u001b[1;36m01\u001b[0m: \u001b[1;36m0.87\u001b[0m||Gasp/z everything s w- whirling around me!i can't stand up.\n", + "ResultOCRExtracted#block \u001b[1;36m02\u001b[0m: \u001b[1;36m0.84\u001b[0m||Clark! i'm falling’ _ help! help!\n", + "ResultOCRExtracted#block \u001b[1;36m03\u001b[0m: \u001b[1;36m0.73\u001b[0m||I-i'm passing ohhha.\n", + "ResultOCRExtracted#block \u001b[1;36m04\u001b[0m: \u001b[1;36m0.92\u001b[0m||Action comics\n", + "ResultOCRExtracted#block \u001b[1;36m05\u001b[0m: \u001b[1;36m1.00\u001b[0m||Then, seconds later\u001b[33m...\u001b[0m\n", + "ResultOCRExtracted#block \u001b[1;36m06\u001b[0m: \u001b[1;36m0.90\u001b[0m||Great caesar's ghost! f this \u001b[35m/\u001b[0m§ black magic! : we've been transported to the weirdest world i ever saw!\n", + "ResultOCRExtracted#block \u001b[1;36m07\u001b[0m: \u001b[1;36m0.92\u001b[0m||\u001b[33m...\u001b[0mit certainly isnt our earth, perry! look at the size of those bees.\n", + "ResultOCRExtracted#block \u001b[1;36m08\u001b[0m: \u001b[1;36m0.88\u001b[0m||Watch out, clark!\n", + "ResultOCRExtracted#block \u001b[1;36m09\u001b[0m: \u001b[1;36m0.67\u001b[0m||Owwww.,\n", + "ResultOCRExtracted#block \u001b[1;36m10\u001b[0m: \u001b[1;36m0.99\u001b[0m||Yet the bee's stinger went right through my uniform and penetrated my skin! that means. the fabric of my superman costume has become ordinary cloth!\n", + "ResultOCRExtracted#block \u001b[1;36m11\u001b[0m: \u001b[1;36m0.96\u001b[0m||Hurry! let's beat it before we get stung, too!\n", + "ResultOCRExtracted#block \u001b[1;36m12\u001b[0m: \u001b[1;36m0.91\u001b[0m||Ggreat guns!\u001b[33m...\u001b[0m2gasp/:\u001b[33m...\u001b[0m pain i feel fan! as superman, i should be invulnerable! \u001b[1;36m1\u001b[0m have unbreakabl skin! under my clark kent clothes, i'm wearing an indestructible superman uniform !\n", + "ResultOCRExtracted#block \u001b[1;36m13\u001b[0m: \u001b[1;36m0.96\u001b[0m||Abruptly\u001b[33m...\u001b[0m.\n", + "ResultOCRExtracted#block \u001b[1;36m14\u001b[0m: \u001b[1;36m0.93\u001b[0m||Great caesar's ghost! he's spinning _ a web of g/ant, \"silk strands -- as tough as steel!\n", + "ResultOCRExtracted#block \u001b[1;36m15\u001b[0m: \u001b[1;36m0.89\u001b[0m||I-i feel the heat of the sun., the pain of the bee-sting\u001b[33m...\u001b[0m the heavy weight of my pack! every human discomfort\u001b[33m...\u001b[0m good grief! i've lost all my super-powers. ve become an ordinary mortal in this world.\n", + "ResultOCRExtracted#block \u001b[1;36m16\u001b[0m: \u001b[1;36m0.96\u001b[0m||Get back! that enormous spider- like creature is going berserk, as if the sight of us excited him into mad spinning gs \u001b[1m[\u001b[0m\n", + "\n", + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_action_experiment: ExperimentOCR = cast(ExperimentOCR, ExperimentOCR.from_image(\n", + " CONTEXT, 'Tesseract', 'Action_Comics_1960-01-00_(262).JPG'))\n", + "image_action_experiment.display()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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UKECli9fjh07diAlJQX29vZwc3PD0KFDpXzKtixevBg+Pj5wdnbG9OnTERkZKU24lpVt27Zh5cqV2LJlCzZu3Ai5XI6yZcti69at6Nmzp1rehIQEnQLnj21TRtu3b8eYMWMwefJkpKamwsPDA8ePH9dYDqxkyZJYtmwZli9fDk9PT6SlpWHTpk0ak6UBYg/6/v374e/vj02bNmH+/PmwsbFBnz59sGDBgjy933j37t0aM3Ur1xSfPXt2tuuU9+rVCytXrkRKSopau8qXL4+TJ09iypQp6Ny5M/T19eHl5YXAwECNe9XT0tLURgbo6enh+PHjWLp0Kfbs2YPAwECYmpqiQoUKOHz4MFq2bCnlPXToED58+ICLFy9qndH+3r17akPJ9+/fDxsbGzRr1izbY0NERERE9CWTCVl1WRKpePPmDaytrbFs2TKMGDGioJvzVXn8+DFKliyJzZs3a8x0/6lJS0uDi4sLevbsifnz5+u8XXx8PCwsLLD+wiAYmxrkYwuJiIgoL/Qst6qgm0BUoJS/X+Pi4jRGOKviPd2ks1OnTkm9/fTfcnBwwNixYzF//nxpPe1P1datW5GQkCCtfU5ERERE9DXj8HLSWevWrTWGc9N/Z+bMmTA2NsajR49QvHjxgm5Opj58+IBt27ZluVwZEREREdHXgkE30WfCzMwMs2fPLuhmZKt///4F3QQiIiIiok8Gh5cTERERERER5RMG3URERERERET5hEE3ERERERERUT7hPd1E9EnpWubbLJdcICIiIiL6nLCnm4iIiIiIiCifMOgmIiIiIiIiyicMuomIiIiIiIjyCYNuIiIiIiIionzCoJuIiIiIiIgon3D2ciL6pBSd3xYyBb+aiIiIiL4UCXPDC7oJBYo93URERERERET5hEE3ERERERERUT5h0E1ERERERESUTxh0ExEREREREeUTBt1ERERERERE+YRBNxEREREREVE+YdBNRERERERElE++uqA7ODgYMplMeujr66NYsWLo378/Hj16lGf1ODs7w9fXV+f2REdH51ndvr6+cHZ2zjafIAhYv349atasCXNzcxQuXBiNGjXCoUOH8qwtH2v79u1YtmxZgbZh9erVCA4OzpeyZTIZ/P39dcp7+vRpKBQK3L9/Xy394sWLaNKkCUxNTWFpaQkfHx/cvXtXpzJTUlKwdOlSVKpUCSYm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Pj44evQonj17Bjs7O7Ro0QKOjo552T4iIiIiIiKiz1aug24AcHR0xMCBA/OqLURERERERERflFwH3QkJCTh8+DCio6ORnJys8b6fn99HNYyIiIiIiIjoc5eroPvixYto2bIlXr58mWkeBt1ERERERET0tcv17OUvXrzI9H2ZTJbrBhERERERERF9KXI1e/mlS5cgk8ng6emJFStWYOPGjdi0aZP02LhxY16385MQHBwMmUyW6ePkyZM5Ki8xMRH+/v453i4vPX78GP7+/rh06VKel608XtHR0TneNjo6GjKZDMHBwbmq29nZGb6+vtLrnO6nsu0XLlzIVf35ITY2FjY2Nti5c6da+vPnz+Hr6wsbGxsYGxvD3d0d4eHhOpcbGhoKDw8PWFtbw9LSErVr18aWLVs08sXHx2PGjBkoW7YsjI2N4ejoiC5duuD69etq+YKCguDo6Ii3b9/mbkeJiIiIiL4guerpNjMzw9u3bxEaGgorK6u8btMnb9OmTShfvrxGeoUKFXJUTmJiIubMmQMA8PT0zIum5djjx48xZ84cODs7o1q1agXSBm2KFi2Ks2fPonTp0rnaft++fTA3N5def6r7mRNz5syBg4MDunXrJqWlpKTA29sbsbGxWL58OWxtbbFq1Sq0aNECYWFhaNSoUZZlbty4EQMHDkSnTp0wc+ZMyGQyhISEoG/fvnj58iXGjRsn5W3bti0uXLgAf39/1KpVCw8fPsTcuXPh7u6Oq1evwsnJCQDQr18/LF68GEuWLJE+30REREREX6tcBd29e/dGYGAg7t2791UG3ZUqVUKtWrX+83oTExNhbGz8n9dbEBQKBerWrZvr7atXr56HrSl4r169wo8//ojvv/9e7faNoKAgXLt2Db///jvc3d0BAI0bN0bVqlUxefJk/PHHH1mWu3HjRjg5OWH37t3Q0xMHvjRv3hyXLl1CcHCwFHTfvn0bp06dwsyZMzFp0iRpexcXF9SrVw979+6V8urr6+Obb75BQEAApkyZ8tV8ZomIiIiItNF5ePmpU6ekh6enJ+zs7NChQwesXLkS4eHhau+fOnUqP9v8ydu5cydkMhlWrlyplj579mwUKlQIx48fR3R0NIoUKQJA7MFUDlFXDon29/eHTCbDxYsX0blzZ1hZWUm9vhcuXED37t3h7OwMIyMjODs7o0ePHrh//75GWx49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SBOoAUDx4sURGRmJPXv25GkjiYiIiIiIiD5HuRpeHhAQgISEBBgYGODdu3dSeo8ePbBixQpERkbmWQOJ6Oty+5udWd4TQ0RERET0OclVT3d4eDhkMhmOHDmill65cmUAwIMHDz6+ZURERERERESfuVwF3cplnzIuCSSTyQAAr1+//shmEREREREREX3+chV0K4d+Pn36VC1duYSYlZXVRzaLiIiIiIiI6POXq6C7Zs2aAIChQ4dKaUuWLEHfvn0hk8ng5uaWN60jIiL6v/buOyyK630b+L3u0kEElC5gx44o9gIidjRi76DYjb1EVCwo2GKJvSBg76LEFnsFxdhj7PVrI4IKIiJl3j94d36MuxSNGwTvz3XtdbFnnjnzzLCb+HDOnCEiIiLKx76q6B48eDAEQcDBgwfFKeUTJkwQp5UPHjz422VIRERERERElE99VdHdtm1bjBs3TnxcWObHhv3yyy9o0aLFN02SiIiIiIiIKD+SCcpq+StcvHgRe/bswatXr2BhYYGffvpJnHpORPQl4uPjYWxsjNUoBX3I8ySHbsLtPDkuEREREeU/yn+/vnv3LttH3ub6Od2Zn8etVKVKFVSpUkVtnLa2dm67JiIiIiIiIiqQcl106+np5bpTmUyG1NTUr0qIiIiIiIiIqKDIddH9L2ahExEREREREf2QvmghNZlMJq5WTkRERERERETZ+6KiWznaXbJkScybNw9v3rxBenq6yistLU0jyRIRERERERHlJ7kuus+fP49u3bpBS0sLDx48wNixY1G8eHEMGjQIf/31lyZzJCIiIiIiIsqXcl10u7i4YMOGDXjy5AmmTp0KS0tLvH//HqtWrUKVKlXg7u6Oo0ePajJXIiIiIiIionzli6aXA4C5uTn8/f3x+PFjbNy4EaamphAEASdOnMDSpUs1kSP9R0JDQ8X79mUyGRQKBWxtbeHj44Nnz559s+M4ODjA29s71/k8evTomx3b29sbDg4OOcYJgoDVq1ejevXqKFy4MMzMzNCoUSPs27fvm+XyNaZPn44KFSogPT1d0r5lyxY4OTlBV1cX1tbWGDFiBN6/f5/rfh8/fow+ffrA2toaOjo6sLGxQbt27SQxu3btQteuXVG6dGno6enBwcEB3bt3x927dyVxKSkpKFWqFBYuXPjV50lEREREVFB8cdENZDwEfMmSJfD390dc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tvDx8cGzZ8++2XEcHBzg7e2d63wePXr0zY7t7e0NBweHL9pHEAQ0bNgQMpkMQ4cO/Wa5fI3p06ejQoUKSE9Pl7Rv2bIFTk5O0NXVhbW1NUaMGIH379/nqs+XL19i6NChKFmyJPT09GBvb4++ffviyZMnkrhdu3aha9euKF26NPT09ODg4IDu3bvj7t27kriUlBSUKlUKCxcu/FfnSkRERERUELDoJhUhISGIjIzE4cOH0a9fP2zevBkNGjRAYmJiXqeWJ5YuXYp79+7ldRp4/vw55syZg+nTp6NQof/76m7cuBFdu3aFi4sLDhw4gClTpiA0NBReXl459pmcnIyGDRti69atGDNmDA4cOAA/Pz/s27cPdevWRUJCghg7e/ZsfPjwARMnTsTBgwcxY8YMXL58Gc7Ozvjrr7/EOC0tLfj7+2P69OmIjY39theBiIiIiCifUeR1AvT9qVSpEmrUqAEAcHNzQ1paGgICAhAeHo7u3bvncXb/rUePHmHChAlYt25dropYTVq0aBGKFCkiySMtLQ1jx45F06ZNsXr1agAZvzMjIyN0794dBw4cQIsWLbLs8/Tp07h79y7WrFmDvn37AgBcXV1RuHBhdOvWDUeOHEG7du0AABERETA3N5fs37hxYzg4OGDBggVYs2aN2N61a1eMGjUKK1euhJ+f3ze7BkRERERE+Q1HuilHtWvXBgA8fvwYADBt2jTUqlULpqamKFy4MJydnREcHAxBECT7paSkYNy4cbC0tIS+vj7q16+PCxcuqD1GVFQU6tWrJ06PnjBhAlJSUtTGbt26FXXq1IGBgQEMDQ3RrFkzXL58WSUuNDQU5cqVg46ODsqXL49169Z98bn3798fHh4eYuH5uRMnTkAmk2HDhg0YNWoULC0toaenh0aNGklyev36NYoXL466detKzuvmzZswMDBAz549s83j06dPCA4ORrdu3SSj3FFRUXjx4gV8fHwk8R07doShoSF2796dbb9aWloAAGNjY0l7kSJFAAC6urpi2+cFNwBYW1vD1tYWT58+lbRra2ujc+fOWLVqlcrngoiIiIjoR8Kim3KknFpdrFgxABmjvwMGDMC2bduwa9cueHl54eeff0ZAQIBkv379+mHevHno1asX9uzZg/bt28PLywtv3ryRxN28eRPu7u54+/YtQkNDsWLFCly+fBkzZsxQySUw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ZDKBD9Glf+HmzZuoWLEifv/991wV7597+PAhHB0dMWXKFPj5+X3z/K5du4aqVati6dKl4gh1TkJDQ+Hj44Po6GjUqFHjm+f0tX7++WccPXoUf/311xevSv5fCw4OxvDhw/H06dNcj3THx8fD2NgYkQ+nwNBIddE4IiKi/KyS2S95nQIRfWPKf7++e/cOhQsXzjKO93TTv3L8+HHUqVMnVwX31atX8csvv2Dv3r04ceIEVq5ciSZNmqBw4cIqz4X+t+7fv49jx46hf//+sLKyUjsKnt9MmjQJz549w86dO/M6lWylpqZi9uzZmDBhAqeWExEREdEPj0U3/StDhgzBuXPnchVrYGCAixcvom/fvvDw8MDEiRNRrVo1nDlzJsvHhn2tgIAAeHh44P3799i+fTv09fW/af95wcLCAhs3bvyuHgWnztOnT9GjRw+MHj06r1MhIiIiIspznF5ORN8FTi8nIqKCjNPLiQoeTi8nIiIiIiIiymMsuomIiIiIiIg0hEU3ERERERERkYbwOd1E9F2pYDoq23tiiIiIiIjyE450ExEREREREWkIi24iIiIiIiIiDWHRTURERERERKQhLLqJiIiIiIiINIRFNxEREREREZGGsOgmIiIiIiIi0hA+MoyIviu+hwdBy0A7r9MgIiIiytLG5iF5nQLlIxzpJiIiIiIiItIQFt1EREREREREGsKim4iIiIiIiEhDWHQTERERERERaQiLbiIiIiIiIiINYdFNREREREREpCEsuomIiIiIiIg0hEU3fbdCQ0Mhk8nEl66uLiwtLeHm5oagoCDExMT8q/6PHj2KGjVqwMDAADKZDOHh4d8m8c94e3vDwcFB0hYYGPjFx7t//z50dHQQGRkpaX/w4AG8vLxQpEgRGBoawsPDA5cuXcpVn4IgYPXq1ahevToKFy4MMzMzNGrUCPv27VOJzfy7yPyaNWuWJG7y5MlwdnZGenr6F50fEREREVFBxKKbvnshISGIjIzE4cOHsXTpUjg5OWH27NkoX748jhw58lV9CoKATp06QUtLC3v37kVkZCQaNWr0jTPP2tcU3WPGjIGHhwfq1Kkjtv3zzz9o0KAB7ty5g7Vr12Lbtm34+PEjXF1dcfv27Rz7nDJlCvr374+aNWti586dCA0NhY6ODlq3bo1du3apxHfo0AGRkZGSV69evVTyfPjwIcLCwr7o/IiIiIiICiJFXidAlJNKlSqhRo0a4vv27dtj5MiRqF+/Pry8vHD37l1YWFh8UZ/Pnz9HXFwc2rVrB3d392+d8jf3999/Izw8HAcPHpS0z507F//88w/OnTsHe3t7AED9+vVRqlQp+Pv7Y+vWrdn2u3btWtSvXx/Lly8X2zw8PGBpaYmwsDB4eXlJ4i0sLFC7du1s+zQ2NkaPHj0wa9YseHt7QyaTfcmpEhEREREVKBzppnzJzs4Ov/76KxISErBy5UrJtosXL6JNmzYwNTWFrq4uqlWrhm3btonbp06dCltbWwDA+PHjIZPJxOnf9+7dg4+PD8qUKQN9fX3Y2NjA09MT169flxxDOfX90aNHkvYTJ05AJpPhxIkTWeYuk8mQmJiIsLAwcYq2q6trtue7fPlyWFpawsPDQ9K+e/duNG7cWCy4AaBw4cLw8vJCREQEUlNTs+1XS0sLxsbGkjZdXV3x9bV69uyJO3fu4Pjx41/dBxERERFRQcCim/Ktli1bQi6X49SpU2Lb8ePHUa9ePbx9+xYrVqzAnj174OTkhM6dOyM0NBQA4OvrK06d/vnnnxEZGYndu3cDyBgBNzMzw6xZs3Dw4EEsXboUCoUCtWrVytV07dyIjIyEnp4eWrZsKU7RXrZsWbb77Nu3Dw0bNkShQv/3lU1KSsL9+/dRpUoVlfgqVaogKSkJDx48yLbf4cOH4+DBgwgODsabN2/w4sULjBo1Cu/evcOwYcNU4jdt2gQ9PT3o6OigevXqCAkJUdtv9erVYWhoqPbecCIiIiKiHwmnl1O+ZWBggKJFi+L58+di2+DBg1GxYkUcO3YMCkXGx7tZs2Z4/fo1/Pz80KtXL9ja2oojwHZ2dpLp0g0bNkTDhg3F92lpaWjVqhUqVqyIlStXYv78+f8679q1a6NQoUIoVqxYjlO1ASAmJgYPHjxA//79Je1v3ryBIAgwNTVV2UfZFhsbm23fI0aMgJ6eHoYMGQJfX19x34iICNSrV08S261bN7Rq1QrFixdHTEwMgoOD0adPHzx48AABAQGSWLlcjqpVq+Ls2bNZHjs5ORnJycni+/j4+GxzJSIiIiLKjzjSTfmaIAjiz/fu3cOtW7fQvXt3AEBqaqr4atmyJV68eJHjaHVqaioCAwNRoUIFaGtrQ6FQQFtbG3fv3sXff/+t0XPJivKPCubm5mq3Z3fPdE73U4eEhGD48OEYOnQojhw5gv3796Np06Zo27YtDh06JInduHEjunXrhgYNGqB9+/bYv38/WrdujVmzZuGff/5R6dvc3BzPnj3L8thBQUEwNjYWX8WLF882VyIiIiKi/IhFN+VbiYmJiI2NhbW1NQDg1atXADJWz9bS0pK8Bg8eDAB4/fp1tn2OGjUKkydPxk8//YSIiAicP38e0dHRqFq1KpKSkjR7QllQHvfze6xNTEwgk8nUjmbHxcUBgNpRcKU3b96II9zz5s2Du7s7WrRogc2bN8PFxQUDBw7MMbcePXogNTUVFy9eVNmmq6ub7TWbMGEC3r17J76ePn2a4/GIiIiIiPIbTi+nfGvfvn1IS0sTFyErWrQogIxi7vNVt5XKlSuXbZ8bNmxAr169EBgYKGl//fo1ihQpIr5XFsCZp0cr47415XkpC2klPT09lC5dWmWRNwC4fv069PT0ULJkySz7vX37NpKSkuDi4qKyrUaNGjh58iTev38PQ0PDLPtQzjTIfK+5UlxcnJi7Ojo6OtDR0clyOxERERFRQcCRbsqXnjx5gjFjxsDY2BgDBgwAkFFQlylTBlevXkWNGjXUvoyMjLLtVyaTqRSC+/btU5kmrVzt/Nq1a5L2vXv35ip/HR2dXI+c29vbQ09PD/fv31fZ1q5dOxw7dkwySpyQkIBdu3ahTZs24n3t6ihnCERFRUnaBUFAVFQUTExMYGBgkG1u69evh5aWFqpXr66y7cGDB6hQoUK2+xMRERERFXQc6abv3o0bN8R7s2NiYnD69GmEhIRALpdj9+7dKFasmBi7cuVKtGjRAs2aNYO3tzdsbGwQFxeHv//+G5cuXcL27duzPVbr1q0RGhoKR0dHVKlSBX/++Sfmzp0rPmJMycXFBeXKlcOYMWOQmpoKExMT7N69G2fOnMnVOVWuXBknTpxAREQErKysYGRklOUovLa2NurUqaNSHAMZU+nXr1+PVq1aYfr06dDR0cGsWbPw8eNHTJ06VRJbunRpABn3vgMZi8h5eXlh1apV0NHRQcuWLZGcnIywsDCcPXsWAQEB4j3hc+fOxc2bN+Hu7g5bW1txIbU//vgDU6dOVRnRjo2Nxd27d/Hzzz/n6noQERERERVULLrpu+fj4wMgo/gsUqQIypcvj/Hjx8PX11dScAOAm5sbLly4gJkzZ2LEiBF48+YNzMzMUKFCBXTq1CnHYy1atAhaWloICgrC+/fv4ezsjF27dmHSpEmSOLlcjoiICAwdOhQDBw6Ejo4OunTpgiVLlqBVq1a5Os6QIUPQpUsXfPjwAY0aNcr22d7du3dH//798eLFC1hZWYntxYoVw+nTpzFmzBj07t0bqampqFOnDk6cOAFHR0dJH+qe2b1x40YsWbIE69evx9q1a6GlpYWyZctiw4YN6Natmxjn6OiIvXv3Yt++fXjz5g309PTg5OSEzZs3o0uXLir97tmzB1paWrm65kREREREBZlMyLz8MxF9lz5+/Ag7OzuMHj0a48ePz+t0ctSgQQPY2dlh48aNud4nPj4exsbG6LijG7QMtDWYHREREdG/s7F5SF6nQN8B5b9f3717h8KFC2cZx3u6ifIBXV1dTJs2DfPnz0diYmJep5OtU6dOITo6WuXZ3UREREREPyJOLyfKJ/r374+3b9/iwYMHqFy5cl6nk6XY2FisW7cu25XTiYiIiIh+FCy6ifIJuVyOCRMm5HUaOWrXrl1ep0BERERE9N3g9HIiIiIiIiIiDWHRTURERERERKQhLLqJiIiIiIiINIT3dBPRd2WNx/JsH7lARERERJSfcKSbiIiIiIiISENYdBMRERERERFpCItuIiIiIiIiIg1h0U1ERERERESkISy6iYiIiIiIiDSERTcRERERERGRhvCRYUT0XWkf0Q8Kfe28ToOIiIjomzrQbn1ep0B5hCPdRERERERERBrCopuIiIiIiIhIQ1h0ExEREREREWkIi24iIiIiIiIiDWHRTURERERERKQhLLqJiIiIiIiINIRFNxEREREREZGGsOimbyY0NBQymQwXL178qv1lMhmmTp0qvr958yamTp2KR48eqcR6e3vDwcHhq46T231dXV1RqVKlrzqGpqxbtw7FihVDQkKCpP3IkSOoU6cO9PX1UbRoUXh7eyMmJiZXfcbHx2PixIkoW7Ys9PX1YWNjg44dO+Kvv/5SG3/mzBm0bNkSJiYm0NPTQ5kyZRAQECCJadiwIUaMGPFV50hEREREVJCw6KbvRmRkJHx9fcX3N2/exLRp09QW3ZMnT8bu3bv/w+zy3ocPH+Dn54fx48fDyMhIbD958iRatGgBCwsL7NmzB4sWLcKRI0fg7u6O5OTkHPv19PTEwoUL0a9fP+zbtw+zZs3ClStXUKdOHTx+/FgSu2nTJjRq1AjGxsZYt24d9u/fj/Hjx0MQBElcQEAAli1bhtu3b3+bkyciIiIiyqcUeZ0AkVLt2rVzHVuqVCkNZvJ9CgsLQ2xsrOQPEwAwduxYlC1bFjt27IBCkfGVLlGiBOrVq4e1a9di0KBBWfZ57949nDp1CpMmTcLYsWPF9tKlS6Nu3brYtWsXRo4cCQB49uwZ+vfvjwEDBmDZsmVirJubm0q/jRo1Qrly5fDrr79i1apV/+q8iYiIiIjyM450k0Z5e3vD0NAQ9+7dQ8uWLWFoaIjixYtj9OjRKqOwmaeXh4aGomPHjgAyijqZTAaZTIbQ0FCx38+niC9duhQNGzaEubk5DAwMULlyZcyZMwcpKSn/6hxOnz6N2rVrQ09PDzY2Npg8eTLS0tIkMXFxcRg8eDBsbGygra2NkiVLYuLEieI5fvz4EdWqVUPp0qXx7t07cb+XL1/C0tISrq6uKn1+bvny5fD09ESRIkXEtmfPniE6Oho9e/YUC24AqFu3LsqWLZvjbAAtLS0AgLGxsaRdeQxdXV2xbc2aNUhMTMT48eOz7VOpZ8+e2LRpk8pUeCIiIiKiHwmLbtK4lJQUtGnTBu7u7tizZw/69OmDBQsWYPbs2Vnu06pVKwQGBgLIKKYjIyMRGRmJVq1aZbnP/fv30a1bN6xfvx6///47+vbti7lz52LAgAFfnfvLly/RpUsXdO/eHXv27EGHDh0wY8YMDB8+XIz5+PEj3NzcsG7dOowaNQr79u1Djx49MGfOHHh5eQHIKF63bduGmJgY9OnTBwCQnp6O7t27QxAEbN68GXK5PMs8/ve//+H69esqo8o3btwAAFSpUkVlnypVqojbs2Jvb4+2bdtiwYIFOH78ON6/f49bt25h2LBhsLOzQ5cuXcTYU6dOwdTUFLdu3YKTkxMUCgXMzc0xcOBAxMfHq/Tt6uqKxMREnDhxQu2xk5OTER8fL3kRERERERU0nF5OGvfp0ydMmzZNHLl2d3fHxYsXsWnTJvj7+6vdp1ixYihTpgwAoEKFCrmaej5//nzx5/T0dDRo0ABmZmbw8fHBr7/+ChMTky/OPTY2Fnv27EGbNm0AAE2bNkVSUhKWL1+OcePGwc7ODmFhYbh27Rq2bdsmnqOHhwcMDQ0xfvx4HD58GB4eHihTpgzWrFmDzp07Y9GiRYiLi8OJEydw8OBBWFlZZZvHuXPnAADOzs4q+QGAqampyj6mpqbi9uxs374dQ4YMQePGjcW2KlWq4OTJk5Jr9uzZM3z48AEdO3bEhAkTsHDhQkRHR2PKlCm4ceMGTp8+DZlMJsZXq1YNMpkMZ8+ehaenp8pxg4KCMG3atBzzIyIiIiLKzzjSTRonk8lUiq4qVaqoLNL1b12+fBlt2rSBmZkZ5HI5tLS00KtXL6SlpeHOnTtf1aeRkZFYcCt169YN6enpOHXqFADg2LFjMDAwQIcOHSRx3t7eAICjR4+KbZ06dcKgQYMwduxYzJgxA35+fvDw8Mgxj+fPnwMAzM3N1W7PXOzmpj2zQYMGYefOnViwYAFOnjyJrVu3QltbG40bN5b8jtLT0/Hx40f4+flhwoQJcHV1xdixYxEUFISzZ89KzhPImLpepEgRPHv2TO1xJ0yYgHfv3omvp0+f5pgrEREREVF+w6KbNE5fX19ybzAA6Ojo4OPHj9/sGE+ePEGDBg3w7NkzLFq0CKdPn0Z0dDSWLl0KAEhKSvqqfi0sLFTaLC0tAfzfKHNsbCwsLS1VClxzc3MoFAqV0eY+ffogJSUFCoUCw4YNy1Ueyvw/v45mZmaSXDKLi4tTOwKe2cGDBxEcHIyVK1dixIgRaNiwITp16oTDhw8jLi5O8gg35bGaNWsm6aNFixYAgEuXLqn0r6urm+W119HRQeHChSUvIiIiIqKChkU3FQjh4eFITEzErl270KNHD9SvXx81atSAtrb2v+r31atXKm0vX74E8H9FqJmZGV69eqXy2KyYmBikpqaiaNGiYltiYiJ69uyJsmXLQk9PT2Ul8qwo+4iLi5O0K58jfv36dZV9rl+/nuNzxq9cuQIAcHFxkbQXKVIEpUuXltwTru6+cQDieRcqpPqfkzdv3kjOn4iIiIjoR8Oim75bOjo6AHI3Sq0cZVbuA2QUg6tXr/5XOSQkJGDv3r2Stk2bNqFQoUJo2LAhgIx71N+/f4/w8HBJ3Lp168TtSgMHDsSTJ0+wa9cuBAcHY+/evViwYEGOeTg6OgLIWCwuMxsbG9SsWRMbNmyQrH4eFRWF27dviwu5ZcXa2lqMzyw2NhZ37tyBra2t2Na+fXsAwIEDBySx+/fvB6D6yLfnz5/j48ePqFChQo7nR0RERERUUHEhNfpuKUdpV61aBSMjI+jq6qJEiRLiCHNmHh4e0NbWRteuXTFu3Dh8/PgRy5cvx5s3b/5VDmZmZhg0aBCePHmCsmXLYv/+/Vi9ejUGDRoEOzs7AECvXr2wdOlS9O7dG48ePULlypVx5swZBAYGomXLlmjSpAmAjEdubdiwASEhIahYsSIqVqyIoUOHYvz48ahXrx5q1qyZZR61atWCnp4eoqKiVO4xnz17Njw8PNCxY0cMHjwYMTEx+OWXX1CpUiX4+PiIcY8fP0apUqXQu3dvBAcHAwC8vLzg7++PQYMG4X//+x+cnZ3x4sULzJ07Fx8+fJCs0t60aVN4enpi+vTpSE9PR+3atXHx4kVMmzYNrVu3Rv369SV5KQt5dc/xJiIiIiL6UXCkm75bJUqUwMKFC3H16lW4urrCxcUFERERamMdHR2xc+dOvHnzBl5eXvj555/h5OSE33777V/lYGlpiU2bNiEsLAxt2rTBtm3b4OfnJ+lXV1cXx48fR/fu3TF37ly0aNECoaGhGDNmDHbt2gUgY6r3sGHD0Lt3b3GBNQCYN28eqlSpgs6dO+Pt27dZ5qGtrY0OHTpgz549KttcXV2xf/9+vHjxAp6envj555/h5uaGo0ePqoz8p6WlSUbEDQ0NERUVhe7du2PFihVo2bIlxo4dCxsbG5w5cwaurq6SY23duhUjRozAqlWr0KJFCyxfvhwjR47Ejh07VPIKDw9H5cqVUbly5ZwuMxERERFRgSUTPr8RlYi+SxcvXoSLiwuioqJQq1atvE4nW/Hx8bC2tsaCBQvQr1+/XO9jbGyMJhs6QaH/7+7FJyIiIvreHGi3Pq9ToG9M+e/Xd+/eZbsoMEe6ifKJGjVqoFOnTggICMjrVHK0YMEC2NnZSaa3ExERERH9iFh0E+Ujv/76K1xcXJCQkJDXqWSrcOHCCA0NhULBZSOIiIiI6MfG6eVE9F3g9HIiIiIqyDi9vODh9HIiIiIiIiKiPMaim4iIiIiIiEhDWHQTERERERERaQhXOSKi78pOz9XZ3hNDRERERJSfcKSbiIiIiIiISENYdBMRERERERFpCItuIiIiIiIiIg1h0U1ERERERESkISy6iYiIiIiIiDSERTcRERERERGRhrDoJiIiIiIiItIQFt1EREREREREGsKim4iIiIiIiEhDWHQTERERERERaQiLbiIiIiIiIiINYdFNREREREREpCEsuomIiIiIiIg0hEU3ERERERERkYaw6CYiIiIiIiLSEBbdRERERERERBqiyOsEiIgAQBAEAEB8fHweZ0JERERElDPlv1uV/47NCotuIvouJCQkAACKFy+ex5kQEREREeVeQkICjI2Ns9wuE3Iqy4mI/gPp6el4/vw5jIyMIJPJ8jodIsqCi4sLoqOj8zoNogKP3zX60eWH74AgCEhISIC1tTUKFcr6zm2OdBPRd6FQoUKwtbXN6zSIKAdyuRyFCxfO6zSICjx+1+hHl1++A9mNcCtxITUiIiLKtSFDhuR1CkQ/BH7X6EdXkL4DnF5OREREREREpCEc6SYiIiIiIiLSEBbdRERERERERBrCopuIiIiIiIhIQ1h0ExEREREREWkIi24iIiL6brRr1w4mJibo0KFDXqdCVKDxu0Y/uv/yO8Cim4iIiL4bw4YNw7p16/I6DaICj981+tH9l98BFt1ERET03XBzc4ORkVFep0FU4PG7Rj+6//I7wKKbiIgon3NwcIBMJlN5DRky5Jsd49SpU/D09IS1tTVkMhnCw8PVxi1btgwlSpSArq4uqlevjtOnT3+zHIjyWmpqKiZNmoQSJUpAT08PJUuWxPTp05Genv7NjsHvGn3PEhISMGLECNjb20NPTw9169ZFdHT0Nz1GQfwOsOgmIiLK56Kjo/HixQvxdfjwYQBAx44d1cafPXsWKSkpKu23bt3Cy5cv1e6TmJiIqlWrYsmSJVnmsXXrVowYMQITJ07E5cuX0aBBA7Ro0QJPnjwRY6pXr45KlSqpvJ4/f/4lp0yUJ2bPno0VK1ZgyZIl+PvvvzFnzhzMnTsXixcvVhvP7xoVNL6+vjh8+DDWr1+P69evo2nTpmjSpAmePXumNp7fgf9PICIiogJl+PDhQqlSpYT09HSVbWlpaULVqlWFDh06CKmpqWL77du3BUtLS2H27Nk59g9A2L17t0p7zZo1hYEDB0raHB0dhV9++eWL8j9+/LjQvn37L9qH6L/QqlUroU+fPpI2Ly8voUePHiqx/K5RQfPhwwdBLpcLv//+u6S9atWqwsSJE1Xi+R34PxzpJiIiKkA+ffqEDRs2oE+fPpDJZCrbCxUqhP379+Py5cvo1asX0tPTcf/+fTRu3Bht2rTBuHHjvvq4f/75J5o2bSppb9q0Kc6dO/dVfRJ9b+rXr4+jR4/izp07AICrV6/izJkzaNmypUosv2tU0KSmpiItLQ26urqSdj09PZw5c0Ylnt+B/6PI6wSIiIjo2wkPD8fbt2/h7e2dZYy1tTWOHTuGhg0bolu3boiMjIS7uztWrFjx1cd9/fo10tLSYGFhIWm3sLDIcgqhOs2aNcOlS5eQmJgIW1tb7N69Gy4uLl+dF9G3NH78eLx79w6Ojo6Qy+VIS0vDzJkz0bVrV7Xx/K5RQWJkZIQ6deogICAA5cuXh4WFBTZv3ozz58+jTJkyavfhdyADi24iIqICJDg4GC1atIC1tXW2cXZ2dli3bh0aNWqEkiVLIjg4WO3I+Jf6vA9BEL6o30OHDv3rHIg0ZevWrdiwYQM2bdqEihUr4sqVKxgxYgSsra3Ru3dvtfvwu0YFyfr169GnTx/Y2NhALpfD2dkZ3bp1w6VLl7Lch98BLqRGRERUYDx+/BhHjhyBr69vjrGvXr1C//794enpiQ8fPmDkyJH/6thFixaFXC5XGWWIiYlRGY0gyq/Gjh2LX375BV26dEHlypXRs2dPjBw5EkFBQVnuw+8aFSSlSpXCyZMn8f79ezx9+hQXLlxASkoKSpQokeU+/A6w6CYiIiowQkJCYG5ujlatWmUb9/r1a7i7u6N8+fLYtWsXjh07hm3btmHMmDFffWxtbW1Ur15dXDld6fDhw6hbt+5X90v0Pfnw4QMKFZL+81kul2f5yDB+16igMjAwgJWVFd68eYNDhw6hbdu2auP4HcjA6eVEREQFQHp6OkJCQtC7d28oFFn/7z09PR3NmzeHvb09tm7dCoVCgfLly+PIkSNwc3ODjY2N2lGI9+/f4969e+L7hw8f4sqVKzA1NYWdnR0AYNSoUejZsydq1KiBOnXqYNWqVXjy5AkGDhz47U+YKA94enpi5syZsLOzQ8WKFXH58mXMnz8fffr0UYnld40KokOHDkEQBJQrVw737t3D2LFjUa5cOfj4+KjE8juQicbXRyciIiKNO3TokABAuH37do6xf/zxh5CUlKTSfvnyZeHJkydq9zl+/LgAQOXVu3dvSdzSpUsFe3t7QVtbW3B2dhZOnjz5VedD9D2Kj48Xhg8fLtjZ2Qm6urpCyZIlhYkTJwrJyclq4/ldo4Jm69atQsmSJQVtbW3B0tJSGDJkiPD27dss4/kdyCATBEH470t9IiIiIiIiooKP93QTERERERERaQiLbiIiIiIiIiINYdFNREREREREpCEsuomIiIiIiIg0hEU3ERERERERkYaw6CYiIiIiIiLSEBbdRERERERERBrCopuIiIiIiIhIQ1h0ExEREREREWkIi24iIqICqm/fvpDJZOJr2rRpeZ1SvsVrqXl3796Fjo4OZDIZdu7cKba7urqK1/3Ro0d5l2AeU14DBweHr9pfEARUrlwZMpkMffr0+bbJEVG2WHQTEREVQB8+fMD27dslbWFhYRAEIY8yyr94Lf8bo0aNwqdPn1C5cmV4eXnldToFjkwmg7+/PwAgNDQU0dHReZwR0Y+DRTcREVEBtHPnTiQkJADI+Mc2ADx8+BCnTp3Ky7QAZBSx+cn3fC1z63u/5levXsXvv/8OABg0aJB4nenb8vLygoWFBQRBQFBQUF6nQ/TDYNFNRERUAIWGhoo/Dxw4UG373r17xSmr/fv3l+x/9uxZcVuHDh3E9piYGIwePRqOjo7Q09ODgYEBXFxcsHLlSsnI76NHj8T9XV1d8fvvv6NGjRrQ1dXF4MGDAQCrVq2Cu7s7bG1tYWBgAG1tbdja2qJLly64du2ayjnt3bsXTk5O0NXVRYkSJTBr1iysXbtWPM7UqVMl8VevXkX37t1ha2sLbW1tmJqaonnz5jh69Og3v5aZ/f333+jbty9KlCgBHR0dGBsbw8nJCcuWLZPEnT9/Hl26dJHkV6tWLezYsUPtNczMwcFB3KZ04sQJsc3b2xshISGoVKkStLW1MWfOHADAzJkz0aBBA1hbW0NPTw+6urooWbIk+vbtq3bqdnbnkp6ejjJlykAmk0FfXx9xcXGSfatUqQKZTAYdHR38888/2V5j5bVRKBTo0qVLtrFKgiBgzZo1qFevHoyNjaGtrQ17e3v06dMH9+7dU4n/0s+POr///jsaNWoEExMTKBQKmJmZwcnJCX379sWbN28ksVu2bIGHhweKFi0KbW1tWFpaolmzZrhx4wYA4OPHj/Dx8YGTkxOKFSsGbW1tGBgYoEqVKvD390diYmKursP79+8xbdo0VKlSBQYGBtDT00PlypUxa9YsfPr0SRIrl8vRuXNn8Xo8e/YsV8cgon9JICIiogLl8ePHQqFChQQAgrW1tRAfHy/o6ekJAARDQ0Ph/fv3giAIQmpqqmBjYyMAEIoUKSJ8/PhR7GPAgAECAAGAcOjQIUEQBOH+/fuClZWV2P75q0uXLuL+Dx8+FNtNTEzEfAAIvXv3FgRBENq2bZtlX4aGhsKdO3fE/nbt2iXIZDKVuOLFi4s/T5kyRYzfs2ePoKWlpbZvmUwmLF++/JteS6V9+/YJOjo6ao/btm1bMW7VqlWSa5L5NXz4cJVr2KhRI8lx7O3txW1Kx48fF9uKFi0q6VN5bapWrZrlNbeyshJiY2O/6FyWLVsmts2ePVvc99q1a2o/F1mxtbUVAAjOzs4q2xo1aiT29fDhQ0EQBCE9PV3o2LFjtp+fqKgosY8v/fyoc/HiRUGhUGR5zLt374qxPXv2zDJu9+7dgiAIwps3b7KMASA0bdpUcnxlu729vdgWGxsrVKhQIcs+GjZsKCQnJ0v62bVrl7h9zZo1Of5uiOjf40g3ERFRARMWFob09HQAQMeOHWFkZISWLVsCyBgVU46kyuVycUGlt2/fIiIiAgDw6dMnbNu2DQBQokQJeHh4AACGDx+OFy9eQKFQYPv27fjw4QNevXqFjh07AsgY2du3b59KPm/evEHHjh3x9OlTxMfHw8/PDwAwePBgXLx4Ea9fv0ZKSgpiY2MxadIkMc8VK1YAyBjRHDlypDiS7ufnh7dv3+L06dNqRwOTkpLg6+uLlJQUODg4IDo6GsnJybh9+zbKlSsHQRAwatQovH79+ptdS+D/Ri6Tk5MBAH369MGjR4+QkJCAM2fOiPs9f/4cw4YNE/v18/PDixcv8PbtW/zxxx+oU6dOjnnl5PXr1xgxYgRevXqF2NhY9O7dGwAwdepUXLt2DXFxcUhJScGrV6/g4+MDAHjx4gU2btz4Refi7e2NYsWKAQCWL18untOGDRvEXDLPDlDnf//7H/73v/8BAKpWrZqr89uxY4d4n729vT3+/PNPvH37FuPHjweQ8bvp27cvgC///GTl5MmTSE1NBQBs3boVnz59QkxMDM6dOwd/f38YGhoCAHbt2oX169cDAAwMDLBhwwa8ffsWL168QFhYGGxsbAAAenp62LhxI+7fv4+EhAR8+vQJ9+7dg5OTEwDgjz/+wPXr17PNacqUKbh58yYAYMmSJYiPj8fbt28xbNgwAMCpU6ewevVqyT7Ozs7iz1FRUbk+fyL6F/K05CciIqJvrnTp0uJI1rlz5wRBEIRt27aJba6urmJs5pFcT09PQRAEYceOHWLszJkzBUEQhKSkpGxH+ZSvoUOHCoIgHaUtXLiwyoiwIAjC1atXhS5dugjFixcXtLW1Vfpq3ry5IAiCcOvWLckIbmpqqtjH+PHjVUYqDx8+nGOeAIQdO3Z802t55MgRsb1UqVKSPDNbs2aN2v0/929GukuXLi2kpaWp9Hnq1CnB09NTsLKyUjsTYODAgV90LoIgCFOnThVj9+zZI6Snp4sjyOXLl89yP6ULFy6I+48bN05lu7qR7u7du4ttixYtEmNTUlIEMzMzcdu9e/e++POTlfDwcMkIckBAgLBt2zbJjAxBEIQePXqIcVOnTs22z+DgYKF+/foqs0GUry1btoixyrbMI93KmSrZvVq3bi05ZmJioritVatW2eZHRN+GItfVOREREX33Tp8+Ld7PamJiAl1dXVy5cgU2NjZQKBRITU3FyZMn8ejRIzg4OMDOzg7NmjXDgQMHcPDgQbx+/VocpVMoFOJIeGxsrDjKlx11o8flypWDgYGBpO3x48eoW7dutiONSUlJKn3a2tpCLpeL79U9PunVq1c55plVrpl96bV8+fKluG+FChUkeWaWOa5y5cq5ylX4bKX0nH4X1apVQ6FC0gmN58+fh5ubG9LS0rLcT3nNc3suADBkyBDMnj0bSUlJWLJkCQoXLoynT58CAAYMGJBtnl8r8+/Y3t5e/FmhUMDW1haxsbFiXOZrl5vPT1batm2L0aNHY/ny5Th16pRkIT1nZ2dERETA2to617/fX3/9FWPGjMn2mMrfR1Zy81n//HPOReqI/nucXk5ERFSAZF7c682bN3B2dka1atVQr149sVATBAFhYWFiXL9+/QAAKSkpWLp0Kfbv3w8AaNOmDSwtLQEAZmZmUCgy/lZvZGSE5ORkCIKg8tq0aZNKTvr6+ipt4eHhYsHduHFjPHv2DIIgYO/evSqxyunLQMbUbOUUZiBjFfHPWVhYiD83a9ZMbZ7p6ek5FoRfei2V1wrIWIAsc56ZZY5TLqqljq6urvhz5tXH379/Lyns1FF3zbds2SIW3N27d8fr168hCAJ+++23bHPM7lwAoGjRovD29gYAHDlyRHyGuZ6enjitPTtWVlbizzktuKaU+Xf8+PFj8ee0tDRxqroy7ks/P9mZN28e4uLiEB0djW3btmHIkCEAgEuXLmH69OkAcv/7zTwFf9GiRfjw4QMEQfiix6Upr4NMJsPz58/VftbPnTsn2ScmJkb8OXOuRKQ5LLqJiIgKCHXPk85K5udMe3p6ioXPjBkzkJKSAgCSFc11dXXRvHlzAEBCQoJ4j29KSgqePn2KsLAw1KtXL9eP0VIW8ADEVZvv37+PGTNmqMSWKVNGHJGMiYnBzJkzxXuL16xZoxJfr149sdD6448/MG/ePMTGxiI5ORm3bt3C7NmzUbp06Wzz+5prWa9ePZibmwMA7t27hwEDBuDJkydITEzE+fPnsWrVKgBAixYtxIL6+PHj8Pf3x6tXrxAfH4/jx49j69atADIKKmXcX3/9hYcPHyItLQ0TJ07MdrQ6K5mvua6uLvT09HD16lUsWrRIJTa356I0atQoFCpUCIIg4MSJEwCALl26oEiRIjnmZWtrK97nfOXKlVydS5s2bcSfFyxYgCtXriA+Ph6TJ08WR7krVKiAUqVKffHnJysnT55EYGAg/vrrLzg4OOCnn37CTz/9JG5/8uQJAEiK5rlz52LLli2Ij49HTEwMNm3aJD4fO/Pvw9DQEDKZDHv27FG7LkJW2rVrByDjjz+9e/fG33//jZSUFLx8+RI7duxA8+bNxZkrSpcuXRJ/rl27dq6PRUT/wn81j52IiIg0a926deK9mtWqVVPZnnm1cgDCiRMnxG1+fn6S+0AdHBxU7gl+8OBBjveQHj9+XBCE7O9HVvalr6+vsn/ZsmXV7pfV6tOZ88l8/+zevXvV3iee+aWJa/ktVy8XBEHo16+f2C6XywV9fX1BoVBIzk0p8z3dyhXiMzt37pzaY2a+5pn3y+25KLVv314Sc/78+WyvcWbK85TL5UJcXJxkW1arl3t5eWX5u9XX1xfOnj0r9vGlnx911q9fn+3nafHixWJsr169soxTrl4+a9YslW2FChUSSpUqJb4PCQkR+1S2fb56ecWKFbPNK3MfgiAIw4YNE4/15MmTXP+OiOjrcaSbiIiogMg8ZVx5L3ZmcrlcMt038/Tpfv36Se719PX1VbknuESJErhy5QrGjRuHChUqiKOlJUuWhKenJ5YvXy5ZGTk7JUqUwP79+1G7dm3o6+vDysoKY8aMUTvVGcgY0du9ezeqVq0KbW1t2NnZISAgAEOHDhVjihYtKv7s6emJP//8E7169YKdnR20tLRgbGyM8uXLo1evXuJocla+9lq2bNkSly9fho+PDxwcHKCtrQ0jIyNUrVoVTZs2FeP79euHc+fOoXPnzrCxsYGWlhaKFCmCmjVron79+mLc/PnzMWDAAFhZWUFbWxsuLi44duyYZEp2btWpUwfbt29HlSpVoKurC3t7ewQGBuKXX35RG5/bc1HKfH9ytWrVULNmzVznpnx2e1paWo6/GyBjOvX27duxYsUK1K5dG0ZGRlAoFChevDh69+6Ny5cvo27dumL8l35+1KlevTp8fX1RuXJlmJqaQi6Xw8jICLVr18aqVaskfYWFhWHTpk1wd3eHqakpFAoFzM3N4eHhIc6yGDNmDKZPnw4HBwfo6OigatWq2L17t+T3nxNTU1OcP38eAQEBqFatGgwMDKCjowN7e3t4eHjg119/RYsWLcT4tLQ08ckEbdq0QfHixXN9LCL6ejJB+GxlDiIiIqLvTEJCAi5cuICGDRtCS0sLAHDz5k20atUKjx49QqFChXDz5k2UK1cujzP9ce3evVucWr127VrxUWS51bp1a+zbtw9Vq1bF5cuXv+mCX/z8ZNi+fTs6deoEmUyG8+fPw8XFJa9TIvohsOgmIiKi796jR49QokQJaGlpwdzcHB8/fhTv3QWAadOmwd/fPw8z/HFNmDAB27Ztw8OHDyEIAhwdHXH9+nXJPcu5cffuXVSqVAmfPn3Cjh070L59+2+WIz8/Gfd9V61aFdevX4ePjw/Wrl2b1ykR/TD4yDAiIiL67hUpUgQ9evRAZGQkXr58iU+fPsHa2hq1atXCwIED1U53pv/Gixcv8ODBAxgZGaF+/fpYunTpFxfcQMaCecnJyRrIkJ8fIGNK/rVr1/I6DaIfEke6iYiIiIiIiDSEC6kRERERERERaQiLbiIiIiIiIiINYdFNREREREREpCEsuomIiIiIiIg0hEU3ERERERERkYaw6CYiIiIiIiLSEBbdRERERERERBrCopuIiIiIiIhIQ1h0ExEREREREWnI/wMd2zX/ZNR0/QAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_action_experiment.plot_accuracies()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Great caesars ghost. he's spinning a web of g/ant, | silk strands as tough as steel!
0.91
\n", + "
\n", + "
Great Caesar's Ghost! He's spinning a web of giant,⎕⎕ silk strands-- as tough as steel!

Great caesars ghost. he's spinning a web of g/ant,⎕| silk strands⎕⎕ as tough as steel!
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_action_experiment.result(14, CropMethod.PADDED_4)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 116, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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UKECli9fjh07diAlJQX29vZwc3PD0KFDpXzKtixevBg+Pj5wdnbG9OnTERkZKU24lpVt27Zh5cqV2LJlCzZu3Ai5XI6yZcti69at6Nmzp1rehIQEnQLnj21TRtu3b8eYMWMwefJkpKamwsPDA8ePH9dYDqxkyZJYtmwZli9fDk9PT6SlpWHTpk0ak6UBYg/6/v374e/vj02bNmH+/PmwsbFBnz59sGDBgjy933j37t0aM3Ur1xSfPXt2tuuU9+rVCytXrkRKSopau8qXL4+TJ09iypQp6Ny5M/T19eHl5YXAwECNe9XT0tLURgbo6enh+PHjWLp0Kfbs2YPAwECYmpqiQoUKOHz4MFq2bCnlPXToED58+ICLFy9qndH+3r17akPJ9+/fDxsbGzRr1izbY0NERERE9CWTCVl1WRKpePPmDaytrbFs2TKMGDGioJvzVXn8+DFKliyJzZs3a8x0/6lJS0uDi4sLevbsifnz5+u8XXx8PCwsLLD+wiAYmxrkYwuJiIgoL/Qst6qgm0BUoJS/X+Pi4jRGOKviPd2ks1OnTkm9/fTfcnBwwNixYzF//nxpPe1P1datW5GQkCCtfU5ERERE9DXj8HLSWevWrTWGc9N/Z+bMmTA2NsajR49QvHjxgm5Opj58+IBt27ZluVwZEREREdHXgkE30WfCzMwMs2fPLuhmZKt///4F3QQiIiIiok8Gh5cTERERERER5RMG3URERERERET5hEE3ERERERERUT7hPd1E9EnpWubbLJdcICIiIiL6nLCnm4iIiIiIiCifMOgmIiIiIiIiyicMuomIiIiIiIjyCYNuIiIiIiIionzCoJuIiIiIiIgon3D2ciL6pBSd3xYyBb+aiIiIiL4UCXPDC7oJBYo93URERERERET5hEE3ERERERERUT5h0E1ERERERESUTxh0ExEREREREeUTBt1ERERERERE+YRBNxEREREREVE+YdBNRERERERElE++uqA7ODgYMplMeujr66NYsWLo378/Hj16lGf1ODs7w9fXV+f2REdH51ndvr6+cHZ2zjafIAhYv349atasCXNzcxQuXBiNGjXCoUOH8qwtH2v79u1YtmxZgbZh9erVCA4OzpeyZTIZ/P39dcp7+vRpKBQK3L9/Xy394sWLaNKkCUxNTWFpaQkfHx/cvXtXpzJTUlKwdOlSVKpUCSYm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Pj44evQonj17Bjs7O7Ro0QKOjo552T4iIiIiIiKiz1aug24AcHR0xMCBA/OqLURERERERERflFwH3QkJCTh8+DCio6ORnJys8b6fn99HNYyIiIiIiIjoc5eroPvixYto2bIlXr58mWkeBt1ERERERET0tcv17OUvXrzI9H2ZTJbrBhERERERERF9KXI1e/mlS5cgk8ng6emJFStWYOPGjdi0aZP02LhxY16385MQHBwMmUyW6ePkyZM5Ki8xMRH+/v453i4vPX78GP7+/rh06VKel608XtHR0TneNjo6GjKZDMHBwbmq29nZGb6+vtLrnO6nsu0XLlzIVf35ITY2FjY2Nti5c6da+vPnz+Hr6wsbGxsYGxvD3d0d4eHhOpcbGhoKDw8PWFtbw9LSErVr18aWLVs08sXHx2PGjBkoW7YsjI2N4ejoiC5duuD69etq+YKCguDo6Ii3b9/mbkeJiIiIiL4guerpNjMzw9u3bxEaGgorK6u8btMnb9OmTShfvrxGeoUKFXJUTmJiIubMmQMA8PT0zIum5djjx48xZ84cODs7o1q1agXSBm2KFi2Ks2fPonTp0rnaft++fTA3N5def6r7mRNz5syBg4MDunXrJqWlpKTA29sbsbGxWL58OWxtbbFq1Sq0aNECYWFhaNSoUZZlbty4EQMHDkSnTp0wc+ZMyGQyhISEoG/fvnj58iXGjRsn5W3bti0uXLgAf39/1KpVCw8fPsTcuXPh7u6Oq1evwsnJCQDQr18/LF68GEuWLJE+30REREREX6tcBd29e/dGYGAg7t2791UG3ZUqVUKtWrX+83oTExNhbGz8n9dbEBQKBerWrZvr7atXr56HrSl4r169wo8//ojvv/9e7faNoKAgXLt2Db///jvc3d0BAI0bN0bVqlUxefJk/PHHH1mWu3HjRjg5OWH37t3Q0xMHvjRv3hyXLl1CcHCwFHTfvn0bp06dwsyZMzFp0iRpexcXF9SrVw979+6V8urr6+Obb75BQEAApkyZ8tV8ZomIiIiItNF5ePmpU6ekh6enJ+zs7NChQwesXLkS4eHhau+fOnUqP9v8ydu5cydkMhlWrlyplj579mwUKlQIx48fR3R0NIoUKQJA7MFUDlFXDon29/eHTCbDxYsX0blzZ1hZWUm9vhcuXED37t3h7OwMIyMjODs7o0ePHrh//75GWx49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SBOoAUDx4sURGRmJPXv25GkjiYiIiIiIiD5HuRpeHhAQgISEBBgYGODdu3dSeo8ePbBixQpERkbmWQOJ6Oty+5udWd4TQ0RERET0OclVT3d4eDhkMhmOHDmill65cmUAwIMHDz6+ZURERERERESfuVwF3cplnzIuCSSTyQAAr1+//shmEREREREREX3+chV0K4d+Pn36VC1duYSYlZXVRzaLiIiIiIiI6POXq6C7Zs2aAIChQ4dKaUuWLEHfvn0hk8ng5uaWN60jIiL6v/buOyyK630b+L3u0kEElC5gx44o9gIidjRi76DYjb1EVCwo2GKJvSBg76LEFnsFxdhj7PVrI4IKIiJl3j94d36MuxSNGwTvz3XtdbFnnjnzzLCb+HDOnCEiIiLKx76q6B48eDAEQcDBgwfFKeUTJkwQp5UPHjz422VIRERERERElE99VdHdtm1bjBs3TnxcWObHhv3yyy9o0aLFN02SiIiIiIiIKD+SCcpq+StcvHgRe/bswatXr2BhYYGffvpJnHpORPQl4uPjYWxsjNUoBX3I8ySHbsLtPDkuEREREeU/yn+/vnv3LttH3ub6Od2Zn8etVKVKFVSpUkVtnLa2dm67JiIiIiIiIiqQcl106+np5bpTmUyG1NTUr0qIiIiIiIiIqKDIddH9L2ahExEREREREf2QvmghNZlMJq5WTkRERERERETZ+6KiWznaXbJkScybNw9v3rxBenq6yistLU0jyRIRERERERHlJ7kuus+fP49u3bpBS0sLDx48wNixY1G8eHEMGjQIf/31lyZzJCIiIiIiIsqXcl10u7i4YMOGDXjy5AmmTp0KS0tLvH//HqtWrUKVKlXg7u6Oo0ePajJXIiIiIiIionzli6aXA4C5uTn8/f3x+PFjbNy4EaamphAEASdOnMDSpUs1kSP9R0JDQ8X79mUyGRQKBWxtbeHj44Nnz559s+M4ODjA29s71/k8evTomx3b29sbDg4OOcYJgoDVq1ejevXqKFy4MMzMzNCoUSPs27fvm+XyNaZPn44KFSogPT1d0r5lyxY4OTlBV1cX1tbWGDFiBN6/f5/rfh8/fow+ffrA2toaOjo6sLGxQbt27SQxu3btQteuXVG6dGno6enBwcEB3bt3x927dyVxKSkpKFWqFBYuXPjV50lEREREVFB8cdENZDwEfMmSJfD390dc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tvDx8cGzZ8++2XEcHBzg7e2d63wePXr0zY7t7e0NBweHL9pHEAQ0bNgQMpkMQ4cO/Wa5fI3p06ejQoUKSE9Pl7Rv2bIFTk5O0NXVhbW1NUaMGIH379/nqs+XL19i6NChKFmyJPT09GBvb4++ffviyZMnkrhdu3aha9euKF26NPT09ODg4IDu3bvj7t27kriUlBSUKlUKCxcu/FfnSkRERERUELDoJhUhISGIjIzE4cOH0a9fP2zevBkNGjRAYmJiXqeWJ5YuXYp79+7ldRp4/vw55syZg+nTp6NQof/76m7cuBFdu3aFi4sLDhw4gClTpiA0NBReXl459pmcnIyGDRti69atGDNmDA4cOAA/Pz/s27cPdevWRUJCghg7e/ZsfPjwARMnTsTBgwcxY8YMXL58Gc7Ozvjrr7/EOC0tLfj7+2P69OmIjY39theBiIiIiCifUeR1AvT9qVSpEmrUqAEAcHNzQ1paGgICAhAeHo7u3bvncXb/rUePHmHChAlYt25dropYTVq0aBGKFCkiySMtLQ1jx45F06ZNsXr1agAZvzMjIyN0794dBw4cQIsWLbLs8/Tp07h79y7WrFmDvn37AgBcXV1RuHBhdOvWDUeOHEG7du0AABERETA3N5fs37hxYzg4OGDBggVYs2aN2N61a1eMGjUKK1euhJ+f3ze7BkRERERE+Q1HuilHtWvXBgA8fvwYADBt2jTUqlULpqamKFy4MJydnREcHAxBECT7paSkYNy4cbC0tIS+vj7q16+PCxcuqD1GVFQU6tWrJ06PnjBhAlJSUtTGbt26FXXq1IGBgQEMDQ3RrFkzXL58WSUuNDQU5cqVg46ODsqXL49169Z98bn3798fHh4eYuH5uRMnTkAmk2HDhg0YNWoULC0toaenh0aNGklyev36NYoXL466detKzuvmzZswMDBAz549s83j06dPCA4ORrdu3SSj3FFRUXjx4gV8fHwk8R07doShoSF2796dbb9aWloAAGNjY0l7kSJFAAC6urpi2+cFNwBYW1vD1tYWT58+lbRra2ujc+fOWLVqlcrngoiIiIjoR8Kim3KknFpdrFgxABmjvwMGDMC2bduwa9cueHl54eeff0ZAQIBkv379+mHevHno1asX9uzZg/bt28PLywtv3ryRxN28eRPu7u54+/YtQkNDsWLFCly+fBkzZsxQySUw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ZDKBD9Glf+HmzZuoWLEifv/991wV7597+PAhHB0dMWXKFPj5+X3z/K5du4aqVati6dKl4gh1TkJDQ+Hj44Po6GjUqFHjm+f0tX7++WccPXoUf/311xevSv5fCw4OxvDhw/H06dNcj3THx8fD2NgYkQ+nwNBIddE4IiKi/KyS2S95nQIRfWPKf7++e/cOhQsXzjKO93TTv3L8+HHUqVMnVwX31atX8csvv2Dv3r04ceIEVq5ciSZNmqBw4cIqz4X+t+7fv49jx46hf//+sLKyUjsKnt9MmjQJz549w86dO/M6lWylpqZi9uzZmDBhAqeWExEREdEPj0U3/StDhgzBuXPnchVrYGCAixcvom/fvvDw8MDEiRNRrVo1nDlzJsvHhn2tgIAAeHh44P3799i+fTv09fW/af95wcLCAhs3bvyuHgWnztOnT9GjRw+MHj06r1MhIiIiIspznF5ORN8FTi8nIqKCjNPLiQoeTi8nIiIiIiIiymMsuomIiIiIiIg0hEU3ERERERERkYbwOd1E9F2pYDoq23tiiIiIiIjyE450ExEREREREWkIi24iIiIiIiIiDWHRTURERERERKQhLLqJiIiIiIiINIRFNxEREREREZGGsOgmIiIiIiIi0hA+MoyIviu+hwdBy0A7r9MgIiIiytLG5iF5nQLlIxzpJiIiIiIiItIQFt1EREREREREGsKim4iIiIiIiEhDWHQTERERERERaQiLbiIiIiIiIiINYdFNREREREREpCEsuomIiIiIiIg0hEU3fbdCQ0Mhk8nEl66uLiwtLeHm5oagoCDExMT8q/6PHj2KGjVqwMDAADKZDOHh4d8m8c94e3vDwcFB0hYYGPjFx7t//z50dHQQGRkpaX/w4AG8vLxQpEgRGBoawsPDA5cuXcpVn4IgYPXq1ahevToKFy4MMzMzNGrUCPv27VOJzfy7yPyaNWuWJG7y5MlwdnZGenr6F50fEREREVFBxKKbvnshISGIjIzE4cOHsXTpUjg5OWH27NkoX748jhw58lV9CoKATp06QUtLC3v37kVkZCQaNWr0jTPP2tcU3WPGjIGHhwfq1Kkjtv3zzz9o0KAB7ty5g7Vr12Lbtm34+PEjXF1dcfv27Rz7nDJlCvr374+aNWti586dCA0NhY6ODlq3bo1du3apxHfo0AGRkZGSV69evVTyfPjwIcLC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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_action_experiment.perform_methods(plot_acc=True)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Everything Everywhere All at Once\n" + ] + }, + { + "cell_type": "code", + "execution_count": 117, + "metadata": {}, + "outputs": [], + "source": [ + "CONTEXT.reset()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## ExperimentsVisor" + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class ExperimentsVisor(ContextVisor):\n", + " ctx: OCRExperimentContext\n", + "\n", + " def update_output(self, \n", + " model: OCRModel | None = None,\n", + " image_idx: ImgIdT | None = None,\n", + " display_option: DisplayOptions | None = None, \n", + " **kwargs):\n", + " model_selector, image_selector, content_selector, result_visor = self._comps()\n", + " if model is not None:\n", + " exp_ctx = result_visor.ctx\n", + " exp_ctx.ocr_model = list(model_selector.models.keys())[model.value]\n", + " result_visor.ctx = exp_ctx\n", + " if image_idx is not None:\n", + " img_ctx = ImageContext(self.ctx, image_idx)\n", + " result_visor.ctx.ctx = img_ctx\n", + " display_option = content_selector.values['display_option']\n", + " if display_option is not None and display_option != DisplayOptions.RESULTS:\n", + " result_visor.hide()\n", + " if display_option == DisplayOptions.BEST_RESULTS:\n", + " result_visor.best_results()\n", + " elif display_option == DisplayOptions.DATAFRAME:\n", + " result_visor.pd_to_html()\n", + " else:\n", + " content_selector.display_content(image_selector.image_ctx, display_option)\n", + " else:\n", + " result_visor.show()\n", + " result_visor.update_output(**kwargs)\n", + "\n", + " def _comps(self):\n", + " cc = self.comps\n", + " msel: ModelSelector = cc['model_selector'] # type: ignore\n", + " isel: ImageSelector = cc['image_selector'] # type: ignore\n", + " cs: ContentSelector = cc['content_selector'] # type: ignore\n", + " rv: ResultVisor = cc['result_visor'] # type: ignore\n", + " return msel, isel, cs, rv\n", + "\n", + " def setup_ui(self):\n", + " ctls = self.controls.values()\n", + " msw, isw, csw, rvw = [_.w for _ in self._comps()]\n", + " return W.VBox([W.HBox([msw, isw, csw, *ctls]), rvw,])\n", + "\n", + " def __init__(self, \n", + " ctx: OCRExperimentContext,\n", + " image_idx: ImgIdT | str | Path = 0,\n", + " ocr_model: OCRModel = OCRModel.TESSERACT,\n", + " display_option: DisplayOptions = DisplayOptions.RESULTS,\n", + " all_boxes: bool = False,\n", + " box_idx: int = 0,\n", + " all_methods: bool = False,\n", + " method: CropMethod=CropMethod.INITIAL_BOX,\n", + " ocr_models: dict[str, OCRModel] = {'Tesseract': OCRModel.TESSERACT},\n", + " out: W.Output | None = None,\n", + " ):\n", + " if not isinstance(ctx, OCRExperimentContext):\n", + " raise ValueError(\"ctx must be an OCRExperimentContext\")\n", + " exp = ExperimentOCR.from_image(ctx, 'Tesseract', image_idx)\n", + " if not exp:\n", + " raise ValueError(f\"Image {image_idx} not found in experiment context\")\n", + " \n", + " out = out or self.out\n", + " model_selector = ModelSelector(ctx, ocr_model=ocr_model, \n", + " ocr_models=ocr_models, out=out)\n", + " image_selector = ImageSelector(ctx, image_idx=image_idx, out=out)\n", + " content_selector = ContentSelector(ctx, display_option=display_option, out=out)\n", + " result_visor = ResultVisor(exp, out=out,\n", + " all_boxes=all_boxes, box_idx=box_idx, all_methods=all_methods, method=method)\n", + "\n", + " super().__init__(ctx, {}, out=out, \n", + " ctxs={'model_selector': model_selector, 'image_selector': image_selector, 'content_selector': content_selector, \n", + " 'result_visor': result_visor}\n", + " )\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualize all" + ] + }, + { + "cell_type": "code", + "execution_count": 122, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c722001cdbcf444da9b56a366c6723e1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(HBox(children=(HBox(children=(Dropdown(layout=Layout(width='fit-content'), optio…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8a95135fcb03495f94fb726416f3df54", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# tesseract_experiment = ExperimentsVisor(CONTEXT)\n", + "tesseract_experiment = ExperimentsVisor(CONTEXT, BASE_IMAGE_IDX)\n", + "\n", + "test_eq(tesseract_experiment.all_values, {\n", + " 'image_selector': {'image_idx': 20},\n", + " 'content_selector': {'display_option': DisplayOptions.RESULTS},\n", + " 'result_visor': {\n", + " 'all_boxes': False,\n", + " 'box_idx': 0,\n", + " 'all_methods': False,\n", + " 'method': CropMethod.INITIAL_BOX,\n", + " },\n", + " 'model_selector': {'model': OCRModel.TESSERACT},\n", + " 'self': {}\n", + "})\n", + "\n", + "tesseract_experiment\n" + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "metadata": {}, + "outputs": [], + "source": [ + "tesseract_experiment.update(display_option=DisplayOptions.BOXES)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Colophon\n", + "----\n" + ] + }, + { + "cell_type": "code", + "execution_count": 122, + "metadata": {}, + "outputs": [], + "source": [ + "import fastcore.all as FC\n", + "from nbdev.export import nb_export\n" + ] + }, + { + "cell_type": "code", + "execution_count": 124, + "metadata": {}, + "outputs": [], + "source": [ + "if FC.IN_NOTEBOOK:\n", + " nb_export('experiments.ipynb', '.')\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/_testbed/experiments.py b/_testbed/experiments.py new file mode 100644 index 00000000..27d56ba6 --- /dev/null +++ b/_testbed/experiments.py @@ -0,0 +1,1995 @@ +# AUTOGENERATED! DO NOT EDIT! File to edit: experiments.ipynb. + +# %% experiments.ipynb 7 +from __future__ import annotations + +import dataclasses +import difflib +import functools +import json +import shutil +from collections import defaultdict +from enum import Enum +from pathlib import Path +from typing import Any +from typing import Callable +from typing import cast +from typing import Mapping +from typing import Self +from typing import TypeAlias + +import fastcore.all as FC +import ipywidgets as W +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import pcleaner.config as cfg +import pcleaner.ctd_interface as ctm +import pcleaner.image_ops as ops +import pcleaner.ocr.ocr as ocr +import pcleaner.structures as st +import torch +from IPython.display import clear_output +from IPython.display import display +from IPython.display import HTML +from ipywidgets.widgets.interaction import show_inline_matplotlib_plots +from loguru import logger +from pcleaner.ocr.ocr_tesseract import TesseractOcr +from PIL import Image +from PIL import ImageFilter +from rich.console import Console +from tqdm.notebook import tqdm + + +# %% auto 0 +__all__ = ['CM', 'SubjIdT', 'ImgIdT', 'BoxIdT', 'ImgSpecT', 'remove_multiple_whitespaces', 'postprocess_ocr', + 'accuracy_ocr_naive', 'accuracy_ocr_difflib', 'ground_truth_path', 'read_ground_truth', + 'dilate_by_fractional_pixel', 'extract_text', 'lang2pcleaner', 'lang2tesseract', 'ResultOCR', + 'ResultOCRExtracted', 'CropMethod', 'crop_by_image', 'crop_by_extracted', 'ResultSet', 'ResultSetDefault', + 'results_to_dict', 'dict_to_results', 'ExperimentSubject', 'ExperimentContext', 'ImageContext', + 'OCRExperimentContext', 'ContextVisor', 'ImageSelector', 'OCRContextVisor', 'OCRModel', 'ModelSelector', + 'DisplayOptions', 'ContentSelector', 'ImageContextVisor', 'Experiment', 'ExperimentOCR', + 'ExperimentOCRMethod', 'ResultVisor', 'ExperimentVisor', 'ExperimentsVisor'] + +# %% experiments.ipynb 8 +from helpers import * +from ocr_metric import * + + +# %% experiments.ipynb 12 +console = Console(width=104, tab_size=4, force_jupyter=True) +cprint = console.print + + +# %% experiments.ipynb 20 +def remove_multiple_whitespaces(text): + return ' '.join(text.split()) + + +def postprocess_ocr(text): + "Basic postprocessing for English Tesseract OCR results." + return ' '.join(remove_multiple_whitespaces(text).splitlines()).capitalize() + +def accuracy_ocr_naive(text, ground_truth): + return sum(1 for a, b in zip(text, ground_truth) if a == b) / len(text) + + +def accuracy_ocr_difflib(text, ground_truth): + """ + Calculates the OCR accuracy based on the similarity between the OCR text and the ground truth text, + using difflib's SequenceMatcher to account for differences in a manner similar to git diffs. + + :param text: The OCR-generated text. + :param ground_truth: The ground truth text. + :return: A float representing the similarity ratio between the OCR text and the ground truth, + where 1.0 is identical. + """ + # Initialize the SequenceMatcher with the OCR text and the ground truth + matcher = difflib.SequenceMatcher(None, text, ground_truth) + + # Get the similarity ratio + similarity_ratio = matcher.ratio() + + return similarity_ratio + +# %% experiments.ipynb 22 +def ground_truth_path(page_data: st.PageData): + path = Path(page_data.original_path) + return path.with_stem(path.stem + '_gt').with_suffix('.txt') + + +def read_ground_truth(page_data: st.PageData): + gts_path = ground_truth_path(page_data) + if gts_path.exists(): + gts = gts_path.read_text(encoding="utf-8").splitlines() + else: + gts = ["" for _ in range(len(page_data.boxes))] + return gts + + +# %% experiments.ipynb 24 +def dilate_by_fractional_pixel(image, dilation_fraction, filter_base_size=3): + """ + Dilates an image by a specified fractional pixel amount. The function calculates + the necessary scaling factor and filter size based on the desired dilation fraction. + + :param image: A PIL Image object (1-bit mode). + :param dilation_fraction: The desired fractional pixel amount for dilation (e.g., 0.2). + :param filter_base_size: The base size of the dilation filter to apply on the scaled image. + This size is adjusted based on the scaling factor to achieve the + desired dilation effect. + :return: A PIL Image object after dilation, converted back to grayscale. + """ + # Calculate the scale factor based on the desired dilation fraction + scale_factor = int(1 / dilation_fraction) + + # Adjust the filter size based on the scale factor + # This ensures the dilation effect is proportional to the desired fraction + filter_size = max(1, filter_base_size * scale_factor // 5) + + # Convert the image to grayscale for more nuanced intermediate values + image_gray = image.convert("L") + + # Resize the image to a larger size using bicubic interpolation + larger_size = (int(image.width * scale_factor), int(image.height * scale_factor)) + image_resized = image_gray.resize(larger_size, Image.BICUBIC) + + # Apply the dilation filter to the resized image + dilated_image = image_resized.filter(ImageFilter.MaxFilter(filter_size)) + + # Resize the image back to its original size using bicubic interpolation + image_dilated_fractional_pixel = dilated_image.resize(image.size, Image.BICUBIC) + + return image_dilated_fractional_pixel + + +# %% experiments.ipynb 25 +def extract_text(image, text_mask, box): + cropped_image = crop_box(box, image) + cropped_mask = crop_box(box, text_mask) + extracted = ops.extract_text(cropped_image, cropped_mask) + return cropped_image, cropped_mask, extracted + + +# %% experiments.ipynb 27 +_lang2pcleaner = {'English': st.DetectedLang.ENG, 'Japanese': st.DetectedLang.JA, 'Spanish': st.DetectedLang.ENG, + 'French':st.DetectedLang.ENG} +# _lang2tesseract = {'English': 'eng', 'Japanese': 'jpn'} +_lang2tesseract = {'English': 'eng', 'Japanese': 'jpn_vert', 'Spanish': 'spa', 'French': 'fra'} + + +# %% experiments.ipynb 28 +def lang2pcleaner(lang: str): + return _lang2pcleaner[lang] + +def lang2tesseract(lang: str): + return _lang2tesseract[lang] + + +# %% experiments.ipynb 35 +@dataclasses.dataclass +class ResultOCR: + block_idx: int + image: Image.Image | None + ocr: str + page_data: st.PageData + gts: list[str] + description: str = dataclasses.field(default='', kw_only=True) + + def __post_init__(self): + if self.image is None: + cache_path = self.cache_path() + if cache_path.exists(): + self.image = Image.open(cache_path) + + @property + def acc(self): + self._acc = accuracy_ocr_difflib(self.ocr, self.gts[self.block_idx]) + return self._acc + @property + def suffix(self): return f"{self.block_idx}_{self.description}" + + def diff_tagged(self): + _, html2 = get_text_diffs_html(self.gts[self.block_idx], self.ocr, False) + return f"{html2}" + + def cache_path(self, suffix: str | None = None): + suffix = self.suffix + (('_'+suffix) if suffix else '') + parent = Path(self.page_data.image_path).parent + img_name = Path(self.page_data.original_path).stem + box_image_path = parent / f"{img_name}_{suffix}.png" + return box_image_path + + def cache_image(self, image: Image.Image | None = None, suffix: str | None = None): + image = image or (self.image if not suffix else None) + box_image_path = self.cache_path(suffix) + if image and not box_image_path.exists(): + image.save(box_image_path) + return box_image_path + + + def as_html(self): + acc_html = f"
{self.acc:.2f}" + box_image_path = self.cache_image() + html1 = get_columns_html([[box_image_path], [self.ocr + acc_html]]) + html_str1, html_str2 = get_text_diffs_html(self.gts[self.block_idx], self.ocr) + html2 = f"
{html_str1}
{html_str2}
" + return html1 + '\n
\n' + html2 + + def __repr__(self): + return f"{type(self).__name__}#block {self.block_idx:02}: {self.acc:.2f}||{self.ocr}" + + def display(self): display(HTML(self.as_html())) + + def _ipython_display_(self): self.display() + + def to_dict(self): + d = dataclasses.asdict(self) + d['image'] = d['page_data'] = d['gts'] = None + return d + + # @classmethod + # def from_dict(cls, d: dict, page_data: st.PageData, gts: list[str]): + # return cls(**(d | {'page_data':page_data, 'gts':gts})) + + +@dataclasses.dataclass +class ResultOCRExtracted(ResultOCR): + + def __repr__(self): return super().__repr__() + def as_html(self): + html_str1, html_str2 = get_text_diffs_html(self.gts[self.block_idx], self.ocr) + diff_html = f"
{html_str1}
{html_str2}
" + cropped_image_path = self.cache_image(None, "cropped") + cropped_mask_path = self.cache_image(None, "mask") + result_path = self.cache_image() + return '\n
\n'.join([ + get_image_grid_html([cropped_image_path, cropped_mask_path, result_path], 1, 3), + acc_as_html(self.acc), + diff_html + ]) + + +# %% experiments.ipynb 37 +class CropMethod(Enum): + INITIAL_BOX = 'Initial box' + DEFAULT = 'Default' + DEFAULT_GREY_PAD = 'Default, grey pad' + PADDED_4 = 'Padded 4px' + PADDED_8 = 'Padded 8px' + EXTRACTED_INIT_BOX = 'Extracted, init box' + PADDED_4_EXTRACTED = 'Padded 4, extracted' + PADDED_8_EXTRACTED = 'Padded 8, extracted' + PADDED_8_DILATION_1 = 'Padded 8, dilation 1' + PAD_8_FRACT_0_5 = 'Pad 8, fract. 0.5' + PAD_8_FRACT_0_2 = 'Pad 8, fract. 0.2' + + @classmethod + def __display_names__(cls): + return dict( + zip([_.value for _ in cls], + cls)) + + +CM = CropMethod + +_IMAGE_METHODS = [CM.INITIAL_BOX, CM.DEFAULT, CM.DEFAULT_GREY_PAD, + CM.PADDED_4, CM.PADDED_8] +_EXTRACTED_METHODS = [CM.EXTRACTED_INIT_BOX, CM.PADDED_4_EXTRACTED, + CM.PADDED_8_EXTRACTED, CM.PADDED_8_DILATION_1, + CM.PAD_8_FRACT_0_5, CM.PAD_8_FRACT_0_2] + + +def crop_by_image(method: CM, + box: st.Box, + base: Image.Image, + preproc: cfg.PreprocessorConfig, + ): + image = None + match method: + case CM.INITIAL_BOX : + image = crop_box(box, base) + case CM.DEFAULT: + padded2_4 = ( + box.pad(preproc.box_padding_initial, base.size).right_pad( + preproc.box_right_padding_initial, base.size)) + image = crop_box(padded2_4, base) + case CM.DEFAULT_GREY_PAD: + image = crop_box(box, base) + image = ops.pad_image(image, 8, fill_color=(128, 128, 128)) + case CM.PADDED_4: + padded4 = box.pad(4, base.size) + image = crop_box(padded4, base) + case CM.PADDED_8: + padded4 = box.pad(8, base.size) + image = crop_box(padded4, base) + case _: pass + return image + + +def crop_by_extracted(method: CM, + box: st.Box, + base: Image.Image, + mask: Image.Image, + cropped_image_path: Path, + cropped_mask_path: Path, + dilated: dict[float, Image.Image] + ): + cropped_image, cropped_mask, image = None, None, None + if method in _EXTRACTED_METHODS: + if not cropped_image_path.exists() or not cropped_mask_path.exists(): + match method: + case CM.EXTRACTED_INIT_BOX: + cropped_image, cropped_mask, image = extract_text(base, mask, box) + case CM.PADDED_4_EXTRACTED: + padded4 = box.pad(4, base.size) + cropped_image, cropped_mask, image = extract_text(base, mask, padded4) + case CM.PADDED_8_EXTRACTED: + padded8 = box.pad(8, base.size) + cropped_image, cropped_mask, image = extract_text(base, mask, padded8) + case CM.PADDED_8_DILATION_1: + padded8 = box.pad(8, base.size) + cropped_image, cropped_mask, image = extract_text( + base, dilated[1], padded8) + case CM.PAD_8_FRACT_0_5: + padded8 = box.pad(8, base.size) + cropped_image, cropped_mask, image = extract_text( + base, dilated[0.5], padded8) + case CM.PAD_8_FRACT_0_2: + padded8 = box.pad(8, base.size) + cropped_image, cropped_mask, image = extract_text( + base, dilated[0.2], padded8) + case _: pass + + return image, cropped_image, cropped_mask + + + +# %% experiments.ipynb 39 +SubjIdT: TypeAlias = int +ImgIdT = SubjIdT +BoxIdT: TypeAlias = int + +class ResultSet(dict[BoxIdT, dict[CropMethod, ResultOCR]]): ... + +class ResultSetDefault(defaultdict[BoxIdT, dict[CropMethod, ResultOCR]]): ... + +def results_to_dict(results: ResultSet) -> dict[BoxIdT, dict[str, str]]: + d = {} + for box, box_methods in results.items(): + for method, result in box_methods.items(): + if box not in d: + d[box] = {} + d[box][method.name] = result.ocr + return d + +def dict_to_results( + image_idx: ImgIdT, + results_dict: dict[BoxIdT, dict[str, str]], + result_factory: Callable + ) -> ResultSetDefault: + results = ResultSetDefault(dict[CropMethod, ResultOCR]) + for box_idx, box_methods in results_dict.items(): + box_idx = int(box_idx) + for method, ocr in box_methods.items(): + m = CM[method] + results[box_idx][m] = result_factory(image_idx, box_idx, m, ocr) + return results + + + +# %% experiments.ipynb 41 +# class ExperimentSubject(Protocol): +# @property +# def exp(self) -> 'ExperimentContext': ... +# @property +# def idx(self) -> SubjIdT: ... +# def setup(self, +# exp: 'ExperimentContext', +# idx: Any, +# *args, **kwargs +# ): ... + + +# class ExperimentContext(Protocol): +# def subject_factory(self) -> Callable[..., ExperimentSubject]: ... +# def normalize_idx(self, idx: Any) -> SubjIdT: ... +# def experiment_subject(self, idx: Any, /, +# create: bool = False, *args, **kwargs) -> ExperimentSubject | None: +# """Get or create an `ExperimentSubject` for the given identifier. +# Returns `None` if `idx` is out of domain range. +# """ +# ... + + +# %% experiments.ipynb 42 +class ExperimentSubject: + exp: ExperimentContext + idx: SubjIdT + + def setup(self, exp: ExperimentContext, idx: Any, *args, **kwargs): + self.exp = exp + self.idx = cast(SubjIdT, exp.normalize_idx(idx)) + return self + + def __new__(cls, + exp: ExperimentContext, + idx: Any, + *args, **kwargs): + self = exp.experiment_subject(idx) + if self is None: + self = super().__new__(cls) + self = exp.experiment_subject(idx, new_subject=self, *args, **kwargs) + if self is None: + raise ValueError(f"Can't create new subject with idx: {idx}: out of range") + return self + + +class ExperimentContext: + "Class to maintain shared state across all file-based experiments within the experiment domain." + + subject_cls: Callable[..., ExperimentSubject] + def subject_factory(self) -> Callable[..., ExperimentSubject]: return type(self).subject_cls + + def normalize_idx(self, idx: int | str | Path) -> SubjIdT | None: + nidx = None + if isinstance(idx, int) and idx < len(self._paths): + nidx = idx + elif isinstance(idx, str): + try: + nidx = [_.name for _ in self._paths].index(idx) + except Exception: + pass + elif isinstance(idx, Path): + idx = idx.resolve() + if idx in self._paths: + nidx = self._paths.index(idx) + return nidx + + def path_from_idx(self, idx: int | str | Path): + _idx = self.normalize_idx(idx) + if _idx is None: + raise ValueError(f"{_idx} not found in context.") + path = Path(self._paths[_idx]) + if not path.exists(): + raise ValueError(f"{path} not found in context.") + return path + + @property + def count(self): return len(self._paths) + @property + def cache_dir(self): return Path(".cache/") + @functools.lru_cache() + def _cache_dir(self, idx: SubjIdT): + # create one folder for each image to cache and save results + path = self.path_from_idx(idx) + cache_dir = self.cache_dir / path.stem + cache_dir.mkdir(parents=True, exist_ok=True) + return cache_dir + def subject_cache_dir(self, idx: int | str | Path): + return self._cache_dir(idx) + + def empty_cache(self, idx: SubjIdT | None = None): + cache_dir = self.cache_dir + if idx is None: + shutil.rmtree(cache_dir, ignore_errors=True) + cache_dir.mkdir(parents=True, exist_ok=True) + else: + path = Path(self._paths[idx]) + cache_dir = cache_dir / path.stem + for p in cache_dir.glob("*"): + p.unlink(missing_ok=True) + if not any(cache_dir.iterdir()): + cache_dir.rmdir() + + def empty_cache_warn(self, idx: SubjIdT | None=None, *, warn: bool=True, out: W.Output | None=None): + def on_confirm_clicked(b): + try: + self.empty_cache(idx) + print("Cache cleared successfully.") + except Exception as e: + print(f"Failed to clear cache: {e}") + finally: + for widget in confirmation_box.children: + widget.close() + + def on_cancel_clicked(b): + print("Cache clear cancelled.") + for widget in confirmation_box.children: + widget.close() + + if out is None: + out = W.Output() + with out: + if FC.IN_NOTEBOOK: + confirm_button = W.Button(description="Confirm") + cancel_button = W.Button(description="Cancel") + confirm_button.on_click(on_confirm_clicked) + cancel_button.on_click(on_cancel_clicked) + label = W.Label('Are you sure you want to clear the cache? This action cannot be undone.') + confirmation_box = W.VBox([label, W.HBox([confirm_button, cancel_button])]) + display(confirmation_box) + else: + on_confirm_clicked(None) + + def experiment_subject(self, idx: SubjIdT | str | Path, /, + new_subject: ExperimentSubject | None = None, *args, **kwargs) -> ExperimentSubject | None: + "Cached subject. If provided, `new_subject` replaces value at the index." + if (nidx := self.normalize_idx(idx)) is None: + return None + if new_subject is None: + subject = self._subjects.get(nidx) + else: + new_subject.setup(self, nidx, *args, **kwargs) + self._subjects[nidx] = subject = new_subject + return subject + + def reset(self): + self._subjects.clear() + self._cache_dir.cache_clear() + + def __init__(self, paths: list[Path], root: Path | None = None): + self._root = (root or Path('.')).resolve() + self._paths = [p.resolve().relative_to(self._root) for p in paths] + self._subjects: dict[SubjIdT, ExperimentSubject] = {} + + +# %% experiments.ipynb 48 +ImgSpecT: TypeAlias = ImgIdT | str | Path + +class ImageContext(ExperimentSubject): + """ + A utility class to maintain image state for a ExperimentContext. + This class encapsulates state necessary for conducting OCR experiments. + + Attributes: + json_data (dict): JSON data loaded from cached files. + page_data (st.PageData): PanelClaner page data. + base_image (Image.Image): The base image loaded from the page data. + mask (Image.Image): The mask image used for text detection. + gts (list[str]): Ground truth data for the text in the images. + ocr_model (str): Name or identifier of the OCR model used. + mocr (ocr.OCRModel): OCR model configured for the experiment. + mask_dilated1 (Image.Image): Image mask dilated by 1 pixel. + mask_dilated05 (Image.Image): Image mask dilated by 0.5 pixels. + mask_dilated02 (Image.Image): Image mask dilated by 0.2 pixels. + + Methods: + init(config: cfg.Config, img_path: Path, cache_dir: Path, ocr_model: str): + Initializes the experiment context. It also handles the generation of text boxes + if they are not already present. + + setup_ground_truth(): + Loads or initializes ground truth data for the experiment based on the page data. + + setup_crop_masks(): + Prepares various dilated versions of the mask image to be used in different cropping + strategies during the experiments. + """ + exp: ExperimentContext + idx: ImgIdT + base_image: Image.Image + mask: Image.Image + json_data: dict | None + page_data: st.PageData + # ocr_model: str + # mocr: ocr.OCRModel + # postprocess_ocr: Callable[..., str] + _page_lang: str + _gts: list[str] + _mask_dilated1: Image.Image | None + _mask_dilated05: Image.Image | None + _mask_dilated02: Image.Image | None + + + # # this methods will be set downstream, declared here to make the type checker happy + # def result(self: Self, + # box_idx: int, method: CropMethod, ocr: bool = True, reset: bool=False) -> ResultOCR: ... + # def summary_box(self: Self, box_idx: int): ... + + def to_dict(self): + return { + 'image_idx': self.idx, + 'page_lang': self.page_lang, + } + + @property + def image_idx(self): return self.idx + @property + def cache_dir(self): + return self.exp.subject_cache_dir(self.idx) + cache_dir_image = cache_dir + + @property + def image_info(self): + img = self.base_image + w, h = img.size + print_size_in = size(w, h, 'in', 300) + print_size_cm = size(w, h, 'cm', 300) + required_dpi = dpi(w, h, 'Modern Age') + return (w, h), print_size_in, print_size_cm, required_dpi + + @property + def original_image_path(self): return Path(self.page_data.original_path) + @property + def image_path(self): return Path(self.page_data.image_path) + @property + def image_name(self): return self.original_image_path.name + @property + def image_size(self): return self.base_image.size + @property + def image_dim(self):return size(*self.image_size) + @property + def image_dpi(self): return dpi(*self.image_size) + @property + def image_print(self): + return self.image_size, self.image_dim, self.image_dpi + @property + def image_name_rich(self): + siz, dim, res = self.image_print + return f"{self.image_name} - {siz[0]}x{siz[1]} px: {dim[0]:.2f}x{dim[1]:.2f}\" @ {res:.2f} dpi" + + def setup_page_lang(self, page_lang: str | None = None): + path = Path(self.page_data.original_path).with_suffix('.json') + metadata = json.load(open(path)) if path.exists() else {} + if 'lang' in metadata and (page_lang == metadata['lang'] or page_lang is None): + self._page_lang = metadata['lang'] + return + self._page_lang = metadata['lang'] = page_lang or 'English' + json.dump(metadata, open(path, 'w'), indent=2) + @property + def page_lang(self): + if self._page_lang == None: + self.setup_page_lang() + return self._page_lang + + @property + def boxes(self): return self.page_data.boxes + + def setup_ground_truth(self): + self._gts = read_ground_truth(self.page_data) + @property + def gts(self): + if self._gts is None: + self.setup_ground_truth() + return self._gts + + @functools.lru_cache(typed=True) + def dilated_mask(self, fraction: float): + return dilate_by_fractional_pixel(self.mask, fraction) + + def mask_dilated1(self): + if self._mask_dilated1 is None: + self._mask_dilated1 = self.mask.filter(ImageFilter.MaxFilter(3)) + return self._mask_dilated1 + + def mask_dilated05(self): + if self._mask_dilated05 is None: + self._mask_dilated05 = self.dilated_mask(0.5) + return self._mask_dilated05 + + def mask_dilated02(self): + if self._mask_dilated02 is None: + self._mask_dilated02 = self.dilated_mask(0.2) + return self._mask_dilated02 + + def dilated(self): + return {1: self.mask_dilated1(), + 0.5: self.mask_dilated05(), + 0.2: self.mask_dilated02(),} + + def __new__(cls, + exp: ExperimentContext, + idx: ImgSpecT, + *args, **kwargs) -> Self: + return super().__new__(cls, exp, idx, *args, **kwargs) # type: ignore + + +# %% experiments.ipynb 50 +class OCRExperimentContext(ExperimentContext): + """ + A utility class to maintain shared state across all experiments within OCR domain. + This class encapsulates state necessary for conducting PanelCleaner OCR experiments. + """ + + config: cfg.Config + image_paths: list[Path] + # OCR engine -> Image index -> Box index -> Crop method -> Result + _results: dict[str, dict[ImgIdT, ResultSet]] + + + engines = { + 'Tesseract': cfg.OCREngine.TESSERACT, + 'Idefics': None, + 'manga-ocr': cfg.OCREngine.MANGAOCR} + + # subject_cls: ImageContext + # def subject_factory(self) -> Callable[..., ExperimentSubject]: return type(self).subject_cls + + @classmethod + def get_config(cls, cache_dir: Path | None = None) -> cfg.Config: + config = cfg.load_config() + config.cache_dir = cache_dir or Path(".") + profile = config.current_profile + preprocessor_conf = profile.preprocessor + # Modify the profile to OCR all boxes. + # Make sure OCR is enabled. + preprocessor_conf.ocr_enabled = True + # Make sure the max size is infinite, so no boxes are skipped in the OCR process. + preprocessor_conf.ocr_max_size = 10**10 + # Make sure the sus box min size is infinite, so all boxes with "unknown" language are skipped. + preprocessor_conf.suspicious_box_min_size = 10**10 + # Set the OCR blacklist pattern to match everything, so all text gets reported in the analytics. + preprocessor_conf.ocr_blacklist_pattern = ".*" + return config + + def to_dict(self): + return { + 'image_paths': list(map(str, self.image_paths)), + 'cache_dir': str(self.config.cache_dir) + } + def to_json(self): + return json.dumps(self.to_dict(), indent=2) + @classmethod + def from_json_data(cls, d: dict): + return cls(cls.get_config(Path(d['cache_dir'])), d['image_paths']) + @classmethod + def from_json_path(cls, path: Path): + return cls.from_json_data(json.loads(path.read_text())) + + + @functools.lru_cache() + def mocr(self, ocr_model: str, lang: str): + engine = self.engines[ocr_model] + ocr_processor = ocr.get_ocr_processor(True, engine) + proc = ocr_processor[lang2pcleaner(lang)] + if isinstance(proc, TesseractOcr): + proc.lang = lang2tesseract(lang) + return proc + + def ocr_box(self, result: ResultOCR, ocr_model: str, lang: str): + assert result.image is not None + text = self.mocr(ocr_model, lang)(result.image) + result.ocr = postprocess_ocr(text) + return result + + @property + def cache_dir(self): return self.config.get_cleaner_cache_dir() + image_cache_dir = ExperimentContext.subject_cache_dir + + @functools.lru_cache() + def _load_page_data(self, image_idx: int): + config = self.config + cache_dir = self.image_cache_dir(image_idx) + img_path = self.path_from_idx(image_idx) + image_name = img_path.stem + # read cached json + jsons = [_ for _ in cache_dir.glob("*#raw.json") if image_name in _.stem] + assert len(jsons) <= 1 + # generate text boxes if needed + if not jsons: + pfl = config.current_profile + gpu = torch.cuda.is_available() or torch.backends.mps.is_available() + model_path = config.get_model_path(gpu) + ctm.model2annotations(pfl.general, pfl.text_detector, model_path, [img_path], cache_dir) + # we don't need unique names for this tests, strip uuids + for p in cache_dir.glob(f"*{image_name}*"): + p.rename(strip_uuid(p)) + jsons = [_ for _ in cache_dir.glob("*#raw.json") if image_name in _.stem] + + # adapt paths to be relative to this notebook + this_path = self._root + json_file_path = jsons[0] + json_data = json.loads(json_file_path.read_text(encoding="utf-8")) + json_data["image_path"] = str(strip_uuid(json_data["image_path"]).relative_to(this_path)) + json_data["mask_path"] = str(strip_uuid(json_data["mask_path"]).relative_to(this_path)) + json.dump(json_data, open(json_file_path, "w"), indent=2) + else: + json_file_path = jsons[0] + json_data = json.loads(json_file_path.read_text(encoding="utf-8")) + + page_data = st.PageData( + json_data["image_path"], json_data["mask_path"], + json_data["original_path"], json_data["scale"], + [st.Box(*data["xyxy"]) for data in json_data["blk_list"]], + [], [], []) + # Merge boxes that have mutually overlapping centers. + page_data.resolve_total_overlaps() + return json_data, page_data + + def page_data(self, image_idx: int): + _, page_data = self._load_page_data(image_idx) + return page_data + def json_data(self, image_idx: int): + json_data, _ = self._load_page_data(image_idx) + return json_data + + def experiment_image(self, image_idx: ImgIdT | str | Path) -> ImageContext | None: + "Cached image context." + return cast(ImageContext, self.experiment_subject(image_idx)) + + def update_results(self, ocr_model: str, img_idx: ImgIdT, results: ResultSetDefault): + self._results[ocr_model][img_idx] = cast(ResultSet, results) + + + def _result_from(self, image_idx: ImgIdT, box_idx: BoxIdT, method: CropMethod, ocr: str | None = None): + img_ctx = ImageContext(self, image_idx) + extracted = method in _EXTRACTED_METHODS + result_cls = ResultOCRExtracted if extracted else ResultOCR + result = result_cls(int(box_idx), None, '', img_ctx.page_data, + img_ctx.gts, description=f"{method.value}") + if ocr is not None: + result.ocr = ocr + return result + + def result(self, + ocr_model: str, + image_idx: ImgIdT, box_idx: BoxIdT, method: CropMethod, + ocr: bool=True, + rebuild: bool=False) -> ResultOCR | None: + img_ctx = ImageContext(self, image_idx) + result = self._results[ocr_model][image_idx][box_idx].get(method) + if not rebuild and result is not None: + return result + + result = self._result_from(image_idx, box_idx, method) + image, cropped_image, cropped_mask = result.image, None, None + base_image = img_ctx.base_image + box = img_ctx.boxes[box_idx] + if image is None and method in _IMAGE_METHODS: + image = crop_by_image( + method, box, base_image, self.config.current_profile.preprocessor) + + if image is None and method in _EXTRACTED_METHODS: + mask = img_ctx.mask + cropped_image_path = result.cache_image(cropped_image, "cropped") + cropped_mask_path = result.cache_image(cropped_mask, "mask") + if not cropped_image_path.exists() or not cropped_mask_path.exists(): + image, cropped_image, cropped_mask = crop_by_extracted( + method, box, base_image, mask, + cropped_image_path, cropped_mask_path, img_ctx.dilated()) + + assert image is not None + if result.image is None: + result.image = image + result.cache_image() + if cropped_image is not None: + result.cache_image(cropped_image, "cropped") + if cropped_mask is not None: + result.cache_image(cropped_mask, "mask") + + if ocr: + result = self.ocr_box(result, ocr_model, img_ctx.page_lang) + self._results[ocr_model][image_idx][box_idx][method] = result + return result + + def results(self, ocr_model: str | None = None, img_idx: ImgIdT | None = None): + if ocr_model is None: return self._results + if img_idx is None: return self._results[ocr_model] + return self._results[ocr_model][img_idx] + def model_results(self, ocr_model: str): + return cast(dict[ImgIdT, ResultSet], self.results(ocr_model)) + def image_results(self, ocr_model: str, img_idx: ImgIdT): + return cast(ResultSet, self.results(ocr_model, img_idx)) + def box_results(self, ocr_model: str, img_idx: ImgIdT, box_idx: BoxIdT): + return cast(ResultSet, self.results(ocr_model, img_idx))[box_idx] + def method_results(self, ocr_model: str, img_idx: ImgIdT, method: CropMethod): + image_results = self.image_results(ocr_model, img_idx) + return {i: box_results.get(method) for i,box_results in image_results.items()} + + def _reset_results(self): + results = defaultdict(lambda: defaultdict(lambda: ResultSetDefault(dict))) + self._results = cast(dict[str, dict[ImgIdT, ResultSet]], results) + def reset_results(self, + ocr_model: str | None = None, + image_idx: int | None = None, + box_idx: int | None = None, + method: CropMethod | None = None): + if ocr_model is None and image_idx is None and box_idx is None and method is None: + self._reset_results() + return + results = self._results + models = tuple(results.keys()) if ocr_model is None else [ocr_model] if ocr_model in results else [] + for ocr_model in models: + img_nodes = results[ocr_model] + imgs = tuple(img_nodes.keys()) if image_idx is None else [image_idx] if image_idx in img_nodes else [] + for img_idx in imgs: + box_nodes = img_nodes[img_idx] + boxes = tuple(box_nodes.keys()) if box_idx is None else [box_idx] if box_idx in box_nodes else [] + for box_idx in boxes: + if method is None: + del box_nodes[box_idx] + else: + methods = box_nodes[box_idx] + if method in methods: + del methods[method] + if not box_nodes[box_idx]: + del box_nodes[box_idx] + if not img_nodes[img_idx]: + del img_nodes[img_idx] + if not results[ocr_model]: + del results[ocr_model] + def reset(self): + super().reset() + self.reset_results() + self._load_page_data.cache_clear() + self.mocr.cache_clear() + + def __init__(self, + config: cfg.Config | None, + image_paths: list[Path] + ): + super().__init__(list(map(lambda p: p.resolve(), image_paths))) + self.config = config or type(self).get_config() + self.image_paths = self._paths + self._reset_results() + self._images = self._subjects + + +# %% experiments.ipynb 51 +@FC.patch_to(ImageContext) +def setup(self, exp: OCRExperimentContext, image_idx: ImgSpecT, page_lang: str | None = None): + super(type(self), self).setup(exp, image_idx) + self._mask_dilated1 = self._mask_dilated05 = self._mask_dilated02 = None + # if ocr_model not in exp.engines: + # raise ValueError(f"OCR model {ocr_model} not supported.") + # self.ocr_model = ocr_model + # self.idx = exp.normalize_idx(image_idx) + self.json_data, self.page_data = exp._load_page_data(self.idx) + self.setup_page_lang(page_lang) + self.mask = Image.open(self.page_data.mask_path) + self.base_image = Image.open(self.page_data.image_path) + self.setup_ground_truth() + + + +# %% experiments.ipynb 54 +class ContextVisor: + ctx: Any + # control_names: list[str] + values: dict[str, Any] + + _css = '' + + _ctxs: dict[str, ContextVisor] + _hdlrs: dict[str, ContextVisor] + + @property + def w(self) -> W.DOMWidget: + if getattr(self, '_w', None) is None: + self._w = self.setup_ui() + return self._w + @property + def out(self) -> W.Output: + if getattr(self, '_out', None) is None: + self._out = W.Output() + self._out.clear_output(wait=True) + return self._out # type: ignore + @property + def controls(self) -> dict[str, W.ValueWidget | W.fixed]: + if getattr(self, '_controls', None) is None: + self._controls = self.setup_controls() + return self._controls + @property + def all_controls(self) -> dict[str, W.ValueWidget | W.fixed]: + if getattr(self, '_all_controls', None) is None: + controls = {} + for visor in self._ctxs.values(): + controls.update(visor.all_controls) + controls.update(self.controls) + self._all_controls = controls + return self._all_controls + + @property + def all_values(self): + return {**{k:v.values for k,v in (self._ctxs | {'self': self}).items()}, **self.values} + + @property + def comps(self): return self._ctxs + def comp(self, k: str) -> ContextVisor | None: + return self._ctxs.get(k) + def handler(self, k: str) -> ContextVisor | None: + return self._hdlrs.get(k) + + @property + def styler(self) -> W.Output | None: + if (stl := self.setup_style()) is None: + return None + if getattr(self, '_style', None) is None: + self._style = W.Output(layout={'height': '0px'}) + with self._style: + display(stl) + return self._style + def setup_style(self): + return HTML(f"") if self._css else None + + def update_output(self, **kwargs): + cprint(kwargs) + + def setup_controls(self) -> dict[str, W.ValueWidget | W.fixed]: + return {k: W.Label(value=k) for k,v in self.values.items()} + + def hide(self): + self.w.layout.visibility = 'hidden' + def show(self): + self.w.layout.visibility = 'visible' + + def setup_ui(self): + comps = [] + for visor in self._ctxs.values(): + comps.append(visor.w) + return W.HBox([*comps, *self.controls.values()]) + + def setup_display(self): + if getattr(self, '_w', None) is None: + self._w = self.setup_ui() + + + def _output(self, **kwargs): + collator = defaultdict(dict) + show_inline_matplotlib_plots() + with self.out: + clear_output(wait=True) + for k,v in kwargs.items(): + if (comp := self.handler(k)) is not None: + collator[comp][k] = v + else: + assert 0 + # self.update_output(**{k: v}) + for comp, kw in collator.items(): + comp.update_output(**kw) + show_inline_matplotlib_plots() + def interactive_output(self): + controls = self.all_controls + controls2names = {v:k for k,v in controls.items()} + def observer(change): + control_name = controls2names[change['owner']] + kwargs = {control_name: change['new']} + updated = self._update(**kwargs) + self._output(**updated) + for w in controls.values(): + w.observe(observer, 'value') + def display(self, **kwargs): + if getattr(self, '_w', None) is None: + self.setup_display() + self.interactive_output() + self._update(**(self.values | kwargs)) + all_values= {} + for comp in list(self.comps.values()) + [self]: all_values.update(comp.values) + self._hdlrs = {k:self._hdlrs.get(k, self) for k in all_values} + self._output(**all_values) + display(self.styler, self.w, self.out) if self.styler else display(self.w, self.out) + else: + self.update(**kwargs) + def _ipython_display_(self): self.display() + + def _update(self, update_value: bool=True, **kwargs): + updated = {} + for visor in self.comps.values(): + updated.update(visor._update(update_value=update_value, **kwargs)) + values = self.values + my_vals = _pops_(kwargs, self.values.keys()) + for k,v in my_vals.items(): + if v is not None and v != values[k]: + if update_value: values[k] = v + updated[k] = v + return updated + def update(self, **kwargs): + updated = self._update(update_value=False, **kwargs) + controls = self.all_controls + for k in updated: + controls[k].value = updated[k] + # self._output(**updated) + + def __init__(self, + ctx: Any, + values: dict[str, Any], + out: W.Output | None = None, + ctxs: dict[str, ContextVisor] | None = None, + hdlrs: dict[str, ContextVisor] | None = None, + ): + self._ctxs = ctxs or {} + self._hdlrs = hdlrs or {} + self.ctx = ctx + self._out = out + self.values = values + + + +# %% experiments.ipynb 62 +class ImageSelector(ContextVisor): + ctx: OCRExperimentContext + + @property + def image_ctx(self): + return ImageContext(self.ctx, self.values['image_idx']) + + def setup_controls(self): + paths = self.ctx.image_paths + w = W.Dropdown( + options={_.stem:i for i,_ in enumerate(paths)}, + value=self.values['image_idx'], + layout={'width': 'fit-content'}, + style={'description_width': 'initial'}) + return {'image_idx': w} + + def update(self, image_idx: ImgSpecT | None = None, **kwargs): + if image_idx is None: return + idx = self.ctx.normalize_idx(image_idx) + if idx is None: return + super().update(image_idx=idx, **kwargs) + + + def __init__(self, + ctx: OCRExperimentContext, /, + image_idx: ImgSpecT = 0, *, + out: W.Output | None=None): + idx = ctx.normalize_idx(image_idx) + assert idx is not None, f"Image {image_idx} not found in experiment context" + super().__init__(ctx, {'image_idx': idx}, out) + + +# %% experiments.ipynb 66 +class OCRContextVisor(ContextVisor): + ctx: OCRExperimentContext + + def update_output(self, /, image_idx: ImgIdT, **kwargs): + img_path = self.ctx.path_from_idx(image_idx) + display_image_grid([img_path], 1, 1) + + def update(self, image_idx: ImgSpecT | None = None, **kwargs): + if image_idx is None: return + idx = self.ctx.normalize_idx(image_idx) + if idx is None: return + super().update(image_idx=idx, **kwargs) + + def __init__(self, + ctx: OCRExperimentContext, /, + image_idx: ImgSpecT = 0, *, + out: W.Output | None=None): + super().__init__(ctx, {}, out, + ctxs={'image_idx': ImageSelector(ctx, image_idx, out=self.out)}) + + +# %% experiments.ipynb 84 +class OCRModel(Enum): + TESSERACT = 0 + IDEFICS = 1 + @staticmethod + def __display_names__() -> dict[str, OCRModel]: + return dict( + zip("Tesseract, Idefics".split(', '), + OCRModel)) + + +class ModelSelector(ContextVisor): + ctx: OCRExperimentContext + + def setup_controls(self): + options = self.models + w = W.Dropdown( + options=options, + value=self.values['model'], + layout={'width': 'fit-content'}, + style={'description_width': 'initial'}) + return {'model': w} + + def setup_ui(self): + ctls = self.controls + model_grp = W.HBox([ctls['model']]) + model_grp.add_class('model_grp') + comps = [] + for visor in self.comps.values(): + comps.append(visor.setup_ui()) + ui = W.HBox([*comps, model_grp]) + return ui + + def __init__(self, + exp_ctx: OCRExperimentContext, + ocr_model: OCRModel | None=OCRModel.TESSERACT, + ocr_models: dict[str, OCRModel] | None = None, + out: W.Output | None = None + ): + self.models: dict[str, OCRModel] = ocr_models or OCRModel.__display_names__() + super().__init__(exp_ctx, + {'model': ocr_model or OCRModel.TESSERACT}, + out=out or self.out)#, ctxs=[exp_visor]) + + +# %% experiments.ipynb 87 +class DisplayOptions(Enum): + BOXES = 0 + IMAGE = 1 + MASK = 2 + IMAGE_MASK = 3 + PAGE_DATA = 4 + GROUND_TRUTH = 5 + ALL = 6 + RESULTS = 7 + BEST_RESULTS = 8 + DATAFRAME = 9 + + @staticmethod + def __display_names__(): + return dict( + zip("Boxes, Image, Mask, Image & Mask, Page data, Ground truth, All, Results, " + "Best results, Dataframe".split(', '), + DisplayOptions)) + + +class ContentSelector(ContextVisor): + ctx: OCRExperimentContext + + def image_info(self, image_ctx: ImageContext): + img = image_ctx.base_image + (w, h), print_size_in, print_size_cm, required_dpi = image_ctx.image_info + format = PRINT_FORMATS['Modern Age'] + cprint( f"{'Width x Height':>30}: {w} x {h} pixels\n" + f"{'PIL Info DPI':>30}: {repr(img.info.get('dpi', None))}\n" + f"{'Print Size 300 DPI':>30}: {print_size_in[0]:.3f} x {print_size_in[1]:.3f} in" + f" / {print_size_cm[0]:.3f} x {print_size_cm[1]:.3f} cm\n" + f"Required DPI Modern Age format: {required_dpi:.3f} dpi " + f"({format[0]:.3f} x {format[1]:.3f} in)") + + + def display_content(self, image_ctx: ImageContext, display_option: DisplayOptions): + page_data = image_ctx.page_data + if display_option in (DisplayOptions.ALL, DisplayOptions.PAGE_DATA): + self.image_info(image_ctx) + RenderJSON(image_ctx.json_data, 350, 2).display() + if display_option in (DisplayOptions.ALL, DisplayOptions.GROUND_TRUTH): + cprint(image_ctx.gts) + if display_option == DisplayOptions.IMAGE: + display_image_grid([page_data.image_path], 1, 1) + if display_option == DisplayOptions.MASK: + display_image_grid([page_data.mask_path], 1, 1) + if display_option in (DisplayOptions.ALL, DisplayOptions.IMAGE_MASK): + display_image_grid([page_data.image_path, page_data.mask_path], 1, 2) + if display_option in (DisplayOptions.ALL, DisplayOptions.BOXES): + _, out_path = page_boxes(page_data) + display_image_grid([out_path], 1, 1) + + + def setup_controls(self): + options = self.display_options or {**DisplayOptions.__display_names__()} + display_option_wdgt = W.Dropdown( + options=options, + value=self.values['display_option'], + layout={'width': '120px'}, + style={'description_width': 'initial'}) + return {'display_option': display_option_wdgt} + + + def setup_ui(self): + ctls = self.controls + display_option_grp = W.HBox([ctls['display_option']]) + display_option_grp.add_class('display_option_grp') + comps = [] + for visor in self.comps.values(): + comps.append(visor.setup_ui()) + ui = W.HBox([*comps, display_option_grp]) + return ui + + + def __init__(self, + exp_ctx: OCRExperimentContext, + display_option: DisplayOptions | None=DisplayOptions.BOXES, + display_options: Mapping[str, DisplayOptions] | None = None, + out: W.Output | None = None + ): + self.display_options = display_options + super().__init__(exp_ctx, + {'display_option': display_option or DisplayOptions.BOXES}, + out=out or self.out)#, ctxs=[exp_visor]) + + +# %% experiments.ipynb 91 +class ImageContextVisor(ContextVisor): + ctx: ImageContext + # control_names: list[str] = ['display_option'] + + _css = """ + .display_option_grp { + background-color: lightblue; + } + """ + + def image_info(self): + content_selector = cast(ContentSelector, self.comp('display_option')) + content_selector.image_info(self.ctx) + + def update_output(self, + display_option: DisplayOptions | None = None, + image_idx: ImgIdT | None = None, + **kwargs): + content_selector = cast(ContentSelector, self.comp('display_option')) + if image_idx is not None and image_idx != self.ctx.image_idx: + ctx = ImageContext(self.ctx.exp, image_idx) + assert ctx is not None + self.ctx = ctx + display_option = content_selector.values['display_option'] + if display_option is None: + return + content_selector.display_content(self.ctx, display_option) + + def update(self, + display_option: DisplayOptions | None=None, + image_idx: ImgSpecT | None=None, + **kwargs): + if image_idx is not None: + if (idx := self.ctx.exp.normalize_idx(image_idx)) is not None: + kwargs['image_idx'] = idx + super().update(display_option=display_option, **kwargs) + + def __init__(self, + exp_ctx: OCRExperimentContext, + img_idx: ImgIdT | str | Path | ImageContext, + display_option: DisplayOptions=DisplayOptions.BOXES, + display_options: Mapping[str, DisplayOptions] | None = None, + out: W.Output | None = None + ): + if isinstance(img_idx, ImageContext): + ctx = img_idx + else: + assert exp_ctx is not None, "exp_ctx must be provided if img_idx is not an ImageContext" + ctx = ImageContext(exp_ctx, img_idx) + assert ctx is not None, f"Image {img_idx} not found in experiment context" + if display_options is None: + display_options = {**DisplayOptions.__display_names__()} + del display_options['Results'] + out = out or self.out + content_selector = ContentSelector(exp_ctx, + display_option=display_option, display_options=display_options, out=out) + image_selector = ImageSelector(exp_ctx, ctx.image_idx, out=out) + super().__init__(ctx, {}, out=out, + ctxs={'image_idx': image_selector, 'display_option': content_selector}) + + +# %% experiments.ipynb 104 +def trimmed_mean(data, trim_percent): + sorted_data = np.sort(data) + n = len(data) + trim_count = int(trim_percent * n) + trimmed_data = sorted_data[trim_count:-trim_count] + return np.mean(trimmed_data) + +def mad_based_outlier(points, threshold=3.5): + median = np.median(points) + diff = np.abs(points - median) + mad = np.median(diff) + modified_z_score = 0.6745 * diff / mad + return points[modified_z_score < threshold] + +def iqr_outlier_removal(data): + q1 = np.percentile(data, 25) + q3 = np.percentile(data, 75) + iqr = q3 - q1 + lower_bound = q1 - 1.5 * iqr + upper_bound = q3 + 1.5 * iqr + return data[(data >= lower_bound) & (data <= upper_bound)] + + +# %% experiments.ipynb 105 +@dataclasses.dataclass +class Experiment: + ctx: ExperimentContext + + +@dataclasses.dataclass +class ExperimentOCR(Experiment): + ctx: ImageContext + ocr_model: str + + @property + def img_ctx(self): return self.ctx + @property + def ctxs(self): + img_ctx = self.img_ctx + return cast(OCRExperimentContext, img_ctx.exp), img_ctx + + @classmethod + def file_path_of(cls, page_data: st.PageData, ocr_model: str): + return f"{Path(page_data.original_path).stem}_{ocr_model}.json" + + def file_path(self): + img_ctx = self.img_ctx + return type(self).file_path_of(img_ctx.page_data, self.ocr_model) + + def to_dict(self): + "JSON serializable dict of the experiment" + img_ctx = self.img_ctx + img_idx = img_ctx.image_idx + results = results_to_dict(self.results()) + return { + 'image_name': img_ctx.image_name, + 'ocr_model': self.ocr_model, + 'results': results, + } + + def to_json(self, out_dir: Path | None = None): + img_ctx = self.img_ctx + fp = (out_dir or img_ctx.cache_dir_image) / self.file_path() + data = self.to_dict() + with open(fp, 'w') as f: + json.dump(data, f, indent=2) + return fp, data + + @classmethod + def from_json(cls, experiment: OCRExperimentContext, json_path: Path) -> Self: + try: + with open(json_path, 'r') as f: + data = json.load(f) + except Exception as e: + logger.error(f"Error loading {json_path}: {e}") + raise e + ocr_model = data['ocr_model'] + img_ctx = ImageContext(experiment, data['image_name']) + results: ResultSetDefault = dict_to_results( + img_ctx.image_idx, + data['results'], + result_factory=experiment._result_from) + experiment.update_results(ocr_model, img_ctx.image_idx, results) + return cls(img_ctx, ocr_model) + + @classmethod + def from_image(cls, + ctx: OCRExperimentContext, + ocr_model: str, + image_idx: ImgSpecT): + idx = cast(ImgIdT, ctx.normalize_idx(image_idx)) + img_ctx = ImageContext(ctx, idx) + if img_ctx is None: + raise ValueError(f"Image {image_idx} not found in experiment context") + fp = img_ctx.cache_dir / cls.file_path_of(img_ctx.page_data, ocr_model) + if fp.exists(): + return cast(Self, cls.from_json(cast(OCRExperimentContext, img_ctx.exp), fp)) + return cls(img_ctx, ocr_model) + + @classmethod + def from_method(cls, + ctx: OCRExperimentContext, + ocr_model: str, + image_idx: ImgIdT | str | Path, + method: CropMethod): + experiment = cls.from_image(ctx, ocr_model, image_idx) + if experiment is None: + return None + return experiment.method_experiment(method) + + @classmethod + def saved_experiment(cls, + ctx: OCRExperimentContext, ocr_model: str, image_idx: ImgIdT | str | Path): + idx = ctx.normalize_idx(image_idx) + if idx is None: + logger.warning(f"Image {image_idx} not found in experiment context") + return None + return cls.from_image(ctx, ocr_model, idx) + + @classmethod + def saved_experiments(cls, ctx: OCRExperimentContext, ocr_model: str) -> list[Self]: + return [exp for i in range(len(ctx.image_paths)) + if (exp := cls.from_image(ctx, ocr_model, i)) is not None] + + + def result(self, box_idx: BoxIdT, method: CropMethod, ocr: bool=True, rebuild: bool=False): + ctx, img_ctx = self.ctxs + return ctx.result(self.ocr_model, img_ctx.image_idx, box_idx, method, ocr, rebuild) + + def results(self): + ctx, img_ctx = self.ctxs + return cast(ResultSet, ctx.results(self.ocr_model, img_ctx.image_idx)) + + def has_run(self): + "at least one method has run" + img_ctx = self.img_ctx + return len(self.results()) == len(img_ctx.page_data.boxes) + + def best_results(self): + img_ctx = self.img_ctx + results = self.results() + if len(results) < len(img_ctx.page_data.boxes): # at least one method has run + return None + best = [] + for box_idx in results: + methods = results[box_idx] + best_method = max(methods, key=lambda m: methods[m].acc) # type: ignore + best.append((best_method, methods[best_method])) + return best + + def save_results_as_ground_truth(self, overwrite=False): + img_ctx = self.img_ctx + gts_path = ground_truth_path(img_ctx.page_data) + if overwrite or not gts_path.exists(): + best_results = self.best_results() + if best_results: + tt = [r.ocr for m,r in best_results] + gts_path.write_text('\n'.join(tt), encoding="utf-8") + img_ctx.setup_ground_truth() + logger.info(f"Ground truth data saved successfully to {gts_path}") + return True + else: + logger.info("No best results available to save.") + return False + else: + return False + + @property + def experiments(self): + if not hasattr(self, '_experiments'): + self._experiments = {} + return self._experiments + def method_experiment(self, method: CropMethod) -> ExperimentOCRMethod: + if method not in self.experiments: + self.experiments[method] = ExperimentOCRMethod(self, method) + return self.experiments[method] + + + def to_dataframe(self): + "Dataframe with crop methods as columns and box ids as rows" + methods = list(CropMethod.__members__.values()) + experiments = [self.method_experiment(m) for m in methods] + accuracies = [[result.acc for result in exp.results()] for exp in experiments] + # transpose accuracies + accuracies = list(zip(*accuracies)) + return pd.DataFrame(accuracies, columns=CropMethod.__display_names__()) + + def plot_accuracies(self, + methods: list[CropMethod] | None = None, + ): + "Plots a horizontal bar chart of the accuracies for a list of method experiments." + methods = methods or list(CropMethod.__members__.values()) + experiments = [self.method_experiment(m) for m in methods] + if not experiments: return + + ctx, img_ctx = self.ctxs + page_data = img_ctx.page_data + model = self.ocr_model + accuracies = [[result.acc for result in exp.results()] for exp in experiments] + accuracies = [np.mean(a) for a in accuracies] + # accuracies = [np.mean([result.acc for result in exp.results()]) for exp in experiments] + + _, ax = plt.subplots(figsize=(10, 5)) + + # Normalize the accuracies for color mapping + norm = plt.Normalize(min(accuracies), max(accuracies)) + # Color map from red to green + cmap = plt.get_cmap('RdYlGn') + colors = cmap(norm(accuracies)) + + ax.barh([m.value for m in methods], accuracies, color=colors) + + ax.set_xscale('log') # Set the x-axis to a logarithmic scale + ax.set_xlabel('Average Accuracy (log scale)', fontsize=12, fontweight='bold') + + ax.set_ylabel('Method', fontsize=12, fontweight='bold') + ax.set_yticks(range(len(methods))) + ax.set_yticklabels([f'{method.value} ({acc:.2f})' + for method, acc in zip(methods, accuracies)], fontsize=12) + max_acc_index = np.argmax(accuracies) + ax.get_yticklabels()[max_acc_index].set(color='blue', fontweight='bold') + + title_text = (f"{page_data.original_path} - OCR model: {model}") + ax.set_title(title_text, fontsize=12, fontweight='bold') + + plt.tight_layout() + plt.show() + + + def summary_box(self, box_idx: int): + results: list[tuple[CropMethod, ResultOCR]] = [] + pb = tqdm(CropMethod.__members__.values(), leave=False, desc=f"Box #{box_idx+1}") + for m in pb: + r = cast(ResultOCR, self.result(box_idx, m)) + results.append((m, r)) + methods, images, ocrs, accs = zip( + *map( + lambda t: (t[0].value, t[1].cache_image(), t[1].diff_tagged(), acc_as_html(t[1].acc)), + results)) + display_columns([methods, images, accs, ocrs], + headers=["Method", f"Box #{box_idx+1}", "Accuracy", "OCR"]) + + + def summary_method(self, method: CropMethod): + results = self.method_experiment(method).results() + methods, images, ocrs, accs = zip( + *map( + lambda r: (r.block_idx+1, r.cache_image(), r.diff_tagged(), acc_as_html(r.acc)), + results)) + display_columns([methods, images, accs, ocrs], + headers=["Box #", "Box", "Accuracy", f"{method.value} OCR"]) + + + def display(self): + out = [] + for method in CropMethod: + out.append(f"---------- {method.value} ----------") + results = self.method_experiment(method).results() + out.extend(results) + out.append('\n') + cprint(*out, soft_wrap=True) + + + def reset(self, box_idx: int | None = None, method: CropMethod | None = None): + ctx, img_ctx = self.ctxs + ctx.reset_results(None, img_ctx.image_idx, box_idx, method) + + def perform_methods(self, + methods: CropMethod | list[CropMethod] | None = None, + box_idxs: BoxIdT | list[BoxIdT] | None = None, + rebuild: bool = False, + plot_acc: bool = False + ): + if methods is None: + methods = [*CropMethod.__members__.values()] + elif isinstance(methods, CropMethod): + methods = [methods] + if rebuild: + _methods = tqdm(methods, desc="Methods") + else: + _methods = methods + for method in _methods: + method_exp = self.method_experiment(method) + if method_exp: + if rebuild: + method_exp(box_idxs, rebuild=rebuild) + if plot_acc: + self.plot_accuracies() + + def __call__(self, + box_idxs: BoxIdT | list[BoxIdT] | None = None, + methods: CropMethod | list[CropMethod] | None = None, + save: bool = True, + display=False, + rebuild: bool=False, + save_as_ground_truth=False): + self.perform_methods(methods, box_idxs, rebuild=rebuild) + if save_as_ground_truth: + self.save_results_as_ground_truth(overwrite=True) + if save: + self.to_json() + if display: + self.display() + + +@dataclasses.dataclass +class ExperimentOCRMethod: + ctx: ExperimentOCR + method: CropMethod + + @property + def exp_ctx(self): return self.ctx + @property + def img_ctx(self): return self.ctx.ctx + @property + def ctxs(self): + img_ctx = self.img_ctx + return cast(OCRExperimentContext, img_ctx.exp), img_ctx, self.ctx + + def result(self, box_idx: BoxIdT, ocr: bool=True, rebuild: bool=False) -> ResultOCR | None: + ctx, img_ctx, exp_ctx = self.ctxs + return ctx.result(exp_ctx.ocr_model, img_ctx.image_idx, box_idx, self.method, ocr, rebuild) + + def results(self, + box_idxs: BoxIdT | list[BoxIdT] | None = None, + ocr: bool=True, rebuild: bool=False) -> list[ResultOCR]: + ctx, img_ctx, exp_ctx = self.ctxs + if box_idxs is None: + box_idxs = list(range(len(img_ctx.boxes))) + elif isinstance(box_idxs, int): + box_idxs = [box_idxs] + model = exp_ctx.ocr_model + results = ctx.method_results(model, img_ctx.image_idx, self.method) + results = {i:results[i] if i in results else None for i in box_idxs} + pb = rebuild or not results or any(r is None for r in results.values()) + if pb and len(results) > 2: + progress_bar = tqdm(list(results.keys()), desc=f"{self.method.value} - {model}") + else: + progress_bar = list(results.keys()) + results = [] + for i in progress_bar: + results.append(self.result(i, ocr, rebuild=rebuild)) + return results + + + def get_results_html(self, + box_idxs: BoxIdT | list[BoxIdT] | None = None, + max_image_width: int | None = None): + _, img_ctx, exp_ctx = self.ctxs + results: list[ResultOCR] = self.results(box_idxs) + accs = np.array([r.acc for r in results]) + mean_accuracy = np.mean(accs) + mean_trimmed = trimmed_mean(accs, 0.1) + # filtered_data = mad_based_outlier(accs) + # mean_mad = np.mean(filtered_data) + # filtered_data = iqr_outlier_removal(accs) + # mean_iqr = np.mean(filtered_data) + + descriptions, images, ocrs, accs = zip(*map( + lambda r: ( + r.block_idx+1, + r.cache_image(), + r.diff_tagged(), + acc_as_html(r.acc) + ), results)) + non_breakin_space = u'\u00A0' + tmpl = "{}" + padded_s = lambda s,n: tmpl.format(s.rjust(n)) + acc_fmt = f"{mean_accuracy:.2f}/{mean_trimmed:.2f}" + w, h = img_ctx.base_image.size + dim, _dpi = size(w, h), dpi(w, h) + dim_fmt = f"{w}x{h} px: {dim[0]:.2f} x {dim[1]:.2f} in @ {_dpi:.2f} dpi" + return '\n
\n'.join([ + ("
" + f"{padded_s('Page', 24)}: {img_ctx.page_data.original_path}
" + f"{padded_s('Size', 24)}: {dim_fmt}
" + f"{padded_s('Model', 24)}: {exp_ctx.ocr_model}
" + f"{padded_s('Crop Method', 24)}: {self.method.value}
" + f"{padded_s('Accuracy Mean/Trimmed', 24)}: {acc_fmt}" + "
"), + get_columns_html( + [descriptions, images, accs, ocrs], + max_image_width, + headers=["Box #", "Image", "Accuracy", "OCR"]), + ]) + + def display(self, + box_idxs: BoxIdT | list[BoxIdT] | None = None, max_image_width: int | None = None): + display(HTML(self.get_results_html(box_idxs, max_image_width))) + + + def summary(self): + results = self.results() + methods, images, ocrs, accs = zip( + *map( + lambda r: (r.block_idx+1, r.cache_image(), r.diff_tagged(), acc_as_html(r.acc)), + results)) + display_columns([methods, images, accs, ocrs], + headers=["Box #", "Box", "Accuracy", f"{self.method.value} OCR"]) + + + def reset(self): + _, _, exp_ctx = self.ctxs + exp_ctx.reset(method=self.method) + + def __call__(self, box_idxs: BoxIdT | list[BoxIdT] | None = None, display=False, rebuild=False): + if isinstance(box_idxs, int): + result = self.result(cast(BoxIdT, box_idxs), rebuild=rebuild) + if result is not None and display: + result.display() + else: + results = self.results(box_idxs, rebuild=rebuild) + if results and display: + self.display(box_idxs) + + +# %% experiments.ipynb 141 +class ResultVisor(ContextVisor): + ctx: ExperimentOCR + control_names: list[str] = ['all_boxes', 'box_idx', 'all_methods', 'method'] + + _css = """ + .box_grp { + background-color: aliceblue; + } + .method_grp { + background-color: #ededed; + } + """ + + def best_results(self): + ll = self.ctx.best_results() + if ll: + cprint([(m.value, f"{r.acc:.3f}", r.ocr) for m,r in ll]) + + def pd_to_html(self): + df = self.ctx.to_dataframe() + # set float precision + df = df.round(3) + # display floats with 3 decimal digits + df = df.applymap(lambda x: f"{x:.3f}") + # highlight max value in each row + stl = df.style.highlight_max(axis=0) + display(HTML(stl.to_html())) + + def update_output(self, **kwargs): + all_boxes: bool = self.values['all_boxes'] + box_idx: int = self.values['box_idx'] + all_methods: bool = self.values['all_methods'] + method: CropMethod = self.values['method'] + + # cprint(f"all_boxes: {all_boxes}, box_idx: {box_idx}, all_methods: {all_methods}, method: {method}") + + if all_boxes and all_methods: + self.ctx.plot_accuracies() + elif all_boxes: + self.ctx.summary_method(method) + elif all_methods: + self.ctx.summary_box(box_idx) + else: + result = self.ctx.result(box_idx, method) + if result is not None: + result.display() + + def setup_controls(self): + _, img_ctx = self.ctx.ctxs + values = self.values + box_wdgt = W.BoundedIntText( + value=values['box_idx'], min=0, max=len(img_ctx.boxes)-1, step=1, + disabled=values['all_boxes'], + layout={'width': '50px'}, + style={'description_width': 'initial'}) + methods_wdgt = W.Dropdown( + options=CropMethod.__display_names__(), + value=values['method'], + layout={'width': '150px'}, + style={'description_width': 'initial'}) + all_boxes_wdgt = W.Checkbox(label='All', value=values['all_boxes'], + description="all", + layout={'width': 'initial'}, + style={'description_width': '0px'}) + all_methods_wdgt = W.Checkbox(label='All', value=values['all_methods'], + description="all", + layout={'width': 'initial'}, + style={'description_width': '0px'}) + return {'all_boxes': all_boxes_wdgt, 'box_idx': box_wdgt, + 'all_methods': all_methods_wdgt, 'method': methods_wdgt} + + def setup_ui(self): + ctls = self.controls + _, img_ctx = self.ctx.ctxs + box_label = W.Label( + value=f"Box # (of {len(img_ctx.boxes)}):", + layout={'width': 'initial', 'padding': '0px 0px 0px 10px'}) + method_label = W.Label(value='Method:', layout={'width': 'initial', 'padding': '0px 0px 0px 10px'}) + + box_grp = W.HBox([box_label, ctls['all_boxes'], ctls['box_idx']]) + box_grp.add_class('box_grp') + method_grp = W.HBox([method_label, ctls['all_methods'], ctls['method']]) + method_grp.add_class('method_grp') + + return W.HBox([box_grp, method_grp]) + + def __init__(self, + ctx: OCRExperimentContext | ExperimentOCR, + img_idx: int | str | Path | None = None, + all_boxes: bool = False, + box_idx: int = 0, + all_methods: bool = False, + method: CropMethod=CropMethod.INITIAL_BOX, + out: W.Output | None = None, + ): + if isinstance(ctx, OCRExperimentContext): + assert img_idx is not None, "img_idx must be provided if ctx is an ExperimentContext" + exp = ExperimentOCR.from_image(ctx, 'Tesseract', img_idx) + if not exp: + raise ValueError(f"Image {img_idx} not found in experiment context") + ctx = exp + else: + if not isinstance(ctx, ExperimentOCR): + raise ValueError("ctx must be an ExperimentOCR or OCRExperimentContext") + + super().__init__(ctx, {'all_boxes': all_boxes, 'box_idx': box_idx, + 'all_methods': all_methods, 'method': method}, out=out or self.out) + + +# %% experiments.ipynb 144 +class ExperimentVisor(ContextVisor): + ctx: ExperimentOCR + + def update_output(self, + image_idx: int | None = None, + **kwargs): + exp_ctx, img_ctx = self.ctx.ctxs + if image_idx is not None and image_idx != img_ctx.image_idx: + ctx = ImageContext(exp_ctx, image_idx) + assert ctx is not None + self.ctx.ctx = ctx + result_visor = self.comp('result_visor') + if result_visor is not None: + result_visor.update_output(**kwargs) + + def __init__(self, + ctx: OCRExperimentContext | ExperimentOCR, + img_idx: int | str | Path | None = None, + all_boxes: bool = False, + box_idx: int = 0, + all_methods: bool = False, + method: CropMethod=CropMethod.INITIAL_BOX, + out: W.Output | None = None, + ): + if isinstance(ctx, OCRExperimentContext): + assert img_idx is not None, "img_idx must be provided if ctx is an ExperimentContext" + exp = ExperimentOCR.from_image(ctx, 'Tesseract', img_idx) + if not exp: + raise ValueError(f"Image {img_idx} not found in experiment context") + ctx = exp + else: + if not issubclass(type(ctx), ExperimentOCR): + raise ValueError("ctx must be an ExperimentOCR or OCRExperimentContext") + + exp_ctx, img_ctx = ctx.ctxs + out = out or self.out + image_selector = ImageSelector(exp_ctx, image_idx=img_ctx.image_idx, out=out) + result_visor = ResultVisor(ctx, out=out, + all_boxes=all_boxes, box_idx=box_idx, all_methods=all_methods, method=method) + + super().__init__(ctx, {}, out=out, + ctxs={'image_selector': image_selector, 'result_visor': result_visor}, + hdlrs={'display_option': result_visor} + ) + + +# %% experiments.ipynb 198 +class ExperimentsVisor(ContextVisor): + ctx: OCRExperimentContext + + def update_output(self, + model: OCRModel | None = None, + image_idx: ImgIdT | None = None, + display_option: DisplayOptions | None = None, + **kwargs): + model_selector, image_selector, content_selector, result_visor = self._comps() + if model is not None: + exp_ctx = result_visor.ctx + exp_ctx.ocr_model = list(model_selector.models.keys())[model.value] + result_visor.ctx = exp_ctx + if image_idx is not None: + img_ctx = ImageContext(self.ctx, image_idx) + result_visor.ctx.ctx = img_ctx + display_option = content_selector.values['display_option'] + if display_option is not None and display_option != DisplayOptions.RESULTS: + result_visor.hide() + if display_option == DisplayOptions.BEST_RESULTS: + result_visor.best_results() + elif display_option == DisplayOptions.DATAFRAME: + result_visor.pd_to_html() + else: + content_selector.display_content(image_selector.image_ctx, display_option) + else: + result_visor.show() + result_visor.update_output(**kwargs) + + def _comps(self): + cc = self.comps + msel: ModelSelector = cc['model_selector'] # type: ignore + isel: ImageSelector = cc['image_selector'] # type: ignore + cs: ContentSelector = cc['content_selector'] # type: ignore + rv: ResultVisor = cc['result_visor'] # type: ignore + return msel, isel, cs, rv + + def setup_ui(self): + ctls = self.controls.values() + msw, isw, csw, rvw = [_.w for _ in self._comps()] + return W.VBox([W.HBox([msw, isw, csw, *ctls]), rvw,]) + + def __init__(self, + ctx: OCRExperimentContext, + image_idx: ImgIdT | str | Path = 0, + ocr_model: OCRModel = OCRModel.TESSERACT, + display_option: DisplayOptions = DisplayOptions.RESULTS, + all_boxes: bool = False, + box_idx: int = 0, + all_methods: bool = False, + method: CropMethod=CropMethod.INITIAL_BOX, + ocr_models: dict[str, OCRModel] = {'Tesseract': OCRModel.TESSERACT}, + out: W.Output | None = None, + ): + if not isinstance(ctx, OCRExperimentContext): + raise ValueError("ctx must be an OCRExperimentContext") + exp = ExperimentOCR.from_image(ctx, 'Tesseract', image_idx) + if not exp: + raise ValueError(f"Image {image_idx} not found in experiment context") + + out = out or self.out + model_selector = ModelSelector(ctx, ocr_model=ocr_model, + ocr_models=ocr_models, out=out) + image_selector = ImageSelector(ctx, image_idx=image_idx, out=out) + content_selector = ContentSelector(ctx, display_option=display_option, out=out) + result_visor = ResultVisor(exp, out=out, + all_boxes=all_boxes, box_idx=box_idx, all_methods=all_methods, method=method) + + super().__init__(ctx, {}, out=out, + ctxs={'model_selector': model_selector, 'image_selector': image_selector, 'content_selector': content_selector, + 'result_visor': result_visor} + ) + diff --git a/_testbed/helpers.ipynb b/_testbed/helpers.ipynb new file mode 100644 index 00000000..32a6fa6d --- /dev/null +++ b/_testbed/helpers.ipynb @@ -0,0 +1,835 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp helpers" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "# %reload_ext autoreload\n", + "# %autoreload 0\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# install (Colab)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# try: \n", + "# import fastcore as FC\n", + "# except ImportError: \n", + "# !pip install -q fastcore\n", + "# try:\n", + "# import rich\n", + "# except ImportError:\n", + "# !pip install -q rich\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install -q git+https://github.com/civvic/PanelCleaner.git@basic-tesseract" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Testing `Tesseract` OCR for Comics\n", + "> Accuracy Enhancements for OCR in `PanelCleaner`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Prologue" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "from __future__ import annotations\n", + "\n", + "import base64\n", + "import json\n", + "import re\n", + "import sys\n", + "import uuid\n", + "from importlib import resources\n", + "from io import BytesIO\n", + "from pathlib import Path\n", + "from typing import Any\n", + "from typing import Iterable\n", + "from typing import Mapping\n", + "from typing import Sequence\n", + "\n", + "import pcleaner.data\n", + "import pcleaner.structures as st\n", + "from IPython.display import clear_output\n", + "from IPython.display import display\n", + "from IPython.display import HTML\n", + "from PIL import Image\n", + "from PIL import ImageDraw\n", + "from PIL import ImageFont\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "from collections import defaultdict\n", + "\n", + "import fastcore.xtras # patch Path with some utils\n", + "import ipywidgets as W\n", + "import rich\n", + "from fastcore.test import * # type: ignore\n", + "from rich.console import Console\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Helpers" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# pretty print by default\n", + "# %load_ext rich" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "console = Console(width=104, tab_size=4, force_jupyter=True)\n", + "cprint = console.print\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## dict helpers: _pops_\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "_all_ = ['_pops_', '_pops_values_', '_gets_']\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def _pops_(d: dict, ks: Iterable) -> dict: \n", + " \"Pops `ks` keys from `d` and returns them in a dict. Note: `d` is changed in-place.\"\n", + " return {k:d.pop(k) for k in ks if k in d}\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(_pops_({'a': 1, 'b': 2, 'c': 3}, ['a', 'b']), {'a': 1, 'b': 2})\n", + "test_eq(_pops_({'a': 1, 'b': 2, 'c': 3}, ['d']), {})\n", + "test_eq(_pops_({'a': 1, 'b': 2, 'c': 3}, ['a', 'c', 'd']), {'a': 1, 'c': 3})\n", + "test_eq(_pops_({}, ['a']), {})\n", + "test_eq(_pops_({'a': 1}, ['a', 'a']), {'a': 1})\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def _pops_values_(d: dict, ks: Iterable) -> tuple:\n", + " \"Pops `ks` keys from `d` and returns them as a tuple. Note: `d` is changed in-place.\"\n", + " return tuple(d.pop(k, None) for k in ks)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(_pops_values_({'a': 1, 'b': 2, 'c': 3}, ['a', 'b']), (1, 2))\n", + "test_eq(_pops_values_({'a': 1, 'b': 2, 'c': 3}, ['d']), (None,))\n", + "test_eq(_pops_values_({'a': 1, 'b': 2, 'c': 3}, ['a', 'c', 'd']), (1, 3, None))\n", + "test_eq(_pops_values_({}, ['a']), (None,))\n", + "test_eq(_pops_values_({'a': 1}, ['a', 'a']), (1, None))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def _gets_(d: Mapping[str, Any], ks: Iterable):\n", + " \"Fetches values from a mapping for a given list of keys, returning `None` for missing keys.\"\n", + " return (d.get(k, None) for k in ks)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "test_eq(_gets_({'a': 1, 'b': 2}, ('a', 'c', 'b')), [1, None, 2])\n", + "test_eq(_gets_({'a': 1, 'b': 2}, ()), [])\n", + "a, b = _gets_({'a': 1, 'b': 2}, ('b', 'a'))\n", + "test_eq((a, b), (2, 1))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## cleanupwidget\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "#| exporti\n", + "def _get_globals(mod: str):\n", + " if hasattr(sys, '_getframe'):\n", + " glb = sys._getframe(2).f_globals\n", + " else:\n", + " glb = sys.modules[mod].__dict__\n", + " return glb\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "# _all_ = ['_get_globals']\n", + "def _gtest():\n", + " return _get_globals(__name__)\n", + "g1 = _gtest()\n", + "g2 = globals()\n", + "test_eq(g1, g2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "def cleanupwidgets(*ws, mod: str|None=None, clear=True):\n", + " glb = _get_globals(mod or __name__)\n", + " if clear: clear_output(wait=True)\n", + " for w in ws:\n", + " _w = glb.get(w) if isinstance(w, str) else w\n", + " if _w:\n", + " try: _w.close() # type: ignore\n", + " except: pass\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "_b = W.Button()\n", + "test_ne(_b.comm, None)\n", + "cleanupwidgets('_b')\n", + "test_is(_b.comm, None)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Collapsable JSON in a notebook cell" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "class RenderJSON(object):\n", + " def __init__(self, json_data, max_height=200, init_level=0):\n", + " if isinstance(json_data, (Sequence, Mapping)):\n", + " s = json.dumps(json_data)\n", + " elif hasattr(json_data, 'to_dict'):\n", + " s = json.dumps(json_data.to_dict())\n", + " elif hasattr(json_data, 'to_json'):\n", + " s = json_data.to_json()\n", + " else:\n", + " s = json_data\n", + " self.json_str = s\n", + " self.uuid = str(uuid.uuid4())\n", + " self.max_height = max_height\n", + " self.init_level = init_level\n", + "\n", + " def display(self):\n", + " html_content = f\"\"\"\n", + "
\n", + "
\n", + " \n", + "
\n", + " \"\"\"\n", + " display(HTML(html_content))\n", + "\n", + " def _ipython_display_(self):\n", + " self.display()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "json_data = {\n", + " \"name\": \"Petronila\",\n", + " \"age\": 28,\n", + " \"interests\": [\"reading\", \"cycling\", \"technology\"],\n", + " \"education\": {\n", + " \"bachelor\": \"Computer Science\",\n", + " \"master\": \"Data Science\",\n", + " \"phd\": \"Not enrolled\"\n", + " }\n", + "}\n", + "\n", + "RenderJSON(json_data, init_level=1).display()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualize boxes on the page image\n", + "> adapted from `PageData.visualize` but returns the image instead of saving it." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "def page_boxes(self: st.PageData, out_dir: Path | None = None) -> tuple[Image.Image, Path]:\n", + " \"\"\"\n", + " Visualize the boxes on an image.\n", + " Typically, this would be used to check where on the original image the\n", + " boxes are located.\n", + "\n", + " :param image_path: The path to the image to visualize the boxes on.\n", + " \"\"\"\n", + " image_path = Path(self.image_path)\n", + " image = Image.open(image_path)\n", + " draw = ImageDraw.Draw(image)\n", + " data_path = resources.files(pcleaner.data)\n", + " font_path = str(data_path / \"LiberationSans-Regular.ttf\")\n", + " # Figure out the optimal font size based on the image size. E.g. 30 for a 1600px image.\n", + " font_size = int(image.size[0] / 50) + 5\n", + "\n", + " for index, box in enumerate(self.boxes):\n", + " draw.rectangle(box.as_tuple, outline=\"green\")\n", + " # Draw the box number, with a white background, respecting font size.\n", + " draw.text(\n", + " (box.x1 + 4, box.y1),\n", + " str(index + 1),\n", + " fill=\"green\",\n", + " font=ImageFont.truetype(font_path, font_size),\n", + " stroke_fill=\"white\",\n", + " stroke_width=3,\n", + " )\n", + "\n", + " for box in self.extended_boxes:\n", + " draw.rectangle(box.as_tuple, outline=\"red\")\n", + " for box in self.merged_extended_boxes:\n", + " draw.rectangle(box.as_tuple, outline=\"purple\")\n", + " for box in self.reference_boxes:\n", + " draw.rectangle(box.as_tuple, outline=\"blue\")\n", + "\n", + " # Save the image.\n", + " extension = \"_boxes\"\n", + " out_path = image_path.with_stem(image_path.stem + extension)\n", + " if out_dir is not None:\n", + " out_dir.mkdir(parents=True, exist_ok=True)\n", + " out_path = out_dir / image_path.name\n", + " image.save(out_path)\n", + "\n", + " return image, out_path" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Simple crop" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "def crop_box(box: st.Box, image: Image.Image) -> Image.Image:\n", + " return image.crop(box.as_tuple)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Print size & resolution" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "PRINT_FORMATS = {\n", + " 'Golden Age': (7.75, 10.5), # (1930s-40s) \n", + " 'Siver Age': (7, 10.375), # (1950s-60s)\n", + " 'Modern Age': (6.625,10.25), # North American comic books\n", + " 'Magazine': (8.5, 11), \n", + " 'Digest': (5.5, 8.5), \n", + " 'Manga': (5.0, 7.5),\n", + "}\n", + "\n", + "\n", + "def size(w: int, h: int, unit: str = 'in', dpi: float = 300.) -> tuple:\n", + " \"\"\"\n", + " Calculate the print size of an image in inches or centimeters.\n", + "\n", + " Args:\n", + " w (int): Width of the image in pixels.\n", + " h (int): Height of the image in pixels.\n", + " unit (str): Unit of measurement ('in' for inches, 'cm' for centimeters).\n", + " dpi (float): Dots per inch (resolution).\n", + "\n", + " Returns:\n", + " tuple: Width and height of the image in the specified unit.\n", + " \"\"\"\n", + " if unit == 'cm':\n", + " return (w / dpi * 2.54, h / dpi * 2.54)\n", + " else: # default to inches\n", + " return (w / dpi, h / dpi)\n", + "\n", + "\n", + "def dpi(w: int, h: int, print_format: str = 'Modern Age') -> float:\n", + " \"\"\"\n", + " Calculate the dpi (dots per inch) needed to print an image at a specified format size.\n", + "\n", + " Args:\n", + " w (int): Width of the image in pixels.\n", + " h (int): Height of the image in pixels.\n", + " print_format (str): Print format as defined in the formats dictionary.\n", + "\n", + " Returns:\n", + " float: Required dpi to achieve the desired print format size.\n", + " \"\"\"\n", + " # Default to 'Modern Age' if format not found\n", + " format_size = PRINT_FORMATS.get(print_format, PRINT_FORMATS['Modern Age'])\n", + " width_inch, height_inch = format_size\n", + " dpi_w = w / width_inch\n", + " dpi_h = h / height_inch\n", + " return (dpi_w + dpi_h) / 2 # Average dpi for width and height\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Show images and texts on HTML tables" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "def get_image_html(image: Image.Image | Path | str, max_width: int | None = None):\n", + " \"\"\"\n", + " Converts a PIL image to an HTML image tag containing the image as a base64 blob.\n", + "\n", + " :param image: A PIL Image object.\n", + " :param max_size: A PIL Image object.\n", + " :return: A string containing an HTML tag with the image.\n", + " \"\"\"\n", + " style = f' style=\"max-width: {max_width}px;\"' if max_width is not None else ''\n", + " if isinstance(image, (Path, str)):\n", + " return f''\n", + " else:\n", + " buffered = BytesIO()\n", + " image.save(buffered, format='PNG')\n", + " img_str = base64.b64encode(buffered.getvalue()).decode()\n", + " return f''\n", + "\n", + "\n", + "def get_columns_html(\n", + " columns: list[list], max_image_width: int | None = None, headers: list[str] | None = None\n", + "):\n", + " if not all(len(col) == len(columns[0]) for col in columns):\n", + " raise ValueError(\"All columns must have the same length.\")\n", + "\n", + " # Calculate the maximum width of images in each column\n", + " max_widths = []\n", + " for col_index in range(len(columns)):\n", + " max_col_width = 0\n", + " for item in columns[col_index]:\n", + " if isinstance(item, (Image.Image, Path)):\n", + " if isinstance(item, (Path, str)):\n", + " item = Image.open(item)\n", + " width, _ = item.size\n", + " max_col_width = max(max_col_width, width)\n", + " if max_col_width > 0:\n", + " max_widths.append(\n", + " f\"{min(max_col_width, max_image_width)}px\"\n", + " if max_image_width is not None else \n", + " f\"{max_col_width}px\"\n", + " )\n", + " else:\n", + " max_widths.append('auto')\n", + "\n", + " html_str = \"\"\n", + "\n", + " # Apply calculated column widths using and elements\n", + " html_str += \"\"\n", + " for width in max_widths:\n", + " html_str += f\"\"\n", + " html_str += \"\"\n", + "\n", + " if headers:\n", + " if len(headers) != len(columns):\n", + " raise ValueError(\"Headers list must match the number of columns.\")\n", + " html_str += (\n", + " \"\"\n", + " + \"\".join(\n", + " f\"\"\n", + " for header in headers\n", + " )\n", + " + \"\"\n", + " )\n", + "\n", + " for row_items in zip(*columns):\n", + " html_str += \"\"\n", + " for i, item in enumerate(row_items):\n", + " if isinstance(item, (Image.Image, Path)):\n", + " img_html = get_image_html(item, max_width=max_image_width)\n", + " html_str += f\"\"\n", + " else: # Assume the item is a string\n", + " style = \"font-weight: bold;\" if i == 0 else \"\"\n", + " html_str += f\"\"\n", + " html_str += \"\"\n", + "\n", + " html_str += \"
{header}
{img_html}{item}
\"\n", + " return html_str\n", + "\n", + "\n", + "def display_columns(\n", + " columns: list[list], max_image_width: int | None = None, headers: list[str] | None = None\n", + "):\n", + " \"\"\"\n", + " Displays a table with any combination of columns, which can be lists of strings or lists \n", + " of PIL Image objects, within a Jupyter notebook cell.\n", + "\n", + " :param columns: A list of lists, where each sublist represents a column in the table. \n", + " Each sublist can contain either strings or PIL Image objects.\n", + " :param max_image_width: The maximum size of the images in pixels. This controls the max-height \n", + " of the images.\n", + " :param headers: A list of header labels for the table. If None, no headers are displayed.\n", + " \"\"\"\n", + " return display(HTML(get_columns_html(columns, max_image_width, headers)))\n", + "\n", + "\n", + "def get_image_grid_html(\n", + " images: list[Image.Image | Path | str],\n", + " rows: int,\n", + " columns: int,\n", + " titles: list[str] | None = None,\n", + " max_image_width: int | None = None,\n", + " caption: str | None = None\n", + "):\n", + " if titles and len(titles) != len(images):\n", + " raise ValueError(\"Titles list must match the number of images if provided.\")\n", + "\n", + " html_str = \"\"\n", + "\n", + " if caption:\n", + " html_str += (f\"\")\n", + "\n", + " image_index = 0\n", + " for row in range(rows):\n", + " html_str += \"\"\n", + " for col in range(columns):\n", + " if image_index < len(images):\n", + " img_html = get_image_html(images[image_index], max_width=max_image_width)\n", + " title_html = (\n", + " f\"
{titles[image_index]}
\"\n", + " if titles\n", + " else \"\"\n", + " )\n", + " html_str += f\"\"\n", + " else:\n", + " html_str += \"\" # Empty cell if no more images\n", + " image_index += 1\n", + " html_str += \"\"\n", + "\n", + " html_str += \"
{caption}
{title_html}{img_html}
\"\n", + " return html_str\n", + "\n", + "\n", + "def display_image_grid(\n", + " images: list[Image.Image | Path | str],\n", + " rows: int,\n", + " columns: int,\n", + " titles: list[str] | None = None,\n", + " max_image_width: int | None = None,\n", + " caption: str | None = None,\n", + "):\n", + " \"\"\"\n", + " Displays a grid of images in a HTML table within a Jupyter notebook cell.\n", + "\n", + " :param images: A list of PIL Image objects to be displayed.\n", + " :param rows: The number of rows in the grid.\n", + " :param columns: The number of columns in the grid.\n", + " :param titles: An optional list of titles for each image. If provided, it must match the length \n", + " of the images list.\n", + " :param max_image_width: The maximum width of the images in pixels.\n", + " \"\"\"\n", + " display(HTML(get_image_grid_html(images, rows, columns, titles, max_image_width, caption)))\n", + "\n", + "\n", + "def acc_as_html(acc):\n", + " return f\"
{acc:.2f}
\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## UUIDs" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "def strip_uuid(p: Path | str):\n", + " _p: Path = p if isinstance(p, Path) else Path(p)\n", + " new_stem = re.sub(r'(?i)[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}', '', _p.stem).strip('_')\n", + " return _p.with_stem(new_stem)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Path('a/b/c/Strange Tales 172_boxes.png')" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "strip_uuid(Path(\"a/b/c/ac265dc1-51a0-46ca-9101-7195cbad33f2_Strange Tales 172_boxes.png\"))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Other" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "# Deep copy a defaultdict of defaultdicts to a dict of dicts if it is not already a dict\n", + "def defaultdict_to_dict(d) -> dict:\n", + " if not isinstance(d, defaultdict):\n", + " return d\n", + " return {k: defaultdict_to_dict(v) for k, v in d.items()}\n" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "aaa\n" + ] + } + ], + "source": [ + "print('aaa')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Colophon\n", + "----\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "import fastcore.all as FC\n", + "from nbdev.export import nb_export\n" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "if FC.IN_NOTEBOOK:\n", + " nb_export('helpers.ipynb', '.')\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/_testbed/helpers.py b/_testbed/helpers.py new file mode 100644 index 00000000..a7095701 --- /dev/null +++ b/_testbed/helpers.py @@ -0,0 +1,372 @@ +# AUTOGENERATED! DO NOT EDIT! File to edit: helpers.ipynb. + +# %% helpers.ipynb 7 +from __future__ import annotations + +import base64 +import json +import re +import sys +import uuid +from importlib import resources +from io import BytesIO +from pathlib import Path +from typing import Any +from typing import Iterable +from typing import Mapping +from typing import Sequence + +import pcleaner.data +import pcleaner.structures as st +from IPython.display import clear_output +from IPython.display import display +from IPython.display import HTML +from PIL import Image +from PIL import ImageDraw +from PIL import ImageFont + + +# %% auto 0 +__all__ = ['PRINT_FORMATS', 'cleanupwidgets', 'RenderJSON', 'page_boxes', 'crop_box', 'size', 'dpi', 'get_image_html', + 'get_columns_html', 'display_columns', 'get_image_grid_html', 'display_image_grid', 'acc_as_html', + 'strip_uuid', '_pops_', '_pops_values_', '_gets_'] + +# %% helpers.ipynb 13 +_all_ = ['_pops_', '_pops_values_', '_gets_'] + + +# %% helpers.ipynb 14 +def _pops_(d: dict, ks: Iterable) -> dict: + "Pops `ks` keys from `d` and returns them in a dict. Note: `d` is changed in-place." + return {k:d.pop(k) for k in ks if k in d} + + +# %% helpers.ipynb 16 +def _pops_values_(d: dict, ks: Iterable) -> tuple: + "Pops `ks` keys from `d` and returns them as a tuple. Note: `d` is changed in-place." + return tuple(d.pop(k, None) for k in ks) + + +# %% helpers.ipynb 18 +def _gets_(d: Mapping[str, Any], ks: Iterable): + "Fetches values from a mapping for a given list of keys, returning `None` for missing keys." + return (d.get(k, None) for k in ks) + + +# %% helpers.ipynb 21 +def _get_globals(mod: str): + if hasattr(sys, '_getframe'): + glb = sys._getframe(2).f_globals + else: + glb = sys.modules[mod].__dict__ + return glb + + +# %% helpers.ipynb 23 +def cleanupwidgets(*ws, mod: str|None=None, clear=True): + glb = _get_globals(mod or __name__) + if clear: clear_output(wait=True) + for w in ws: + _w = glb.get(w) if isinstance(w, str) else w + if _w: + try: _w.close() # type: ignore + except: pass + + +# %% helpers.ipynb 26 +class RenderJSON(object): + def __init__(self, json_data, max_height=200, init_level=0): + if isinstance(json_data, (Sequence, Mapping)): + s = json.dumps(json_data) + elif hasattr(json_data, 'to_dict'): + s = json.dumps(json_data.to_dict()) + elif hasattr(json_data, 'to_json'): + s = json_data.to_json() + else: + s = json_data + self.json_str = s + self.uuid = str(uuid.uuid4()) + self.max_height = max_height + self.init_level = init_level + + def display(self): + html_content = f""" +
+
+ +
+ """ + display(HTML(html_content)) + + def _ipython_display_(self): + self.display() + +# %% helpers.ipynb 29 +def page_boxes(self: st.PageData, out_dir: Path | None = None) -> tuple[Image.Image, Path]: + """ + Visualize the boxes on an image. + Typically, this would be used to check where on the original image the + boxes are located. + + :param image_path: The path to the image to visualize the boxes on. + """ + image_path = Path(self.image_path) + image = Image.open(image_path) + draw = ImageDraw.Draw(image) + data_path = resources.files(pcleaner.data) + font_path = str(data_path / "LiberationSans-Regular.ttf") + # Figure out the optimal font size based on the image size. E.g. 30 for a 1600px image. + font_size = int(image.size[0] / 50) + 5 + + for index, box in enumerate(self.boxes): + draw.rectangle(box.as_tuple, outline="green") + # Draw the box number, with a white background, respecting font size. + draw.text( + (box.x1 + 4, box.y1), + str(index + 1), + fill="green", + font=ImageFont.truetype(font_path, font_size), + stroke_fill="white", + stroke_width=3, + ) + + for box in self.extended_boxes: + draw.rectangle(box.as_tuple, outline="red") + for box in self.merged_extended_boxes: + draw.rectangle(box.as_tuple, outline="purple") + for box in self.reference_boxes: + draw.rectangle(box.as_tuple, outline="blue") + + # Save the image. + extension = "_boxes" + out_path = image_path.with_stem(image_path.stem + extension) + if out_dir is not None: + out_dir.mkdir(parents=True, exist_ok=True) + out_path = out_dir / image_path.name + image.save(out_path) + + return image, out_path + +# %% helpers.ipynb 31 +def crop_box(box: st.Box, image: Image.Image) -> Image.Image: + return image.crop(box.as_tuple) + +# %% helpers.ipynb 33 +PRINT_FORMATS = { + 'Golden Age': (7.75, 10.5), # (1930s-40s) + 'Siver Age': (7, 10.375), # (1950s-60s) + 'Modern Age': (6.625,10.25), # North American comic books + 'Magazine': (8.5, 11), + 'Digest': (5.5, 8.5), + 'Manga': (5.0, 7.5), +} + + +def size(w: int, h: int, unit: str = 'in', dpi: float = 300.) -> tuple: + """ + Calculate the print size of an image in inches or centimeters. + + Args: + w (int): Width of the image in pixels. + h (int): Height of the image in pixels. + unit (str): Unit of measurement ('in' for inches, 'cm' for centimeters). + dpi (float): Dots per inch (resolution). + + Returns: + tuple: Width and height of the image in the specified unit. + """ + if unit == 'cm': + return (w / dpi * 2.54, h / dpi * 2.54) + else: # default to inches + return (w / dpi, h / dpi) + + +def dpi(w: int, h: int, print_format: str = 'Modern Age') -> float: + """ + Calculate the dpi (dots per inch) needed to print an image at a specified format size. + + Args: + w (int): Width of the image in pixels. + h (int): Height of the image in pixels. + print_format (str): Print format as defined in the formats dictionary. + + Returns: + float: Required dpi to achieve the desired print format size. + """ + # Default to 'Modern Age' if format not found + format_size = PRINT_FORMATS.get(print_format, PRINT_FORMATS['Modern Age']) + width_inch, height_inch = format_size + dpi_w = w / width_inch + dpi_h = h / height_inch + return (dpi_w + dpi_h) / 2 # Average dpi for width and height + + +# %% helpers.ipynb 35 +def get_image_html(image: Image.Image | Path | str, max_width: int | None = None): + """ + Converts a PIL image to an HTML image tag containing the image as a base64 blob. + + :param image: A PIL Image object. + :param max_size: A PIL Image object. + :return: A string containing an HTML tag with the image. + """ + style = f' style="max-width: {max_width}px;"' if max_width is not None else '' + if isinstance(image, (Path, str)): + return f'' + else: + buffered = BytesIO() + image.save(buffered, format='PNG') + img_str = base64.b64encode(buffered.getvalue()).decode() + return f'' + + +def get_columns_html( + columns: list[list], max_image_width: int | None = None, headers: list[str] | None = None +): + if not all(len(col) == len(columns[0]) for col in columns): + raise ValueError("All columns must have the same length.") + + # Calculate the maximum width of images in each column + max_widths = [] + for col_index in range(len(columns)): + max_col_width = 0 + for item in columns[col_index]: + if isinstance(item, (Image.Image, Path)): + if isinstance(item, (Path, str)): + item = Image.open(item) + width, _ = item.size + max_col_width = max(max_col_width, width) + if max_col_width > 0: + max_widths.append( + f"{min(max_col_width, max_image_width)}px" + if max_image_width is not None else + f"{max_col_width}px" + ) + else: + max_widths.append('auto') + + html_str = "" + + # Apply calculated column widths using and elements + html_str += "" + for width in max_widths: + html_str += f"" + html_str += "" + + if headers: + if len(headers) != len(columns): + raise ValueError("Headers list must match the number of columns.") + html_str += ( + "" + + "".join( + f"" + for header in headers + ) + + "" + ) + + for row_items in zip(*columns): + html_str += "" + for i, item in enumerate(row_items): + if isinstance(item, (Image.Image, Path)): + img_html = get_image_html(item, max_width=max_image_width) + html_str += f"" + else: # Assume the item is a string + style = "font-weight: bold;" if i == 0 else "" + html_str += f"" + html_str += "" + + html_str += "
{header}
{img_html}{item}
" + return html_str + + +def display_columns( + columns: list[list], max_image_width: int | None = None, headers: list[str] | None = None +): + """ + Displays a table with any combination of columns, which can be lists of strings or lists + of PIL Image objects, within a Jupyter notebook cell. + + :param columns: A list of lists, where each sublist represents a column in the table. + Each sublist can contain either strings or PIL Image objects. + :param max_image_width: The maximum size of the images in pixels. This controls the max-height + of the images. + :param headers: A list of header labels for the table. If None, no headers are displayed. + """ + return display(HTML(get_columns_html(columns, max_image_width, headers))) + + +def get_image_grid_html( + images: list[Image.Image | Path | str], + rows: int, + columns: int, + titles: list[str] | None = None, + max_image_width: int | None = None, + caption: str | None = None +): + if titles and len(titles) != len(images): + raise ValueError("Titles list must match the number of images if provided.") + + html_str = "" + + if caption: + html_str += (f"") + + image_index = 0 + for row in range(rows): + html_str += "" + for col in range(columns): + if image_index < len(images): + img_html = get_image_html(images[image_index], max_width=max_image_width) + title_html = ( + f"
{titles[image_index]}
" + if titles + else "" + ) + html_str += f"" + else: + html_str += "" # Empty cell if no more images + image_index += 1 + html_str += "" + + html_str += "
{caption}
{title_html}{img_html}
" + return html_str + + +def display_image_grid( + images: list[Image.Image | Path | str], + rows: int, + columns: int, + titles: list[str] | None = None, + max_image_width: int | None = None, + caption: str | None = None, +): + """ + Displays a grid of images in a HTML table within a Jupyter notebook cell. + + :param images: A list of PIL Image objects to be displayed. + :param rows: The number of rows in the grid. + :param columns: The number of columns in the grid. + :param titles: An optional list of titles for each image. If provided, it must match the length + of the images list. + :param max_image_width: The maximum width of the images in pixels. + """ + display(HTML(get_image_grid_html(images, rows, columns, titles, max_image_width, caption))) + + +def acc_as_html(acc): + return f"
{acc:.2f}
" + + +# %% helpers.ipynb 37 +def strip_uuid(p: Path | str): + _p: Path = p if isinstance(p, Path) else Path(p) + new_stem = re.sub(r'(?i)[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}', '', _p.stem).strip('_') + return _p.with_stem(new_stem) + diff --git a/_testbed/ocr_idefics.py b/_testbed/ocr_idefics.py new file mode 100644 index 00000000..67a50c42 --- /dev/null +++ b/_testbed/ocr_idefics.py @@ -0,0 +1,184 @@ +# AUTOGENERATED! DO NOT EDIT! File to edit: test_idefics.ipynb. + +# %% test_idefics.ipynb 12 +from __future__ import annotations + +import functools +from pathlib import Path + +import pcleaner.ocr.ocr as ocr +import torch +import transformers +from pcleaner.ocr.ocr_tesseract import TesseractOcr +from PIL import Image +from rich.console import Console +from transformers import AutoProcessor +from transformers import Idefics2ForConditionalGeneration +from transformers import PreTrainedModel + + +# %% auto 0 +__all__ = ['IdeficsOCR', 'IdeficsExperimentContext'] + +# %% test_idefics.ipynb 17 +console = Console(width=104, tab_size=4, force_jupyter=True) +cprint = console.print + + +# %% test_idefics.ipynb 20 +from experiments import * +from helpers import * +from ocr_metric import * + + +# %% test_idefics.ipynb 21 +def load_image(img_or_path) -> Image.Image: + if isinstance(img_or_path, (str, Path)): + return Image.open(img_or_path) + elif isinstance(img_or_path, Image.Image): + return img_or_path + else: + raise ValueError(f"img_or_path must be a path or PIL.Image, got: {type(img_or_path)}") + + +# %% test_idefics.ipynb 36 +processor = AutoProcessor.from_pretrained("HuggingFaceM4/idefics2-8b") + +# %% test_idefics.ipynb 37 +device = "mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu" + +model = Idefics2ForConditionalGeneration.from_pretrained( + "HuggingFaceM4/idefics2-8b", + torch_dtype=torch.bfloat16, + _attn_implementation="flash_attention_2", + ).to(device) # type: ignore + + +# %% test_idefics.ipynb 39 +prompt_text_tmpl = ( + "Please perform optical character recognition (OCR) on this image, which displays " + "speech balloons from a comic book. The text is in {}. Extract the text and " + "format it as follows: transcribe in standard sentence case, avoid using all capital " + "letters. Provide the transcribed text clearly and double check the sentence is not all capital letters.") + +# prompt_text_tmpl = ("Please perform optical character recognition (OCR) on this image, which displays " +# f"speech balloons from a manga comic. The text is in {}. Extract the text and " +# "format it without newlines. Provide the transcribed text clearly.") + +# prompt_text_tmpl = ("Please perform optical character recognition (OCR) on this image, which displays " +# "speech balloons from a comic book. The text is in {}. Extract the text and " +# "format it as follows: transcribe in standard sentence case (avoid using all capital " +# "letters) and use asterisks to denote any words that appear in bold within the image. " +# "Provide the transcribed text clearly.") + +# prompt_text_tmpl = ("Please perform optical character recognition (OCR) on this image, which displays " +# "speech balloons from a comic book. The text is in {}. Extract the text and " +# "format it as follows: transcribe in standard sentence case, capitalized. Avoid using " +# "all capital letters. In comics, it is common to use two hyphens '--' to interrupt a sentence. " +# "Retain any hyphens as they appear in the original text. Provide the transcribed text " +# "clearly, ensuring it is capitalized where appropriate, including proper nouns.") + +prompt_text_tmpl = ( + "Please perform optical character recognition (OCR) on this image, which displays " + "speech balloons from a comic book. The text is in {}. Extract the text and " + "format it as follows: transcribe in standard sentence case, capitalized. Avoid using " + "all capital letters, but ensure it is capitalized where appropriate, including proper nouns. " + "Provide the transcribed text clearly. Double check the text is not all capital letters.") + +default_prompt_text_tmpl = prompt_text_tmpl + +# %% test_idefics.ipynb 41 +class IdeficsOCR: + prompt_text_tmpl: str = default_prompt_text_tmpl + + def __init__(self, + lang: str | None = None, + prompt_text_tmpl: str|None = None, + device: str | None = None + ): + self.lang = lang + self.prompt_text_tmpl = prompt_text_tmpl or self.prompt_text_tmpl + self.device = (device or + "mps" if torch.backends.mps.is_available() else "cuda" if torch.cuda.is_available() else "cpu") + + @staticmethod + def is_idefics_available() -> bool: + return True + + def _generation_args(self, image: Image.Image, resulting_messages: list[dict]): + prompt = processor.apply_chat_template(resulting_messages, add_generation_prompt=True) + inputs = processor(text=prompt, images=[image], return_tensors="pt") + inputs = {k: v.to(self.device) for k, v in inputs.items()} + + max_new_tokens = 512 + repetition_penalty = 1.2 + decoding_strategy = "Greedy" + temperature = 0.4 + top_p = 0.8 + + generation_args = { + "max_new_tokens": max_new_tokens, + "repetition_penalty": repetition_penalty, + } + + assert decoding_strategy in [ + "Greedy", + "Top P Sampling", + ] + + if decoding_strategy == "Greedy": + generation_args["do_sample"] = False + elif decoding_strategy == "Top P Sampling": + generation_args["temperature"] = temperature + generation_args["do_sample"] = True + generation_args["top_p"] = top_p + + generation_args.update(inputs) + return prompt, generation_args + + def __call__( + self, + img_or_path: Image.Image | Path | str, + prompt_text: str | None = None, + lang: str | None = None, + config: str | None = None, + show_prompt: bool = False, + **kwargs, + ) -> str: + if not self.is_idefics_available(): + raise RuntimeError("Idefics is not installed or not found.") + resulting_messages = [ + { + "role": "user", + "content": [{"type": "image"}] + [ + {"type": "text", "text": prompt_text or self.prompt_text_tmpl.format(lang or self.lang)} + ] + } + ] + image = load_image(img_or_path) + prompt, generation_args = self._generation_args(image, resulting_messages) + generated_ids = model.generate(**generation_args) + generated_texts = processor.batch_decode( + generated_ids[:, generation_args["input_ids"].size(1):], skip_special_tokens=True) + if show_prompt: + cprint("INPUT:", prompt, "|OUTPUT:", generated_texts) + return generated_texts[0]#.strip('"') + + def postprocess_ocr(self, text): + return ' '.join(remove_multiple_whitespaces(text).splitlines()) + + +# %% test_idefics.ipynb 43 +class IdeficsExperimentContext(OCRExperimentContext): + @functools.lru_cache() + def mocr(self, ocr_model: str, lang: str): + if ocr_model == 'Idefics': + proc = IdeficsOCR(lang) + else: + engine = self.engines[ocr_model] + ocr_processor = ocr.get_ocr_processor(True, engine) + proc = ocr_processor[lang2pcleaner(lang)] + if isinstance(proc, TesseractOcr): + proc.lang = lang2tesseract(lang) + return proc + diff --git a/_testbed/ocr_metric.ipynb b/_testbed/ocr_metric.ipynb new file mode 100644 index 00000000..41f2fad4 --- /dev/null +++ b/_testbed/ocr_metric.ipynb @@ -0,0 +1,276 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp ocr_metric" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "# %reload_ext autoreload\n", + "# %autoreload 0\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# install (Colab)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# try: \n", + "# import fastcore as FC\n", + "# except ImportError: \n", + "# !pip install -q fastcore\n", + "# try:\n", + "# import rich\n", + "# except ImportError:\n", + "# !pip install -q rich\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Developing a metric for OCR of Comics/Manga texts\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Prologue" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "from __future__ import annotations\n", + "\n", + "import difflib\n", + "import html\n", + "\n", + "from IPython.display import display\n", + "from IPython.display import HTML\n", + "from rich.console import Console\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import fastcore.all as FC\n", + "import fastcore.xtras # patch Path with some utils\n", + "import rich\n", + "from fastcore.test import * # type: ignore\n", + "from loguru import logger\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Helpers" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# pretty print by default\n", + "# %load_ext rich" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "#| exporti\n", + "console = Console(width=104, tab_size=4, force_jupyter=True)\n", + "cprint = console.print\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## OCR metric\n", + "> Some basic ways to compare OCR results" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "def get_text_diffs_html(str1, str2, ignore_align: bool = False):\n", + " matcher = difflib.SequenceMatcher(None, str1, str2)\n", + " html_str1, html_str2 = \"\", \"\"\n", + " _ch ='⎕' # ▿\n", + " ch = f'&#x{ord(_ch):x};'\n", + " span1_g = lambda l: f\"{ch*l}\" if l > 0 else \"\"\n", + " span1_r = lambda l: f\"{ch*l}\" if l > 0 else \"\"\n", + " span2 = lambda s: f\"{html.escape(s)}\" if s else \"\"\n", + "\n", + " for opcode in matcher.get_opcodes():\n", + " tag, i1, i2, j1, j2 = opcode\n", + " if tag == \"equal\":\n", + " html_str1 += html.escape(str1[i1:i2])\n", + " html_str2 += html.escape(str2[j1:j2])\n", + " elif tag == \"replace\":\n", + " max_span = max(i2 - i1, j2 - j1)\n", + " # str1_segment = str1[i1:i2].ljust(max_span)\n", + " html_str1 += html.escape(str1[i1:i2]) + span1_g(max_span - (i2 - i1))\n", + " html_str2 += span2(str2[j1:j2]) + (span1_r(max_span - (j2 - j1)) if not ignore_align else '')\n", + " elif tag == \"delete\":\n", + " deleted_segment = str1[i1:i2]\n", + " html_str1 += html.escape(deleted_segment)\n", + " if not ignore_align: html_str2 += span1_r(len(deleted_segment))\n", + " elif tag == \"insert\":\n", + " inserted_segment = str2[j1:j2].replace(\" \", _ch)\n", + " html_str1 += span1_g(len(inserted_segment))\n", + " html_str2 += span2(inserted_segment)\n", + " html_str1 = f\"
{html_str1}
\"\n", + " html_str2 = f\"
{html_str2}
\"\n", + " return html_str1, html_str2\n", + "\n", + "def display_text_diffs(str1, str2):\n", + " \"\"\"\n", + " Displays two strings one above the other, with differing characters highlighted in red in the \n", + " second string only, using difflib.SequenceMatcher to align the strings and ensure matching \n", + " sequences are vertically aligned.\n", + "\n", + " :param str1: The first string to compare.\n", + " :param str2: The second string to compare.\n", + " \"\"\"\n", + " html_str1, html_str2 = get_text_diffs_html(str1, str2)\n", + " display(HTML(f\"
{html_str1}
{html_str2}
\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
This is an awesome_⎕⎕⎕ test string.This is an awesome_⎕⎕⎕ test string.This is an awesome_⎕⎕⎕ test string.This is an awesome_⎕⎕⎕ test string.

This was an a⎕⎕⎕⎕mazing test spring.This was an a⎕⎕⎕⎕mazing test spring.This was an a⎕⎕⎕⎕mazing test spring.This was an a⎕⎕⎕⎕mazing test spring.
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "str1 = \"This is an awesome_ test string.\"*4\n", + "str2 = \"This was an amazing test▿ spring.\"*4\n", + "display_text_diffs(str1, str2)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
I was in a bad mood, and Curt sensed it immediately...

I was in a bad mood, and Curt sensed it immediately...
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "str1 = \"I was in a bad mood, and Curt sensed it immediately...\"\n", + "str2 = \"I was in a bad mood, and Curt sensed it immediately ...\"\n", + "display_text_diffs(str1, str2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Colophon\n", + "----\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "import fastcore.all as FC\n", + "from nbdev.export import nb_export\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "if FC.IN_NOTEBOOK:\n", + " nb_export('ocr_metric.ipynb', '.')\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/_testbed/ocr_metric.py b/_testbed/ocr_metric.py new file mode 100644 index 00000000..f81b9a5e --- /dev/null +++ b/_testbed/ocr_metric.py @@ -0,0 +1,64 @@ +# AUTOGENERATED! DO NOT EDIT! File to edit: ocr_metric.ipynb. + +# %% ocr_metric.ipynb 6 +from __future__ import annotations + +import difflib +import html + +from IPython.display import display +from IPython.display import HTML +from rich.console import Console + + +# %% auto 0 +__all__ = ['get_text_diffs_html', 'display_text_diffs'] + +# %% ocr_metric.ipynb 10 +console = Console(width=104, tab_size=4, force_jupyter=True) +cprint = console.print + + +# %% ocr_metric.ipynb 12 +def get_text_diffs_html(str1, str2, ignore_align: bool = False): + matcher = difflib.SequenceMatcher(None, str1, str2) + html_str1, html_str2 = "", "" + _ch ='⎕' # ▿ + ch = f'&#x{ord(_ch):x};' + span1_g = lambda l: f"{ch*l}" if l > 0 else "" + span1_r = lambda l: f"{ch*l}" if l > 0 else "" + span2 = lambda s: f"{html.escape(s)}" if s else "" + + for opcode in matcher.get_opcodes(): + tag, i1, i2, j1, j2 = opcode + if tag == "equal": + html_str1 += html.escape(str1[i1:i2]) + html_str2 += html.escape(str2[j1:j2]) + elif tag == "replace": + max_span = max(i2 - i1, j2 - j1) + # str1_segment = str1[i1:i2].ljust(max_span) + html_str1 += html.escape(str1[i1:i2]) + span1_g(max_span - (i2 - i1)) + html_str2 += span2(str2[j1:j2]) + (span1_r(max_span - (j2 - j1)) if not ignore_align else '') + elif tag == "delete": + deleted_segment = str1[i1:i2] + html_str1 += html.escape(deleted_segment) + if not ignore_align: html_str2 += span1_r(len(deleted_segment)) + elif tag == "insert": + inserted_segment = str2[j1:j2].replace(" ", _ch) + html_str1 += span1_g(len(inserted_segment)) + html_str2 += span2(inserted_segment) + html_str1 = f"
{html_str1}
" + html_str2 = f"
{html_str2}
" + return html_str1, html_str2 + +def display_text_diffs(str1, str2): + """ + Displays two strings one above the other, with differing characters highlighted in red in the + second string only, using difflib.SequenceMatcher to align the strings and ensure matching + sequences are vertically aligned. + + :param str1: The first string to compare. + :param str2: The second string to compare. + """ + html_str1, html_str2 = get_text_diffs_html(str1, str2) + display(HTML(f"
{html_str1}
{html_str2}
")) diff --git a/_testbed/requirements.txt b/_testbed/requirements.txt new file mode 100644 index 00000000..4e3d1c68 --- /dev/null +++ b/_testbed/requirements.txt @@ -0,0 +1,5 @@ +matplotlib +rich +fastcore +nbdev +ipywidgets diff --git a/_testbed/test_idefics.ipynb b/_testbed/test_idefics.ipynb new file mode 100644 index 00000000..a8978d12 --- /dev/null +++ b/_testbed/test_idefics.ipynb @@ -0,0 +1,1497 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "#| default_exp ocr_idefics" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "#| hide\n", + "# %reload_ext autoreload\n", + "# %autoreload 0\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# install (Colab)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# try: \n", + "# import fastcore as FC\n", + "# except ImportError: \n", + "# !pip install -q fastcore\n", + "# try:\n", + "# import rich\n", + "# except ImportError:\n", + "# !pip install -q rich\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install -q git+https://github.com/civvic/PanelCleaner.git@basic-tesseract" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "need version >4.40 of transformers" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# %pip install git+https://github.com/huggingface/transformers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Fash attention doesn't support Metal [#412](https://github.com/Dao-AILab/flash-attention/issues/412) (but see [metal-flash-attention](https://github.com/philipturner/metal-flash-attention))\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# %env FLASH_ATTENTION_SKIP_CUDA_BUILD=TRUE\n", + "# %pip install flash-attn --no-build-isolation" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "notebookRunGroups": { + "groupValue": "" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fri May 10 18:32:10 2024 \n", + "+---------------------------------------------------------------------------------------+\n", + "| NVIDIA-SMI 535.161.08 Driver Version: 535.161.08 CUDA Version: 12.2 |\n", + "|-----------------------------------------+----------------------+----------------------+\n", + "| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |\n", + "| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |\n", + "| | | MIG M. |\n", + "|=========================================+======================+======================|\n", + "| 0 NVIDIA GeForce RTX 3090 Ti On | 00000000:65:00.0 Off | Off |\n", + "| 0% 50C P8 33W / 480W | 1MiB / 24564MiB | 0% Default |\n", + "| | | N/A |\n", + "+-----------------------------------------+----------------------+----------------------+\n", + " \n", + "+---------------------------------------------------------------------------------------+\n", + "| Processes: |\n", + "| GPU GI CI PID Type Process name GPU Memory |\n", + "| ID ID Usage |\n", + "|=======================================================================================|\n", + "| No running processes found |\n", + "+---------------------------------------------------------------------------------------+\n" + ] + } + ], + "source": [ + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Testing `Idefics` OCR for Comics\n", + "> Accuracy Enhancements for OCR in `PanelCleaner`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Prologue" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "from __future__ import annotations\n", + "\n", + "import functools\n", + "from pathlib import Path\n", + "\n", + "import pcleaner.ocr.ocr as ocr\n", + "import torch\n", + "import transformers\n", + "from pcleaner.ocr.ocr_tesseract import TesseractOcr\n", + "from PIL import Image\n", + "from rich.console import Console\n", + "from transformers import AutoProcessor\n", + "from transformers import Idefics2ForConditionalGeneration\n", + "from transformers import PreTrainedModel\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "from typing import cast\n", + "\n", + "import fastcore.xtras # patch Path with some utils\n", + "import pcleaner.config as cfg\n", + "from fastcore.test import * # type: ignore\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "notebookRunGroups": { + "groupValue": "" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'4.41.0.dev0'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "transformers.__version__" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Helpers" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "notebookRunGroups": { + "groupValue": "" + } + }, + "outputs": [], + "source": [ + "# pretty print by default\n", + "# %load_ext rich" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "notebookRunGroups": { + "groupValue": "" + } + }, + "outputs": [], + "source": [ + "#| exporti\n", + "\n", + "console = Console(width=104, tab_size=4, force_jupyter=True)\n", + "cprint = console.print\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Force reload of `experiments` module" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [], + "source": [ + "if 'experiments' in sys.modules:\n", + " import importlib; importlib.reload(experiments) # type: ignore\n", + "else:\n", + " import experiments\n" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "from experiments import *\n", + "from helpers import *\n", + "from ocr_metric import *\n" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "#| exporti\n", + "\n", + "def load_image(img_or_path) -> Image.Image:\n", + " if isinstance(img_or_path, (str, Path)):\n", + " return Image.open(img_or_path)\n", + " elif isinstance(img_or_path, Image.Image):\n", + " return img_or_path\n", + " else:\n", + " raise ValueError(f\"img_or_path must be a path or PIL.Image, got: {type(img_or_path)}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----\n", + "# Idefics basic usage\n", + "\n", + "not working, cuda memory error" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# # Note that passing the image urls (instead of the actual pil images) to the processor is also possible\n", + "# # image1 = load_image(\"https://cdn.britannica.com/61/93061-050-99147DCE/Statue-of-Liberty-Island-New-York-Bay.jpg\")\n", + "# # image2 = load_image(\"https://cdn.britannica.com/59/94459-050-DBA42467/Skyline-Chicago.jpg\")\n", + "# # image3 = load_image(\"https://cdn.britannica.com/68/170868-050-8DDE8263/Golden-Gate-Bridge-San-Francisco.jpg\")\n", + "\n", + "# image1 = Image.open(\"media/Statue-of-Liberty-Island-New-York-Bay.webp\")\n", + "# image2 = Image.open(\"media/Skyline-Chicago.webp\")\n", + "# image3 = Image.open(\"media/Golden-Gate-Bridge-San-Francisco.webp\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "# processor = AutoProcessor.from_pretrained(\"HuggingFaceM4/idefics2-8b\")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "# model = Idefics2ForConditionalGeneration.from_pretrained(\n", + "# \"HuggingFaceM4/idefics2-8b\",\n", + "# torch_dtype=torch.bfloat16,\n", + "# #_attn_implementation=\"flash_attention_2\",\n", + "# )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "# assert isinstance(model, PreTrainedModel)\n", + "# model.to(DEVICE)\n", + "# type(model), model.device\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Create inputs" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "# messages = [\n", + "# {\n", + "# \"role\": \"user\",\n", + "# \"content\": [\n", + "# {\"type\": \"image\"},\n", + "# {\"type\": \"text\", \"text\": \"What do we see in this image?\"},\n", + "# ]\n", + "# },\n", + "# {\n", + "# \"role\": \"assistant\",\n", + "# \"content\": [\n", + "# {\"type\": \"text\", \"text\": \"In this image, we can see the city of New York, and more specifically the Statue of Liberty.\"},\n", + "# ]\n", + "# },\n", + "# {\n", + "# \"role\": \"user\",\n", + "# \"content\": [\n", + "# {\"type\": \"image\"},\n", + "# {\"type\": \"text\", \"text\": \"And how about this image?\"},\n", + "# ]\n", + "# }, \n", + "# ]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "# prompt = processor.apply_chat_template(messages, add_generation_prompt=True)\n", + "# inputs = processor(text=prompt, images=[image1, image2], return_tensors=\"pt\")\n", + "# inputs = {k: v.to(DEVICE) for k, v in inputs.items()}\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Generate" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "# generated_ids = model.generate(**inputs, max_new_tokens=500)\n", + "# generated_texts = processor.batch_decode(generated_ids, skip_special_tokens=True)\n", + "\n", + "# print(generated_texts)\n", + "# # ['User: What do we see in this image? \\nAssistant: In this image, we can see the city of New York, and more specifically the Statue of Liberty. \\nUser: And how about this image? \\nAssistant: In this image we can see buildings, trees, lights, water and sky.']\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [], + "source": [ + "# [\n", + "# 'User: What do we see in this image? '\n", + "# 'Assistant: In this image, we can see the city of New York, and more specifically the Statue of Liberty. '\n", + "# 'User: And how about this image? '\n", + "# 'Assistant: In this image we can see buildings, trees, lights, water and sky.'\n", + "# ]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----\n", + "# Idefics experiments\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Idefics" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Idefics initialization" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Special tokens have been added in the vocabulary, make sure the associated word embeddings are fine-tuned or trained.\n" + ] + } + ], + "source": [ + "#| exporti\n", + "\n", + "processor = AutoProcessor.from_pretrained(\"HuggingFaceM4/idefics2-8b\")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "You are attempting to use Flash Attention 2.0 with a model not initialized on GPU. Make sure to move the model to GPU after initializing it on CPU with `model.to('cuda')`.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5a21b61268674a159f210694841c2149", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Loading checkpoint shards: 0%| | 0/7 [00:00 bool:\n", + " return True\n", + "\n", + " def _generation_args(self, image: Image.Image, resulting_messages: list[dict]):\n", + " prompt = processor.apply_chat_template(resulting_messages, add_generation_prompt=True)\n", + " inputs = processor(text=prompt, images=[image], return_tensors=\"pt\")\n", + " inputs = {k: v.to(self.device) for k, v in inputs.items()}\n", + " \n", + " max_new_tokens = 512\n", + " repetition_penalty = 1.2\n", + " decoding_strategy = \"Greedy\"\n", + " temperature = 0.4\n", + " top_p = 0.8\n", + "\n", + " generation_args = {\n", + " \"max_new_tokens\": max_new_tokens,\n", + " \"repetition_penalty\": repetition_penalty,\n", + " }\n", + "\n", + " assert decoding_strategy in [\n", + " \"Greedy\",\n", + " \"Top P Sampling\",\n", + " ]\n", + "\n", + " if decoding_strategy == \"Greedy\":\n", + " generation_args[\"do_sample\"] = False\n", + " elif decoding_strategy == \"Top P Sampling\":\n", + " generation_args[\"temperature\"] = temperature\n", + " generation_args[\"do_sample\"] = True\n", + " generation_args[\"top_p\"] = top_p\n", + "\n", + " generation_args.update(inputs)\n", + " return prompt, generation_args\n", + "\n", + " def __call__(\n", + " self,\n", + " img_or_path: Image.Image | Path | str,\n", + " prompt_text: str | None = None,\n", + " lang: str | None = None,\n", + " config: str | None = None,\n", + " show_prompt: bool = False,\n", + " **kwargs,\n", + " ) -> str:\n", + " if not self.is_idefics_available():\n", + " raise RuntimeError(\"Idefics is not installed or not found.\")\n", + " resulting_messages = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": [{\"type\": \"image\"}] + [\n", + " {\"type\": \"text\", \"text\": prompt_text or self.prompt_text_tmpl.format(lang or self.lang)}\n", + " ]\n", + " }\n", + " ]\n", + " image = load_image(img_or_path)\n", + " prompt, generation_args = self._generation_args(image, resulting_messages)\n", + " generated_ids = model.generate(**generation_args)\n", + " generated_texts = processor.batch_decode(\n", + " generated_ids[:, generation_args[\"input_ids\"].size(1):], skip_special_tokens=True)\n", + " if show_prompt:\n", + " cprint(\"INPUT:\", prompt, \"|OUTPUT:\", generated_texts)\n", + " return generated_texts[0]#.strip('\"')\n", + "\n", + " def postprocess_ocr(self, text):\n", + " return ' '.join(remove_multiple_whitespaces(text).splitlines())\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## IdeficsExperimentContext" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [], + "source": [ + "#| export\n", + "\n", + "class IdeficsExperimentContext(OCRExperimentContext):\n", + " @functools.lru_cache()\n", + " def mocr(self, ocr_model: str, lang: str):\n", + " if ocr_model == 'Idefics':\n", + " proc = IdeficsOCR(lang)\n", + " else:\n", + " engine = self.engines[ocr_model]\n", + " ocr_processor = ocr.get_ocr_processor(True, engine)\n", + " proc = ocr_processor[lang2pcleaner(lang)]\n", + " if isinstance(proc, TesseractOcr):\n", + " proc.lang = lang2tesseract(lang)\n", + " return proc\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# PanelCleaner Configuration\n", + "> Adapt `PanelCleaner` `Config` current config to this notebook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [], + "source": [ + "config = cfg.load_config()\n", + "config.cache_dir = Path(\".\")\n", + "\n", + "cache_dir = config.get_cleaner_cache_dir()\n", + "\n", + "profile = config.current_profile\n", + "preprocessor_conf = profile.preprocessor\n", + "# Modify the profile to OCR all boxes.\n", + "# Make sure OCR is enabled.\n", + "preprocessor_conf.ocr_enabled = True\n", + "# Make sure the max size is infinite, so no boxes are skipped in the OCR process.\n", + "preprocessor_conf.ocr_max_size = 10**10\n", + "# Make sure the sus box min size is infinite, so all boxes with \"unknown\" language are skipped.\n", + "preprocessor_conf.suspicious_box_min_size = 10**10\n", + "# Set the OCR blacklist pattern to match everything, so all text gets reported in the analytics.\n", + "preprocessor_conf.ocr_blacklist_pattern = \".*\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Test images\n", + "> `IMAGE_PATHS` is a list of image file paths that are used as input for testing the OCR methods." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['00: Action_Comics_1960-01-00_(262).JPG',\n", + " '01: Adolf_Cap_01_008.jpg',\n", + " '02: Barnaby_v1-028.png',\n", + " '03: Barnaby_v1-029.png',\n", + " '04: Buck_Danny_-_12_-_Avions_Sans_Pilotes_-_013.jpg',\n", + " '05: Cannon-292.jpg',\n", + " '06: Contrato_con_Dios_028.jpg',\n", + " '07: Erase_una_vez_en_Francia_02_88.jpg',\n", + " '08: FOX_CHILLINTALES_T17_012.jpg',\n", + " '09: Furari_-_Jiro_Taniguchi_selma_056.jpg',\n", + " '10: Galactus_12.jpg',\n", + " '11: INOUE_KYOUMEN_002.png',\n", + " '12: MCCALL_ROBINHOOD_T31_010.jpg',\n", + " '13: MCCAY_LITTLENEMO_090.jpg',\n", + " '14: Mary_Perkins_On_Stage_v2006_1_-_P00068.jpg',\n", + " '15: PIKE_BOYLOVEGIRLS_T41_012.jpg',\n", + " '16: Sal_Buscema_Spaceknights_&_Superheroes_Ocular_Edition_1_1.png',\n", + " '17: Sal_Buscema_Spaceknights_&_Superheroes_Ocular_Edition_1_1_K.png',\n", + " '18: Sal_Buscema_Spaceknights_&_Superheroes_Ocular_Edition_1_2.png',\n", + " '19: Spirou_Et_Fantasio_Integrale_06_1958_1959_0025_0024.jpg',\n", + " '20: Strange_Tales_172005.jpg',\n", + " '21: Strange_Tales_172021.jpg',\n", + " '22: Tarzan_014-21.JPG',\n", + " '23: Tintin_21_Les_Bijoux_de_la_Castafiore_page_39.jpg',\n", + " '24: Transformers_-_Unicron_000-004.jpg',\n", + " '25: Transformers_-_Unicron_000-016.jpg',\n", + " '26: WARE_ACME_024.jpg',\n", + " '27: Yoko_Tsuno_T01_1972-10.jpg',\n", + " '28: Your_Name_Another_Side_Earthbound_T02_084.jpg',\n", + " '29: manga_0033.jpg',\n", + " '30: ronson-031.jpg',\n", + " '31: 哀心迷図のバベル 第01巻 - 22002_00_059.jpg']" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "media_path = Path(\"media/\")\n", + "\n", + "IMAGE_PATHS = sorted(\n", + " [_ for _ in media_path.glob(\"*\") if _.is_file() and _.suffix.lower() in [\".jpg\", \".png\", \".jpeg\"]])\n", + "\n", + "[f\"{i:02}: {_.name}\" for i,_ in enumerate(IMAGE_PATHS)]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CONTEXT\n" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Current Configuration:\n", + "\n", + "Locale: System default\n", + "Default Profile: Built-in\n", + "Saved Profiles:\n", + "- victess: /home/vic/dev/repo/DL-mac/cleaned/victess.conf\n", + "- victmang: /home/vic/dev/repo/DL-mac/cleaned/vicmang.conf\n", + "\n", + "Profile Editor: System default\n", + "Cache Directory: .\n", + "Default Torch Model Path: /home/vic/.cache/pcleaner/model/comictextdetector.pt\n", + "Default CV2 Model Path: /home/vic/.cache/pcleaner/model/comictextdetector.pt.onnx\n", + "GUI Theme: System default\n", + "\n", + "--------------------\n", + "\n", + "Config file located at: /home/vic/.config/pcleaner/pcleanerrc\n", + "System default cache directory: /home/vic/.cache/pcleaner\n" + ] + }, + { + "data": { + "text/html": [ + "
      cache_dir: Path('cleaner')\n",
+       "     model_path: Path('/home/vic/.cache/pcleaner/model/comictextdetector.pt')\n",
+       "         device: 'cuda'\n",
+       "
\n" + ], + "text/plain": [ + " cache_dir: \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'cleaner'\u001b[0m\u001b[1m)\u001b[0m\n", + " model_path: \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'/home/vic/.cache/pcleaner/model/comictextdetector.pt'\u001b[0m\u001b[1m)\u001b[0m\n", + " device: \u001b[32m'cuda'\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "CONTEXT = IdeficsExperimentContext(None, IMAGE_PATHS)\n", + "\n", + "gpu = torch.cuda.is_available() or torch.backends.mps.is_available()\n", + "model_path = CONTEXT.config.get_model_path(gpu)\n", + "DEVICE = (\"mps\" if torch.backends.mps.is_available() else \"cuda\") if model_path.suffix == \".pt\" else \"cpu\"\n", + "\n", + "CONTEXT.config.show()\n", + "cprint(\n", + " f\"{'cache_dir':>15}: {repr(cache_dir)}\\n\"\n", + " f\"{'model_path':>15}: {repr(model_path)}\\n\"\n", + " f\"{'device':>15}: {repr(DEVICE)}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Base image\n", + "> Change `BASE_IMAGE_IDX` to select a different base image to use in the examples below." + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "BASE_IMAGE_IDX: ImgIdT = cast(ImgIdT, CONTEXT.normalize_idx(\"Strange_Tales_172005.jpg\"))\n", + "assert CONTEXT.path_from_idx(BASE_IMAGE_IDX).exists()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualize images\n" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [], + "source": [ + "IMAGE_CONTEXT = ImageContext(CONTEXT, BASE_IMAGE_IDX)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9c0379d4776a4e0f9facd7e0092c79f3", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output(layout=Layout(height='0px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5f0ae88943a14ea38efb029c40183ffd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(Dropdown(index=20, layout=Layout(width='fit-content'), options={'Action_Comics_1…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "26c6ecdbae6f4952aab7ff3106400f04", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "img_visor = ImageContextVisor(CONTEXT, BASE_IMAGE_IDX)\n", + "img_visor\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Box id\n", + "> change `BOX_IDX` to use any box to test crop methods" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "BOX_IDX = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Idefics inference" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "notebookRunGroups": { + "groupValue": "2" + } + }, + "outputs": [], + "source": [ + "page_lang = IMAGE_CONTEXT.page_lang\n", + "\n", + "resulting_messages = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": [{\"type\": \"image\"}] + [\n", + " {\"type\": \"text\", \"text\": prompt_text_tmpl.format(page_lang)}\n", + " ]\n", + " }\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": {}, + "outputs": [], + "source": [ + "def idefics_generation_args(image: Image.Image, resulting_messages: list[dict]):\n", + " prompt = processor.apply_chat_template(resulting_messages, add_generation_prompt=True)\n", + " inputs = processor(text=prompt, images=[image], return_tensors=\"pt\")\n", + " inputs = {k: v.to(DEVICE) for k, v in inputs.items()}\n", + " \n", + " max_new_tokens = 512\n", + " repetition_penalty = 1.2\n", + " decoding_strategy = \"Greedy\"\n", + " temperature = 0.4\n", + " top_p = 0.8\n", + "\n", + " generation_args = {\n", + " \"max_new_tokens\": max_new_tokens,\n", + " \"repetition_penalty\": repetition_penalty,\n", + " }\n", + "\n", + " assert decoding_strategy in [\n", + " \"Greedy\",\n", + " \"Top P Sampling\",\n", + " ]\n", + "\n", + " if decoding_strategy == \"Greedy\":\n", + " generation_args[\"do_sample\"] = False\n", + " elif decoding_strategy == \"Top P Sampling\":\n", + " generation_args[\"temperature\"] = temperature\n", + " generation_args[\"do_sample\"] = True\n", + " generation_args[\"top_p\"] = top_p\n", + "\n", + " generation_args.update(inputs)\n", + " return prompt, generation_args\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Crop methods" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "# image_experiment = ExperimentOCR(IMAGE_CONTEXT, 'Idefics')\n", + "image_experiment = ExperimentOCR.from_image(CONTEXT, 'Idefics', IMAGE_CONTEXT.image_idx) # use cache\n" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "method = CropMethod.INITIAL_BOX\n", + "\n", + "result = cast(ResultOCR, image_experiment.result(BOX_IDX, method, ocr=False))\n", + "image = cast(Image.Image, result.image)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "No chat template is set for this tokenizer, falling back to a default class-level template. This is very error-prone, because models are often trained with templates different from the class default! Default chat templates are a legacy feature and will be removed in Transformers v4.43, at which point any code depending on them will stop working. We recommend setting a valid chat template before then to ensure that this model continues working without issues.\n", + "The `seen_tokens` attribute is deprecated and will be removed in v4.41. Use the `cache_position` model input instead.\n" + ] + }, + { + "data": { + "text/html": [ + "
INPUT: User:<image>Please perform optical character recognition (OCR) on this image, which displays \n",
+       "speech balloons from a comic book. The text is in English. Extract the text and format it as follows: \n",
+       "transcribe in standard sentence case, capitalized. Avoid using all capital letters, but ensure it is \n",
+       "capitalized where appropriate, including proper nouns. Provide the transcribed text clearly. Double \n",
+       "check the text is not all capital letters.<end_of_utterance>\n",
+       "Assistant: |OUTPUT:\n",
+       "[\n",
+       "    'Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New \n",
+       "Orleans, kept tidy by a white-haired old man known only as Bambu.'\n",
+       "]\n",
+       "
\n" + ], + "text/plain": [ + "INPUT: User:\u001b[1m<\u001b[0m\u001b[1;95mimage\u001b[0m\u001b[39m>Please perform optical character recognition \u001b[0m\u001b[1;39m(\u001b[0m\u001b[39mOCR\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m on this image, which displays \u001b[0m\n", + "\u001b[39mspeech balloons from a comic book. The text is in English. Extract the text and format it as follows: \u001b[0m\n", + "\u001b[39mtranscribe in standard sentence case, capitalized. Avoid using all capital letters, but ensure it is \u001b[0m\n", + "\u001b[39mcapitalized where appropriate, including proper nouns. Provide the transcribed text clearly. Double \u001b[0m\n", + "\u001b[39mcheck the text is not all capital letters.\u001b[0m\n", + "Assistant: |OUTPUT:\n", + "\u001b[1m[\u001b[0m\n", + " \u001b[32m'Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New \u001b[0m\n", + "\u001b[32mOrleans, kept tidy by a white-haired old man known only as Bambu.'\u001b[0m\n", + "\u001b[1m]\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "prompt, generation_args = idefics_generation_args(image, resulting_messages)\n", + "generated_ids = model.generate(**generation_args)\n", + "\n", + "generated_texts = processor.batch_decode(\n", + " generated_ids[:, generation_args[\"input_ids\"].size(1):], skip_special_tokens=True)\n", + "cprint(\"INPUT:\", prompt, \"|OUTPUT:\", generated_texts)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.
1.00
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.

Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "result.ocr = generated_texts[0]\n", + "result\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
INPUT: User:<image>Please perform optical character recognition (OCR) on this image, which displays \n",
+       "speech balloons from a comic book. The text is in English. Extract the text and format it as follows: \n",
+       "transcribe in standard sentence case, capitalized. Avoid using all capital letters, but ensure it is \n",
+       "capitalized where appropriate, including proper nouns. Provide the transcribed text clearly. Double \n",
+       "check the text is not all capital letters.<end_of_utterance>\n",
+       "Assistant: |OUTPUT:\n",
+       "[\n",
+       "    'Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New \n",
+       "Orleans, kept tidy by a white-haired old man known only as Bambu.'\n",
+       "]\n",
+       "
\n" + ], + "text/plain": [ + "INPUT: User:\u001b[1m<\u001b[0m\u001b[1;95mimage\u001b[0m\u001b[39m>Please perform optical character recognition \u001b[0m\u001b[1;39m(\u001b[0m\u001b[39mOCR\u001b[0m\u001b[1;39m)\u001b[0m\u001b[39m on this image, which displays \u001b[0m\n", + "\u001b[39mspeech balloons from a comic book. The text is in English. Extract the text and format it as follows: \u001b[0m\n", + "\u001b[39mtranscribe in standard sentence case, capitalized. Avoid using all capital letters, but ensure it is \u001b[0m\n", + "\u001b[39mcapitalized where appropriate, including proper nouns. Provide the transcribed text clearly. Double \u001b[0m\n", + "\u001b[39mcheck the text is not all capital letters.\u001b[0m\n", + "Assistant: |OUTPUT:\n", + "\u001b[1m[\u001b[0m\n", + " \u001b[32m'Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New \u001b[0m\n", + "\u001b[32mOrleans, kept tidy by a white-haired old man known only as Bambu.'\u001b[0m\n", + "\u001b[1m]\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.
1.00
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.

Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "method = CropMethod.INITIAL_BOX\n", + "\n", + "result = cast(ResultOCR, image_experiment.result(BOX_IDX, method, ocr=False))\n", + "image = cast(Image.Image, result.image)\n", + "\n", + "mocr: IdeficsOCR = cast(IdeficsOCR, CONTEXT.mocr('Idefics', page_lang))\n", + "text = mocr(image, show_prompt=True)\n", + "result.ocr = mocr.postprocess_ocr(text)\n", + "result\n" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tidy by a white-haired old man known only as bambu.
0.98
\n", + "
\n", + "
Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.

Embowered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tidy by a white-haired old man known only as bambu.
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(BOX_IDX, CropMethod.PADDED_4)" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "
\n", + "
0.94
\n", + "
\n", + "
Embow⎕⎕ered by great gnarled cypress trees, the ancient manor stands alone on the outskirts of New Orleans, kept tidy by a white-haired old man known only as Bambu.

Encountered by great charles cypress trees, the ancient manor stands alone on the outskirts of new orleans, kept tidy by a white-haired old man known only as bambu.
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "image_experiment.result(BOX_IDX, CropMethod.PAD_8_FRACT_0_2)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----\n", + "# Visualize results" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "522220949ff540fdbd864dc8d8722cf0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output(layout=Layout(height='0px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "13a3ec1b0ea04630b339e5026e01eee2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(Label(value='Box # (of 15):', layout=Layout(padding='0px 0px 0px 10px', width='i…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "989b4acac70848f2a5da0a74f99bc181", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "result_visor = ResultVisor(image_experiment)\n", + "result_visor\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----\n", + "# Visualize Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [], + "source": [ + "# p, d = image_experiment.to_json()" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bae922f0f53b47b5909334ca7f5d24fc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(Dropdown(index=20, layout=Layout(width='fit-content'), options={'Action_Comics_1…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9c8a0db76d0a40eb9c3451129e803124", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "exp_visor = ExperimentVisor(image_experiment)\n", + "exp_visor\n" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [], + "source": [ + "# p, d = exp_visor.ctx.to_json()\n", + "# p" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----\n", + "# EEAaO" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d441a9415ca94481a85523a6d30eca92", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(HBox(children=(Dropdown(index=1, layout=Layout(width='fit-content'), options={'T…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4f568a40794a4cba951fb6652b5446dd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "idefics_experiment = ExperimentsVisor(CONTEXT, BASE_IMAGE_IDX, \n", + " box_idx=13, method=CropMethod.DEFAULT_GREY_PAD,\n", + " ocr_model=OCRModel.IDEFICS, \n", + " ocr_models={'Tesseract': OCRModel.TESSERACT, 'Idefics': OCRModel.IDEFICS})\n", + "idefics_experiment\n" + ] + }, + { + "cell_type": "code", + "execution_count": 53, + "metadata": {}, + "outputs": [], + "source": [ + "idefics_experiment.update(model=OCRModel.TESSERACT)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Colophon\n", + "----\n" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "metadata": {}, + "outputs": [], + "source": [ + "import fastcore.all as FC\n", + "from nbdev.export import nb_export\n" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": {}, + "outputs": [], + "source": [ + "if FC.IN_NOTEBOOK:\n", + " nb_export('test_idefics.ipynb', '.')\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "panel-cleaner", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/_testbed/test_tesseract.ipynb b/_testbed/test_tesseract.ipynb new file mode 100644 index 00000000..3cb1dc79 --- /dev/null +++ b/_testbed/test_tesseract.ipynb @@ -0,0 +1,638 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# install (Colab)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# try: \n", + "# import fastcore as FC\n", + "# except ImportError: \n", + "# !pip install -q fastcore\n", + "# try:\n", + "# import rich\n", + "# except ImportError:\n", + "# !pip install -q rich\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install -q git+https://github.com/civvic/PanelCleaner.git@testbed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Testing `Tesseract` OCR for Comics\n", + "> Accuracy Enhancements for OCR in `PanelCleaner`\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Prologue" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "\n", + "from pathlib import Path\n", + "from typing import cast\n", + "\n", + "import pcleaner.config as cfg\n", + "import torch\n", + "from rich.console import Console\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "from experiments import *\n", + "from helpers import *\n", + "from ocr_metric import *\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "import fastcore.xtras # patch Path with some utils\n", + "from fastcore.test import * # type: ignore\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Helpers" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "# pretty print by default\n", + "# %load_ext rich" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "#| exporti\n", + "console = Console(width=104, tab_size=4, force_jupyter=True)\n", + "cprint = console.print\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tesseract installation" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['tesseract 5.3.4',\n", + " ' leptonica-1.84.1',\n", + " ' libgif 5.2.1 : libjpeg 8d (libjpeg-turbo 3.0.0) : libpng 1.6.43 : libtiff 4.6.0 : zlib 1.2.11 : libwebp 1.4.0 : libopenjp2 2.5.2',\n", + " ' Found NEON',\n", + " ' Found libarchive 3.7.2 zlib/1.2.11 liblzma/5.4.6 bz2lib/1.0.8 liblz4/1.9.4 libzstd/1.5.6',\n", + " ' Found libcurl/8.4.0 SecureTransport (LibreSSL/3.3.6) zlib/1.2.11 nghttp2/1.51.0']" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "out = !tesseract --version\n", + "out\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Install jpn_vert tesserac lang\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```bash\n", + "cd model\n", + "sudo ln -s jpn_vert_tessdata_best.traineddata /usr/share/tesseract-ocr/5/tessdata/jpn_vert.traineddata\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(Path('/opt/homebrew/share/tessdata'),\n", + " ['afr, amh, ara, asm, aze, aze_cyrl, bel, ben, bod, bos, bre, bul, cat, ceb, ces',\n", + " 'chi_sim, chi_sim_vert, chi_tra, chi_tra_vert, chr, cos, cym, dan, deu, div, dzo, ell, eng, enm, epo',\n", + " 'equ, est, eus, fao, fas, fil, fin, fra, frk, frm, fry, gla, gle, glg, grc',\n", + " 'guj, hat, heb, hin, hrv, hun, hye, iku, ind, isl, ita, ita_old, jav, jpn, jpn_vert',\n", + " 'kan, kat, kat_old, kaz, khm, kir, kmr, kor, kor_vert, lao, lat, lav, lit, ltz, mal',\n", + " 'mar, mkd, mlt, mon, mri, msa, mya, nep, nld, nor, oci, ori, osd, pan, pol',\n", + " 'por, pus, que, ron, rus, san, script/Arabic, script/Armenian, script/Bengali, script/Canadian_Aboriginal, script/Cherokee, script/Cyrillic, script/Devanagari, script/Ethiopic, script/Fraktur',\n", + " 'script/Georgian, script/Greek, script/Gujarati, script/Gurmukhi, script/HanS, script/HanS_vert, script/HanT, script/HanT_vert, script/Hangul, script/Hangul_vert, script/Hebrew, script/Japanese, script/Japanese_vert, script/Kannada, script/Khmer',\n", + " 'script/Lao, script/Latin, script/Malayalam, script/Myanmar, script/Oriya, script/Sinhala, script/Syriac, script/Tamil, script/Telugu, script/Thaana, script/Thai, script/Tibetan, script/Vietnamese, sin, slk',\n", + " 'slv, snd, snum, spa, spa_old, sqi, srp, srp_latn, sun, swa, swe, syr, tam, tat, tel',\n", + " 'tgk, tha, tir, ton, tur, uig, ukr, urd, uzb, uzb_cyrl, vie, yid, yor'])" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "out = !tesseract --list-langs\n", + "tessdata = Path(out[0].split('\"')[1])\n", + "tessdata, [', '.join(sub) for sub in [out[i:i + 15] for i in range(1, len(out), 15)]]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
[\n",
+       "    Path('/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/eng_tessdata_best_410.traineddata'),\n",
+       "    Path('/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/jpn_vert_tessdata_best.traineddata'),\n",
+       "    Path('/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/jpn_tessdata_best.traineddata')\n",
+       "]\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1m[\u001b[0m\n", + " \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/eng_tessdata_best_410.traineddata'\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/jpn_vert_tessdata_best.traineddata'\u001b[0m\u001b[1m)\u001b[0m,\n", + " \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'/Users/vic/dev/repo/DL-mac/PanelCleaner/_testbed/model/jpn_tessdata_best.traineddata'\u001b[0m\u001b[1m)\u001b[0m\n", + "\u001b[1m]\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "langs = tessdata.ls()\n", + "cprint([p.resolve() for p in langs if 'eng' in p.name] + [p.resolve() for p in langs if 'jpn' in p.name])\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "----\n", + "# Tesseract experiments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# PanelCleaner Configuration\n", + "> Adapt `PanelCleaner` `Config` current config to this notebook.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "config = cfg.load_config()\n", + "config.cache_dir = Path(\".\")\n", + "\n", + "cache_dir = config.get_cleaner_cache_dir()\n", + "\n", + "profile = config.current_profile\n", + "preprocessor_conf = profile.preprocessor\n", + "# Modify the profile to OCR all boxes.\n", + "# Make sure OCR is enabled.\n", + "preprocessor_conf.ocr_enabled = True\n", + "# Make sure the max size is infinite, so no boxes are skipped in the OCR process.\n", + "preprocessor_conf.ocr_max_size = 10**10\n", + "# Make sure the sus box min size is infinite, so all boxes with \"unknown\" language are skipped.\n", + "preprocessor_conf.suspicious_box_min_size = 10**10\n", + "# Set the OCR blacklist pattern to match everything, so all text gets reported in the analytics.\n", + "preprocessor_conf.ocr_blacklist_pattern = \".*\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Test images\n", + "> `IMAGE_PATHS` is a list of image file paths that are used as input for testing the OCR methods." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['00: Action_Comics_1960-01-00_(262).JPG',\n", + " '01: Adolf_Cap_01_008.jpg',\n", + " '02: Barnaby_v1-028.png',\n", + " '03: Barnaby_v1-029.png',\n", + " '04: Buck_Danny_-_12_-_Avions_Sans_Pilotes_-_013.jpg',\n", + " '05: Cannon-292.jpg',\n", + " '06: Contrato_con_Dios_028.jpg',\n", + " '07: Erase_una_vez_en_Francia_02_88.jpg',\n", + " '08: FOX_CHILLINTALES_T17_012.jpg',\n", + " '09: Furari_-_Jiro_Taniguchi_selma_056.jpg',\n", + " '10: Galactus_12.jpg',\n", + " '11: INOUE_KYOUMEN_002.png',\n", + " '12: MCCALL_ROBINHOOD_T31_010.jpg',\n", + " '13: MCCAY_LITTLENEMO_090.jpg',\n", + " '14: Mary_Perkins_On_Stage_v2006_1_-_P00068.jpg',\n", + " '15: PIKE_BOYLOVEGIRLS_T41_012.jpg',\n", + " '16: Sal_Buscema_Spaceknights_&_Superheroes_Ocular_Edition_1_1.png',\n", + " '17: Sal_Buscema_Spaceknights_&_Superheroes_Ocular_Edition_1_1_K.png',\n", + " '18: Sal_Buscema_Spaceknights_&_Superheroes_Ocular_Edition_1_2.png',\n", + " '19: Spirou_Et_Fantasio_Integrale_06_1958_1959_0025_0024.jpg',\n", + " '20: Strange_Tales_172005.jpg',\n", + " '21: Strange_Tales_172021.jpg',\n", + " '22: Tarzan_014-21.JPG',\n", + " '23: Tintin_21_Les_Bijoux_de_la_Castafiore_page_39.jpg',\n", + " '24: Transformers_-_Unicron_000-004.jpg',\n", + " '25: Transformers_-_Unicron_000-016.jpg',\n", + " '26: WARE_ACME_024.jpg',\n", + " '27: Yoko_Tsuno_T01_1972-10.jpg',\n", + " '28: Your_Name_Another_Side_Earthbound_T02_084.jpg',\n", + " '29: manga_0033.jpg',\n", + " '30: ronson-031.jpg',\n", + " '31: 哀心迷図のバベル 第01巻 - 22002_00_059.jpg']" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "media_path = Path(\"media/\")\n", + "\n", + "IMAGE_PATHS = sorted(\n", + " [_ for _ in media_path.glob(\"*\") if _.is_file() and _.suffix.lower() in [\".jpg\", \".png\", \".jpeg\"]])\n", + "\n", + "[f\"{i:02}: {_.name}\" for i,_ in enumerate(IMAGE_PATHS)]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CONTEXT\n", + "> `CONTEXT` is an `OCRExperimentContext` object that contains the configuration and the list of image paths.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can get the configuration with `OCRExperimentContext.get_config()`.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Current Configuration:\n", + "\n", + "Locale: System default\n", + "Default Profile: Built-in\n", + "Saved Profiles:\n", + "- victess: /Users/vic/dev/repo/DL-mac/cleaned/victess.conf\n", + "- vicmang: /Users/vic/dev/repo/DL-mac/cleaned/vicmang.conf\n", + "\n", + "Profile Editor: cursor\n", + "Cache Directory: .\n", + "Default Torch Model Path: /Users/vic/Library/Caches/pcleaner/model/comictextdetector.pt\n", + "Default CV2 Model Path: /Users/vic/Library/Caches/pcleaner/model/comictextdetector.pt.onnx\n", + "GUI Theme: System default\n", + "\n", + "--------------------\n", + "\n", + "Config file located at: /Users/vic/Library/Application Support/pcleaner/pcleanerconfig.ini\n", + "System default cache directory: /Users/vic/Library/Caches/pcleaner\n" + ] + }, + { + "data": { + "text/html": [ + "
      cache_dir: Path('cleaner')\n",
+       "     model_path: Path('/Users/vic/Library/Caches/pcleaner/model/comictextdetector.pt')\n",
+       "         device: 'mps'\n",
+       "
\n" + ], + "text/plain": [ + " cache_dir: \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'cleaner'\u001b[0m\u001b[1m)\u001b[0m\n", + " model_path: \u001b[1;35mPath\u001b[0m\u001b[1m(\u001b[0m\u001b[32m'/Users/vic/Library/Caches/pcleaner/model/comictextdetector.pt'\u001b[0m\u001b[1m)\u001b[0m\n", + " device: \u001b[32m'mps'\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "CONTEXT = OCRExperimentContext(None, IMAGE_PATHS)\n", + "\n", + "gpu = torch.cuda.is_available() or torch.backends.mps.is_available()\n", + "model_path = CONTEXT.config.get_model_path(gpu)\n", + "DEVICE = (\"mps\" if torch.backends.mps.is_available() else \"cuda\") if model_path.suffix == \".pt\" else \"cpu\"\n", + "\n", + "CONTEXT.config.show()\n", + "cprint(\n", + " f\"{'cache_dir':>15}: {repr(cache_dir)}\\n\"\n", + " f\"{'model_path':>15}: {repr(model_path)}\\n\"\n", + " f\"{'device':>15}: {repr(DEVICE)}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Base image\n", + "> Change `BASE_IMAGE_IDX` to select a different base image to use in the examples below." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "BASE_IMAGE_IDX: ImgIdT = cast(ImgIdT, CONTEXT.normalize_idx(\"Strange_Tales_172005.jpg\"))\n", + "# BASE_IMAGE_IDX = CONTEXT.normalize_idx(\"0033\")\n", + "# BASE_IMAGE_IDX = CONTEXT.normalize_idx(\"INOUE_KYOUMEN_002\")\n", + "# BASE_IMAGE_IDX = CONTEXT.normalize_idx(\"Action_Comics_1960-01-00_(262)\")\n", + "\n", + "assert BASE_IMAGE_IDX is not None\n", + "img_path = Path(CONTEXT.image_paths[BASE_IMAGE_IDX])\n", + "assert img_path.exists()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Empty cache\n", + "> Clear the image cache used profusely throughout the examples below." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "You will be warned before the cache is emptied." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "# CONTEXT.empty_cache_warn()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# CONTEXT.empty_cache_warn(BASE_IMAGE_IDX)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Base image\n", + "> Change `BASE_IMAGE_IDX` to select a different base image to use in the examples below.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "BASE_IMAGE_IDX: ImgIdT = cast(ImgIdT, CONTEXT.normalize_idx(\"Strange_Tales_172005.jpg\"))\n", + "assert CONTEXT.path_from_idx(BASE_IMAGE_IDX).exists()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualize images\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3de3bd90585a452ab7bd9f5dce716e4e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output(layout=Layout(height='0px'))" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6ab61afc65d84115b81c248ed1d0ab03", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(Dropdown(index=20, layout=Layout(width='fit-content'), options={'Action_Comics_1…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8c79a1393b5a4feaaa8c6d7cf2b458bc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "img_visor = ImageContextVisor(CONTEXT, BASE_IMAGE_IDX)\n", + "img_visor\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tesseract experiments\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d65bd435e6774637ac667defde594c4d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "HBox(children=(HBox(children=(HBox(children=(Dropdown(layout=Layout(width='fit-content'), options={'Tesseract'…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f2ca36a4d81144d59ba835c40b990d34", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# tesseract_experiment = ExperimentsVisor(CONTEXT)\n", + "tesseract_experiment = ExperimentsVisor(CONTEXT, BASE_IMAGE_IDX)\n", + "\n", + "test_eq(tesseract_experiment.all_values, {\n", + " 'image_selector': {'image_idx': 20},\n", + " 'content_selector': {'display_option': DisplayOptions.RESULTS},\n", + " 'result_visor': {\n", + " 'all_boxes': False,\n", + " 'box_idx': 0,\n", + " 'all_methods': False,\n", + " 'method': CropMethod.INITIAL_BOX,\n", + " },\n", + " 'model_selector': {'model': OCRModel.TESSERACT},\n", + " 'self': {}\n", + "})\n", + "\n", + "tesseract_experiment\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}