{"id":112039,"date":"2026-08-17T21:57:32","date_gmt":"2026-08-17T21:57:32","guid":{"rendered":"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/"},"modified":"2026-08-17T21:57:32","modified_gmt":"2026-08-17T21:57:32","slug":"developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs","status":"publish","type":"post","link":"https:\/\/youzum.net\/es\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/","title":{"rendered":"Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs"},"content":{"rendered":"<p class=\"wp-block-paragraph\">In this tutorial, we develop an end-to-end OCR workflow with<a href=\"https:\/\/github.com\/mindee\/doctr\"> <strong>docTR<\/strong><\/a> and explore how modern document understanding pipelines combine text detection, recognition, geometry, layout analysis, structured extraction, and export. We generate realistic synthetic invoice documents, load images and PDFs through DocumentFile, construct GPU-aware OCR predictors, and benchmark different detection\u2013recognition architecture combinations for speed and accuracy. We then inspect the internal Document hierarchy, visualize confidence-aware bounding boxes, use standalone detection and recognition models, implement two-pass recognition for low-confidence words, tune detection thresholds, and introduce custom pipeline hooks for box filtering and padding. We also handle rotated and skewed documents, experiment with layout detection and KIE, reconstruct reading order and tabular information, extract structured invoice fields, and export results as text, JSON, hOCR, synthesized document images, and searchable PDFs. Finally, we examine practical performance, fine-tuning, batching, and deployment considerations to understand how to move from a basic OCR example to a production-oriented document intelligence pipeline.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">import os, sys, io, json, time, math, re, subprocess, warnings\nfrom collections import Counter, defaultdict\nwarnings.filterwarnings(\"ignore\")\nos.environ.setdefault(\"USE_TORCH\", \"1\")\ndef _pip(*pkgs):\n   subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", *pkgs], check=False)\ntry:\n   import doctr\nexcept ImportError:\n   print(\"&gt;&gt; Installing python-doctr (this takes ~1-2 min on Colab)...\")\n   _pip(\"python-doctr[viz]\")\ntry:\n   import reportlab\nexcept ImportError:\n   _pip(\"reportlab\")\nimport numpy as np\nimport torch\nimport matplotlib\nimport matplotlib.pyplot as plt\nfrom matplotlib import font_manager\nfrom matplotlib.patches import Rectangle, Polygon as MplPolygon\nfrom PIL import Image, ImageDraw, ImageFont\nimport doctr\nfrom doctr.io import DocumentFile\nfrom doctr.models import (\n   ocr_predictor,\n   kie_predictor,\n   detection_predictor,\n   recognition_predictor,\n)\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"=\" * 78)\nprint(f\"docTR      : {doctr.__version__}\")\nprint(f\"torch      : {torch.__version__}\")\nprint(f\"device     : {DEVICE}\"\n     + (f\"  ({torch.cuda.get_device_name(0)})\" if DEVICE == \"cuda\" else \"\"))\nprint(f\"python     : {sys.version.split()[0]}\")\nprint(\"=\" * 78)\nprint(\"NOTE: if the import above failed, restart the runtime \"\n     \"(Runtime &gt; Restart session) and re-run this cell.n\")\nCFG = dict(\n   RUN_BENCHMARK   = True,\n   RUN_SECOND_PASS = True,\n   RUN_ROTATION    = True,\n   RUN_LAYOUT      = True,\n   RUN_KIE         = True,\n   RUN_SYNTHESIS   = True,\n   RUN_PDF_EXPORT  = True,\n)\nWORK = \"\/content\/doctr_demo\" if os.path.isdir(\"\/content\") else \".\/doctr_demo\"\nos.makedirs(WORK, exist_ok=True)\nprint(f\"working dir: {WORK}n\")\n_FONT = font_manager.findfont(font_manager.FontProperties(family=\"DejaVu Sans\"))\n_FONT_B = font_manager.findfont(\n   font_manager.FontProperties(family=\"DejaVu Sans\", weight=\"bold\"))\nA4 = (1240, 1754)\nINVOICE_LINES = [\n   ( 80,  70, \"NORTHWIND TRADING CO.\",                    38, True ),\n   ( 80, 122, \"42 Harbour Road, Bristol BS1 5TY\",         22, False),\n   ( 80, 152, \"VAT GB 884 5521 09\",                       22, False),\n   (820,  70, \"INVOICE\",                                  44, True ),\n   (820, 132, \"Invoice No: INV-2024-00817\",               22, False),\n   (820, 162, \"Date: 14\/03\/2024\",                         22, False),\n   (820, 192, \"Due Date: 13\/04\/2024\",                     22, False),\n   ( 80, 260, \"BILL TO\",                                  24, True ),\n   ( 80, 296, \"Aurora Robotics Ltd\",                      24, False),\n   ( 80, 328, \"Unit 7 Fenway Business Park\",              22, False),\n   ( 80, 358, \"Cambridge CB4 0WS\",                        22, False),\n   ( 80, 388, \"Contact: procurement@aurorarobotics.co.uk\",22, False),\n   ( 80, 470, \"DESCRIPTION\",                              24, True ),\n   (640, 470, \"QTY\",                                      24, True ),\n   (780, 470, \"UNIT PRICE\",                               24, True ),\n   (1010,470, \"AMOUNT\",                                   24, True ),\n   ( 80, 520, \"Servo controller board Rev C\",             22, False),\n   (640, 520, \"12\",                                       22, False),\n   (780, 520, \"84.50\",                                    22, False),\n   (1010,520, \"1014.00\",                                  22, False),\n   ( 80, 560, \"Harmonic drive gearbox 50:1\",              22, False),\n   (640, 560, \"4\",                                        22, False),\n   (780, 560, \"312.75\",                                   22, False),\n   (1010,560, \"1251.00\",                                  22, False),\n   ( 80, 600, \"Shielded encoder cable 2m\",                22, False),\n   (640, 600, \"20\",                                       22, False),\n   (780, 600, \"11.40\",                                    22, False),\n   (1010,600, \"228.00\",                                   22, False),\n   ( 80, 640, \"Calibration service on-site\",              22, False),\n   (640, 640, \"1\",                                        22, False),\n   (780, 640, \"450.00\",                                   22, False),\n   (1010,640, \"450.00\",                                   22, False),\n   (780, 720, \"Subtotal\",                                 22, False),\n   (1010,720, \"2943.00\",                                  22, False),\n   (780, 756, \"VAT 20%\",                                  22, False),\n   (1010,756, \"588.60\",                                   22, False),\n   (780, 796, \"TOTAL DUE\",                                26, True ),\n   (1010,796, \"3531.60\",                                  26, True ),\n   ( 80, 900, \"PAYMENT TERMS\",                            24, True ),\n   ( 80, 936, \"Net 30 days. Late payments accrue interest at 2% per month.\", 20, False),\n   ( 80, 968, \"Bank: Lloyds  Sort Code: 30-96-26  Account: 41775302\",       20, False),\n   ( 80,1010, \"Reference: INV-2024-00817\",                20, False),\n]\nPAGE2_LINES = [\n   ( 80,  70, \"APPENDIX A - DELIVERY SCHEDULE\",           34, True ),\n   ( 80, 140, \"All shipments leave the Bristol warehouse before 16:00 GMT.\", 22, False),\n   ( 80, 176, \"Tracking numbers are emailed on the day of dispatch.\",       22, False),\n   ( 80, 240, \"MILESTONE\",                                24, True ),\n   (700, 240, \"TARGET DATE\",                              24, True ),\n   ( 80, 288, \"Purchase order acknowledged\",              22, False),\n   (700, 288, \"18\/03\/2024\",                               22, False),\n   ( 80, 328, \"Controller boards shipped\",                22, False),\n   (700, 328, \"25\/03\/2024\",                               22, False),\n   ( 80, 368, \"Gearboxes shipped\",                        22, False),\n   (700, 368, \"02\/04\/2024\",                               22, False),\n   ( 80, 408, \"On-site calibration window\",               22, False),\n   (700, 408, \"08\/04\/2024\",                               22, False),\n   ( 80, 480, \"Questions? Call +44 117 496 0022 or email ops@northwind.example\", 20, False),\n]\ndef render_page(lines, size=A4, bg=250):\n   \"\"\"Draw a clean document page from a list of (x, y, text, size, bold).\"\"\"\n   img = Image.new(\"RGB\", size, (bg, bg, bg))\n   d = ImageDraw.Draw(img)\n   for x, y, text, sz, bold in lines:\n       font = ImageFont.truetype(_FONT_B if bold else _FONT, sz)\n       d.text((x, y), text, fill=(18, 18, 22), font=font)\n   d.line([(80, 455), (1160, 455)], fill=(60, 60, 60), width=2)\n   d.line([(80, 505), (1160, 505)], fill=(160, 160, 160), width=1)\n   d.line([(760, 700), (1160, 700)], fill=(60, 60, 60), width=2)\n   return img\ndef scanify(img, angle=0.0, noise=6.0, jpeg_quality=72, blur_shadow=True):\n   \"\"\"Degrade a clean render so it behaves like a phone photo \/ flatbed scan.\"\"\"\n   if angle:\n       img = img.rotate(angle, expand=True, resample=Image.BICUBIC,\n                        fillcolor=(250, 250, 250))\n   arr = np.asarray(img).astype(np.float32)\n   if blur_shadow:\n       h, w = arr.shape[:2]\n       gx = np.linspace(-1, 1, w)[None, :]\n       gy = np.linspace(-1, 1, h)[:, None]\n       shade = 1.0 - 0.10 * (gx ** 2 + 0.6 * gy ** 2)\n       arr *= shade[..., None]\n   if noise:\n       arr += np.random.normal(0, noise, arr.shape)\n   arr = np.clip(arr, 0, 255).astype(np.uint8)\n   out = Image.fromarray(arr)\n   if jpeg_quality:\n       buf = io.BytesIO()\n       out.save(buf, format=\"JPEG\", quality=jpeg_quality)\n       buf.seek(0)\n       out = Image.open(buf).convert(\"RGB\")\n   return out\nclean1 = render_page(INVOICE_LINES)\nclean2 = render_page(PAGE2_LINES)\npage1_path   = os.path.join(WORK, \"invoice_p1.png\")\npage2_path   = os.path.join(WORK, \"invoice_p2.png\")\nrotated_path = os.path.join(WORK, \"invoice_rotated.png\")\npdf_path     = os.path.join(WORK, \"invoice.pdf\")\nscanify(clean1, angle=0.4).save(page1_path)\nscanify(clean2, angle=-0.3).save(page2_path)\nscanify(clean1, angle=13.0, noise=8.0).save(rotated_path)\nclean1.save(pdf_path, save_all=True, append_images=[clean2], resolution=150)\nGT_WORDS_P1 = [w for _, _, t, _, _ in INVOICE_LINES for w in t.split()]\nprint(f\"generated: {page1_path}, {page2_path}, {rotated_path}, {pdf_path}\")\nprint(f\"ground-truth words on page 1: {len(GT_WORDS_P1)}n\")\nfig, ax = plt.subplots(1, 3, figsize=(15, 7))\nfor a, im, t in zip(ax, [Image.open(page1_path), Image.open(page2_path),\n                        Image.open(rotated_path)],\n                   [\"page 1 (scanified)\", \"page 2\", \"rotated 13 deg\"]):\n   a.imshow(im); a.set_title(t, fontsize=10); a.axis(\"off\")\nplt.tight_layout(); plt.show()\nimgs_doc  = DocumentFile.from_images([page1_path, page2_path])\npdf_doc   = DocumentFile.from_pdf(pdf_path)\npdf_hi    = DocumentFile.from_pdf(pdf_path, scale=3)\nrot_doc   = DocumentFile.from_images(rotated_path)\nprint(\"from_images :\", [p.shape for p in imgs_doc], imgs_doc[0].dtype)\nprint(\"from_pdf    :\", [p.shape for p in pdf_doc])\nprint(\"from_pdf x3 :\", [p.shape for p in pdf_hi])\nprint(\"\"\"\nRules of thumb for `scale`:\n * body text should be &gt;= ~10 px tall for the recognition model to be happy\n * scale=2 (default) suits 150-300 dpi scans; bump to 3-4 for dense 8pt text\n * you can also pass raw numpy arrays straight to any predictor:\n       predictor([np.asarray(pil_image)])\n * DocumentFile.from_url(...) exists too, but needs the [html] extra\n\"\"\")\ndef build_ocr(det=\"db_resnet50\", reco=\"crnn_vgg16_bn\", **kw):\n   \"\"\"Construct an OCR predictor and move it to the GPU when there is one.\"\"\"\n   model = ocr_predictor(det_arch=det, reco_arch=reco, pretrained=True, **kw)\n   if DEVICE == \"cuda\":\n       try:\n           model = model.cuda()\n       except Exception as e:\n           print(f\"  (cuda placement skipped: {e})\")\n   return model\ndef timeit(fn, *args, warmup=1, runs=3, **kw):\n   \"\"\"Warm up (weight load \/ cudnn autotune \/ lazy init), then time properly.\"\"\"\n   for _ in range(warmup):\n       fn(*args, **kw)\n   if DEVICE == \"cuda\":\n       torch.cuda.synchronize()\n   t0 = time.perf_counter()\n   out = None\n   for _ in range(runs):\n       out = fn(*args, **kw)\n   if DEVICE == \"cuda\":\n       torch.cuda.synchronize()\n   return out, (time.perf_counter() - t0) \/ runs\npredictor = build_ocr()\nresult, dt = timeit(predictor, imgs_doc, runs=2)\nprint(f\"nbaseline end-to-end: {dt:.2f}s for {len(imgs_doc)} pages \"\n     f\"({dt\/len(imgs_doc):.2f}s\/page on {DEVICE})\")\nprint(f\"first 90 chars of page 1: {result.pages[0].render()[:90]!r}\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We set up the docTR environment, install the required dependencies, detect GPU availability, and configure the tutorial runtime. We generate synthetic invoice pages, apply realistic scan degradations, load images and PDFs through DocumentFile, and prepare ground-truth text for evaluation. We then construct the baseline OCR predictor and measure end-to-end inference performance across the generated document pages.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">def norm(w):\n   return re.sub(r\"[^w@:.\/+-]\", \"\", w.lower())\ndef bag_accuracy(gt_words, pred_words):\n   \"\"\"Order-insensitive word recall \u2014 good enough to rank models quickly.\"\"\"\n   g, p = Counter(map(norm, gt_words)), Counter(map(norm, pred_words))\n   return sum((g &amp; p).values()) \/ max(len(gt_words), 1)\ndef page_words(page):\n   return [w.value for b in page.blocks for l in b.lines for w in l.words]\nif CFG[\"RUN_BENCHMARK\"]:\n   combos = [\n       (\"db_mobilenet_v3_large\", \"crnn_mobilenet_v3_small\"),\n       (\"fast_base\",             \"crnn_vgg16_bn\"),\n       (\"db_resnet50\",           \"crnn_vgg16_bn\"),\n       (\"db_resnet50\",           \"parseq\"),\n   ]\n   rows = []\n   for det, reco in combos:\n       try:\n           m = build_ocr(det, reco)\n           res, dt = timeit(m, [imgs_doc[0]], warmup=1, runs=2)\n           pw = page_words(res.pages[0])\n           rows.append((f\"{det} + {reco}\", dt, len(pw), bag_accuracy(GT_WORDS_P1, pw)))\n           del m\n           if DEVICE == \"cuda\":\n               torch.cuda.empty_cache()\n       except Exception as e:\n           rows.append((f\"{det} + {reco}\", float(\"nan\"), 0, float(\"nan\")))\n           print(f\"  !! {det}+{reco} failed: {e}\")\n   print(\"n\" + \"-\" * 78)\n   print(f\"{'architecture':&lt;46}{'sec\/page':&gt;10}{'#words':&gt;9}{'word acc':&gt;11}\")\n   print(\"-\" * 78)\n   for name, dt, n, acc in rows:\n       print(f\"{name:&lt;46}{dt:&gt;10.2f}{n:&gt;9}{acc:&gt;10.1%}\")\n   print(\"-\" * 78)\n   print(\"\"\"\nReading the table:\n * detection choice drives RECALL (#words found); recognition drives accuracy\n * mobilenet variants are 5-10x cheaper and lose only a couple of points on\n   clean documents \u2014 they are usually the right default for bulk pipelines\n * parseq \/ master are worth it on noisy, handwritten or curved text only\n * these numbers are for ONE synthetic page; always benchmark on your own data\n\"\"\")\npage = result.pages[0]\nprint(f\"page dimensions   : {page.dimensions}   (H, W in px)\")\nprint(f\"page orientation  : {page.orientation}\")\nprint(f\"page language     : {page.language}\")\nprint(f\"blocks\/lines\/words: {len(page.blocks)}, \"\n     f\"{sum(len(b.lines) for b in page.blocks)}, {len(page_words(page))}n\")\nfor b_i, block in enumerate(page.blocks[:1]):\n   print(f\"Block {b_i}  geometry={np.round(np.array(block.geometry), 3).tolist()}\")\n   for l_i, line in enumerate(block.lines[:2]):\n       print(f\"  Line {l_i}: {' '.join(w.value for w in line.words)}\")\n       for w in line.words[:4]:\n           geo = np.round(np.array(w.geometry), 4).tolist()\n           print(f\"    Word {w.value!r:&lt;22} conf={w.confidence:.3f} \"\n                 f\"objectness={getattr(w, 'objectness_score', None)} \"\n                 f\"crop_orient={getattr(w, 'crop_orientation', None)}\")\n           print(f\"      geometry={geo}\")\nprint(\"\"\"\nKey facts about geometry:\n * coordinates are RELATIVE (0-1), so multiply by (W, H) to get pixels\n * assume_straight_pages=True  -&gt; ((xmin, ymin), (xmax, ymax))\n * assume_straight_pages=False -&gt; a 4-point polygon [(x,y) x 4], clockwise\n * confidence       = recognition softmax confidence for the whole word\n * objectness_score = how sure the DETECTOR was that this is text\n   -&gt; filter on objectness to kill hallucinated boxes, on confidence to\n      flag words a human should review. They fail differently.\n\"\"\")\ndef geom_to_pixels(geom, w, h):\n   g = np.asarray(geom, dtype=np.float32)\n   if g.ndim == 2 and g.shape == (2, 2):\n       (x0, y0), (x1, y1) = g\n       return np.array([[x0, y0], [x1, y0], [x1, y1], [x0, y1]]) * [w, h]\n   return g[:4] * [w, h]\ndef draw_result(page_obj, image, title=\"\", min_conf=0.0, figsize=(13, 18),\n               label=True):\n   img = np.asarray(image)\n   h, w = img.shape[:2]\n   cmap = matplotlib.colormaps[\"RdYlGn\"]\n   fig, ax = plt.subplots(figsize=figsize)\n   ax.imshow(img); ax.axis(\"off\"); ax.set_title(title)\n   for block in page_obj.blocks:\n       for line in block.lines:\n           for word in line.words:\n               if word.confidence &lt; min_conf:\n                   continue\n               pts = geom_to_pixels(word.geometry, w, h)\n               c = cmap(float(word.confidence))\n               ax.add_patch(MplPolygon(pts, closed=True, fill=False,\n                                       edgecolor=c, linewidth=1.4))\n               if label and word.confidence &lt; 0.85:\n                   ax.text(pts[:, 0].min(), pts[:, 1].min() - 4,\n                           f\"{word.value} {word.confidence:.2f}\",\n                           fontsize=6, color=\"crimson\")\n   sm = matplotlib.cm.ScalarMappable(cmap=cmap,\n                                     norm=matplotlib.colors.Normalize(0, 1))\n   fig.colorbar(sm, ax=ax, fraction=0.025, label=\"recognition confidence\")\n   plt.tight_layout(); plt.show()\ndraw_result(page, imgs_doc[0], \"page 1 \u2014 words coloured by confidence\")\nconfs = [w.confidence for w in\n        (wd for b in page.blocks for l in b.lines for wd in l.words)]\nprint(f\"confidence: mean={np.mean(confs):.3f}  p10={np.percentile(confs,10):.3f}  \"\n     f\"min={np.min(confs):.3f}   below 0.8: {sum(c &lt; .8 for c in confs)} words\")\ndet = detection_predictor(\"db_resnet50\", pretrained=True,\n                         assume_straight_pages=True, preserve_aspect_ratio=True)\nif DEVICE == \"cuda\":\n   det = det.cuda()\ndet_out = det([imgs_doc[0]])[0]\nkey = list(det_out.keys())[0]\nboxes = det_out[key]\nprint(f\"detection output: key={key!r} shape={boxes.shape}  \"\n     f\"(last column is the objectness score)\")\nprint(\"first 3 boxes (relative):n\", np.round(boxes[:3], 4))\ndef crop_words(image, boxes, pad=0.004):\n   \"\"\"Cut relative boxes out of an image, with a little padding.\"\"\"\n   img = np.asarray(image)\n   h, w = img.shape[:2]\n   crops = []\n   for b in boxes:\n       x0, y0, x1, y1 = b[:4]\n       x0 = int(max(0, (x0 - pad)) * w); x1 = int(min(1, (x1 + pad)) * w)\n       y0 = int(max(0, (y0 - pad)) * h); y1 = int(min(1, (y1 + pad)) * h)\n       if x1 &gt; x0 + 2 and y1 &gt; y0 + 2:\n           crops.append(img[y0:y1, x0:x1])\n   return crops\ncrops = crop_words(imgs_doc[0], boxes)\nprint(f\"nextracted {len(crops)} crops\")\nreco = recognition_predictor(\"crnn_vgg16_bn\", pretrained=True)\nif DEVICE == \"cuda\":\n   reco = reco.cuda()\nreco_out = reco(crops[:24])\nprint(\"crop-level predictions (text, confidence):\")\nprint(reco_out[:8])\nprint(f\"nmodel vocab ({len(reco.model.cfg['vocab'])} chars): \"\n     f\"{reco.model.cfg['vocab'][:70]}...\")\nprint(\"\"\"\nThe vocab matters: the default checkpoints ship with a French\/Latin vocab.\nIf your text contains characters outside it, the model literally cannot emit\nthem and you must fine-tune with a wider `vocab` (see doctr.datasets.VOCABS).\n\"\"\")\nfig, axes = plt.subplots(4, 3, figsize=(11, 5))\nfor a, c, (txt, cf) in zip(axes.ravel(), crops, reco_out):\n   a.imshow(c); a.axis(\"off\"); a.set_title(f\"{txt} ({cf:.2f})\", fontsize=8)\nplt.tight_layout(); plt.show()\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We benchmark multiple detection and recognition architecture combinations to compare their processing speed, detected word count, and recognition accuracy. We inspect the hierarchical docTR Document structure and visualize detected words using their geometries and recognition confidence scores. We also separate text detection from recognition, extract individual word crops, and examine how standalone recognition models process detected regions.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">if CFG[\"RUN_SECOND_PASS\"]:\n   CONF_GATE = 0.85\n   fast_model = build_ocr(\"db_resnet50\", \"crnn_mobilenet_v3_small\")\n   res_fast = fast_model([imgs_doc[0]])\n   pg = res_fast.pages[0]\n   weak = [(w, w.geometry) for b in pg.blocks for l in b.lines for w in l.words\n           if w.confidence &lt; CONF_GATE]\n   print(f\"pass 1 (crnn_mobilenet_v3_small): {len(page_words(pg))} words, \"\n         f\"{len(weak)} below {CONF_GATE}\")\n   if weak:\n       h, w_ = imgs_doc[0].shape[:2]\n       rects = []\n       for _, g in weak:\n           pts = geom_to_pixels(g, 1.0, 1.0)\n           rects.append([pts[:, 0].min(), pts[:, 1].min(),\n                         pts[:, 0].max(), pts[:, 1].max()])\n       weak_crops = crop_words(imgs_doc[0], np.array(rects), pad=0.006)\n       strong = recognition_predictor(\"parseq\", pretrained=True)\n       if DEVICE == \"cuda\":\n           strong = strong.cuda()\n       redo = strong(weak_crops)\n       print(f\"n{'before':&lt;26}{'conf':&gt;7}   {'after (parseq)':&lt;26}{'conf':&gt;7}\")\n       print(\"-\" * 72)\n       changed = 0\n       for (word, _), (new_txt, new_cf) in zip(weak, redo):\n           flag = \"  &lt;-- changed\" if new_txt != word.value else \"\"\n           changed += new_txt != word.value\n           print(f\"{word.value:&lt;26}{word.confidence:&gt;7.3f}   \"\n                 f\"{new_txt:&lt;26}{new_cf:&gt;7.3f}{flag}\")\n       print(f\"n{changed}\/{len(weak)} words revised, \"\n             f\"but parseq only ran on {len(weak)\/max(len(page_words(pg)),1):.0%} \"\n             f\"of the crops.\")\n   del fast_model\ntuner = build_ocr(\"db_resnet50\", \"crnn_vgg16_bn\")\npp = tuner.det_predictor.model.postprocessor\norig = (pp.bin_thresh, pp.box_thresh)\nprint(f\"defaults: bin_thresh={orig[0]}, box_thresh={orig[1]}n\")\nprint(f\"{'bin':&gt;6}{'box':&gt;7}{'#words':&gt;9}{'mean conf':&gt;12}{'sec':&gt;8}\")\nprint(\"-\" * 42)\nfor bin_t, box_t in [(0.1, 0.05), (0.3, 0.1), (0.5, 0.2), (0.7, 0.4), (0.9, 0.6)]:\n   pp.bin_thresh, pp.box_thresh = bin_t, box_t\n   t0 = time.perf_counter()\n   r = tuner([imgs_doc[0]])\n   dt = time.perf_counter() - t0\n   ws = [w for b in r.pages[0].blocks for l in b.lines for w in l.words]\n   mc = np.mean([w.confidence for w in ws]) if ws else 0\n   print(f\"{bin_t:&gt;6}{box_t:&gt;7}{len(ws):&gt;9}{mc:&gt;12.3f}{dt:&gt;8.2f}\")\npp.bin_thresh, pp.box_thresh = orig\nprint(\"\"\"\nHow to tune in practice:\n * LOW thresholds  -&gt; more boxes: faint stamps, dot-matrix, carbon copies.\n                      Cost: noise boxes, which you then filter by objectness.\n * HIGH thresholds -&gt; fewer, cleaner boxes for crisp born-digital scans.\n * Sweep against a small labelled set and optimise F1, not eyeballs.\n\"\"\")\nclass PadBoxesHook:\n   \"\"\"Recognition often improves when crops aren't cut flush to the glyphs.\"\"\"\n   def __init__(self, dx=0.004, dy=0.006):\n       self.dx, self.dy = dx, dy\n   def _pad(self, arr):\n       a = np.array(arr, copy=True, dtype=np.float32)\n       if a.ndim == 2 and a.shape[-1] &gt;= 4:\n           a[:, 0] = np.clip(a[:, 0] - self.dx, 0, 1)\n           a[:, 1] = np.clip(a[:, 1] - self.dy, 0, 1)\n           a[:, 2] = np.clip(a[:, 2] + self.dx, 0, 1)\n           a[:, 3] = np.clip(a[:, 3] + self.dy, 0, 1)\n       elif a.ndim == 3:\n           pts = a[:, :4, :]\n           ctr = pts.mean(axis=1, keepdims=True)\n           a[:, :4, :] = np.clip(ctr + (pts - ctr) * 1.06, 0, 1)\n       return a\n   def __call__(self, loc_preds):\n       out = []\n       for p in loc_preds:\n           out.append({k: self._pad(v) for k, v in p.items()}\n                      if isinstance(p, dict) else self._pad(p))\n       return out\nclass DropTinyBoxesHook:\n   \"\"\"Kill speckle boxes before they waste a recognition forward pass.\"\"\"\n   def __init__(self, min_h=0.006, min_w=0.004):\n       self.min_h, self.min_w = min_h, min_w\n   def _filt(self, arr):\n       a = np.asarray(arr)\n       if a.ndim == 2 and a.shape[-1] &gt;= 4:\n           keep = ((a[:, 2] - a[:, 0]) &gt; self.min_w) &amp; \n                  ((a[:, 3] - a[:, 1]) &gt; self.min_h)\n           return a[keep]\n       if a.ndim == 3:\n           pts = a[:, :4, :]\n           wd = pts[..., 0].max(1) - pts[..., 0].min(1)\n           ht = pts[..., 1].max(1) - pts[..., 1].min(1)\n           return a[(wd &gt; self.min_w) &amp; (ht &gt; self.min_h)]\n       return a\n   def __call__(self, loc_preds):\n       return [{k: self._filt(v) for k, v in p.items()} if isinstance(p, dict)\n               else self._filt(p) for p in loc_preds]\nhooked = build_ocr(\"db_resnet50\", \"crnn_vgg16_bn\")\nbefore = hooked([imgs_doc[0]]).pages[0]\nhooked.add_hook(DropTinyBoxesHook())\nhooked.add_hook(PadBoxesHook())\nafter = hooked([imgs_doc[0]]).pages[0]\nbw, aw = page_words(before), page_words(after)\nprint(f\"no hooks : {len(bw):&gt;4} words  mean conf \"\n     f\"{np.mean([w.confidence for b in before.blocks for l in b.lines for w in l.words]):.4f}\")\nprint(f\"hooked   : {len(aw):&gt;4} words  mean conf \"\n     f\"{np.mean([w.confidence for b in after.blocks for l in b.lines for w in l.words]):.4f}\")\nprint(f\"word accuracy vs GT: {bag_accuracy(GT_WORDS_P1, bw):.1%} -&gt; \"\n     f\"{bag_accuracy(GT_WORDS_P1, aw):.1%}\")\nprint(\"\"\"\nOther things hooks are good for:\n * snapping boxes to a known form template \/ table grid\n * merging boxes that the detector split across a hyphen or thin space\n * masking a redacted region so its crops never reach the recogniser\n\"\"\")\nif CFG[\"RUN_ROTATION\"]:\n   print(\"Three strategies for non-straight pages:n\"\n         \"  A) assume_straight_pages=True   fastest, breaks past ~5 deg skewn\"\n         \"  B) assume_straight_pages=False  returns 4-point polygonsn\"\n         \"  C) straighten_pages=True        de-skews the page first, then An\")\n   variants = {\n       \"A straight (default)\": dict(assume_straight_pages=True),\n       \"B polygons\":           dict(assume_straight_pages=False,\n                                    preserve_aspect_ratio=True),\n       \"C straighten first\":   dict(assume_straight_pages=False,\n                                    straighten_pages=True,\n                                    detect_orientation=True),\n       \"B' polygons -&gt; boxes\": dict(assume_straight_pages=False,\n                                    export_as_straight_boxes=True),\n   }\n   rot_results = {}\n   for name, kw in variants.items():\n       try:\n           m = build_ocr(\"db_resnet50\", \"crnn_vgg16_bn\", **kw)\n           t0 = time.perf_counter()\n           r = m(rot_doc)\n           dt = time.perf_counter() - t0\n           p = r.pages[0]\n           ws = page_words(p)\n           rot_results[name] = (r, p)\n           print(f\"{name:&lt;24} words={len(ws):&gt;4}  acc={bag_accuracy(GT_WORDS_P1, ws):&gt;6.1%}  \"\n                 f\"{dt:&gt;5.2f}s  orientation={p.orientation}\")\n           del m\n       except Exception as e:\n           print(f\"{name:&lt;24} failed: {e}\")\n   if \"B polygons\" in rot_results:\n       draw_result(rot_results[\"B polygons\"][1], rot_doc[0],\n                   \"rotated page \u2014 polygon boxes\", figsize=(11, 14), label=False)\n   print(\"\"\"\nExtra speed switches once you know your data:\n disable_page_orientation=True  skip the 0\/90\/180\/270 page classifier\n disable_crop_orientation=True  skip the per-word orientation classifier\nBoth only matter when assume_straight_pages=False \/ straighten_pages=True.\n\"\"\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We implement a two-pass recognition strategy that identifies low-confidence words and reprocesses only those crops with a stronger PARSeq recognizer. We tune detection post-processing thresholds and introduce custom hooks that filter small detections and pad bounding boxes before recognition. We also evaluate different strategies for handling rotated and skewed documents, including polygon-based detection, page straightening, and orientation detection.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">if CFG[\"RUN_LAYOUT\"]:\n   try:\n       lay = ocr_predictor(pretrained=True, detect_layout=True)\n       if DEVICE == \"cuda\":\n           lay = lay.cuda()\n       lres = lay(imgs_doc)\n       lpage = lres.pages[0]\n       regions = getattr(lpage, \"layout\", []) or []\n       print(f\"detected {len(regions)} layout regions on page 1:\")\n       counts = Counter()\n       for r in regions:\n           counts[r.type] += 1\n           print(f\"  {r.type:&lt;16} conf={r.confidence:.3f}  \"\n                 f\"geom={np.round(np.array(r.geometry), 3).tolist()}\")\n       print(\"nregion histogram:\", dict(counts))\n       h, w = imgs_doc[0].shape[:2]\n       colors = {\"Title\": \"tab:red\", \"Text\": \"tab:blue\", \"Table\": \"tab:green\",\n                 \"Page-header\": \"tab:orange\", \"Page-footer\": \"tab:purple\"}\n       fig, ax = plt.subplots(figsize=(10, 14))\n       ax.imshow(imgs_doc[0]); ax.axis(\"off\")\n       ax.set_title(\"layout regions\")\n       for r in regions:\n           pts = geom_to_pixels(r.geometry, w, h)\n           ax.add_patch(MplPolygon(pts, closed=True, fill=False, linewidth=2.2,\n                                   edgecolor=colors.get(r.type, \"black\")))\n           ax.text(pts[:, 0].min(), pts[:, 1].min() - 6, r.type, fontsize=9,\n                   color=colors.get(r.type, \"black\"))\n       plt.tight_layout(); plt.show()\n       print(\"\"\"\nWhy layout matters: it gives you *document structure*, not just text. Route\nTable regions to a table parser, drop Page-header\/Page-footer before feeding\nan LLM, and use Title regions to chunk long documents sensibly.\n\"\"\")\n       del lay\n   except TypeError:\n       print(\"detect_layout not supported by this docTR version \"\n             \"(needs &gt;= 1.0) \u2014 upgrade with: pip install -U python-doctr\")\n   except Exception as e:\n       print(f\"layout detection unavailable: {e}\")\nif CFG[\"RUN_KIE\"]:\n   kie = kie_predictor(det_arch=\"db_resnet50\", reco_arch=\"crnn_vgg16_bn\",\n                       pretrained=True)\n   if DEVICE == \"cuda\":\n       kie = kie.cuda()\n   kres = kie([imgs_doc[0]])\n   preds = kres.pages[0].predictions\n   for cls, items in preds.items():\n       print(f\"class {cls!r}: {len(items)} predictions\")\n       for p in items[:5]:\n           print(f\"   {p.value!r:&lt;24} conf={p.confidence:.3f} \"\n                 f\"geom={np.round(np.array(p.geometry), 3).tolist()}\")\n   print(\"\"\"\nTo make this genuinely useful, train a detection model with several classes\n(references\/detection\/train_pytorch.py with a multi-class label file), e.g.\nclasses = [\"invoice_number\", \"total\", \"date\"]. Then KIE returns exactly those\nfields already transcribed \u2014 no regex layer required.\n\"\"\")\n   del kie\nres = predictor(imgs_doc)\ntxt = res.render()\nprint(\"--- render() -------------------------------------------------------\")\nprint(txt[:320], \"...n\")\nopen(os.path.join(WORK, \"output.txt\"), \"w\").write(txt)\njs = res.export()\nprint(\"--- export() keys --------------------------------------------------\")\nprint(\"document:\", list(js.keys()))\nprint(\"page    :\", list(js[\"pages\"][0].keys()))\nprint(\"word    :\", list(js[\"pages\"][0][\"blocks\"][0][\"lines\"][0][\"words\"][0].keys()))\nwith open(os.path.join(WORK, \"output.json\"), \"w\") as f:\n   json.dump(js, f, indent=2, default=str)\nxml_out = res.export_as_xml()\nxml_bytes, xml_tree = xml_out[0]\nprint(\"n--- export_as_xml() (hOCR) ----------------------------------------\")\nprint(xml_bytes.decode()[:520], \"...\")\nfor i, (b, _) in enumerate(xml_out):\n   open(os.path.join(WORK, f\"page_{i+1}.hocr\"), \"wb\").write(b)\nif CFG[\"RUN_SYNTHESIS\"]:\n   synth = res.synthesize()\n   fig, ax = plt.subplots(1, 2, figsize=(14, 10))\n   ax[0].imshow(imgs_doc[0]); ax[0].set_title(\"original\"); ax[0].axis(\"off\")\n   ax[1].imshow(synth[0]);    ax[1].set_title(\"synthesize()\"); ax[1].axis(\"off\")\n   plt.tight_layout(); plt.show()\n   print(\"synthesize() re-renders text into the detected boxes. If the \"\n         \"reconstruction looks right, geometry AND transcription are both OK.\")\npg = res.pages[0]\nH, W = pg.dimensions\ndef word_rect(word):\n   \"\"\"Relative geometry -&gt; (x0, y0, x1, y1) axis-aligned, works for polygons.\"\"\"\n   p = np.asarray(word.geometry, dtype=np.float32)\n   if p.shape == (2, 2):\n       return float(p[0, 0]), float(p[0, 1]), float(p[1, 0]), float(p[1, 1])\n   return (float(p[:, 0].min()), float(p[:, 1].min()),\n           float(p[:, 0].max()), float(p[:, 1].max()))\nflat = []\nfor b in pg.blocks:\n   for l in b.lines:\n       for w in l.words:\n           x0, y0, x1, y1 = word_rect(w)\n           flat.append(dict(text=w.value, conf=w.confidence,\n                            x0=x0, y0=y0, x1=x1, y1=y1,\n                            cx=(x0 + x1) \/ 2, cy=(y0 + y1) \/ 2, h=y1 - y0))\ndef group_rows(words, tol_factor=0.6):\n   ws = sorted(words, key=lambda d: d[\"cy\"])\n   rows, cur, ref = [], [], None\n   for w in ws:\n       tol = max(w[\"h\"] * tol_factor, 0.004)\n       if ref is None or abs(w[\"cy\"] - ref) &lt;= tol:\n           cur.append(w); ref = np.mean([c[\"cy\"] for c in cur])\n       else:\n           rows.append(sorted(cur, key=lambda d: d[\"x0\"])); cur, ref = [w], w[\"cy\"]\n   if cur:\n       rows.append(sorted(cur, key=lambda d: d[\"x0\"]))\n   return rows\nrows = group_rows(flat)\nprint(f\"--- reading order: {len(rows)} rows ---\")\nfor r in rows[:8]:\n   print(\"   \" + \" \".join(w[\"text\"] for w in r))\nfull_text = \"n\".join(\" \".join(w[\"text\"] for w in r) for r in rows)\nFIELDS = {\n   \"invoice_no\":  r\"Invoices*No[:s]*([A-Z0-9-]+)\",\n   \"date\":        r\"bDate[:s]*(d{2}\/d{2}\/d{4})\",\n   \"due_date\":    r\"Dues*Date[:s]*(d{2}\/d{2}\/d{4})\",\n   \"vat_id\":      r\"VATs*(GB[sd]{8,})\",\n   \"total_due\":   r\"TOTALs*DUEs*([d.,]+)\",\n   \"subtotal\":    r\"Subtotals*([d.,]+)\",\n   \"email\":       r\"([w.+-]+@[w-]+.[w.]+)\",\n   \"sort_code\":   r\"Sorts*Code[:s]*([d-]{6,10})\",\n}\nprint(\"n--- extracted fields ---\")\nextracted = {}\nfor name, pat in FIELDS.items():\n   m = re.search(pat, full_text, flags=re.IGNORECASE)\n   extracted[name] = m.group(1).strip() if m else None\n   print(f\"  {name:&lt;12}: {extracted[name]}\")\ndef detect_columns(rows, y_lo, y_hi, gap=0.03):\n   \"\"\"1-D clustering of word left-edges inside a band -&gt; column boundaries.\"\"\"\n   xs = sorted(w[\"x0\"] for r in rows for w in r if y_lo &lt;= w[\"cy\"] &lt;= y_hi)\n   if not xs:\n       return []\n   cols, cur = [], [xs[0]]\n   for x in xs[1:]:\n       if x - cur[-1] &lt; gap:\n           cur.append(x)\n       else:\n           cols.append(cur)\n           cur = [x]\n   cols.append(cur)\n   return [float(np.min(c)) for c in cols if c]\nband_lo, band_hi = 0.25, 0.40\ncol_x = detect_columns(rows, band_lo, band_hi)\nprint(f\"n--- table: {len(col_x)} columns at x={np.round(col_x, 3).tolist()} ---\")\ntable = []\nfor r in rows:\n   if not (band_lo &lt;= np.mean([w[\"cy\"] for w in r]) &lt;= band_hi):\n       continue\n   cells = [\"\"] * len(col_x)\n   for w in r:\n       idx = int(np.argmin([abs(w[\"x0\"] - cx) for cx in col_x]))\n       cells[idx] = (cells[idx] + \" \" + w[\"text\"]).strip()\n   table.append(cells)\nfor row in table:\n   print(\"  | \" + \" | \".join(f\"{c:&lt;28}\" if i == 0 else f\"{c:&lt;10}\"\n                             for i, c in enumerate(row)))\nprint(\"\"\"\nEscalation path when this gets hairy:\n * per-page dict -&gt; pandas.DataFrame for downstream joins\n * detect_layout=True to isolate Table regions before column clustering\n * or hand result.render() \/ the hOCR to an LLM for schema-guided extraction \u2014\n   docTR's job is faithful text + geometry, not semantics\n\"\"\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We extend the OCR pipeline with layout detection and KIE capabilities to identify document regions and support structured information extraction. We export OCR results into plain text, JSON, hOCR, and synthesized document representations while preserving text and geometry information. We then reconstruct reading order, extract invoice fields with regular expressions, and organize detected words into table-like structures using their spatial coordinates.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\"no-line-numbers\"><code class=\"no-wrap language-php\">if CFG[\"RUN_PDF_EXPORT\"]:\n   from reportlab.pdfgen import canvas as rl_canvas\n   from reportlab.lib.utils import ImageReader\n   def make_searchable_pdf(pages_np, doc_result, out_path, dpi=150):\n       c = rl_canvas.Canvas(out_path)\n       for img_np, page_obj in zip(pages_np, doc_result.pages):\n           h_px, w_px = img_np.shape[:2]\n           w_pt, h_pt = w_px * 72.0 \/ dpi, h_px * 72.0 \/ dpi\n           c.setPageSize((w_pt, h_pt))\n           c.drawImage(ImageReader(Image.fromarray(img_np)), 0, 0,\n                       width=w_pt, height=h_pt)\n           c.setFillColorRGB(0, 0, 0)\n           for b in page_obj.blocks:\n               for l in b.lines:\n                   for wd in l.words:\n                       if not wd.value.strip():\n                           continue\n                       x0, y0, x1, y1 = word_rect(wd)\n                       bx, by = x0 * w_pt, (1 - y1) * h_pt\n                       bw_, bh_ = (x1 - x0) * w_pt, (y1 - y0) * h_pt\n                       size = max(bh_ * 0.82, 1.0)\n                       t = c.beginText()\n                       t.setTextRenderMode(3)\n                       t.setFont(\"Helvetica\", size)\n                       adv = c.stringWidth(wd.value, \"Helvetica\", size) or 1.0\n                       t.setHorizScale(100.0 * bw_ \/ adv)\n                       t.setTextOrigin(bx, by + bh_ * 0.18)\n                       t.textOut(wd.value)\n                       c.drawText(t)\n           c.showPage()\n       c.save()\n       return out_path\n   out_pdf = make_searchable_pdf(imgs_doc, res,\n                                 os.path.join(WORK, \"invoice_searchable.pdf\"))\n   print(f\"searchable PDF written: {out_pdf} \"\n         f\"({os.path.getsize(out_pdf)\/1024:.0f} KB)\")\n   print(\"Open it and Ctrl+F for 'INV-2024-00817' \u2014 the scan is unchanged, \"\n         \"but the text is selectable.\")\n   try:\n       from google.colab import files\n       print(\"Run  files.download(out_pdf)  to pull it down from Colab.\")\n   except ImportError:\n       pass\nprint(\"\"\"\n=============================== PERFORMANCE ==================================\nBatch sizes (biggest single lever on GPU):\n   ocr_predictor(pretrained=True, det_bs=4, reco_bs=1024)\n Detection is memory-bound (1024x1024 feature maps) so det_bs stays small;\n recognition crops are tiny (32x128) so reco_bs can be huge. On a T4 start at\n det_bs=2, reco_bs=512 and raise reco_bs until you OOM.\nCheap wins, in rough order of payoff:\n 1. swap to db_mobilenet_v3_large + crnn_mobilenet_v3_small   (5-10x)\n 2. pass ALL pages in one call \u2014 predictor(list_of_pages) batches internally\n 3. assume_straight_pages=True + disable_*_orientation when data allows\n 4. lower the PDF `scale` if your text is already large\n 5. half precision:  predictor = predictor.half()  (test accuracy first;\n    some post-processors expect float32, so keep a fallback)\nStructure knobs (handled by DocumentBuilder):\n   resolve_lines=True      group words into lines            (default True)\n   resolve_blocks=False    group lines into blocks           (default False)\n   paragraph_break=0.035   relative gap that splits paragraphs\n============================== FINE-TUNING ===================================\nStock checkpoints are trained on a French\/Latin vocab and generic documents.\nFine-tune when you have a custom alphabet, a specialist font, or a domain-\nspecific layout. In the repo:\n   references\/detection\/train_pytorch.py\n   references\/recognition\/train_pytorch.py\n   references\/classification\/train_pytorch.py   (orientation classifiers)\nRecognition wants word crops + labels.json; detection wants full pages with\npolygon labels (multi-class supported -&gt; feeds kie_predictor).\nThen load your weights:\n   from doctr.models import db_resnet50, ocr_predictor\n   det = db_resnet50(pretrained=False)\n   det.load_state_dict(torch.load(\"my_det.pt\", map_location=\"cpu\"))\n   model = ocr_predictor(det_arch=det, reco_arch=\"crnn_vgg16_bn\",\n                         pretrained=True)\ndocTR also pushes\/pulls checkpoints from the Hugging Face Hub\n(doctr.models.factory: push_to_hf_hub \/ from_hub).\n============================== DEPLOYMENT ====================================\n * FastAPI template in api\/ with \/detection \/recognition \/ocr \/kie routes\n * GPU-ready Docker images: ghcr.io\/mindee\/doctr\n * Streamlit demo: streamlit run demo\/app.py\n * Live demo: huggingface.co\/spaces\/mindee\/doctr\n * Full docs: mindee.github.io\/doctr\n==============================================================================\n\"\"\")\nprint(f\"nAll artefacts are in {WORK}:\")\nfor f in sorted(os.listdir(WORK)):\n   print(f\"   {f:&lt;28}{os.path.getsize(os.path.join(WORK, f))\/1024:&gt;8.0f} KB\")\nprint(\"nDone.\")\n<\/code><\/pre>\n<\/div>\n<\/div>\n<p class=\"wp-block-paragraph\">We create a searchable PDF by overlaying an invisible OCR text layer on top of the original scanned document while preserving its visual appearance. We examine practical performance improvements such as batching, lightweight detection and recognition models, orientation controls, and PDF scaling. We also review fine-tuning and deployment approaches so we can adapt docTR models to specialized datasets and integrate the resulting OCR pipeline into production applications.<\/p>\n<p class=\"wp-block-paragraph\">In conclusion, we developed a comprehensive understanding of how docTR can support much more than simple text recognition by combining OCR, document geometry, layout awareness, structured post-processing, and production-oriented optimization in a single workflow. We compared model architectures, inspected detection and recognition confidence, improved difficult predictions through selective second-pass recognition, tuned post-processing thresholds, and modified intermediate detections with custom hooks. We also processed rotated documents, explored layout and KIE capabilities, converted raw OCR output into ordered text, extracted fields, and reconstructed tables, and generated multiple reusable output formats, including searchable PDFs with invisible text layers.<\/p>\n<p class=\"wp-block-paragraph\">\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n<\/p><p class=\"wp-block-paragraph\">\n<\/p><p class=\"wp-block-paragraph\">Check out the\u00a0<strong><a href=\"https:\/\/github.com\/MARKTECHPOST-AI-MEDIA-INC\/AI-Agents-Projects-Tutorials\/blob\/main\/Computer%20Vision\/doctr_advanced_document_intelligence_ocr_tutorial_Marktechpost.ipynb\" target=\"_blank\" rel=\"noreferrer noopener\">FULL CODES here<\/a><\/strong>.<strong>\u00a0<\/strong>Also,\u00a0feel free to follow us on\u00a0<strong><a href=\"https:\/\/x.com\/intent\/follow?screen_name=marktechpost\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Twitter<\/mark><\/a><\/strong>\u00a0and don\u2019t forget to join our\u00a0<strong><a href=\"https:\/\/www.reddit.com\/r\/machinelearningnews\/\" target=\"_blank\" rel=\"noreferrer noopener\">150k+ML SubReddit<\/a><\/strong>\u00a0and Subscribe to\u00a0<strong><a href=\"https:\/\/magic.beehiiv.com\/v1\/f5e63dd4-5653-4f09-83e2-321a8b1ba526?email=%7B%7Bemail%7D%7D\" target=\"_blank\" rel=\"noreferrer noopener\">our Newsletter<\/a><\/strong>. Wait! are you on telegram?\u00a0<strong><a href=\"https:\/\/t.me\/machinelearningresearchnews\" target=\"_blank\" rel=\"noreferrer noopener\">now you can join us on telegram as well.<\/a><\/strong><\/p>\n<p class=\"wp-block-paragraph\">Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.?\u00a0<strong><a href=\"https:\/\/forms.gle\/wbash1wF6efRj8G58\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Connect with us<\/mark><\/a><\/strong><\/p>\n<p>The post <a href=\"https:\/\/www.marktechpost.com\/2026\/08\/17\/end-to-end-document-intelligence-pipeline-with-doctr-for-ocr\/\">Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs<\/a> appeared first on <a href=\"https:\/\/www.marktechpost.com\/\">MarkTechPost<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>In this tutorial, we develop an end-to-end OCR workflow with docTR and explore how modern document understanding pipelines combine text detection, recognition, geometry, layout analysis, structured extraction, and export. We generate realistic synthetic invoice documents, load images and PDFs through DocumentFile, construct GPU-aware OCR predictors, and benchmark different detection\u2013recognition architecture combinations for speed and accuracy. We then inspect the internal Document hierarchy, visualize confidence-aware bounding boxes, use standalone detection and recognition models, implement two-pass recognition for low-confidence words, tune detection thresholds, and introduce custom pipeline hooks for box filtering and padding. We also handle rotated and skewed documents, experiment with layout detection and KIE, reconstruct reading order and tabular information, extract structured invoice fields, and export results as text, JSON, hOCR, synthesized document images, and searchable PDFs. Finally, we examine practical performance, fine-tuning, batching, and deployment considerations to understand how to move from a basic OCR example to a production-oriented document intelligence pipeline. Copy CodeCopiedUse a different Browser import os, sys, io, json, time, math, re, subprocess, warnings from collections import Counter, defaultdict warnings.filterwarnings(&#8220;ignore&#8221;) os.environ.setdefault(&#8220;USE_TORCH&#8221;, &#8220;1&#8221;) def _pip(*pkgs): subprocess.run([sys.executable, &#8220;-m&#8221;, &#8220;pip&#8221;, &#8220;install&#8221;, &#8220;-q&#8221;, *pkgs], check=False) try: import doctr except ImportError: print(&#8220;&gt;&gt; Installing python-doctr (this takes ~1-2 min on Colab)&#8230;&#8221;) _pip(&#8220;python-doctr[viz]&#8221;) try: import reportlab except ImportError: _pip(&#8220;reportlab&#8221;) import numpy as np import torch import matplotlib import matplotlib.pyplot as plt from matplotlib import font_manager from matplotlib.patches import Rectangle, Polygon as MplPolygon from PIL import Image, ImageDraw, ImageFont import doctr from doctr.io import DocumentFile from doctr.models import ( ocr_predictor, kie_predictor, detection_predictor, recognition_predictor, ) DEVICE = &#8220;cuda&#8221; if torch.cuda.is_available() else &#8220;cpu&#8221; print(&#8220;=&#8221; * 78) print(f&#8221;docTR : {doctr.__version__}&#8221;) print(f&#8221;torch : {torch.__version__}&#8221;) print(f&#8221;device : {DEVICE}&#8221; + (f&#8221; ({torch.cuda.get_device_name(0)})&#8221; if DEVICE == &#8220;cuda&#8221; else &#8220;&#8221;)) print(f&#8221;python : {sys.version.split()[0]}&#8221;) print(&#8220;=&#8221; * 78) print(&#8220;NOTE: if the import above failed, restart the runtime &#8221; &#8220;(Runtime &gt; Restart session) and re-run this cell.n&#8221;) CFG = dict( RUN_BENCHMARK = True, RUN_SECOND_PASS = True, RUN_ROTATION = True, RUN_LAYOUT = True, RUN_KIE = True, RUN_SYNTHESIS = True, RUN_PDF_EXPORT = True, ) WORK = &#8220;\/content\/doctr_demo&#8221; if os.path.isdir(&#8220;\/content&#8221;) else &#8220;.\/doctr_demo&#8221; os.makedirs(WORK, exist_ok=True) print(f&#8221;working dir: {WORK}n&#8221;) _FONT = font_manager.findfont(font_manager.FontProperties(family=&#8221;DejaVu Sans&#8221;)) _FONT_B = font_manager.findfont( font_manager.FontProperties(family=&#8221;DejaVu Sans&#8221;, weight=&#8221;bold&#8221;)) A4 = (1240, 1754) INVOICE_LINES = [ ( 80, 70, &#8220;NORTHWIND TRADING CO.&#8221;, 38, True ), ( 80, 122, &#8220;42 Harbour Road, Bristol BS1 5TY&#8221;, 22, False), ( 80, 152, &#8220;VAT GB 884 5521 09&#8221;, 22, False), (820, 70, &#8220;INVOICE&#8221;, 44, True ), (820, 132, &#8220;Invoice No: INV-2024-00817&#8221;, 22, False), (820, 162, &#8220;Date: 14\/03\/2024&#8221;, 22, False), (820, 192, &#8220;Due Date: 13\/04\/2024&#8221;, 22, False), ( 80, 260, &#8220;BILL TO&#8221;, 24, True ), ( 80, 296, &#8220;Aurora Robotics Ltd&#8221;, 24, False), ( 80, 328, &#8220;Unit 7 Fenway Business Park&#8221;, 22, False), ( 80, 358, &#8220;Cambridge CB4 0WS&#8221;, 22, False), ( 80, 388, &#8220;Contact: procurement@aurorarobotics.co.uk&#8221;,22, False), ( 80, 470, &#8220;DESCRIPTION&#8221;, 24, True ), (640, 470, &#8220;QTY&#8221;, 24, True ), (780, 470, &#8220;UNIT PRICE&#8221;, 24, True ), (1010,470, &#8220;AMOUNT&#8221;, 24, True ), ( 80, 520, &#8220;Servo controller board Rev C&#8221;, 22, False), (640, 520, &#8220;12&#8221;, 22, False), (780, 520, &#8220;84.50&#8221;, 22, False), (1010,520, &#8220;1014.00&#8221;, 22, False), ( 80, 560, &#8220;Harmonic drive gearbox 50:1&#8221;, 22, False), (640, 560, &#8220;4&#8221;, 22, False), (780, 560, &#8220;312.75&#8221;, 22, False), (1010,560, &#8220;1251.00&#8221;, 22, False), ( 80, 600, &#8220;Shielded encoder cable 2m&#8221;, 22, False), (640, 600, &#8220;20&#8221;, 22, False), (780, 600, &#8220;11.40&#8221;, 22, False), (1010,600, &#8220;228.00&#8221;, 22, False), ( 80, 640, &#8220;Calibration service on-site&#8221;, 22, False), (640, 640, &#8220;1&#8221;, 22, False), (780, 640, &#8220;450.00&#8221;, 22, False), (1010,640, &#8220;450.00&#8221;, 22, False), (780, 720, &#8220;Subtotal&#8221;, 22, False), (1010,720, &#8220;2943.00&#8221;, 22, False), (780, 756, &#8220;VAT 20%&#8221;, 22, False), (1010,756, &#8220;588.60&#8221;, 22, False), (780, 796, &#8220;TOTAL DUE&#8221;, 26, True ), (1010,796, &#8220;3531.60&#8221;, 26, True ), ( 80, 900, &#8220;PAYMENT TERMS&#8221;, 24, True ), ( 80, 936, &#8220;Net 30 days. Late payments accrue interest at 2% per month.&#8221;, 20, False), ( 80, 968, &#8220;Bank: Lloyds Sort Code: 30-96-26 Account: 41775302&#8221;, 20, False), ( 80,1010, &#8220;Reference: INV-2024-00817&#8221;, 20, False), ] PAGE2_LINES = [ ( 80, 70, &#8220;APPENDIX A &#8211; DELIVERY SCHEDULE&#8221;, 34, True ), ( 80, 140, &#8220;All shipments leave the Bristol warehouse before 16:00 GMT.&#8221;, 22, False), ( 80, 176, &#8220;Tracking numbers are emailed on the day of dispatch.&#8221;, 22, False), ( 80, 240, &#8220;MILESTONE&#8221;, 24, True ), (700, 240, &#8220;TARGET DATE&#8221;, 24, True ), ( 80, 288, &#8220;Purchase order acknowledged&#8221;, 22, False), (700, 288, &#8220;18\/03\/2024&#8221;, 22, False), ( 80, 328, &#8220;Controller boards shipped&#8221;, 22, False), (700, 328, &#8220;25\/03\/2024&#8221;, 22, False), ( 80, 368, &#8220;Gearboxes shipped&#8221;, 22, False), (700, 368, &#8220;02\/04\/2024&#8221;, 22, False), ( 80, 408, &#8220;On-site calibration window&#8221;, 22, False), (700, 408, &#8220;08\/04\/2024&#8221;, 22, False), ( 80, 480, &#8220;Questions? Call +44 117 496 0022 or email ops@northwind.example&#8221;, 20, False), ] def render_page(lines, size=A4, bg=250): &#8220;&#8221;&#8221;Draw a clean document page from a list of (x, y, text, size, bold).&#8221;&#8221;&#8221; img = Image.new(&#8220;RGB&#8221;, size, (bg, bg, bg)) d = ImageDraw.Draw(img) for x, y, text, sz, bold in lines: font = ImageFont.truetype(_FONT_B if bold else _FONT, sz) d.text((x, y), text, fill=(18, 18, 22), font=font) d.line([(80, 455), (1160, 455)], fill=(60, 60, 60), width=2) d.line([(80, 505), (1160, 505)], fill=(160, 160, 160), width=1) d.line([(760, 700), (1160, 700)], fill=(60, 60, 60), width=2) return img def scanify(img, angle=0.0, noise=6.0, jpeg_quality=72, blur_shadow=True): &#8220;&#8221;&#8221;Degrade a clean render so it behaves like a phone photo \/ flatbed scan.&#8221;&#8221;&#8221; if angle: img = img.rotate(angle, expand=True, resample=Image.BICUBIC, fillcolor=(250, 250, 250)) arr = np.asarray(img).astype(np.float32) if blur_shadow: h, w = arr.shape[:2] gx = np.linspace(-1, 1, w)[None, :] gy = np.linspace(-1, 1, h)[:, None] shade = 1.0 &#8211; 0.10 * (gx ** 2 + 0.6 * gy ** 2) arr *= shade[&#8230;, None] if noise: arr += np.random.normal(0, noise, arr.shape) arr = np.clip(arr, 0, 255).astype(np.uint8) out = Image.fromarray(arr) if jpeg_quality: buf = io.BytesIO() out.save(buf, format=&#8221;JPEG&#8221;, quality=jpeg_quality) buf.seek(0) out = Image.open(buf).convert(&#8220;RGB&#8221;) return out clean1 = render_page(INVOICE_LINES) clean2 = render_page(PAGE2_LINES) page1_path = os.path.join(WORK, &#8220;invoice_p1.png&#8221;) page2_path = os.path.join(WORK, &#8220;invoice_p2.png&#8221;) rotated_path = os.path.join(WORK, &#8220;invoice_rotated.png&#8221;) pdf_path = os.path.join(WORK, &#8220;invoice.pdf&#8221;) scanify(clean1, angle=0.4).save(page1_path) scanify(clean2, angle=-0.3).save(page2_path) scanify(clean1, angle=13.0, noise=8.0).save(rotated_path) clean1.save(pdf_path, save_all=True, append_images=[clean2], resolution=150) GT_WORDS_P1 = [w for _, _, t, _, _ in INVOICE_LINES for w in t.split()] print(f&#8221;generated: {page1_path}, {page2_path}, {rotated_path}, {pdf_path}&#8221;) print(f&#8221;ground-truth words on page 1:<\/p>","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"pmpro_default_level":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"_pvb_checkbox_block_on_post":false,"footnotes":""},"categories":[52,5,7,1],"tags":[],"class_list":["post-112039","post","type-post","status-publish","format-standard","hentry","category-ai-club","category-committee","category-news","category-uncategorized","pmpro-has-access"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v25.3 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs - YouZum<\/title>\n<meta name=\"description\" content=\"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/youzum.net\/es\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/\" \/>\n<meta property=\"og:locale\" content=\"es_ES\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs - YouZum\" \/>\n<meta property=\"og:description\" content=\"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19\" \/>\n<meta property=\"og:url\" content=\"https:\/\/youzum.net\/es\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/\" \/>\n<meta property=\"og:site_name\" content=\"YouZum\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/DroneAssociationTH\/\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-17T21:57:32+00:00\" \/>\n<meta name=\"author\" content=\"admin NU\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Escrito por\" \/>\n\t<meta name=\"twitter:data1\" content=\"admin NU\" \/>\n\t<meta name=\"twitter:label2\" content=\"Tiempo de lectura\" \/>\n\t<meta name=\"twitter:data2\" content=\"27 minutos\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/\"},\"author\":{\"name\":\"admin NU\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c\"},\"headline\":\"Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs\",\"datePublished\":\"2026-08-17T21:57:32+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/\"},\"wordCount\":689,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#organization\"},\"articleSection\":[\"AI\",\"Committee\",\"News\",\"Uncategorized\"],\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/\",\"url\":\"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/\",\"name\":\"Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs - YouZum\",\"isPartOf\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#website\"},\"datePublished\":\"2026-08-17T21:57:32+00:00\",\"description\":\"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19\",\"breadcrumb\":{\"@id\":\"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/#breadcrumb\"},\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/youzum.net\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/yousum.gpucore.co\/#website\",\"url\":\"https:\/\/yousum.gpucore.co\/\",\"name\":\"YouSum\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/yousum.gpucore.co\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/yousum.gpucore.co\/#organization\",\"name\":\"Drone Association Thailand\",\"url\":\"https:\/\/yousum.gpucore.co\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"es\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png\",\"contentUrl\":\"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png\",\"width\":300,\"height\":300,\"caption\":\"Drone Association Thailand\"},\"image\":{\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/DroneAssociationTH\/\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c\",\"name\":\"admin NU\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"es\",\"@id\":\"https:\/\/yousum.gpucore.co\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png\",\"contentUrl\":\"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png\",\"caption\":\"admin NU\"},\"url\":\"https:\/\/youzum.net\/es\/members\/adminnu\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs - YouZum","description":"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/youzum.net\/es\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/","og_locale":"es_ES","og_type":"article","og_title":"Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs - YouZum","og_description":"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19","og_url":"https:\/\/youzum.net\/es\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/","og_site_name":"YouZum","article_publisher":"https:\/\/www.facebook.com\/DroneAssociationTH\/","article_published_time":"2026-08-17T21:57:32+00:00","author":"admin NU","twitter_card":"summary_large_image","twitter_misc":{"Escrito por":"admin NU","Tiempo de lectura":"27 minutos"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/#article","isPartOf":{"@id":"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/"},"author":{"name":"admin NU","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c"},"headline":"Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs","datePublished":"2026-08-17T21:57:32+00:00","mainEntityOfPage":{"@id":"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/"},"wordCount":689,"commentCount":0,"publisher":{"@id":"https:\/\/yousum.gpucore.co\/#organization"},"articleSection":["AI","Committee","News","Uncategorized"],"inLanguage":"es","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/","url":"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/","name":"Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs - YouZum","isPartOf":{"@id":"https:\/\/yousum.gpucore.co\/#website"},"datePublished":"2026-08-17T21:57:32+00:00","description":"\u0e01\u0e34\u0e08\u0e01\u0e23\u0e23\u0e21\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e14\u0e23\u0e19","breadcrumb":{"@id":"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/#breadcrumb"},"inLanguage":"es","potentialAction":[{"@type":"ReadAction","target":["https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/youzum.net\/developing-an-end-to-end-document-intelligence-pipeline-with-doctr-for-ocr-layout-analysis-kie-benchmarking-and-searchable-pdfs\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/youzum.net\/"},{"@type":"ListItem","position":2,"name":"Developing an End-to-End Document Intelligence Pipeline with docTR for OCR, Layout Analysis, KIE, Benchmarking, and Searchable PDFs"}]},{"@type":"WebSite","@id":"https:\/\/yousum.gpucore.co\/#website","url":"https:\/\/yousum.gpucore.co\/","name":"YouSum","description":"","publisher":{"@id":"https:\/\/yousum.gpucore.co\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/yousum.gpucore.co\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es"},{"@type":"Organization","@id":"https:\/\/yousum.gpucore.co\/#organization","name":"Drone Association Thailand","url":"https:\/\/yousum.gpucore.co\/","logo":{"@type":"ImageObject","inLanguage":"es","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/","url":"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png","contentUrl":"https:\/\/youzum.net\/wp-content\/uploads\/2024\/11\/tranparent-logo.png","width":300,"height":300,"caption":"Drone Association Thailand"},"image":{"@id":"https:\/\/yousum.gpucore.co\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/DroneAssociationTH\/"]},{"@type":"Person","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/person\/97fa48242daf3908e4d9a5f26f4a059c","name":"admin NU","image":{"@type":"ImageObject","inLanguage":"es","@id":"https:\/\/yousum.gpucore.co\/#\/schema\/person\/image\/","url":"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png","contentUrl":"https:\/\/youzum.net\/wp-content\/uploads\/avatars\/2\/1746849356-bpfull.png","caption":"admin NU"},"url":"https:\/\/youzum.net\/es\/members\/adminnu\/"}]}},"rttpg_featured_image_url":null,"rttpg_author":{"display_name":"admin NU","author_link":"https:\/\/youzum.net\/es\/members\/adminnu\/"},"rttpg_comment":0,"rttpg_category":"<a href=\"https:\/\/youzum.net\/es\/category\/ai-club\/\" rel=\"category tag\">AI<\/a> <a href=\"https:\/\/youzum.net\/es\/category\/committee\/\" rel=\"category tag\">Committee<\/a> <a href=\"https:\/\/youzum.net\/es\/category\/news\/\" rel=\"category tag\">News<\/a> <a href=\"https:\/\/youzum.net\/es\/category\/uncategorized\/\" rel=\"category tag\">Uncategorized<\/a>","rttpg_excerpt":"In this tutorial, we develop an end-to-end OCR workflow with docTR and explore how modern document understanding pipelines combine text detection, recognition, geometry, layout analysis, structured extraction, and export. We generate realistic synthetic invoice documents, load images and PDFs through DocumentFile, construct GPU-aware OCR predictors, and benchmark different detection\u2013recognition architecture combinations for speed and accuracy.&hellip;","_links":{"self":[{"href":"https:\/\/youzum.net\/es\/wp-json\/wp\/v2\/posts\/112039","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/youzum.net\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/youzum.net\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/youzum.net\/es\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/youzum.net\/es\/wp-json\/wp\/v2\/comments?post=112039"}],"version-history":[{"count":0,"href":"https:\/\/youzum.net\/es\/wp-json\/wp\/v2\/posts\/112039\/revisions"}],"wp:attachment":[{"href":"https:\/\/youzum.net\/es\/wp-json\/wp\/v2\/media?parent=112039"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/youzum.net\/es\/wp-json\/wp\/v2\/categories?post=112039"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/youzum.net\/es\/wp-json\/wp\/v2\/tags?post=112039"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}