Skip to main content

docvortex_core/
dedup.rs

1//! 保持分桶访问次序、候选上限及最早来源规则的字符重复计算。
2
3use crate::geometry::{Box4, Size};
4use std::collections::HashMap;
5
6pub type PaintRecord = (Option<Box4>, Option<Size>, usize, usize, bool);
7pub type Pair = (usize, usize);
8pub type Offset = (usize, usize, f64, f64);
9pub type MappingRecord = (Option<i64>, i64, i64, usize, f64, Option<Size>, Box4, bool);
10
11/// 批量识别连续的一对多来源映射,只返回范围,Unicode 合并仍由 Python 执行。
12pub fn mapping_runs(records: Vec<MappingRecord>) -> Option<Vec<(usize, usize)>> {
13    if records.iter().any(|r| {
14        !r.4.is_finite()
15            || r.6.iter().any(|v| !v.is_finite())
16            || r.5.is_some_and(|p| p.iter().any(|v| !v.is_finite()))
17    }) {
18        return None;
19    }
20    let mut groups = Vec::new();
21    let mut start = 0;
22    for i in 1..records.len() {
23        let a = &records[i - 1];
24        let b = &records[i];
25        let same = a.0.is_some()
26            && a.0 == b.0
27            && a.2.checked_add(1) == Some(b.1)
28            && a.3 == b.3
29            && a.4 == b.4
30            && a.7
31            && b.7
32            && matches!((a.5, b.5), (Some(x), Some(y)) if close(x, y, 0.001))
33            && close(a.6, b.6, 0.001);
34        if !same {
35            groups.push((start, i));
36            start = i;
37        }
38    }
39    if !records.is_empty() {
40        groups.push((start, records.len()));
41    }
42    Some(groups)
43}
44
45/// 数值缺失、非有限或超出容差时不得作为相同位置的证据。
46fn close<const N: usize>(a: [f64; N], b: [f64; N], epsilon: f64) -> bool {
47    a.iter()
48        .zip(b)
49        .all(|(x, y)| x.is_finite() && y.is_finite() && (*x - y).abs() <= epsilon)
50}
51
52/// 计算覆盖较小矩形的比例,与 Python 的减法、乘除次序一致。
53fn overlap(a: Box4, b: Box4) -> f64 {
54    let area = ((a[2] - a[0]) * (a[3] - a[1])).min((b[2] - b[0]) * (b[3] - b[1]));
55    if area > 0.0 {
56        0.0_f64.max(a[2].min(b[2]) - a[0].max(b[0])) * 0.0_f64.max(a[3].min(b[3]) - a[1].max(b[1]))
57            / area
58    } else {
59        0.0
60    }
61}
62
63/// 按签名和相邻九桶生成精确重复与平移候选,保留每桶最多 64 项。
64pub fn paint_pairs(records: Vec<PaintRecord>) -> Option<(Vec<Pair>, Vec<Offset>)> {
65    if records
66        .iter()
67        .any(|r| r.0.is_some_and(|b| b.iter().any(|v| v.abs() > 1e15)))
68    {
69        return None;
70    }
71    let mut buckets: HashMap<(usize, i64, i64), Vec<usize>> = HashMap::new();
72    let mut exact = Vec::new();
73    let mut offsets = Vec::new();
74    for (i, (box_, origin, signature, object, eligible)) in records.iter().enumerate() {
75        if !eligible {
76            continue;
77        }
78        let (Some(b), Some(co)) = (box_, origin) else {
79            continue;
80        };
81        let bx = (b[0] / 2.5).floor() as i64;
82        let by = (b[1] / 2.5).floor() as i64;
83        for x in bx - 1..=bx + 1 {
84            for y in by - 1..=by + 1 {
85                if let Some(candidates) = buckets.get(&(*signature, x, y)) {
86                    if candidates.len() >= 64 {
87                        continue;
88                    }
89                    for &j in candidates {
90                        let (a, po, _, previous_object, _) = records[j];
91                        if previous_object == *object {
92                            continue;
93                        }
94                        let a = a.unwrap();
95                        let po = po.unwrap();
96                        if close(a, *b, 0.001) && close(po, *co, 0.001) {
97                            exact.push((j, i));
98                            continue;
99                        }
100                        let dx = co[0] - po[0];
101                        let dy = co[1] - po[1];
102                        if !dx.is_finite() || !dy.is_finite() || dx.abs().max(dy.abs()) > 2.5 {
103                            continue;
104                        }
105                        if [b[0] - a[0], b[1] - a[1], b[2] - a[2], b[3] - a[3]]
106                            .iter()
107                            .zip([dx, dy, dx, dy])
108                            .all(|(d, t)| (*d - t).abs() <= 0.1)
109                            && overlap(a, *b) >= 0.45
110                        {
111                            offsets.push((j, i, dx, dy));
112                        }
113                    }
114                }
115            }
116        }
117        let bucket = buckets.entry((*signature, bx, by)).or_default();
118        if bucket.len() < 64 {
119            bucket.push(i);
120        }
121    }
122    Some((exact, offsets))
123}
124
125/// 精确复现原并查集的路径压缩与最终一次父节点跳转。
126pub fn components(count: usize, pairs: &[Pair]) -> Vec<usize> {
127    let mut parents: Vec<_> = (0..count).collect();
128    for &(mut a, mut b) in pairs {
129        while parents[a] != a {
130            parents[a] = parents[parents[a]];
131            a = parents[a];
132        }
133        while parents[b] != b {
134            parents[b] = parents[parents[b]];
135            b = parents[b];
136        }
137        parents[a.max(b)] = a.min(b);
138    }
139    for i in 0..count {
140        parents[i] = parents[parents[i]];
141    }
142    parents
143}
144
145pub type EvidenceRecord = (Option<Box4>, f64, usize, usize, bool);
146/// 连续证据中间仅允许空白或两端字形的副本。
147fn endpoints(records: &[EvidenceRecord], roots: &[usize], start: usize, end: usize) -> bool {
148    end - start <= 128
149        && (start + 1..end)
150            .all(|i| records[i].4 || roots[i] == roots[start] || roots[i] == roots[end])
151}
152
153/// 保留字典插入次序聚类平移证据,再以连续三字形、两种文本确认阴影。
154pub fn confirmed_offsets(
155    records: Vec<EvidenceRecord>,
156    pairs: Vec<Offset>,
157    exact: Vec<Pair>,
158) -> Vec<Pair> {
159    if pairs.is_empty() {
160        return Vec::new();
161    }
162    let all: Vec<_> = exact
163        .into_iter()
164        .chain(pairs.iter().map(|p| (p.0, p.1)))
165        .collect();
166    let roots = components(records.len(), &all);
167    let mut bins: HashMap<(usize, i64, i64, i64), Vec<usize>> = HashMap::new();
168    let mut key_order = Vec::new();
169    let mut clusters: Vec<Vec<Offset>> = Vec::new();
170    for p in pairs {
171        let r = &records[p.0];
172        let normal = r.1;
173        let key = (
174            r.2,
175            (p.2 / 0.1).floor() as i64,
176            (p.3 / 0.1).floor() as i64,
177            (normal / 0.1).floor() as i64,
178        );
179        let mut found = None;
180        'search: for x in key.1 - 1..=key.1 + 1 {
181            for y in key.2 - 1..=key.2 + 1 {
182                for n in key.3 - 1..=key.3 + 1 {
183                    if let Some(ids) = bins.get(&(key.0, x, y, n)) {
184                        for &id in ids {
185                            let first = clusters[id][0];
186                            if (p.2 - first.2).abs() <= 0.1
187                                && (p.3 - first.3).abs() <= 0.1
188                                && (normal - records[first.0].1).abs() <= 0.1
189                            {
190                                found = Some(id);
191                                break 'search;
192                            }
193                        }
194                    }
195                }
196            }
197        }
198        if let Some(id) = found {
199            clusters[id].push(p);
200        } else {
201            if !bins.contains_key(&key) {
202                key_order.push(key);
203            }
204            bins.entry(key).or_default().push(clusters.len());
205            clusters.push(vec![p]);
206        }
207    }
208    let mut confirmed = Vec::new();
209    for key in key_order {
210        for &id in &bins[&key] {
211            let ordered = &mut clusters[id];
212            ordered.sort_by_key(|p| (p.0, p.1));
213            let mut runs: Vec<Vec<Offset>> = Vec::new();
214            for &p in ordered.iter() {
215                let continuous = if let Some(prev) = runs.last().and_then(|r| r.last()) {
216                    let pb = records[prev.0].0.unwrap();
217                    let cb = records[p.0].0.unwrap();
218                    p.0 > prev.0
219                        && p.1 > prev.1
220                        && cb[0] >= pb[0]
221                        && cb[0] - pb[2] <= 1.0_f64.max(0.6 * (pb[3] - pb[1]).min(cb[3] - cb[1]))
222                        && endpoints(&records, &roots, prev.0, p.0)
223                        && endpoints(&records, &roots, prev.1, p.1)
224                } else {
225                    false
226                };
227                if !continuous {
228                    runs.push(Vec::new());
229                }
230                runs.last_mut().unwrap().push(p);
231            }
232            for run in runs {
233                let unique_roots: std::collections::HashSet<_> =
234                    run.iter().map(|p| roots[p.0]).collect();
235                let unique_text: std::collections::HashSet<_> =
236                    run.iter().map(|p| records[p.0].3).collect();
237                if unique_roots.len() >= 3 && unique_text.len() >= 2 {
238                    confirmed.extend(run.into_iter().map(|p| (p.0, p.1)));
239                }
240            }
241        }
242    }
243    confirmed
244}
245
246/// 用 Python 预计算的 sin/cos 投影,避免跨平台三角函数差异。
247pub fn project(b: Box4, co: f64, si: f64) -> Box4 {
248    let points = [(b[0], b[1]), (b[0], b[3]), (b[2], b[1]), (b[2], b[3])]
249        .map(|(x, y)| (co * x + si * y, -si * x + co * y));
250    let mut out = [points[0].0, points[0].1, points[0].0, points[0].1];
251    for (x, y) in &points[1..] {
252        out = [
253            out[0].min(*x),
254            out[1].min(*y),
255            out[2].max(*x),
256            out[3].max(*y),
257        ];
258    }
259    out
260}
261
262pub type HiddenRecord = (
263    Option<Box4>,
264    Option<Size>,
265    usize,
266    i8,
267    bool,
268    bool,
269    f64,
270    f64,
271    f64,
272    usize,
273);
274
275/// 产生隐藏文本和可见文本的几何配对;Unicode 文本判据仍由 Python 执行。
276pub fn hidden_candidates(records: Vec<HiddenRecord>) -> Option<Vec<(Vec<usize>, Vec<usize>)>> {
277    if records.iter().any(|r| {
278        r.0.is_some_and(|b| b.iter().any(|v| !v.is_finite() || v.abs() > 1e15))
279    }) {
280        return None;
281    }
282    let mut grid: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
283    let mut key_order = Vec::new();
284    let mut runs: Vec<Vec<usize>> = Vec::new();
285    for (i, r) in records.iter().enumerate() {
286        let Some(b) = r.0 else {
287            continue;
288        };
289        if !r.5 {
290            continue;
291        }
292        if [0, 1, 2, 4, 5, 6].contains(&r.3) && r.4 {
293            let key = (
294                ((b[0] + b[2]) / 2.0 / 32.0).floor() as i64,
295                ((b[1] + b[3]) / 2.0 / 32.0).floor() as i64,
296            );
297            if !grid.contains_key(&key) {
298                key_order.push(key);
299            }
300            grid.entry(key).or_default().push(i);
301        } else if r.3 == 3 {
302            let mut same = false;
303            if let Some(previous) = runs.last().and_then(|v| v.last()).map(|j| &records[*j]) {
304                if previous.2 == r.2 && (previous.6 - r.6).abs() <= 0.001 {
305                    let pb = project(previous.0.unwrap(), r.7, r.8);
306                    let cb = project(b, r.7, r.8);
307                    if let (Some(po), Some(co)) = (previous.1, r.1) {
308                        same = (-r.8 * (po[0] - co[0]) + r.7 * (po[1] - co[1])).abs() <= 0.1
309                            && cb[0] >= pb[0]
310                            && cb[0] - pb[2] <= 1.0_f64.max(0.6 * (cb[3] - cb[1]));
311                    }
312                }
313            }
314            if !same {
315                runs.push(Vec::new());
316            }
317            runs.last_mut().unwrap().push(i);
318        }
319    }
320    let mut result = Vec::new();
321    for run in runs {
322        let first = &records[run[0]];
323        let boxes: Vec<_> = run.iter().map(|i| records[*i].0.unwrap()).collect();
324        let b = crate::geometry::union(&boxes).unwrap();
325        let p = project(b, first.7, first.8);
326        let margin = 0.1 * (p[3] - p[1]);
327        let x0 = ((b[0] - margin) / 32.0).floor() as i64;
328        let x1 = ((b[2] + margin) / 32.0).floor() as i64;
329        let y0 = ((b[1] - margin) / 32.0).floor() as i64;
330        let y1 = ((b[3] + margin) / 32.0).floor() as i64;
331        let keys: Vec<_> = if (x1 - x0 + 1) as i128 * (y1 - y0 + 1) as i128 > 4 * grid.len() as i128
332        {
333            key_order
334                .iter()
335                .copied()
336                .filter(|(x, y)| x0 <= *x && *x <= x1 && y0 <= *y && *y <= y1)
337                .collect()
338        } else {
339            (x0..=x1)
340                .flat_map(|x| (y0..=y1).map(move |y| (x, y)))
341                .collect()
342        };
343        let mut candidates = Vec::new();
344        for key in keys {
345            if let Some(items) = grid.get(&key) {
346                for &i in items {
347                    let r = &records[i];
348                    if (r.6 - first.6).abs() > 0.001 {
349                        continue;
350                    }
351                    let q = project(r.0.unwrap(), first.7, first.8);
352                    if p[0] - margin <= (q[0] + q[2]) / 2.0
353                        && (q[0] + q[2]) / 2.0 <= p[2] + margin
354                        && p[1] <= (q[1] + q[3]) / 2.0
355                        && (q[1] + q[3]) / 2.0 <= p[3]
356                    {
357                        candidates.push(i);
358                    }
359                }
360            }
361        }
362        if candidates.is_empty() {
363            continue;
364        }
365        candidates.sort_by(|a, b| {
366            let x = project(records[*a].0.unwrap(), first.7, first.8)[0];
367            let y = project(records[*b].0.unwrap(), first.7, first.8)[0];
368            x.partial_cmp(&y)
369                .unwrap()
370                .then(records[*a].9.cmp(&records[*b].9))
371        });
372        let projected: Vec<_> = candidates
373            .iter()
374            .map(|i| project(records[*i].0.unwrap(), first.7, first.8))
375            .collect();
376        let v = crate::geometry::union(&projected).unwrap();
377        let axis = 0.0_f64.max(p[2].min(v[2]) - p[0].max(v[0]));
378        let normal = 0.0_f64.max(p[3].min(v[3]) - p[1].max(v[1]));
379        if axis < 0.8 * (p[2] - p[0]) || normal < 0.8 * (p[3] - p[1]).min(v[3] - v[1]) {
380            continue;
381        }
382        result.push((run, candidates));
383    }
384    Some(result)
385}