Skip to content

test(bench): sweep the in-region thresholds — it is not a tuning problem - #28

Merged
hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-1363-pdfgrab-cell-inference-inside-a-detected-region
Sep 17, 2026
Merged

hallelx2 merged 1 commit into
mainfrom
halleluyaholudele/hal-1363-pdfgrab-cell-inference-inside-a-detected-region

Conversation

@hallelx2

@hallelx2 hallelx2 commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Answers the cheap question from HAL-1363 before anyone spends days on the expensive one.

The hypothesis, and why it was backwards

HAL-1363 proposed the precision gap after region detection (0.281 vs camelot's 0.514) was a threshold tuned for a whole page and wrong for a small region — a five-row table has five words per column, so MinWordsVertical=3 should be producing spurious boundaries.

Twelve configurations, 125 documents:

config precision recall F1
pad=none 0.289 0.629 0.342
pad=0.5 0.288 0.630 0.342
minwh=2 0.283 0.635 0.338
baseline 0.281 0.635 0.337
pad=2.0 0.274 0.642 0.332
pad=4.0 0.247 0.623 0.308
minwv=2 0.227 0.606 0.283
minwv=1 0.128 0.464 0.167

Lowering MinWordsVertical is catastrophic. The default is already the strictest value tested and is the only thing holding precision up. Padding beyond the default degrades monotonically. Best config beats baseline by 0.005 — noise.

Reachable precision range: 0.128–0.289. Camelot's 0.514 is not in it.

What this settles

The gap is not configuration. No default change fixes it, and no further sweeping is worth the wall-clock. What remains is the gridding algorithm itself — given the same region, camelot divides it into cells differently, and that difference is worth ~0.23 precision. _generate_columns_and_rows in camelot/parsers/base.py is the next and only sensible step.

The oracle result still bounds the opportunity at 0.935 given a correct grid, so the ceiling is not the problem.

A method note worth keeping

A three-document smoke run put pad=2.0 first at F1 0.628. Over 125 documents that config is sixth, and the ranking inverts. The cheap sample is for checking the harness runs, never for reading a result off — it would have sent the next few days in exactly the wrong direction.

Also in here

PadLines is now configurable and follows the same zero-means-default rule as every other field in TextEdgeOpts, with a negative value as the explicit "none". The inconsistent version that shipped in #27 would have silently disabled padding for anyone using a partially-filled struct — covered by a new test.

Both sweeps agree to every decimal place across all twelve configs. Local: gofmt, go build, go vet, go test ./... green. CI red for HAL-1354.

Closes HAL-1363

Summary by Sourcery

Determine whether in-region precision can be improved through threshold tuning and establish algorithmic gridding as the next area of investigation.

New Features:

  • Add a benchmark sweep for evaluating region-gridding thresholds across the ICDAR 2013 corpus.
  • Expose configurable region padding and threshold overrides in the extraction benchmark.

Bug Fixes:

  • Ensure zero-valued TextEdgeOpts fields use defaults while negative PadLines explicitly disables padding.

Enhancements:

  • Document benchmark results showing the precision gap is caused by the gridding algorithm rather than threshold configuration.

Documentation:

  • Add the region-gridding threshold sweep evaluation and link it from the benchmark and documentation indexes.

Tests:

  • Add coverage for PadLines defaulting and explicit padding disablement.

HAL-1363 proposed that the precision gap after region detection (0.281
against camelot's 0.514) was a threshold tuned for a whole page and
wrong for a small region: a five-row table has five words per column, so
MinWordsVertical=3 should be emitting spurious boundaries.

Twelve configurations say no, and say the hypothesis was backwards.

Lowering MinWordsVertical is catastrophic — minwv=1 halves precision to
0.128 and drags recall down with it. The default is already the
strictest value tested and sits at or near the optimum. It is not
causing the problem, it is the only thing holding precision up.

Padding is similarly exhausted. Beyond the default it degrades
monotonically, 0.274 to 0.261 to 0.247 across pad 2 to 4, buying no
recall worth having. The best config in the whole sweep beats baseline
by 0.005, which is noise.

So the reachable precision range is 0.128 to 0.289 and camelot's 0.514
is not in it. No default change fixes this and no further sweeping is
worth the wall-clock. What remains is the gridding algorithm itself:
given the same region, camelot divides it into cells differently, and
that difference is worth about 0.23 precision.

One method note worth keeping. A three-document smoke run put pad=2.0
first at F1 0.628; over 125 documents that config is sixth and the
ranking inverts. The cheap sample is for checking the harness runs, not
for reading a result off — it would have sent the next few days in
precisely the wrong direction.

PadLines is now configurable, following the same zero-means-default rule
as every other field in TextEdgeOpts, with a negative value as the
explicit none. The inconsistent version shipped briefly in the previous
commit would have silently disabled padding for anyone using a partially
filled struct.

Both sweeps agree to every decimal place across all twelve configs.
Copilot AI lite review requested due to automatic review settings September 17, 2026 22:42
@sourcery-ai

sourcery-ai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR adds a reproducible 125-document sweep of in-region gridding thresholds and padding, showing that configuration changes cannot close the precision gap and identifying the gridding algorithm as the next investigation target. It also formalizes configurable region padding with consistent zero-means-default semantics and regression tests.

Sequence diagram for reproducible configuration scoring

sequenceDiagram
    participant Sweep as sweep.py
    participant Extract as bench-extract
    participant Score as score.py
    participant Corpus as ICDAR corpus

    loop each document and configuration
        Sweep->>Extract: run_cfg(exe, pdf, cfg)
        Extract->>Corpus: extract table rows
        Corpus-->>Extract: JSON rows
        Extract-->>Sweep: relations
        Sweep->>Score: score(gt, got)
        Score-->>Sweep: precision, recall, F1
    end
    Sweep->>Sweep: sort results by F1
Loading

Flow diagram for the in-region threshold sweep

flowchart LR
    Corpus[125-document ICDAR corpus] --> Sweep[sweep.py]
    Sweep --> Extract[bench-extract]
    Extract --> Metrics[Per-document precision recall F1]
    Metrics --> Results[Compare 12 configurations]
    Results --> Decision{Precision reaches camelot's 0.514?}
    Decision -- No --> Algorithm[Investigate region gridding algorithm]
    Decision -- Yes --> Defaults[Consider threshold or padding change]
Loading

File-Level Changes

Change Details Files
Adds a reproducible benchmark sweep to evaluate whether region-gridding thresholds explain the precision gap.
  • Introduces twelve pdfgrab-only configurations covering vertical/horizontal word thresholds and region padding.
  • Adds extractor CLI overrides for threshold and padding values, including default and no-padding semantics.
  • Aggregates per-document precision, recall, and F1 across the 125-document ICDAR corpus and supports JSON output.
  • Documents the results, methodology, and conclusion that algorithmic gridding—not threshold tuning—is the bottleneck.
bench/README.md
bench/icdar2013/extract.go
bench/icdar2013/sweep.py
docs/README.md
docs/evaluations/2026-09-17-region-gridding-threshold-sweep.md
Makes region padding configurable while preserving partial-configuration defaults and explicit disablement.
  • Adds TextEdgeOpts.PadLines with a default of one average line height.
  • Treats zero as default and negative values as no padding during option normalization.
  • Uses the configured padding multiplier when expanding detected regions.
  • Adds regression coverage for zero-default and negative-disable behavior.
detect_textedge.go
detect_textedge_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 5 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 102c375b-70d3-4216-9295-3677295d4d52

📥 Commits

Reviewing files that changed from the base of the PR and between 1d4beb9 and 62e530b.

📒 Files selected for processing (7)
  • bench/README.md
  • bench/icdar2013/extract.go
  • bench/icdar2013/sweep.py
  • detect_textedge.go
  • detect_textedge_test.go
  • docs/README.md
  • docs/evaluations/2026-09-17-region-gridding-threshold-sweep.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 security issue, and 2 other issues

Security issues:

  • Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'. (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="bench/icdar2013/sweep.py" line_range="47-50" />
<code_context>
+        cmd += ["-pad", str(cfg["pad"])]
+    cmd.append(pdf)
+    try:
+        out = subprocess.run(cmd, capture_output=True, timeout=120).stdout
+        return relations([t["rows"] for t in json.loads(out or b"[]")])
+    except Exception:
+        return Counter()
+
+
</code_context>
<issue_to_address>
**issue (testing):** Extractor failures, timeouts, nonzero exit statuses, and malformed output are all converted into an empty `Counter`, which is scored as a valid zero-result document. A broken configuration therefore silently lowers its precision, recall, and F1 and can produce false conclusions about the threshold sweep.

**Triggers:** When the extractor fails or emits invalid JSON for any document/configuration.

**Suggested fix:** Check `subprocess.run(...).returncode` and raise or record the failure instead of returning an empty result; report failed runs separately from genuine empty extraction results.
</issue_to_address>

### Comment 2
<location path="docs/README.md" line_range="23" />
<code_context>

 | date | subject | headline |
 | --- | --- | --- |
+| [2026-09-17](evaluations/2026-09-17-region-gridding-threshold-sweep.md) | in-region gridding threshold sweep | **negative** — 12 configs, precision never leaves 0.28. Not a tuning problem; the gridding algorithm is |
 | [2026-09-17](evaluations/2026-09-17-text-edge-region-detection.md) | text-edge region detection (Nurminen) | **partial** — F1 0.245 → 0.337 over page-wide `text`, but below `lines` 0.442. Recall transferred, precision did not: gridding is now the bottleneck |
 | [2026-09-17](evaluations/2026-09-17-field-comparison-and-metric-correction.md) | the field, and a metric correction | **per-doc F1 0.442**, 5th of 10, ~10x faster than anything comparable. No Go library comes close. Earlier figures were pooled, not the competition's metric |
</code_context>
<issue_to_address>
**nitpick:** The documentation headline says precision never leaves 0.28, but the same committed results include `minwv=1` at precision 0.128. The headline contradicts the table and the later stated range of 0.128–0.289.

**Suggested fix:** Change the headline to state that precision spans 0.128–0.289, or otherwise qualify the claim so it does not contradict the reported sweep.
</issue_to_address>

### Comment 3
<location path="bench/icdar2013/sweep.py" line_range="47" />
<code_context>
        out = subprocess.run(cmd, capture_output=True, timeout=120).stdout
</code_context>
<issue_to_address>
**security (python.lang.security.audit.dangerous-subprocess-use-audit):** Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread bench/icdar2013/sweep.py
Comment on lines +47 to +50
out = subprocess.run(cmd, capture_output=True, timeout=120).stdout
return relations([t["rows"] for t in json.loads(out or b"[]")])
except Exception:
return Counter()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (testing): Extractor failures, timeouts, nonzero exit statuses, and malformed output are all converted into an empty Counter, which is scored as a valid zero-result document. A broken configuration therefore silently lowers its precision, recall, and F1 and can produce false conclusions about the threshold sweep.

Triggers: When the extractor fails or emits invalid JSON for any document/configuration.

Suggested fix: Check subprocess.run(...).returncode and raise or record the failure instead of returning an empty result; report failed runs separately from genuine empty extraction results.

Comment thread docs/README.md

| date | subject | headline |
| --- | --- | --- |
| [2026-09-17](evaluations/2026-09-17-region-gridding-threshold-sweep.md) | in-region gridding threshold sweep | **negative** — 12 configs, precision never leaves 0.28. Not a tuning problem; the gridding algorithm is |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: The documentation headline says precision never leaves 0.28, but the same committed results include minwv=1 at precision 0.128. The headline contradicts the table and the later stated range of 0.128–0.289.

Suggested fix: Change the headline to state that precision spans 0.128–0.289, or otherwise qualify the claim so it does not contradict the reported sweep.

Comment thread bench/icdar2013/sweep.py
cmd += ["-pad", str(cfg["pad"])]
cmd.append(pdf)
try:
out = subprocess.run(cmd, capture_output=True, timeout=120).stdout

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.security.audit.dangerous-subprocess-use-audit): Detected subprocess function 'run' without a static string. If this data can be controlled by a malicious actor, it may be an instance of command injection. Audit the use of this call to ensure it is not controllable by an external resource. You may consider using 'shlex.escape()'.

Source: opengrep

@hallelx2
hallelx2 merged commit 7dccac2 into main Sep 17, 2026
1 of 5 checks passed
@hallelx2
hallelx2 deleted the halleluyaholudele/hal-1363-pdfgrab-cell-inference-inside-a-detected-region branch September 17, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants