Skip to content

PPStructureV3's parsing_res_list` silently drops layout boxes nested inside another box when using a custom fine-tuned layout model #5207

Description

@rafaliazz

Environment

  • paddlex: 3.7.2
  • paddleocr: 3.7.0
  • paddlepaddle-gpu: 3.3.0
  • Base commit: ffb64904 (2026-06-25)
  • Layout model: PP-DocLayout_plus-L architecture, fine-tuned on a custom 11-category label set (not the stock ~20-category scheme)

Summary

When PPStructureV3 is configured with a custom-trained layout_detection_model_dir whose category scheme differs from the stock model's, res["parsing_res_list"] silently drops any layout box that is geometrically nested inside another detected box (e.g. a caption/headline overlaid directly on a photo -- common in newspaper/magazine layouts). Across our 47-image validation set, 46 of 47 images were affected, in some cases collapsing a 12-box page down to 1.

Critically, res["layout_det_res"]["boxes"] (the raw detection output) is complete and correct on every single one of those 47 images -- matching a standalone create_model("PP-DocLayout_plus-L", model_dir=...).predict() call exactly, same boxes, same confidence scores. The loss happens specifically in the structured-parsing assembly step (get_layout_parsing_res -> get_layout_parsing_objects -> sort_layout_parsing_blocks -> xycut_enhanced).

Root cause (as far as we traced it)

Two separate config dictionaries in this pipeline are keyed by the stock label scheme, and silently misbehave for a custom one:

  1. PPStructureV3's own exposed layout_threshold / layout_merge_bboxes_mode constructor kwargs are dicts keyed by class ID (0-19), tuned for the stock category order (e.g. 0: "large" # paragraph_title, 7: "large" # formula). A custom model with a different 11-class scheme at the same IDs silently inherits whatever stock class happens to share that ID -- e.g. our class 7, "picture", inherited the threshold/merge-mode meant for stock class 7, "formula". This part is fixable by explicitly overriding both dicts for all of a custom model's class IDs.
  2. Deeper in the pipeline, layout_parsing/layout_objects.py's LayoutRegion.init_region_info_from_layout and pipeline_v2.py's reading-order assignment both consult BLOCK_LABEL_MAP, which is keyed by label name (strings like "paragraph_title", "image", "header"). None of a custom model's label strings match any stock name, so every custom-labeled block (including ones that should be recognized as vision_labels, e.g. our "picture") falls through to a generic bucket. get_layout_parsing_res's order_index assignment loop also only assigns an order to labels in BLOCK_LABEL_MAP["visualize_index_labels"] -- a custom-labeled block never gets one. We could not find any constructor-level override for this second part.

The combination appears to make xycut_enhanced's recursive X/Y-cut region-splitting (which assumes non-overlapping blocks) unable to reconcile genuinely nested boxes for a custom label scheme, and it silently drops the nested one rather than erroring.

Minimal repro

from paddleocr import PPStructureV3

pipeline = PPStructureV3(
    layout_detection_model_name="PP-DocLayout_plus-L",
    layout_detection_model_dir="<path to a model fine-tuned on a non-stock label set>",
    use_doc_orientation_classify=False,
    use_doc_unwarping=False,
    layout_threshold={i: 0.5 for i in range(N)},          # N = your model's class count
    layout_merge_bboxes_mode={i: "union" for i in range(N)},
)
res = list(pipeline.predict("<image with overlapping/nested regions, e.g. caption on a photo>"))[0]

print(len(res["layout_det_res"]["boxes"]))   # correct, complete
print(len(res["parsing_res_list"]))          # incomplete -- fewer than the line above

Impact

Any user fine-tuning PP-DocLayout_plus-L (or presumably other layout models used by PPStructureV3) on a custom category scheme that includes classes which can legitimately contain other classes (e.g. an image/photo class with overlaid text) will silently lose most of their page content from parsing_res_list, with no error or warning -- layout_det_res looking correct makes this easy to miss. In our validation, this meant 46/47 pages returning drastically incomplete structured output while looking like they'd worked.

Verified workaround

Replacing the body of get_layout_parsing_res (in paddlex/inference/pipelines/layout_parsing/pipeline_v2.py) with a simple geometric assignment -- every OCR line to its smallest-area enclosing layout_det_res box, sorted top-to-bottom/left-to-right for reading order -- fully resolves this for our dataset (0/47 mismatches after the change vs. 46/47 before). This bypasses standardized_data/get_layout_parsing_objects/sort_layout_parsing_blocks/xycut_enhanced entirely, so it also drops table/seal/chart/formula special-casing, which our model doesn't use -- not proposing this as the upstream fix, just confirming where the problem lives and that a label-scheme-agnostic replacement resolves it completely.

Possible fix directions (not proposing a specific patch -- happy to help test one)

  • Have BLOCK_LABEL_MAP-dependent logic fall back to a sensible default (rather than silently excluding) for label strings it doesn't recognize, and/or expose it as a user-overridable mapping the same way layout_threshold/layout_merge_bboxes_mode already are.
  • Have xycut_enhanced (or the region-matching step feeding it) explicitly detect containment/nesting and represent it (e.g. via LayoutBlock.child_blocks, which already exists as a field but isn't populated by this path) rather than dropping the contained block.
  • At minimum, log a warning when a detected layout_det_res box doesn't make it into the final parsing_res_list, so this failure mode isn't silent.

Happy to provide the full fine-tuned model / dataset details or a minimal repro image if useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions