Skip to content

Commit 45667c7

Browse files
feat: Visualizer for table cells (#325)
* feat: visualizing the tables Signed-off-by: Peter Staar <[email protected]> * Added test for table visualizer Signed-off-by: Peter Staar <[email protected]> * cleaned up code Signed-off-by: Peter Staar <[email protected]> * updated with content-layer Signed-off-by: Peter Staar <[email protected]> --------- Signed-off-by: Peter Staar <[email protected]>
1 parent 7094828 commit 45667c7

File tree

4 files changed

+157
-4
lines changed

4 files changed

+157
-4
lines changed

docling_core/transforms/visualizer/layout_visualizer.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,10 @@ def _draw_clusters(
119119
)
120120

121121
def _draw_doc_layout(
122-
self, doc: DoclingDocument, images: Optional[dict[Optional[int], Image]] = None
122+
self,
123+
doc: DoclingDocument,
124+
images: Optional[dict[Optional[int], Image]] = None,
125+
included_content_layers: Optional[set[ContentLayer]] = None,
123126
):
124127
"""Draw the document clusters and optionaly the reading order."""
125128
clusters = []
@@ -128,6 +131,9 @@ def _draw_doc_layout(
128131
if images is not None:
129132
my_images = images
130133

134+
if included_content_layers is None:
135+
included_content_layers = {c for c in ContentLayer}
136+
131137
# Initialise `my_images` beforehand: sometimes, you have the
132138
# page-images but no DocItems!
133139
for page_nr, page in doc.pages.items():
@@ -141,9 +147,7 @@ def _draw_doc_layout(
141147
prev_image = None
142148
prev_page_nr = None
143149
for idx, (elem, _) in enumerate(
144-
doc.iterate_items(
145-
included_content_layers={ContentLayer.BODY, ContentLayer.FURNITURE}
146-
)
150+
doc.iterate_items(included_content_layers=included_content_layers)
147151
):
148152
if not isinstance(elem, DocItem):
149153
continue
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Define classes for layout visualization."""
2+
3+
import logging
4+
from copy import deepcopy
5+
from typing import Optional
6+
7+
from PIL import ImageDraw
8+
from PIL.Image import Image
9+
from pydantic import BaseModel
10+
from typing_extensions import override
11+
12+
from docling_core.transforms.visualizer.base import BaseVisualizer
13+
from docling_core.types.doc.document import ContentLayer, DoclingDocument, TableItem
14+
15+
_log = logging.getLogger(__name__)
16+
17+
18+
class TableVisualizer(BaseVisualizer):
19+
"""Table visualizer."""
20+
21+
class Params(BaseModel):
22+
"""Table visualization parameters."""
23+
24+
# show_Label: bool = False
25+
show_cells: bool = True
26+
# show_rows: bool = False
27+
# show_cols: bool = False
28+
29+
base_visualizer: Optional[BaseVisualizer] = None
30+
params: Params = Params()
31+
32+
def _draw_table_cells(
33+
self,
34+
table: TableItem,
35+
page_image: Image,
36+
page_height: float,
37+
scale_x: float,
38+
scale_y: float,
39+
):
40+
"""Draw individual table cells."""
41+
draw = ImageDraw.Draw(page_image, "RGBA")
42+
43+
for cell in table.data.table_cells:
44+
if cell.bbox is not None:
45+
46+
tl_bbox = cell.bbox.to_top_left_origin(page_height=page_height)
47+
48+
cell_color = (256, 0, 0, 32) # Transparent black for cells
49+
50+
cx0, cy0, cx1, cy1 = tl_bbox.as_tuple()
51+
cx0 *= scale_x
52+
cx1 *= scale_x
53+
cy0 *= scale_y
54+
cy1 *= scale_y
55+
56+
draw.rectangle(
57+
[(cx0, cy0), (cx1, cy1)],
58+
outline=(256, 0, 0, 128),
59+
fill=cell_color,
60+
)
61+
62+
def _draw_doc_tables(
63+
self,
64+
doc: DoclingDocument,
65+
images: Optional[dict[Optional[int], Image]] = None,
66+
included_content_layers: Optional[set[ContentLayer]] = None,
67+
):
68+
"""Draw the document tables."""
69+
my_images: dict[Optional[int], Image] = {}
70+
71+
if images is not None:
72+
my_images = images
73+
74+
if included_content_layers is None:
75+
included_content_layers = {c for c in ContentLayer}
76+
77+
# Initialise `my_images` beforehand: sometimes, you have the
78+
# page-images but no DocItems!
79+
for page_nr, page in doc.pages.items():
80+
page_image = doc.pages[page_nr].image
81+
if page_image is None or (pil_img := page_image.pil_image) is None:
82+
raise RuntimeError("Cannot visualize document without images")
83+
elif page_nr not in my_images:
84+
image = deepcopy(pil_img)
85+
my_images[page_nr] = image
86+
87+
for idx, (elem, _) in enumerate(
88+
doc.iterate_items(included_content_layers=included_content_layers)
89+
):
90+
if not isinstance(elem, TableItem):
91+
continue
92+
if len(elem.prov) == 0:
93+
continue # Skip elements without provenances
94+
95+
if len(elem.prov) == 1:
96+
97+
page_nr = elem.prov[0].page_no
98+
99+
if page_nr in my_images:
100+
image = my_images[page_nr]
101+
102+
if self.params.show_cells:
103+
self._draw_table_cells(
104+
table=elem,
105+
page_height=doc.pages[page_nr].size.height,
106+
page_image=image,
107+
scale_x=image.width / doc.pages[page_nr].size.width,
108+
scale_y=image.height / doc.pages[page_nr].size.height,
109+
)
110+
111+
else:
112+
raise RuntimeError(f"Cannot visualize page-image for {page_nr}")
113+
114+
else:
115+
_log.error("Can not yet visualise tables with multiple provenances")
116+
117+
return my_images
118+
119+
@override
120+
def get_visualization(
121+
self,
122+
*,
123+
doc: DoclingDocument,
124+
**kwargs,
125+
) -> dict[Optional[int], Image]:
126+
"""Get visualization of the document as images by page."""
127+
base_images = (
128+
self.base_visualizer.get_visualization(doc=doc, **kwargs)
129+
if self.base_visualizer
130+
else None
131+
)
132+
return self._draw_doc_tables(
133+
doc=doc,
134+
images=base_images,
135+
)
Loading

test/test_visualization.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import PIL.Image
44

5+
from docling_core.transforms.visualizer.table_visualizer import TableVisualizer
56
from docling_core.types.doc.document import DoclingDocument
67

78
from .test_data_gen_flag import GEN_TEST_DATA
@@ -52,3 +53,16 @@ def test_doc_visualization_no_label():
5253
exp_file=VIZ_TEST_DATA_PATH / f"{src.stem}_viz_wout_lbl_p{k}.png",
5354
actual=viz_pages[k],
5455
)
56+
57+
58+
def test_table_visualization_no_label():
59+
src = Path("./test/data/doc/2408.09869v3_enriched.json")
60+
doc = DoclingDocument.load_from_json(src)
61+
62+
visualizer = TableVisualizer()
63+
viz_pages = visualizer.get_visualization(doc=doc)
64+
65+
verify(
66+
exp_file=VIZ_TEST_DATA_PATH / f"{src.stem}_table_viz_wout_lbl_p5.png",
67+
actual=viz_pages[5],
68+
)

0 commit comments

Comments
 (0)