Skip to main content

rlx_florence2/
postprocess.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! Task post-processing: turns generated `<loc_N>` token sequences into
17//! captions, bounding boxes, quad boxes (OCR), and polygons. Mirrors
18//! `Florence2PostProcesser` and `post_process_generation`.
19//!
20//! Coordinates are decoded structurally from the token ids (each `<loc_N>` is
21//! bin `N` of 1000), which is equivalent to HF's text-regex parsing but
22//! avoids re-rendering byte-level BPE.
23
24use crate::config::task_post_processing_type;
25use crate::tokenizer::{Florence2Tokenizer, NUM_LOC_BINS};
26use anyhow::Result;
27
28/// One detected object: a box in pixel coordinates and its label.
29#[derive(Debug, Clone, PartialEq)]
30pub struct BBoxInstance {
31    pub bbox: [f64; 4],
32    pub label: String,
33}
34
35/// One OCR region: a quadrilateral (8 coords) and its text.
36#[derive(Debug, Clone, PartialEq)]
37pub struct QuadInstance {
38    pub quad_box: [f64; 8],
39    pub text: String,
40}
41
42/// One polygon group (a label and a list of polygons, each a flat coord list).
43#[derive(Debug, Clone, PartialEq)]
44pub struct PolygonInstance {
45    pub polygons: Vec<Vec<f64>>,
46    pub label: String,
47}
48
49/// Parsed task output.
50#[derive(Debug, Clone, PartialEq)]
51pub enum Florence2Result {
52    PureText(String),
53    BBoxes(Vec<BBoxInstance>),
54    Ocr(Vec<QuadInstance>),
55    Polygons(Vec<PolygonInstance>),
56}
57
58/// Bin `n` (of 1000) → pixel coordinate at the bin centre.
59fn dequant(n: usize, size: f64) -> f64 {
60    let per_bin = size / NUM_LOC_BINS as f64;
61    (n as f64 + 0.5) * per_bin
62}
63
64/// A token run split into a decoded text label and the loc bins that follow.
65struct Segment {
66    label: String,
67    locs: Vec<usize>,
68}
69
70/// Split the generated ids into `(text-run, following loc-run)` segments.
71fn segment(ids: &[u32], tk: &Florence2Tokenizer) -> Result<Vec<Segment>> {
72    let mut segs: Vec<Segment> = Vec::new();
73    let mut text_buf: Vec<u32> = Vec::new();
74    let mut i = 0;
75    while i < ids.len() {
76        if tk.loc_index(ids[i]).is_some() {
77            // Collect the contiguous loc run.
78            let mut locs = Vec::new();
79            while i < ids.len() {
80                if let Some(b) = tk.loc_index(ids[i]) {
81                    locs.push(b);
82                    i += 1;
83                } else {
84                    break;
85                }
86            }
87            let label = tk.decode(&text_buf)?.trim().to_string();
88            text_buf.clear();
89            segs.push(Segment { label, locs });
90        } else {
91            text_buf.push(ids[i]);
92            i += 1;
93        }
94    }
95    Ok(segs)
96}
97
98/// Top-level dispatch for a task token (mirrors `post_process_generation`).
99pub fn post_process(
100    task: &str,
101    ids: &[u32],
102    tk: &Florence2Tokenizer,
103    image_size: (f64, f64),
104) -> Result<Florence2Result> {
105    match task_post_processing_type(task) {
106        "pure_text" => Ok(Florence2Result::PureText(clean_text(ids, tk)?)),
107        "description_with_bboxes" | "bboxes" | "phrase_grounding" => {
108            Ok(Florence2Result::BBoxes(parse_bboxes(ids, tk, image_size)?))
109        }
110        "ocr" => Ok(Florence2Result::Ocr(parse_ocr(ids, tk, image_size)?)),
111        "polygons" => Ok(Florence2Result::Polygons(parse_polygons(
112            ids, tk, image_size,
113        )?)),
114        "description_with_bboxes_or_polygons" => {
115            // `<poly>` selects polygon parsing; otherwise boxes.
116            let has_poly = ids.iter().any(|&t| {
117                tk.decode_keep_special(&[t])
118                    .map(|s| s.contains("<poly>"))
119                    .unwrap_or(false)
120            });
121            if has_poly {
122                Ok(Florence2Result::Polygons(parse_polygons(
123                    ids, tk, image_size,
124                )?))
125            } else {
126                Ok(Florence2Result::BBoxes(parse_bboxes(ids, tk, image_size)?))
127            }
128        }
129        _ => Ok(Florence2Result::PureText(clean_text(ids, tk)?)),
130    }
131}
132
133fn clean_text(ids: &[u32], tk: &Florence2Tokenizer) -> Result<String> {
134    Ok(tk.decode(ids)?.trim().to_string())
135}
136
137/// `([phrase])(<loc>×4)+` → one instance per box, sharing the phrase label.
138fn parse_bboxes(
139    ids: &[u32],
140    tk: &Florence2Tokenizer,
141    image_size: (f64, f64),
142) -> Result<Vec<BBoxInstance>> {
143    let (w, h) = image_size;
144    let mut out = Vec::new();
145    for seg in segment(ids, tk)? {
146        for chunk in seg.locs.chunks_exact(4) {
147            out.push(BBoxInstance {
148                bbox: [
149                    dequant(chunk[0], w),
150                    dequant(chunk[1], h),
151                    dequant(chunk[2], w),
152                    dequant(chunk[3], h),
153                ],
154                label: seg.label.clone(),
155            });
156        }
157    }
158    Ok(out)
159}
160
161/// `([text])(<loc>×8)` → quad box (4 points) per OCR line.
162fn parse_ocr(
163    ids: &[u32],
164    tk: &Florence2Tokenizer,
165    image_size: (f64, f64),
166) -> Result<Vec<QuadInstance>> {
167    let (w, h) = image_size;
168    let mut out = Vec::new();
169    for seg in segment(ids, tk)? {
170        for chunk in seg.locs.chunks_exact(8) {
171            let mut quad = [0f64; 8];
172            for p in 0..4 {
173                quad[p * 2] = dequant(chunk[p * 2], w);
174                quad[p * 2 + 1] = dequant(chunk[p * 2 + 1], h);
175            }
176            out.push(QuadInstance {
177                quad_box: quad,
178                text: seg.label.clone(),
179            });
180        }
181    }
182    Ok(out)
183}
184
185/// `([phrase])(<loc>+)` → polygon(s); `<sep>` (decoded) separates polygons of
186/// the same phrase. Coordinates come in (x,y) pairs.
187fn parse_polygons(
188    ids: &[u32],
189    tk: &Florence2Tokenizer,
190    image_size: (f64, f64),
191) -> Result<Vec<PolygonInstance>> {
192    let (w, h) = image_size;
193    let mut out = Vec::new();
194    for seg in segment(ids, tk)? {
195        if seg.locs.is_empty() {
196            continue;
197        }
198        let mut poly = Vec::new();
199        for pair in seg.locs.chunks_exact(2) {
200            poly.push(dequant(pair[0], w));
201            poly.push(dequant(pair[1], h));
202        }
203        out.push(PolygonInstance {
204            polygons: vec![poly],
205            label: seg.label.clone(),
206        });
207    }
208    Ok(out)
209}