Table Detection and Navigation#

from pathlib import Path
from dessia_drawing.core import Drawing
from drawing_tools.config.default_language_configs import DEFAULT_ENGLISH_CONFIG, DEFAULT_FRENCH_CONFIG
from drawing_tools.featured_drawing import FeaturedDrawing
from drawing_tools.table.detection import SheetTableDetector

_DATA_DIR = Path("data/json")
drawing = Drawing.from_json(str(_DATA_DIR / "2902060102_--A_DEF01_LH SUPPORT ACCUMULATOR ASSEMBLY.json"))
featured_drawing = FeaturedDrawing(drawing, language_configs=[DEFAULT_FRENCH_CONFIG, DEFAULT_ENGLISH_CONFIG])
featured_sheet = featured_drawing.sheets[1]
detector = SheetTableDetector(sheet=featured_sheet.sheet)
tables_dict = detector.detect_all_tables()
table = next((t for tables in tables_dict.values() for t in tables), None)
featured_sheet.plot_data_tables().plot()

Drawing Tools provides table detection and extraction capabilities for technical drawings. This guide explains how to detect, analyze, and extract data from tables.

Overview#

Technical drawings often contain tables such as:

  • Title blocks with drawing metadata

  • Bills of Materials (BOM)

  • Revision tables

  • Custom data tables

Drawing Tools can automatically detect these tables from the raw line geometry and extract their structure and content.

The following visualization is produced by featured_sheet.plot_data_tables(), which renders each reconstructed table in full — every cell gets its own color (texts in the cell color, background in the same color lightened, showing which text was attached to which cell), with a frame and a summary label in the table color; the title block is labeled in magenta. Native Table annotations keep a soft colored rectangle + label. This method is also available on FeaturedDrawing to visualize all sheets at once.

Source: 2902060102_–A_DEF01_LH SUPPORT ACCUMULATOR ASSEMBLY.json, Sheet 1

See also

For a complete working example, see scripts/scripts_tables.py.

For a detailed description of the detection pipeline internals, see Table Detection Pipeline.

Quick Start#

from dessia_drawing.core import Drawing
from drawing_tools.table.detection import SheetTableDetector

drawing = Drawing.from_json("path/to/drawing.json")
sheet = drawing.sheets[0]

detector = SheetTableDetector(sheet=sheet)

# Detect all tables across all views
tables_dict = detector.detect_all_tables()

# Both returned kinds expose nb_rows / nb_cols / cells / source
for view_idx, tables in tables_dict.items():
    for table in tables:
        print(f"View {view_idx}: {table.nb_rows}r x {table.nb_cols}c from {table.source}")

# Detect the title block specifically
title_block = detector.detect_title_block()

Note

SheetTableDetector accepts both dessia_drawing.Sheet and FeaturedSheet objects.

The Different Kinds of Tables#

A drawing carries tables in three different forms, and the detector returns all of them side by side. Each detected table names its origin in its source attribute:

Kind

Returned type

source

Where it comes from

Native table annotation

Table

"annotation"

The source file already describes the table (rows/columns declared by CATIA); nothing is reconstructed. Collected across nesting levels, so tables inside a CompositeEntity are included.

Framed note

DetectedTable (1 cell)

"framed_symbol_<i>"

A Symbol note whose frame is a rectangle — a one-cell table wrapping the note’s text.

Edge-based table

DetectedTable

"<candidate>#pass<n>"

Reconstructed from the raw H/V line segments: the title block, revision and NOTA blocks of the sheet frame (background_geometries#pass0) or grids carried by an annotation group (composite_entity_3#pass0).

Title block

DetectedTable

(edge-based)

Not a fourth branch: the edge-based table of the background view closest to the bottom-right corner, selected by get_title_block_table() and parsed into a TitleBlock by detect_title_block().

for view_index, view_tables in tables_dict.items():
    for detected_table in view_tables:
        print(f"View {view_index}: {type(detected_table).__name__} from {detected_table.source}")

Only nb_rows, nb_cols, cells, source, bounding_rectangle and the plot methods are common to both types. Everything the next section uses — nb_cells, grid, get_table_cell_at, find_table_cells_with_text, non_empty_cells — belongs to DetectedTable only, and a native Table’s cells holds nested lists instead of TableCell objects. Filter by type before using the rich API:

from dessia_drawing.annotations.table import Table

from drawing_tools.table.detection import DetectedTable

reconstructed_tables = [t for view_tables in tables_dict.values() for t in view_tables
                        if isinstance(t, DetectedTable)]
native_tables = [t for view_tables in tables_dict.values() for t in view_tables
                 if isinstance(t, Table)]

Note

Only the background view contributes its own geometries to the edge-based branch (minus the grid reference, so the sheet frame is never taken for a table). For the other views, only the geometries carried by CompositeEntity annotations are candidates — their direct geometries draw the part, not tables.

Working with Detected Tables#

The API below is that of DetectedTable (the reconstructed tables of the previous section); a native Table annotation offers only nb_rows / nb_cols / cells.

Table Properties#

# Table dimensions
print(f"Rows: {table.nb_rows}")
print(f"Columns: {table.nb_cols}")
print(f"Cells: {table.nb_cells}")

# Access all cells
for cell in table.cells:
    print(f"Cell ({cell.row_id}, {cell.col_id}): {cell.get_text_content()}")

TableCell vs GridCell#

Tables have two cell concepts:

  • GridCell: The basic grid unit (single row/column intersection) — see Fig. 3 below

  • TableCell: A logical cell that may span multiple grid cells (merged cells) — see Fig. 2 below

# Get a table cell (may span multiple grid cells)
table_cell = table.get_table_cell_at(row_index=1, column_index=1)

if table_cell:
    print(f"Content: {table_cell.get_text_content()}")
    print(f"Combined text: {table_cell.get_combined_text()}")
    print(f"Row span: {table_cell.row_span}")
    print(f"Column span: {table_cell.column_span}")

# Get the underlying grid cell
grid_cell = table.get_grid_cell_at(row_index=1, column_index=1)

Accessing Cell Content#

# All content objects from all cells
all_content = table.get_all_content()

# Iterate over cells
for cell in table.cells:
    content = cell.get_text_content() or "(empty)"
    print(f"({cell.row_id}, {cell.col_id}): {content}")

# Search for specific text
cells = table.find_table_cells_with_text("DIMENSION", case_sensitive=False)
for cell in cells:
    print(f"Found at ({cell.row_id}, {cell.col_id})")

Working with Title Blocks#

title_block = detector.detect_title_block()
table = title_block.parent_table  # DetectedTable with all methods above

Visualizing a Single Table#

By default each cell cycles through a rotating palette: its texts are drawn in the cell color and its background in the same color lightened, so the pairing between a text and its cell is visible at a glance (a colored cell with no text of its color is an empty cell). To inspect merged cells, use detailed=True: each cell label carries its position and, when merged, the spanned grid ranges.

# Basic visualization — one color per cell (texts + lightened background)
table.plot_data()
Fig. 1 - Default visualization: unified per-cell colors
# Detailed view with cell position labels
table.plot_data(detailed=True)
Fig. 2 - Detailed view showing TableCell boundaries
# Visualize the underlying grid
table.grid.plot_data(extra_primitives=[])
Fig. 3 - GridCell structure visualization

Visualizing a Whole Sheet#

Use the @plot_data_view method on FeaturedSheet to visualize every detected table of the sheet at once (all kinds together, title block labeled in magenta). The rendering goes through FeaturedSheet.table_detector — call featured_sheet.set_table_detector(my_detector) with your own SheetTableDetector to render a non-default-configured detection:

# All detected tables with title block highlighted
featured_sheet.plot_data_tables().plot()

This method is also available on FeaturedDrawing to visualize all sheets at once. See Display & Report Methods for more interactive examples.

Going Further: Inside the Detection#

For readers who want to see how the detector works

The feature is DetectedTable (table/detection.py): a grid of TableLine objects, the TableCell and GridCell of table/core.py built on it, and the content attached to each cell. The title block is the same table read further: TitleBlock (title_block.py) holds its parsed fields.

Two detectors share the work, and FeaturedSheet.table_detector owns them:

  • SheetTableDetector (table/detection.py) walks the views of a sheet and merges the three sources of The Different Kinds of Tables;

  • TableGenerator (same module) is the edge-based branch — the one that rebuilds a table from bare lines — and TitleBlockExtractor (table/title_block.py) reads the fields of the title-block table found by detect_title_block.

The recipe is one loop over the views:

def detect_all_tables(self) -> dict[int, list[TableType]]:
    """
    Detect all tables in the drawing across all views.

    Every view goes through the three branches of :meth:`detect_view_tables`
    (``Table`` annotations, framed ``Symbol`` notes, edge-based detection).

    The background view gets ONE extra candidate source: its own ``geometries``
    (the sheet frame layer, where the title block and the revision block live),
    minus the grid reference geometries. For the other views only the geometries
    carried by ``CompositeEntity`` annotations are candidates — their direct
    geometries draw the part, not tables.

    Edge-based tables are validated to ensure they have sufficient content before
    being included.

    :return: Dictionary mapping view index to list of validated detected tables
    """
    all_tables = {}

    for view_idx, view in enumerate(self.sheet.views):
        is_background = view_idx == self.background_view_index
        view_tables = self.detect_view_tables(view=view, is_background=is_background, _view_index=view_idx)
        if view_tables:
            all_tables[view_idx] = view_tables

    return all_tables

detect_view_tables runs the three branches on one view: _extract_tables_from_tables (native Table annotations), _extract_tables_from_notes (TypeNote symbols with a rectangular frame, _table_from_framed_entity) and _extract_tables_from_geometries, which hands the candidate edges to TableGenerator.detect_tables_from_edges_preprocess_lines merges the collinear segments, _establish_connections links the lines that touch, and _find_table_structures turns each connected component with enough content into a DetectedTable. The full pipeline, stage by stage, is in Table Detection Pipeline.