1#[cfg(feature = "ml")]
9use image::imageops::FilterType;
10#[cfg(feature = "ml")]
11use image::RgbImage;
12#[cfg(feature = "ml")]
13use ort::session::Session;
14#[cfg(feature = "ml")]
15use ort::value::Tensor;
16
17pub const LABELS: [&str; 17] = [
20 "caption",
21 "footnote",
22 "formula",
23 "list_item",
24 "page_footer",
25 "page_header",
26 "picture",
27 "section_header",
28 "table",
29 "text",
30 "title",
31 "document_index",
32 "code",
33 "checkbox_selected",
34 "checkbox_unselected",
35 "form",
36 "key_value_region",
37];
38
39#[derive(Debug, Clone)]
41pub struct Region {
42 pub label: &'static str,
43 pub score: f32,
44 pub l: f32,
45 pub t: f32,
46 pub r: f32,
47 pub b: f32,
48}
49
50const THRESHOLD: f32 = 0.3;
54pub const SIDE: u32 = 640;
56
57pub fn label_threshold(label: &str) -> f32 {
64 match label {
65 "section_header"
66 | "title"
67 | "code"
68 | "checkbox_selected"
69 | "checkbox_unselected"
70 | "form"
71 | "key_value_region"
72 | "document_index" => 0.45,
73 _ => 0.5,
76 }
77}
78
79#[cfg(feature = "ml")]
80pub struct LayoutModel {
81 session: Session,
82 batch_unsupported: bool,
87}
88
89#[cfg(feature = "ml")]
90impl LayoutModel {
91 pub fn load() -> Result<Self, String> {
95 Self::load_with(crate::intra_threads())
96 }
97
98 pub fn load_with(intra: usize) -> Result<Self, String> {
102 let path = crate::model_path(
103 "DOCLING_LAYOUT_ONNX",
104 "models/layout_heron.onnx",
105 "models/layout_heron_int8.onnx",
106 );
107 if crate::timing::enabled() {
108 eprintln!("docling-pdf: layout model: {path}");
109 }
110 let builder = Session::builder()
111 .map_err(|e| format!("layout: builder: {e}"))?
112 .with_intra_threads(intra)
115 .map_err(|e| format!("layout: intra_threads: {e}"))?;
116 let session = crate::ep::apply(builder)
117 .map_err(|e| format!("layout: {e}"))?
118 .commit_from_file(&path)
119 .map_err(|e| format!("layout: load {path}: {e}"))?;
120 Ok(Self {
121 session,
122 batch_unsupported: false,
123 })
124 }
125
126 pub fn predict(
129 &mut self,
130 img: &RgbImage,
131 page_w: f32,
132 page_h: f32,
133 ) -> Result<Vec<Region>, String> {
134 Ok(self
135 .predict_batch(&[(img, page_w, page_h)])?
136 .pop()
137 .expect("one result per input page"))
138 }
139
140 pub fn predict_batch(
146 &mut self,
147 pages: &[(&RgbImage, f32, f32)],
148 ) -> Result<Vec<Vec<Region>>, String> {
149 if pages.len() > 1 && self.batch_unsupported {
150 return self.predict_singly(pages);
151 }
152 match self.run_batch(pages) {
153 Err(e) if pages.len() > 1 => {
154 static WARNED: std::sync::atomic::AtomicBool =
159 std::sync::atomic::AtomicBool::new(false);
160 if !WARNED.swap(true, std::sync::atomic::Ordering::Relaxed) {
161 eprintln!(
162 "docling-pdf: layout model rejected a {}-page batch ({e}); \
163 falling back to per-page inference — re-export with \
164 scripts/install/export_layout.py for batched layout",
165 pages.len()
166 );
167 }
168 self.batch_unsupported = true;
169 self.predict_singly(pages)
170 }
171 other => other,
172 }
173 }
174
175 fn predict_singly(
176 &mut self,
177 pages: &[(&RgbImage, f32, f32)],
178 ) -> Result<Vec<Vec<Region>>, String> {
179 pages
180 .iter()
181 .map(|p| Ok(self.run_batch(&[*p])?.pop().expect("one result")))
182 .collect()
183 }
184
185 fn run_batch(&mut self, pages: &[(&RgbImage, f32, f32)]) -> Result<Vec<Vec<Region>>, String> {
186 if pages.is_empty() {
187 return Ok(Vec::new());
188 }
189 let n = (SIDE * SIDE) as usize;
192 let batch = pages.len();
193 let mut data = vec![0f32; batch * 3 * n];
194 for (p, (img, _, _)) in pages.iter().enumerate() {
195 let resized = image::imageops::resize(*img, SIDE, SIDE, FilterType::Triangle);
196 let page_off = p * 3 * n;
197 for (i, px) in resized.pixels().enumerate() {
198 data[page_off + i] = px[0] as f32 / 255.0;
199 data[page_off + n + i] = px[1] as f32 / 255.0;
200 data[page_off + 2 * n + i] = px[2] as f32 / 255.0;
201 }
202 }
203 let input = Tensor::from_array(([batch, 3, SIDE as usize, SIDE as usize], data))
204 .map_err(|e| format!("layout: input tensor: {e}"))?;
205 let outputs = self
206 .session
207 .run(ort::inputs!["pixel_values" => input])
208 .map_err(|e| format!("layout: inference: {e}"))?;
209 let (lshape, logits) = outputs["logits"]
210 .try_extract_tensor::<f32>()
211 .map_err(|e| format!("layout: extract logits: {e}"))?;
212 let (_, boxes) = outputs["pred_boxes"]
213 .try_extract_tensor::<f32>()
214 .map_err(|e| format!("layout: extract boxes: {e}"))?;
215
216 let num_queries = lshape[1] as usize;
217 let num_classes = lshape[2] as usize;
218
219 let mut all = Vec::with_capacity(batch);
220 for (p, (_, page_w, page_h)) in pages.iter().enumerate() {
221 let logits =
222 &logits[p * num_queries * num_classes..(p + 1) * num_queries * num_classes];
223 let boxes = &boxes[p * num_queries * 4..(p + 1) * num_queries * 4];
224 all.push(decode_layout(
225 logits,
226 boxes,
227 num_queries,
228 num_classes,
229 *page_w,
230 *page_h,
231 ));
232 }
233 Ok(all)
234 }
235}
236
237fn sigmoid(x: f32) -> f32 {
238 1.0 / (1.0 + (-x).exp())
239}
240
241#[cfg(feature = "ocr-prep")]
245pub fn layout_input(img: &image::RgbImage) -> Vec<f32> {
246 let n = (SIDE * SIDE) as usize;
247 let mut data = vec![0f32; 3 * n];
248 let resized = image::imageops::resize(img, SIDE, SIDE, image::imageops::FilterType::Triangle);
249 for (i, px) in resized.pixels().enumerate() {
250 data[i] = px[0] as f32 / 255.0;
251 data[n + i] = px[1] as f32 / 255.0;
252 data[2 * n + i] = px[2] as f32 / 255.0;
253 }
254 data
255}
256
257pub fn decode_layout(
262 logits: &[f32],
263 boxes: &[f32],
264 num_queries: usize,
265 num_classes: usize,
266 page_w: f32,
267 page_h: f32,
268) -> Vec<Region> {
269 let mut scored: Vec<(f32, usize)> = (0..num_queries * num_classes)
270 .map(|idx| (sigmoid(logits[idx]), idx))
271 .collect();
272 scored.sort_unstable_by(|a, b| b.0.total_cmp(&a.0));
273 scored.truncate(num_queries);
274
275 let mut regions = Vec::new();
276 for (score, idx) in scored {
277 if score <= THRESHOLD {
278 continue;
279 }
280 let label_id = idx % num_classes;
281 let q = idx / num_classes;
282 let cx = boxes[q * 4];
283 let cy = boxes[q * 4 + 1];
284 let w = boxes[q * 4 + 2];
285 let h = boxes[q * 4 + 3];
286 let l = (cx - w / 2.0) * page_w;
288 let t = (cy - h / 2.0) * page_h;
289 let r = (cx + w / 2.0) * page_w;
290 let b = (cy + h / 2.0) * page_h;
291 regions.push(Region {
292 label: LABELS.get(label_id).copied().unwrap_or("text"),
293 score,
294 l,
295 t,
296 r,
297 b,
298 });
299 }
300 regions
301}