# QUE-259 — Discriminating tabular images (table regions with no text layer)

Goal: devise and test a method to tell, for a region the pipeline treats as a
table, whether it is a *real text table* or a *pure image of a table* — a table
that exists in the PDF only as pixels, with no text layer. Camelot (which reads
the PDF text layer) silently returns nothing on these, so they are lost.

Sample: 11 daloopa documents from `s3://qubera-docs/daloopa/`
(the 126MB `100724009` was excluded as unnecessarily large). 877 docling
regions analyzed. Discovery only — no production code changed.

## The method

Per region that **docling** identifies as a table (docling runs OCR on the page
image, so its TableFormer fires even on a scanned table), probe the **source
PDF's native text layer** under that region's bbox with PyMuPDF:

```
native_chars = len(page.get_text("text", clip=region_bbox).strip())
if native_chars <= 5:   ->  tabular_image   (table exists only as pixels)
else:                   ->  text_table      (Camelot can read it)
```

Two corroborating signals, both already produced by the pipeline, agree:

- **docling** detects a table region (the oracle: a table *is* there).
- **Camelot/Set-of-Mark** returns `camelot_accuracy = 0` and empty markdown on
  the same region (the blank `--review` render).

To separate a tabular image from a *chart* mistaken as a table, key on docling's
picture classification: an overlapping docling PICTURE classed `bar_chart` /
`line_chart` / etc. with no text under it is `chart_image`, not a table.

## Evidence

Native text chars under docling TABLE regions — clean bimodal split, no overlap:

| class | n | min | median | max |
|---|---|---|---|---|
| **tabular_image** (flagged) | 117 | 0 | 0 | **0** |
| **text_table** (passed) | 498 | 32 | 536 | 4675 |

Per-document label counts (`out/discriminator.md`):

| doc | text_table | tabular_image | chart_image | other_image |
|---|---|---|---|---|
| 100064034 | 110 | 0 | 0 | 8 |
| 100199331 | 0 | 18 | 0 | 2 |
| 100377849 | 0 | 35 | 0 | 130 |
| 101215348 | 0 | 30 | 57 | 35 |
| 101230425 | 0 | 6 | 0 | 2 |
| 101243493 | 12 | 0 | 0 | 0 |
| 101425642 | 0 | 15 | 2 | 13 |
| 101449298 | 21 | 0 | 0 | 0 |
| 102049514 | 353 | 0 | 3 | 1 |
| 102174103 | 2 | 0 | 0 | 8 |

Corroboration on `101230425` (fully scanned, 0 text layer): Set-of-Mark located
7 tables; **all 7** returned `camelot_accuracy = 0.0` and empty markdown — the
exact pages the discriminator flags as `tabular_image`.

Visual confirmation (`out/samples/`): the flagged regions are genuine financial
tables rendered as images (Commerce Bancshares Financial Highlights; an income
statement), and a passed `text_table` region is a real text-layer page.

## What this sample does and does not prove

- **Proven:** with a docling table region, native-text-under-bbox ≈ 0 is a
  reliable, observable discriminator for a tabular image — separation is total
  (0 vs ≥32 chars), corroborated by Camelot returning nothing, visually verified.
- **Sample shape:** these PDFs are homogeneous per document — each is either
  fully text-layer or fully scanned image. No PDF here had a text table on one
  page and an image table on another.
- **Untested (the one gap):** a tabular image *embedded inside an otherwise
  text-layer PDF*, where native text (a caption, an adjacent paragraph) could sit
  near the image-table bbox. Mitigations to carry into design: use docling's
  tight table bbox, optionally shrink the clip margin, and prefer a small
  char-*density* threshold over an exact-zero test. Recommend sourcing a mixed
  document to stress this before the discriminator is finalized.

## Production keying note

For the flag the ticket asks for (on the Camelot+LLM `ExtractedTable`), probe
native text under the **Set-of-Mark located region** directly, not under a
docling table — Set-of-Mark can locate regions docling misses (e.g. it found a
7th table on `101230425` p9 that docling did not). docling then serves to label
table-vs-chart. The `tabular_image` verdict = located region + native text ≈ 0
+ Camelot empty.

## Artifacts

- `discriminate.py` — the discriminator (no LLM, no new model).
- `out/regions.json` — every region with native_text_chars and label.
- `out/discriminator.md` — the aggregate table above.
- `out/samples/*.png` — rendered proof crops.
- `tables/101230425.{tables.json,review.html,boxed.pdf}` — Camelot-empty corroboration.
- `docling/*.docling.json`, `tables/*.tables.json` — raw pipeline outputs.
