Skip to content

Commit 051dfc1

Browse files
committed
refactor(ls_tools): remove H1, H2, H4 heuristics — explicit annotation contract
Replace the three highest-value geometric heuristics in the Label Studio → DoclingDocument pipeline with explicit annotation-driven lookups: H1 (parent resolution via containment tree): - Build `children_by_parent` dict from `parent_ls_id` on rectangle regions - `_determine_parent` and `_has_unresolved_parent` become direct dict lookups - Remove `build_containment_tree`, `ls_element_to_cvat_element`, `_find_tree_node`, `containment_tree`/`tree_index` fields, and CVATElement/TreeNode imports H2 (table structure discovery via bbox containment): - `_create_table` uses `children_by_parent.get(element.ls_id)` instead of tree walk - Table structure elements still consumed geometrically by `compute_cells` (unchanged) H4 (list boundary detection via level comparison): - `should_terminate_list`: remove level-based fallback — group path is the only boundary signal; element outside active group terminates the list - `shtart_list`: remove level-comparison block; warn on list_item with no group H1 reading order (child paths): - `build_global_reading_order` restructured with DFS splice: child paths (polyline `level > 1`) are spliced immediately after their parent element - Validation warnings for mismatched parent_ls_id and level/path-type mismatches H5 (_process_remaining_children) simplifies naturally: - Elements with no `parent_ls_id` absent from reading order are dropped with a warning instead of silently added at root Signed-off-by: Christoph Auer <cau@zurich.ibm.com>
1 parent 2e8b209 commit 051dfc1

2 files changed

Lines changed: 111 additions & 144 deletions

File tree

docling_cvat_tools/ls_tools/list_hierarchy.py

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -123,31 +123,14 @@ def should_terminate_list(
123123
# No active list
124124
return False
125125

126-
# Check group-based termination
126+
# Group-based termination only
127127
if state.active_group_id is not None:
128128
element_groups = element_to_groups.get(element.ls_id, [])
129129
if state.active_group_id not in element_groups:
130130
# Element is not in the active group - terminate
131131
return True
132132
return False
133133

134-
# Fallback: level-based termination (when no group paths)
135-
if not is_list_item_element(element):
136-
# Non-list-item elements don't terminate lists by themselves
137-
# unless they have an explicit lower level
138-
if element.level is not None and element.level < state.level_stack[-1]:
139-
return True
140-
return False
141-
142-
# For list items, check level conditions
143-
if element.level is None:
144-
# Unset level terminates the list
145-
return True
146-
147-
if element.level < state.level_stack[-1]:
148-
# Lower level terminates inner lists
149-
return True
150-
151134
return False
152135

153136

@@ -171,23 +154,17 @@ def should_start_list(
171154

172155
# Check if element is part of a group
173156
element_groups = element_to_groups.get(element.ls_id, [])
157+
if not element_groups:
158+
logger.warning(
159+
"list_item %s has no group path — treating as standalone item",
160+
element.ls_id,
161+
)
162+
return False
163+
174164
if element_groups and state.active_group_id is None:
175165
# Element is in a group but no active group - start new list
176166
return True
177167

178-
if not state.level_stack:
179-
# No active list and this is a list item - start list
180-
return True
181-
182-
# Check for nested list (higher level)
183-
effective_level = (
184-
element.level if element.level is not None else state.current_level
185-
)
186-
if effective_level is not None and state.level_stack:
187-
if effective_level > state.level_stack[-1]:
188-
# Higher level - start nested list
189-
return True
190-
191168
return False
192169

193170

0 commit comments

Comments
 (0)