@@ -30,21 +30,42 @@ pub const CHED: i64 = 10; // column header
3030pub const RHED : i64 = 11 ; // row header
3131pub const SROW : i64 = 12 ; // section row
3232
33+ /// A predicted table cell: an OTSL grid position (with spans) + its box in the
34+ /// 448 image normalized cxcywh, and the OTSL tag.
35+ #[ derive( Debug , Clone ) ]
36+ pub struct TableCell {
37+ pub row : usize ,
38+ pub col : usize ,
39+ pub colspan : usize ,
40+ pub rowspan : usize ,
41+ pub tag : i64 ,
42+ pub cx : f32 ,
43+ pub cy : f32 ,
44+ pub w : f32 ,
45+ pub h : f32 ,
46+ }
47+
3348pub struct TableFormer {
3449 encoder : Session ,
3550 decoder : Session ,
51+ bbox : Session ,
3652}
3753
3854impl TableFormer {
39- /// Load the exported encoder/decoder ONNX graphs (env overrides, else
40- /// `models/tableformer/{encoder,decoder}.onnx`). Returns `None` if either is
55+ /// Load the exported encoder/decoder/bbox ONNX graphs (env overrides, else
56+ /// `models/tableformer/{encoder,decoder,bbox }.onnx`). Returns `None` if any is
4157 /// absent, so the pipeline falls back to geometric reconstruction.
4258 pub fn load ( ) -> Option < Self > {
4359 let enc = std:: env:: var ( "DOCLING_TABLEFORMER_ENCODER" )
4460 . unwrap_or_else ( |_| "models/tableformer/encoder.onnx" . to_string ( ) ) ;
4561 let dec = std:: env:: var ( "DOCLING_TABLEFORMER_DECODER" )
4662 . unwrap_or_else ( |_| "models/tableformer/decoder.onnx" . to_string ( ) ) ;
47- if !std:: path:: Path :: new ( & enc) . exists ( ) || !std:: path:: Path :: new ( & dec) . exists ( ) {
63+ let bbx = std:: env:: var ( "DOCLING_TABLEFORMER_BBOX" )
64+ . unwrap_or_else ( |_| "models/tableformer/bbox.onnx" . to_string ( ) ) ;
65+ if [ & enc, & dec, & bbx]
66+ . iter ( )
67+ . any ( |p| !std:: path:: Path :: new ( p) . exists ( ) )
68+ {
4869 return None ;
4970 }
5071 let build = |path : & str | -> Result < Session , String > {
@@ -55,49 +76,19 @@ impl TableFormer {
5576 . commit_from_file ( path)
5677 . map_err ( |e| format ! ( "tableformer load {path}: {e}" ) )
5778 } ;
58- match ( build ( & enc) , build ( & dec) ) {
59- ( Ok ( encoder) , Ok ( decoder) ) => Some ( Self { encoder, decoder } ) ,
79+ match ( build ( & enc) , build ( & dec) , build ( & bbx) ) {
80+ ( Ok ( encoder) , Ok ( decoder) , Ok ( bbox) ) => Some ( Self {
81+ encoder,
82+ decoder,
83+ bbox,
84+ } ) ,
6085 _ => None ,
6186 }
6287 }
6388
6489 /// Predict the OTSL structure-token sequence for a table-region image.
6590 pub fn predict_otsl ( & mut self , img : & RgbImage ) -> Result < Vec < i64 > , String > {
66- // Preprocess exactly as docling: bilinear (cv2.INTER_LINEAR) resize the
67- // crop to 448², normalize (x/255 − mean)/std, laid out as (C, W, H) —
68- // docling transposes (2,1,0), so width is the major spatial axis (not
69- // C,H,W). The page→1024px box-average (cv2.INTER_AREA) is the caller's.
70- let n = ( SIDE * SIDE ) as usize ;
71- let side = SIDE as usize ;
72- let ( sw, sh) = ( img. width ( ) as i32 , img. height ( ) as i32 ) ;
73- let sxr = sw as f32 / SIDE as f32 ;
74- let syr = sh as f32 / SIDE as f32 ;
75- let mut data = vec ! [ 0f32 ; 3 * n] ;
76- for h in 0 ..side {
77- let fy = ( h as f32 + 0.5 ) * syr - 0.5 ;
78- let wy = fy - fy. floor ( ) ;
79- let y0c = ( fy. floor ( ) as i32 ) . clamp ( 0 , sh - 1 ) as u32 ;
80- let y1c = ( fy. floor ( ) as i32 + 1 ) . clamp ( 0 , sh - 1 ) as u32 ;
81- for w in 0 ..side {
82- let fx = ( w as f32 + 0.5 ) * sxr - 0.5 ;
83- let wx = fx - fx. floor ( ) ;
84- let x0c = ( fx. floor ( ) as i32 ) . clamp ( 0 , sw - 1 ) as u32 ;
85- let x1c = ( fx. floor ( ) as i32 + 1 ) . clamp ( 0 , sw - 1 ) as u32 ;
86- let p00 = img. get_pixel ( x0c, y0c) ;
87- let p01 = img. get_pixel ( x1c, y0c) ;
88- let p10 = img. get_pixel ( x0c, y1c) ;
89- let p11 = img. get_pixel ( x1c, y1c) ;
90- let idx = w * side + h; // (C, W, H): c*n + w*H + h
91- for c in 0 ..3 {
92- let top = p00[ c] as f32 * ( 1.0 - wx) + p01[ c] as f32 * wx;
93- let bot = p10[ c] as f32 * ( 1.0 - wx) + p11[ c] as f32 * wx;
94- let v = top * ( 1.0 - wy) + bot * wy;
95- data[ c * n + idx] = ( v / 255.0 - MEAN [ c] ) / STD [ c] ;
96- }
97- }
98- }
99- let input = Tensor :: from_array ( ( [ 1usize , 3 , SIDE as usize , SIDE as usize ] , data) )
100- . map_err ( |e| format ! ( "tableformer: input: {e}" ) ) ?;
91+ let input = preprocess ( img) ?;
10192 let enc_out = self
10293 . encoder
10394 . run ( ort:: inputs![ "image" => input] )
@@ -144,6 +135,230 @@ impl TableFormer {
144135 }
145136 Ok ( out)
146137 }
138+
139+ /// Full structure prediction: OTSL grid cells with per-cell boxes (in the 448
140+ /// image, normalized cxcywh). Collects per-cell decoder hidden states using
141+ /// docling's exact bbox bookkeeping (skip-after-row-break, first-lcel of a
142+ /// horizontal span), runs the bbox decoder, merges span boxes, then lays the
143+ /// cells onto the OTSL grid with row/col spans.
144+ pub fn predict_table_structure ( & mut self , img : & RgbImage ) -> Result < Vec < TableCell > , String > {
145+ let input = preprocess ( img) ?;
146+ let enc_out = self
147+ . encoder
148+ . run ( ort:: inputs![ "image" => input] )
149+ . map_err ( |e| format ! ( "tableformer: encode: {e}" ) ) ?;
150+ let ( mshape, mem) = enc_out[ "memory" ]
151+ . try_extract_tensor :: < f32 > ( )
152+ . map_err ( |e| format ! ( "tableformer: memory: {e}" ) ) ?;
153+ let mshape: Vec < usize > = mshape. iter ( ) . map ( |& x| x as usize ) . collect ( ) ;
154+ let mem: Vec < f32 > = mem. to_vec ( ) ;
155+ let ( eshape, eo) = enc_out[ "enc_out" ]
156+ . try_extract_tensor :: < f32 > ( )
157+ . map_err ( |e| format ! ( "tableformer: enc_out: {e}" ) ) ?;
158+ let eshape: Vec < usize > = eshape. iter ( ) . map ( |& x| x as usize ) . collect ( ) ;
159+ let eo: Vec < f32 > = eo. to_vec ( ) ;
160+
161+ let mut tags: Vec < i64 > = vec ! [ START ] ;
162+ let mut otsl: Vec < i64 > = Vec :: new ( ) ;
163+ let mut hiddens: Vec < f32 > = Vec :: new ( ) ; // flattened [n, 512]
164+ let mut n = 0usize ;
165+ let mut prev_ucel = false ;
166+ let mut skip = true ; // first tag after <start> is skipped
167+ let mut first_lcel = true ;
168+ let mut bbox_ind = 0usize ;
169+ let mut cur_bbox_ind = 0usize ;
170+ let mut merge: std:: collections:: HashMap < usize , i64 > = std:: collections:: HashMap :: new ( ) ;
171+ while otsl. len ( ) < MAX_STEPS {
172+ let tags_t = Tensor :: from_array ( ( [ tags. len ( ) , 1usize ] , tags. clone ( ) ) )
173+ . map_err ( |e| format ! ( "tableformer: tags: {e}" ) ) ?;
174+ let mem_t = Tensor :: from_array ( ( mshape. clone ( ) , mem. clone ( ) ) )
175+ . map_err ( |e| format ! ( "tableformer: mem: {e}" ) ) ?;
176+ let dout = self
177+ . decoder
178+ . run ( ort:: inputs![ "tags" => tags_t, "memory" => mem_t] )
179+ . map_err ( |e| format ! ( "tableformer: decode: {e}" ) ) ?;
180+ let ( _, logits) = dout[ "logits" ]
181+ . try_extract_tensor :: < f32 > ( )
182+ . map_err ( |e| format ! ( "tableformer: logits: {e}" ) ) ?;
183+ let mut tag = argmax ( logits) as i64 ;
184+ if tag == XCEL {
185+ tag = LCEL ;
186+ }
187+ if prev_ucel && tag == LCEL {
188+ tag = FCEL ;
189+ }
190+ if tag == END {
191+ break ;
192+ }
193+ let ( _, hidden) = dout[ "hidden" ]
194+ . try_extract_tensor :: < f32 > ( )
195+ . map_err ( |e| format ! ( "tableformer: hidden: {e}" ) ) ?;
196+ // docling's tag_H_buf / bboxes_to_merge bookkeeping.
197+ if !skip && matches ! ( tag, FCEL | ECEL | CHED | RHED | SROW | NL | UCEL ) {
198+ hiddens. extend_from_slice ( hidden) ;
199+ n += 1 ;
200+ if !first_lcel {
201+ merge. insert ( cur_bbox_ind, bbox_ind as i64 ) ;
202+ }
203+ bbox_ind += 1 ;
204+ }
205+ if tag != LCEL {
206+ first_lcel = true ;
207+ } else if first_lcel {
208+ hiddens. extend_from_slice ( hidden) ;
209+ n += 1 ;
210+ first_lcel = false ;
211+ cur_bbox_ind = bbox_ind;
212+ merge. insert ( cur_bbox_ind, -1 ) ;
213+ bbox_ind += 1 ;
214+ }
215+ skip = matches ! ( tag, NL | UCEL | XCEL ) ;
216+ prev_ucel = tag == UCEL ;
217+ otsl. push ( tag) ;
218+ tags. push ( tag) ;
219+ }
220+ if n == 0 {
221+ return Ok ( Vec :: new ( ) ) ;
222+ }
223+ let tag_h = Tensor :: from_array ( ( [ n, 512usize ] , hiddens) )
224+ . map_err ( |e| format ! ( "tableformer: tag_h: {e}" ) ) ?;
225+ let eo_t = Tensor :: from_array ( ( eshape, eo) ) . map_err ( |e| format ! ( "tableformer: eo: {e}" ) ) ?;
226+ let bout = self
227+ . bbox
228+ . run ( ort:: inputs![ "enc_out" => eo_t, "tag_h" => tag_h] )
229+ . map_err ( |e| format ! ( "tableformer: bbox: {e}" ) ) ?;
230+ let ( _, raw) = bout[ "boxes" ]
231+ . try_extract_tensor :: < f32 > ( )
232+ . map_err ( |e| format ! ( "tableformer: boxes: {e}" ) ) ?;
233+ let boxes: Vec < [ f32 ; 4 ] > = raw
234+ . chunks_exact ( 4 )
235+ . map ( |c| [ c[ 0 ] , c[ 1 ] , c[ 2 ] , c[ 3 ] ] )
236+ . collect ( ) ;
237+ let merged = merge_spans ( & boxes, & merge) ;
238+ Ok ( build_table_cells ( & otsl, & merged) )
239+ }
240+ }
241+
242+ /// docling's preprocessing: bilinear (cv2.INTER_LINEAR) resize the crop to 448²,
243+ /// normalize `(x/255 − mean)/std`, laid out as (C, W, H) — docling transposes
244+ /// (2,1,0), so width is the major spatial axis. The page→1024px box-average
245+ /// (cv2.INTER_AREA) is the caller's job.
246+ fn preprocess ( img : & RgbImage ) -> Result < Tensor < f32 > , String > {
247+ let nn = ( SIDE * SIDE ) as usize ;
248+ let side = SIDE as usize ;
249+ let ( sw, sh) = ( img. width ( ) as i32 , img. height ( ) as i32 ) ;
250+ let sxr = sw as f32 / SIDE as f32 ;
251+ let syr = sh as f32 / SIDE as f32 ;
252+ let mut data = vec ! [ 0f32 ; 3 * nn] ;
253+ for h in 0 ..side {
254+ let fy = ( h as f32 + 0.5 ) * syr - 0.5 ;
255+ let wy = fy - fy. floor ( ) ;
256+ let y0c = ( fy. floor ( ) as i32 ) . clamp ( 0 , sh - 1 ) as u32 ;
257+ let y1c = ( fy. floor ( ) as i32 + 1 ) . clamp ( 0 , sh - 1 ) as u32 ;
258+ for w in 0 ..side {
259+ let fx = ( w as f32 + 0.5 ) * sxr - 0.5 ;
260+ let wx = fx - fx. floor ( ) ;
261+ let x0c = ( fx. floor ( ) as i32 ) . clamp ( 0 , sw - 1 ) as u32 ;
262+ let x1c = ( fx. floor ( ) as i32 + 1 ) . clamp ( 0 , sw - 1 ) as u32 ;
263+ let p00 = img. get_pixel ( x0c, y0c) ;
264+ let p01 = img. get_pixel ( x1c, y0c) ;
265+ let p10 = img. get_pixel ( x0c, y1c) ;
266+ let p11 = img. get_pixel ( x1c, y1c) ;
267+ let idx = w * side + h; // (C, W, H): c*n + w*H + h
268+ for c in 0 ..3 {
269+ let top = p00[ c] as f32 * ( 1.0 - wx) + p01[ c] as f32 * wx;
270+ let bot = p10[ c] as f32 * ( 1.0 - wx) + p11[ c] as f32 * wx;
271+ let v = top * ( 1.0 - wy) + bot * wy;
272+ data[ c * nn + idx] = ( v / 255.0 - MEAN [ c] ) / STD [ c] ;
273+ }
274+ }
275+ }
276+ Tensor :: from_array ( ( [ 1usize , 3 , side, side] , data) )
277+ . map_err ( |e| format ! ( "tableformer: input: {e}" ) )
278+ }
279+
280+ /// docling's `mergebboxes` (cxcywh): the union box of a horizontal span's first
281+ /// and last cell.
282+ fn mergebboxes ( b1 : [ f32 ; 4 ] , b2 : [ f32 ; 4 ] ) -> [ f32 ; 4 ] {
283+ let new_w = ( b2[ 0 ] + b2[ 2 ] / 2.0 ) - ( b1[ 0 ] - b1[ 2 ] / 2.0 ) ;
284+ let new_h = ( b2[ 1 ] + b2[ 3 ] / 2.0 ) - ( b1[ 1 ] - b1[ 3 ] / 2.0 ) ;
285+ let new_left = b1[ 0 ] - b1[ 2 ] / 2.0 ;
286+ let new_top = ( b2[ 1 ] - b2[ 3 ] / 2.0 ) . min ( b1[ 1 ] - b1[ 3 ] / 2.0 ) ;
287+ [ new_left + new_w / 2.0 , new_top + new_h / 2.0 , new_w, new_h]
288+ }
289+
290+ /// Apply docling's span merges: each merge key combines its box with the partner
291+ /// (`-1` → the last box); partners are dropped.
292+ fn merge_spans ( boxes : & [ [ f32 ; 4 ] ] , merge : & std:: collections:: HashMap < usize , i64 > ) -> Vec < [ f32 ; 4 ] > {
293+ let skip: std:: collections:: HashSet < usize > = merge
294+ . values ( )
295+ . filter ( |& & v| v >= 0 )
296+ . map ( |& v| v as usize )
297+ . collect ( ) ;
298+ let mut out = Vec :: new ( ) ;
299+ for ( i, & b) in boxes. iter ( ) . enumerate ( ) {
300+ if let Some ( & j) = merge. get ( & i) {
301+ let partner = if j < 0 { boxes. len ( ) - 1 } else { j as usize } ;
302+ out. push ( mergebboxes ( b, boxes[ partner. min ( boxes. len ( ) - 1 ) ] ) ) ;
303+ } else if !skip. contains ( & i) {
304+ out. push ( b) ;
305+ }
306+ }
307+ out
308+ }
309+
310+ const CELL_TAGS : [ i64 ; 6 ] = [ FCEL , ECEL , XCEL , CHED , RHED , SROW ] ;
311+
312+ /// Lay the OTSL tag stream onto a grid (docling's `_build_table_cells`, OTSL
313+ /// mode): cell tags create cells at (row, col); `lcel`/`ucel`/`xcel` are spans
314+ /// (counted toward the column index but not cells). Colspan/rowspan are read off
315+ /// the grid (consecutive `lcel`/`ucel` to the right/below). `boxes` are indexed
316+ /// by cell order and aligned with the cells.
317+ fn build_table_cells ( otsl : & [ i64 ] , boxes : & [ [ f32 ; 4 ] ] ) -> Vec < TableCell > {
318+ // 2D grid of tags (rows split on NL) for span lookups.
319+ let mut grid: Vec < Vec < i64 > > = vec ! [ Vec :: new( ) ] ;
320+ for & t in otsl {
321+ if t == NL {
322+ grid. push ( Vec :: new ( ) ) ;
323+ } else {
324+ grid. last_mut ( ) . unwrap ( ) . push ( t) ;
325+ }
326+ }
327+ let mut cells = Vec :: new ( ) ;
328+ let mut cell_id = 0usize ;
329+ for ( r, row) in grid. iter ( ) . enumerate ( ) {
330+ for ( c, & tag) in row. iter ( ) . enumerate ( ) {
331+ if !CELL_TAGS . contains ( & tag) {
332+ continue ;
333+ }
334+ let mut colspan = 1 ;
335+ while c + colspan < row. len ( ) && matches ! ( row[ c + colspan] , LCEL | XCEL ) {
336+ colspan += 1 ;
337+ }
338+ let mut rowspan = 1 ;
339+ while r + rowspan < grid. len ( )
340+ && grid[ r + rowspan]
341+ . get ( c)
342+ . is_some_and ( |& t| matches ! ( t, UCEL | XCEL ) )
343+ {
344+ rowspan += 1 ;
345+ }
346+ let b = boxes. get ( cell_id) . copied ( ) . unwrap_or ( [ 0.0 ; 4 ] ) ;
347+ cells. push ( TableCell {
348+ row : r,
349+ col : c,
350+ colspan,
351+ rowspan,
352+ tag,
353+ cx : b[ 0 ] ,
354+ cy : b[ 1 ] ,
355+ w : b[ 2 ] ,
356+ h : b[ 3 ] ,
357+ } ) ;
358+ cell_id += 1 ;
359+ }
360+ }
361+ cells
147362}
148363
149364fn argmax ( v : & [ f32 ] ) -> usize {
0 commit comments