1use crate::config::DecodeMethod;
19use rten_tensor::NdTensorView;
20use rten_tensor::prelude::*;
21use std::collections::HashMap;
22use std::num::NonZeroU32;
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub struct CtcStep {
27 pub label: u32,
28 pub pos: u32,
29}
30
31#[derive(Clone, Debug)]
33pub struct CtcHypothesis {
34 steps: Vec<CtcStep>,
35 score: f32,
36}
37
38impl CtcHypothesis {
39 pub fn steps(&self) -> &[CtcStep] {
40 &self.steps
41 }
42
43 pub fn score(&self) -> f32 {
44 self.score
45 }
46}
47
48pub fn decode(input_seq: NdTensorView<f32, 2>, method: DecodeMethod) -> CtcHypothesis {
50 let method = std::env::var("OCR_DECODE")
53 .ok()
54 .and_then(|s| {
55 let s = s.trim();
56 if s.eq_ignore_ascii_case("greedy") {
57 Some(DecodeMethod::Greedy)
58 } else if let Some(w) = s.strip_prefix("beam:") {
59 Some(DecodeMethod::BeamSearch {
60 width: w.trim().parse().ok()?,
61 })
62 } else {
63 None
64 }
65 })
66 .unwrap_or(method);
67 match method {
68 DecodeMethod::Greedy => decode_greedy(input_seq),
69 DecodeMethod::BeamSearch { width } => {
70 decode_beam(input_seq, width.max(1)).unwrap_or_else(|| decode_greedy(input_seq))
71 }
72 }
73}
74
75fn decode_greedy(prob_seq: NdTensorView<f32, 2>) -> CtcHypothesis {
76 let mut last_label = 0;
77 let mut steps = Vec::new();
78 let mut score = 0.;
79
80 for pos in 0..prob_seq.size(0) {
81 let mut best_label = 0usize;
82 let mut best_lp = prob_seq[[pos, 0]];
83 for label in 1..prob_seq.size(1) {
84 let lp = prob_seq[[pos, label]];
85 if lp > best_lp {
86 best_lp = lp;
87 best_label = label;
88 }
89 }
90 let label = best_label;
91 score += best_lp;
92 if label == last_label {
93 continue;
94 }
95 last_label = label;
96 if label > 0 {
97 steps.push(CtcStep {
98 label: label as u32,
99 pos: pos as u32,
100 });
101 }
102 }
103
104 CtcHypothesis { steps, score }
105}
106
107#[derive(Debug)]
108struct BeamProbs {
109 prob_blank: f32,
110 prob_no_blank: f32,
111}
112
113fn log_sum_exp<const N: usize>(log_probs: [f32; N]) -> f32 {
114 if log_probs.iter().all(|&x| x == f32::NEG_INFINITY) {
115 f32::NEG_INFINITY
116 } else {
117 let lp_max = log_probs
118 .into_iter()
119 .reduce(f32::max)
120 .unwrap_or(f32::NEG_INFINITY);
121 lp_max
122 + log_probs
123 .iter()
124 .map(|x| (x - lp_max).exp())
125 .sum::<f32>()
126 .ln()
127 }
128}
129
130fn decode_beam(prob_seq: NdTensorView<f32, 2>, beam_size: u32) -> Option<CtcHypothesis> {
131 let beam_size = NonZeroU32::new(beam_size)?;
132 let mut states: HashMap<Vec<CtcStep>, BeamProbs> = HashMap::new();
133 states.insert(
134 Vec::new(),
135 BeamProbs {
136 prob_blank: 0.,
137 prob_no_blank: f32::NEG_INFINITY,
138 },
139 );
140
141 for t in 0..prob_seq.size(0) {
142 let mut next: HashMap<Vec<CtcStep>, BeamProbs> = HashMap::new();
143 let blank_lp = prob_seq[[t, 0]];
144 for (prefix, state) in &states {
145 let p_b = state.prob_blank;
146 let p_nb = state.prob_no_blank;
147 merge_beam(
148 &mut next,
149 prefix.clone(),
150 log_sum_exp([p_b + blank_lp, p_nb + blank_lp]),
151 f32::NEG_INFINITY,
152 );
153 for label in 1..prob_seq.size(1) {
154 let lp = prob_seq[[t, label]];
155 let mut new_prefix = prefix.clone();
156 let step = CtcStep {
157 label: label as u32,
158 pos: t as u32,
159 };
160 let last = new_prefix.last().map(|s| s.label);
161 if last != Some(step.label) {
162 new_prefix.push(step);
163 }
164 let (nb_lp, b_lp) = if last == Some(step.label) {
165 (p_nb + lp, p_b + lp)
166 } else {
167 (log_sum_exp([p_b + lp, p_nb + lp]), f32::NEG_INFINITY)
168 };
169 merge_beam(&mut next, new_prefix, b_lp, nb_lp);
170 }
171 }
172 let mut ranked: Vec<_> = next.into_iter().collect();
173 ranked.sort_by(|(_, a), (_, b)| {
174 let sa = log_sum_exp([a.prob_blank, a.prob_no_blank]);
175 let sb = log_sum_exp([b.prob_blank, b.prob_no_blank]);
176 sb.partial_cmp(&sa).unwrap_or(std::cmp::Ordering::Equal)
177 });
178 ranked.truncate(beam_size.get() as usize);
179 states = ranked.into_iter().collect();
180 }
181
182 let (prefix, probs) = states.into_iter().max_by(|(_, a), (_, b)| {
183 let sa = log_sum_exp([a.prob_blank, a.prob_no_blank]);
184 let sb = log_sum_exp([b.prob_blank, b.prob_no_blank]);
185 sa.partial_cmp(&sb).unwrap_or(std::cmp::Ordering::Equal)
186 })?;
187 Some(CtcHypothesis {
188 steps: prefix,
189 score: log_sum_exp([probs.prob_blank, probs.prob_no_blank]),
190 })
191}
192
193fn merge_beam(map: &mut HashMap<Vec<CtcStep>, BeamProbs>, prefix: Vec<CtcStep>, pb: f32, pnb: f32) {
194 use std::collections::hash_map::Entry;
195 match map.entry(prefix) {
196 Entry::Vacant(e) => {
197 e.insert(BeamProbs {
198 prob_blank: pb,
199 prob_no_blank: pnb,
200 });
201 }
202 Entry::Occupied(mut e) => {
203 let s = e.get_mut();
204 s.prob_blank = log_sum_exp([s.prob_blank, pb]);
205 s.prob_no_blank = log_sum_exp([s.prob_no_blank, pnb]);
206 }
207 }
208}