1use image::imageops::FilterType;
9use image::RgbImage;
10use ort::session::Session;
11use ort::value::Tensor;
12
13pub const LABELS: [&str; 17] = [
16 "caption",
17 "footnote",
18 "formula",
19 "list_item",
20 "page_footer",
21 "page_header",
22 "picture",
23 "section_header",
24 "table",
25 "text",
26 "title",
27 "document_index",
28 "code",
29 "checkbox_selected",
30 "checkbox_unselected",
31 "form",
32 "key_value_region",
33];
34
35#[derive(Debug, Clone)]
37pub struct Region {
38 pub label: &'static str,
39 pub score: f32,
40 pub l: f32,
41 pub t: f32,
42 pub r: f32,
43 pub b: f32,
44}
45
46const THRESHOLD: f32 = 0.3;
50const SIDE: u32 = 640;
51
52pub fn label_threshold(label: &str) -> f32 {
59 match label {
60 "section_header"
61 | "title"
62 | "code"
63 | "checkbox_selected"
64 | "checkbox_unselected"
65 | "form"
66 | "key_value_region"
67 | "document_index" => 0.45,
68 _ => 0.5,
71 }
72}
73
74pub struct LayoutModel {
75 session: Session,
76 batch_unsupported: bool,
81}
82
83impl LayoutModel {
84 pub fn load() -> Result<Self, String> {
88 Self::load_with(crate::intra_threads())
89 }
90
91 pub fn load_with(intra: usize) -> Result<Self, String> {
95 let path = crate::model_path(
96 "DOCLING_LAYOUT_ONNX",
97 "models/layout_heron.onnx",
98 "models/layout_heron_int8.onnx",
99 );
100 if crate::timing::enabled() {
101 eprintln!("docling-pdf: layout model: {path}");
102 }
103 let builder = Session::builder()
104 .map_err(|e| format!("layout: builder: {e}"))?
105 .with_intra_threads(intra)
108 .map_err(|e| format!("layout: intra_threads: {e}"))?;
109 let session = crate::ep::apply(builder)
110 .map_err(|e| format!("layout: {e}"))?
111 .commit_from_file(&path)
112 .map_err(|e| format!("layout: load {path}: {e}"))?;
113 Ok(Self {
114 session,
115 batch_unsupported: false,
116 })
117 }
118
119 pub fn predict(
122 &mut self,
123 img: &RgbImage,
124 page_w: f32,
125 page_h: f32,
126 ) -> Result<Vec<Region>, String> {
127 Ok(self
128 .predict_batch(&[(img, page_w, page_h)])?
129 .pop()
130 .expect("one result per input page"))
131 }
132
133 pub fn predict_batch(
139 &mut self,
140 pages: &[(&RgbImage, f32, f32)],
141 ) -> Result<Vec<Vec<Region>>, String> {
142 if pages.len() > 1 && self.batch_unsupported {
143 return self.predict_singly(pages);
144 }
145 match self.run_batch(pages) {
146 Err(e) if pages.len() > 1 => {
147 eprintln!(
150 "docling-pdf: layout model rejected a {}-page batch ({e}); \
151 falling back to per-page inference — re-export with \
152 scripts/install/export_layout.py for batched layout",
153 pages.len()
154 );
155 self.batch_unsupported = true;
156 self.predict_singly(pages)
157 }
158 other => other,
159 }
160 }
161
162 fn predict_singly(
163 &mut self,
164 pages: &[(&RgbImage, f32, f32)],
165 ) -> Result<Vec<Vec<Region>>, String> {
166 pages
167 .iter()
168 .map(|p| Ok(self.run_batch(&[*p])?.pop().expect("one result")))
169 .collect()
170 }
171
172 fn run_batch(&mut self, pages: &[(&RgbImage, f32, f32)]) -> Result<Vec<Vec<Region>>, String> {
173 if pages.is_empty() {
174 return Ok(Vec::new());
175 }
176 let n = (SIDE * SIDE) as usize;
179 let batch = pages.len();
180 let mut data = vec![0f32; batch * 3 * n];
181 for (p, (img, _, _)) in pages.iter().enumerate() {
182 let resized = image::imageops::resize(*img, SIDE, SIDE, FilterType::Triangle);
183 let page_off = p * 3 * n;
184 for (i, px) in resized.pixels().enumerate() {
185 data[page_off + i] = px[0] as f32 / 255.0;
186 data[page_off + n + i] = px[1] as f32 / 255.0;
187 data[page_off + 2 * n + i] = px[2] as f32 / 255.0;
188 }
189 }
190 let input = Tensor::from_array(([batch, 3, SIDE as usize, SIDE as usize], data))
191 .map_err(|e| format!("layout: input tensor: {e}"))?;
192 let outputs = self
193 .session
194 .run(ort::inputs!["pixel_values" => input])
195 .map_err(|e| format!("layout: inference: {e}"))?;
196 let (lshape, logits) = outputs["logits"]
197 .try_extract_tensor::<f32>()
198 .map_err(|e| format!("layout: extract logits: {e}"))?;
199 let (_, boxes) = outputs["pred_boxes"]
200 .try_extract_tensor::<f32>()
201 .map_err(|e| format!("layout: extract boxes: {e}"))?;
202
203 let num_queries = lshape[1] as usize;
204 let num_classes = lshape[2] as usize;
205
206 let mut all = Vec::with_capacity(batch);
207 for (p, (_, page_w, page_h)) in pages.iter().enumerate() {
208 let logits =
209 &logits[p * num_queries * num_classes..(p + 1) * num_queries * num_classes];
210 let boxes = &boxes[p * num_queries * 4..(p + 1) * num_queries * 4];
211
212 let mut scored: Vec<(f32, usize)> = (0..num_queries * num_classes)
214 .map(|idx| (sigmoid(logits[idx]), idx))
215 .collect();
216 scored.sort_unstable_by(|a, b| b.0.total_cmp(&a.0));
217 scored.truncate(num_queries);
218
219 let mut regions = Vec::new();
220 for (score, idx) in scored {
221 if score <= THRESHOLD {
222 continue;
223 }
224 let label_id = idx % num_classes;
225 let q = idx / num_classes;
226 let cx = boxes[q * 4];
227 let cy = boxes[q * 4 + 1];
228 let w = boxes[q * 4 + 2];
229 let h = boxes[q * 4 + 3];
230 let l = (cx - w / 2.0) * page_w;
232 let t = (cy - h / 2.0) * page_h;
233 let r = (cx + w / 2.0) * page_w;
234 let b = (cy + h / 2.0) * page_h;
235 regions.push(Region {
236 label: LABELS.get(label_id).copied().unwrap_or("text"),
237 score,
238 l,
239 t,
240 r,
241 b,
242 });
243 }
244 all.push(regions);
245 }
246 Ok(all)
247 }
248}
249
250fn sigmoid(x: f32) -> f32 {
251 1.0 / (1.0 + (-x).exp())
252}