fix(layout): recover heading text wrongly absorbed into PICTURE regions#3708
Open
DanielNg0729 wants to merge 2 commits into
Open
fix(layout): recover heading text wrongly absorbed into PICTURE regions#3708DanielNg0729 wants to merge 2 commits into
DanielNg0729 wants to merge 2 commits into
Conversation
…ons (docling-project#3699) The layout model sometimes detects a banner-style heading inside a filled box as a PICTURE. Its text cells are then nested under the picture and dropped from every content layer (BODY and FURNITURE alike), so the heading never reaches the output. Add an opt-in BaseLayoutOptions.recover_text_in_pictures flag (default False). When enabled, a PICTURE whose contained clusters are all text-like and whose text cells cover at least 25% of its area is treated as spurious: the picture is discarded and its text clusters survive so they serialize normally. The default-off path is unchanged, keeping existing figure handling and e2e reference data byte-identical. Closes docling-project#3699 Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
…ons (docling-project#3699) The layout model sometimes detects a banner-style heading inside a filled box as a PICTURE. Its text cells are then nested under the picture and dropped from every content layer (BODY and FURNITURE alike), so the heading never reaches the output. Add an opt-in BaseLayoutOptions.recover_text_in_pictures flag (default False). When enabled, a PICTURE whose contained clusters are all text-like and whose text cells cover at least 25% of its area is treated as spurious: the picture is discarded and its text clusters survive so they serialize normally. The default-off path is unchanged, keeping existing figure handling and e2e reference data byte-identical. Closes docling-project#3699 Signed-off-by: Daniel Nguyen <danielnguyenh07@gmail.com>
Contributor
|
✅ DCO Check Passed Thanks @DanielNg0729, all your commits are properly signed off. 🎉 |
Contributor
Merge Protections🟢 Merge protection satisfied — ready to merge. Show 1 satisfied protection🟢 Enforce conventional commitMake sure that we follow https://www.conventionalcommits.org/en/v1.0.0/
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Recover heading text that gets swallowed by PICTURE regions
The problem
I picked this up from #3699: a heading goes missing from the Markdown output, and it stays missing even with
ContentLayer.FURNITUREturned on. That's what makes it a different animal from #3693. In #3693 the heading is only put in the wrong layer; it's sitting in furniture, so asking for furniture brings it back. Here it's gone from every layer, which means something drops it well before the export stage gets a say.Why it happens
A solid filled box is a strong "this is a picture" signal, so the layout model draws a low-confidence
PICTUREover that header region, on page 2 it lands at about 0.63, fighting with overlappingsection_header/page_header/titleboxes over the exact same text.From there the postprocessor does what it's designed to do. Any regular cluster that sits ≥ 80% inside a special cluster gets adopted as a child of it (
_process_special_clusters), andpostprocess()then strips those children out of the regular set — andSPECIAL_TYPESincludesPICTURE. So the header's text cells become children of the picture and vanish from the regular clusters. The picture serializes as a figure (<!-- image -->) and never emits its child text, so the heading reaches neither BODY nor FURNITURE.This is the right behaviour for a real figure: we don't want labels from inside a chart leaking into the body. The trouble is that a text box the model mistook for a figure looks identical to a genuine one at this stage. I checked the whole chain by running the actual layout model on the page and dumping the clusters: the cluster that survives at the top is a
picture, with the article title sitting underneath it as absorbed children.What I changed
A small, opt-in rescue inside
LayoutPostprocessor. When a PICTURE is really just a box drawn around text, I drop the picture and leave its text clusters in the regular set, so they come out normally with their own labels — the title goes to BODY, the page number to FURNITURE.I only treat a PICTURE as "text-only" (
_is_text_only_picture) when two things are both true: everything inside it is text-like (TEXT,SECTION_HEADER,PAGE_HEADER,PAGE_FOOTER,TITLE,LIST_ITEM,CAPTION,FOOTNOTE— I leftFORMULA/CODE/CHECKBOXout on purpose, so a figure with a formula in it stays safe), and the text cells cover at least 25% of the picture's area. A genuine photo, chart or diagram has little or no text-cell coverage, so it never qualifies. I picked the 25% line by measuring the reporter's PDF — the spurious header pictures come in between 0.31 and 0.44, well clear of a figure that just has a caption.The change touches three files: the new
recover_text_in_picturesflag onBaseLayoutOptions, the_is_text_only_picturecheck plus the drop in_process_special_clusters, and a new test file.How to use it
It's off by default, so nobody's output changes unless they ask for it:
On the reporter's PDF that's the difference between the heading being absent with a stray
<!-- image -->in its place (default), and the heading actually showing up — title in the body, number in furniture (flag on).Tests
tests/test_layout_recover_text_in_pictures.pyexercisesLayoutPostprocessordirectly with hand-built clusters, so it's deterministic and needs neither the layout model nor a PDF fixture. It checks the decision on its own (a text box gets rescued; a real figure with a small caption is left alone; a figure containing a formula is left alone) and the full_process_special_clusterspath both ways. Flag on, the picture's gone and the text survives; flag off, the picture keeps the text exactly as before.Honest limitations
It's a heuristic, and I want to be upfront about where it's soft. If you turn it on, a figure that genuinely is mostly text — a screenshot of a page, a busy infographic — could get reclassified from a picture into text. The all-text-like guard plus the 25% floor keep that corner small, but not zero, which is the main reason this is opt-in rather than default.
There's also a reading-order rough edge: the recovered running header currently lands at the bottom of the page rather than the top where you actually see it, because the rescued cells come back as orphan clusters ordered by native cell index. It's present and correct, just not where you'd want it visually. And because this header repeats on every page, turning the flag on surfaces it on every page — the cross-page de-duplication of headers/footers is the separate problem in #3693, and I deliberately kept it out of here. Finally, the 25% threshold is calibrated on the one PDF in the report (0.31–0.44 on its header pages), so it may want revisiting as more samples turn up. This is a post-processing rescue, not a change to the model, so it never fires unless the picture genuinely contains the text (≥ 80% containment).
A couple of things I'd like your take on
Resolves #3699.
Checklist: