Skip to content

Commit 8d471bc

Browse files
fix(BEE): recover unmatched tokens in table last row when model under-predicts row count
1 parent ef9bb95 commit 8d471bc

1 file changed

Lines changed: 74 additions & 0 deletions

File tree

docling/models/stages/table_structure/table_structure_model.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ def predict_tables(
227227
for table_cluster, tbl_box in in_tables:
228228
# Check if word-level cells are available from backend:
229229
sp = page._backend.get_segmented_page()
230+
using_word_cells = False
230231
if sp is not None:
231232
tcells = sp.get_cells_in_bbox(
232233
cell_unit=TextCellUnit.WORD,
@@ -235,6 +236,8 @@ def predict_tables(
235236
if len(tcells) == 0:
236237
# In case word-level cells yield empty
237238
tcells = table_cluster.cells
239+
else:
240+
using_word_cells = True
238241
else:
239242
# Otherwise - we use normal (line/phrase) cells
240243
tcells = table_cluster.cells
@@ -284,6 +287,77 @@ def predict_tables(
284287
.get("rs_seq", [])
285288
)
286289

290+
# docling-side workaround for the edge case, main root cause is in TFPredictor
291+
# model doesn't see the last row because it's at the very bottom edge of the crop
292+
if (
293+
self.do_cell_matching
294+
and using_word_cells
295+
and tokens
296+
and table_cells
297+
):
298+
matched_ids = {
299+
str(k)
300+
for k in table_out["predict_details"]
301+
.get("matches", {})
302+
.keys()
303+
}
304+
cells_bottom = max(
305+
(tc.bbox.b for tc in table_cells if tc.bbox is not None),
306+
default=0.0,
307+
)
308+
unmatched = []
309+
if cells_bottom > 0.0:
310+
for tok in tokens:
311+
if str(tok["id"]) in matched_ids:
312+
continue
313+
tok_bbox = BoundingBox.model_validate(
314+
tok["bbox"]
315+
).scaled(1 / self.scale)
316+
if (
317+
tok_bbox.t > cells_bottom
318+
and tok_bbox.b <= table_cluster.bbox.b
319+
):
320+
unmatched.append((tok, tok_bbox))
321+
if len(unmatched) > 1:
322+
centres = sorted((tb.t + tb.b) / 2.0 for _, tb in unmatched)
323+
median_centre = centres[len(centres) // 2]
324+
avg_height = sum(tb.b - tb.t for _, tb in unmatched) / len(
325+
unmatched
326+
)
327+
coherent = all(
328+
abs((tb.t + tb.b) / 2.0 - median_centre) <= avg_height
329+
for _, tb in unmatched
330+
)
331+
if not coherent:
332+
unmatched = []
333+
if unmatched:
334+
extra_row_idx = num_rows
335+
num_cols = max(num_cols, 1)
336+
for col_idx, (tok, tok_bbox) in enumerate(unmatched):
337+
table_cells.append(
338+
TableCell(
339+
text=tok["text"],
340+
bbox=tok_bbox,
341+
row_span=1,
342+
col_span=1,
343+
start_row_offset_idx=extra_row_idx,
344+
end_row_offset_idx=extra_row_idx + 1,
345+
start_col_offset_idx=col_idx % num_cols,
346+
end_col_offset_idx=(col_idx % num_cols) + 1,
347+
column_header=False,
348+
row_header=False,
349+
row_section=False,
350+
)
351+
)
352+
num_rows += 1
353+
_log.debug(
354+
"Recovered %d unmatched token(s) into extra row %d "
355+
"for table cluster id=%d",
356+
len(unmatched),
357+
extra_row_idx,
358+
table_cluster.id,
359+
)
360+
287361
tbl = Table(
288362
otsl_seq=otsl_seq,
289363
table_cells=table_cells,

0 commit comments

Comments
 (0)