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}
77
78impl LayoutModel {
79 pub fn load() -> Result<Self, String> {
83 Self::load_with(crate::intra_threads())
84 }
85
86 pub fn load_with(intra: usize) -> Result<Self, String> {
90 let path = crate::model_path(
91 "DOCLING_LAYOUT_ONNX",
92 "models/layout_heron.onnx",
93 "models/layout_heron_int8.onnx",
94 );
95 let session = Session::builder()
96 .map_err(|e| format!("layout: builder: {e}"))?
97 .with_intra_threads(intra)
100 .map_err(|e| format!("layout: intra_threads: {e}"))?
101 .commit_from_file(&path)
102 .map_err(|e| format!("layout: load {path}: {e}"))?;
103 Ok(Self { session })
104 }
105
106 pub fn predict(
109 &mut self,
110 img: &RgbImage,
111 page_w: f32,
112 page_h: f32,
113 ) -> Result<Vec<Region>, String> {
114 let resized = image::imageops::resize(img, SIDE, SIDE, FilterType::Triangle);
117 let n = (SIDE * SIDE) as usize;
118 let mut data = vec![0f32; 3 * n];
119 for (i, px) in resized.pixels().enumerate() {
120 data[i] = px[0] as f32 / 255.0;
121 data[n + i] = px[1] as f32 / 255.0;
122 data[2 * n + i] = px[2] as f32 / 255.0;
123 }
124 let input = Tensor::from_array(([1usize, 3, SIDE as usize, SIDE as usize], data))
125 .map_err(|e| format!("layout: input tensor: {e}"))?;
126 let outputs = self
127 .session
128 .run(ort::inputs!["pixel_values" => input])
129 .map_err(|e| format!("layout: inference: {e}"))?;
130 let (lshape, logits) = outputs["logits"]
131 .try_extract_tensor::<f32>()
132 .map_err(|e| format!("layout: extract logits: {e}"))?;
133 let (_, boxes) = outputs["pred_boxes"]
134 .try_extract_tensor::<f32>()
135 .map_err(|e| format!("layout: extract boxes: {e}"))?;
136
137 let num_queries = lshape[1] as usize;
138 let num_classes = lshape[2] as usize;
139
140 let mut scored: Vec<(f32, usize)> = (0..num_queries * num_classes)
142 .map(|idx| (sigmoid(logits[idx]), idx))
143 .collect();
144 scored.sort_unstable_by(|a, b| b.0.total_cmp(&a.0));
145 scored.truncate(num_queries);
146
147 let mut regions = Vec::new();
148 for (score, idx) in scored {
149 if score <= THRESHOLD {
150 continue;
151 }
152 let label_id = idx % num_classes;
153 let q = idx / num_classes;
154 let cx = boxes[q * 4];
155 let cy = boxes[q * 4 + 1];
156 let w = boxes[q * 4 + 2];
157 let h = boxes[q * 4 + 3];
158 let l = (cx - w / 2.0) * page_w;
160 let t = (cy - h / 2.0) * page_h;
161 let r = (cx + w / 2.0) * page_w;
162 let b = (cy + h / 2.0) * page_h;
163 regions.push(Region {
164 label: LABELS.get(label_id).copied().unwrap_or("text"),
165 score,
166 l,
167 t,
168 r,
169 b,
170 });
171 }
172 Ok(regions)
173 }
174}
175
176fn sigmoid(x: f32) -> f32 {
177 1.0 / (1.0 + (-x).exp())
178}