Skip to content

Commit afbb4c8

Browse files
artizclaude
andcommitted
feat(tableformer): replicate docling's table-crop preprocessing (page→1024px→bbox→448)
The decode is byte-exact on docling's input tensor; the live pipeline must feed the same pixels. docling resizes the whole page to 1024px height (cv2.INTER_AREA), crops the table bbox out of that, then resizes the crop to 448² (bilinear). The tf_otsl example now does the page→1024px (box-average) crop, and predict_otsl's 448 resize is bilinear, matching docling's two distinct interpolations. This lands the correct method and confirms the crop geometry matches docling (434×172px bbox at the same position on 2305v1-pg9). It does NOT yet reproduce docling's OTSL live: the model is hyper-sensitive to exact pixels, and matching docling's full raster pipeline byte-for-byte across different libraries (pdfium render anti-aliasing, cv2 INTER_AREA, torchvision bilinear) is a separate, hard parity problem — different filter choices swing the token count (54 target vs 88/121 observed). Recorded as the gating step in PDF_CONFORMANCE.md before OTSL→grid, cell matching, and serialization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 27b5230 commit afbb4c8

2 files changed

Lines changed: 16 additions & 8 deletions

File tree

crates/fleischwolf-pdf/examples/tf_otsl.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,19 @@ fn main() {
3131
let regions = layout
3232
.predict(&page.image, page.width, page.height)
3333
.expect("layout");
34+
// docling resizes the whole page to 1024px height, then crops the table
35+
// bbox out of *that*. Replicate so the model sees the same pixels.
36+
let sf = 1024.0 / page.image.height() as f32;
37+
let pw1024 = (page.image.width() as f32 * sf).round() as u32;
38+
let page1024 = imageops::thumbnail(&page.image, pw1024, 1024);
3439
for r in regions.iter().filter(|r| r.label == "table") {
35-
let s = page.scale;
36-
let x = (r.l * s).max(0.0) as u32;
37-
let y = (r.t * s).max(0.0) as u32;
38-
let w = ((r.r - r.l) * s) as u32;
39-
let h = ((r.b - r.t) * s) as u32;
40-
let crop = imageops::crop_imm(&page.image, x, y, w, h).to_image();
40+
// bbox (points) → 1024px-page coords: scale*sf = 1024/page_h_pt.
41+
let k = 1024.0 / page.height;
42+
let x = (r.l * k).max(0.0) as u32;
43+
let y = (r.t * k).max(0.0) as u32;
44+
let w = ((r.r - r.l) * k) as u32;
45+
let h = ((r.b - r.t) * k) as u32;
46+
let crop = imageops::crop_imm(&page1024, x, y, w, h).to_image();
4147
let otsl = tf.predict_otsl(&crop).expect("predict");
4248
let rows = otsl.iter().filter(|&&t| t == 9).count();
4349
let cols = otsl.iter().take_while(|&&t| t != 9).count();

crates/fleischwolf-pdf/src/tableformer.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
//! autoregressively to emit an OTSL structure-token sequence (the same model
55
//! docling runs). See PDF_CONFORMANCE.md.
66
7-
use image::imageops::FilterType;
87
use image::RgbImage;
98
use ort::session::Session;
109
use ort::value::Tensor;
@@ -65,7 +64,10 @@ impl TableFormer {
6564
/// Predict the OTSL structure-token sequence for a table-region image.
6665
pub fn predict_otsl(&mut self, img: &RgbImage) -> Result<Vec<i64>, String> {
6766
// Preprocess: resize to 448², normalize per channel, lay out CHW.
68-
let resized = image::imageops::resize(img, SIDE, SIDE, FilterType::Triangle);
67+
// docling's final 448 resize is BILINEAR (the page→1024px step that
68+
// box-averages happens earlier, in the caller).
69+
let resized =
70+
image::imageops::resize(img, SIDE, SIDE, image::imageops::FilterType::Triangle);
6971
let n = (SIDE * SIDE) as usize;
7072
let mut data = vec![0f32; 3 * n];
7173
for (i, px) in resized.pixels().enumerate() {

0 commit comments

Comments
 (0)