Bounding Box Visualization#
Docling provides utilities for rendering detected layout regions—clusters, text cells, and arbitrary bounding boxes—on top of document page images. The core entry point is draw_clusters() in docling/utils/visualization.py. It is used both during live conversion (for debug output) and in standalone demos.
Coordinate Systems#
Two coordinate origins are used throughout the stack, defined in CoordOrigin:
| Origin | Description |
|---|---|
TOPLEFT | Origin at top-left; y increases downward. Default for images and pixel space. |
BOTTOMLEFT | Origin at bottom-left; y increases upward. Native to PDF point space. |
BoundingBox (defined in docling_core/types/doc/base.py) stores coordinates as l, t, r, b and defaults to TOPLEFT. Key conversion methods:
to_top_left_origin(page_height)— flips y-coordinates vianew_y = page_height - old_yfor use in PIL/image space.to_bottom_left_origin(page_height)— inverse; converts back to PDF-native origin.as_tuple()— returns(l, t, r, b)forTOPLEFTor(l, b, r, t)forBOTTOMLEFT.
PDF Point Space → Pixel Space Conversion#
Before rendering, coordinates in document (PDF point) space must be scaled to the rendered image's pixel dimensions. In layout_model.py, the scale factors are computed as:
scale_x = page.image.width / page.size.width
scale_y = page.image.height / page.size.height
These are then passed into draw_clusters(). Inside the function, every coordinate is multiplied by the appropriate factor :
cx0 *= scale_x
cx1 *= scale_x
cy0 *= scale_x # note: uses scale_x (see known quirk below)
cy1 *= scale_y
The same pattern—image_dimension / document_dimension—applies when cropping page regions via Page.get_image(), which chains the full conversion:
cropbox.to_top_left_origin(page_height=self.size.height).scaled(scale).as_tuple()
BoundingBox also exposes higher-level helpers: resize_by_scale(x_scale, y_scale), scale_to_size(old_size, new_size), and scaled(scale).
draw_clusters() — How It Works#
draw_clusters(image, clusters, scale_x, scale_y) mutates the provided PIL Image in-place using ImageDraw with an RGBA mode. For each cluster and its children :
- Text cells are drawn first as semi-transparent black fills .
- Cluster rectangle is drawn with a fill and outline color derived from
DocItemLabel.get_color(label), which returns a per-label RGB tuple (e.g., Text → light yellow, Table → light pink, Title → salmon). - A label + confidence annotation (
"SectionHeader (0.92)") is overlaid with a semi-transparent white background .
The function handles children recursively via [c_tl, *c_tl.children] — nested clusters (e.g., table cells inside a table cluster) are rendered within a single pass.
Debug Visualization in the Pipeline#
In the layout pipeline, draw_clusters_and_cells_side_by_side() produces a side-by-side debug image:
- Left panel: clusters excluding
FORM,KEY_VALUE_REGION, andPICTURE. - Right panel: only those three label types.
Output is saved to the configured debug_output_path as a PNG when show=False .
Standalone Demo (docling-ibm-models)#
demo/demo_layout_predictor.py shows the minimal pattern for visualizing raw LayoutPredictor output . The predictor returns bounding boxes in pixel space with TOPLEFT origin (keys l, t, r, b), so no coordinate conversion is needed before drawing :
draw.rectangle([pred["l"], pred["t"], pred["r"], pred["b"]], outline="orange")
draw.text((pred["l"], pred["t"]), f"{label}: {confidence}", fill="blue")
This contrasts with the full-pipeline path where clusters carry document-space coordinates that require scale_x/scale_y conversion.
Known Quirks#
- In
draw_clusters(), the y-coordinate of text cells usesscale_xinstead ofscale_yforcy0— this is likely a minor bug that would only manifest when the page has non-square pixel density. as_tuple()returns coordinates in different orders depending oncoord_origin, so always check the origin before passing the tuple directly to PIL.