perf: cap cluster_rects component size to avoid O(n²) on vector-heavy pages (#6)
Pages with tens of thousands of vector-drawing rects (e.g. architectural plans with 36k+ rects) caused cluster_rects and sub-rect deduplication to spend 10-50s in O(n²) loops. No real table has thousands of cell rects, so once a union-find component exceeds 2000 elements we skip further comparisons, and skip the O(n²) sub-rect dedup entirely for such pages. Benchmarks on 8 slow production PDFs show 10-40x speedups on vector-heavy files (12s→0.3s) with no regression on other file types. All 342 tests pass.
This commit is contained in:
+59
-21
@@ -8,10 +8,11 @@ use crate::types::{PdfRect, TextItem};
|
|||||||
|
|
||||||
use super::Table;
|
use super::Table;
|
||||||
|
|
||||||
/// Disjoint-set (union-find) for clustering indices.
|
/// Disjoint-set (union-find) with component sizes for clustering indices.
|
||||||
struct UnionFind {
|
struct UnionFind {
|
||||||
parent: Vec<usize>,
|
parent: Vec<usize>,
|
||||||
rank: Vec<usize>,
|
rank: Vec<usize>,
|
||||||
|
size: Vec<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UnionFind {
|
impl UnionFind {
|
||||||
@@ -19,6 +20,7 @@ impl UnionFind {
|
|||||||
Self {
|
Self {
|
||||||
parent: (0..n).collect(),
|
parent: (0..n).collect(),
|
||||||
rank: vec![0; n],
|
rank: vec![0; n],
|
||||||
|
size: vec![1; n],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,15 +37,24 @@ impl UnionFind {
|
|||||||
if ra == rb {
|
if ra == rb {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let new_size = self.size[ra] + self.size[rb];
|
||||||
if self.rank[ra] < self.rank[rb] {
|
if self.rank[ra] < self.rank[rb] {
|
||||||
self.parent[ra] = rb;
|
self.parent[ra] = rb;
|
||||||
|
self.size[rb] = new_size;
|
||||||
} else if self.rank[ra] > self.rank[rb] {
|
} else if self.rank[ra] > self.rank[rb] {
|
||||||
self.parent[rb] = ra;
|
self.parent[rb] = ra;
|
||||||
|
self.size[ra] = new_size;
|
||||||
} else {
|
} else {
|
||||||
self.parent[rb] = ra;
|
self.parent[rb] = ra;
|
||||||
|
self.size[ra] = new_size;
|
||||||
self.rank[ra] += 1;
|
self.rank[ra] += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn component_size(&mut self, x: usize) -> usize {
|
||||||
|
let root = self.find(x);
|
||||||
|
self.size[root]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if two rects overlap after expanding each by `tol` on all sides.
|
/// Check if two rects overlap after expanding each by `tol` on all sides.
|
||||||
@@ -64,8 +75,19 @@ pub(crate) fn rects_overlap(a: &(f32, f32, f32, f32), b: &(f32, f32, f32, f32),
|
|||||||
!(a_right < b_left || b_right < a_left || a_top < b_bottom || b_top < a_bottom)
|
!(a_right < b_left || b_right < a_left || a_top < b_bottom || b_top < a_bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Maximum component size for rect clustering. No real table has thousands
|
||||||
|
/// of cell rects — once a component exceeds this, it is a vector drawing or
|
||||||
|
/// page-spanning clipping path. We skip overlap checks for rects already in
|
||||||
|
/// an oversized component, keeping the original O(n²) loop but making it
|
||||||
|
/// effectively O(n) for pathological pages.
|
||||||
|
const MAX_CLUSTER_RECTS: usize = 2000;
|
||||||
|
|
||||||
/// Cluster rects by spatial overlap using union-find.
|
/// Cluster rects by spatial overlap using union-find.
|
||||||
/// Returns groups of rect indices; only groups with ≥ `min_size` rects are returned.
|
/// Returns groups of rect indices; only groups with ≥ `min_size` rects are returned.
|
||||||
|
///
|
||||||
|
/// Skips overlap checks for rects whose component has already exceeded
|
||||||
|
/// [`MAX_CLUSTER_RECTS`], so pages with tens of thousands of vector-drawing
|
||||||
|
/// rects complete in milliseconds instead of minutes.
|
||||||
pub(crate) fn cluster_rects(
|
pub(crate) fn cluster_rects(
|
||||||
rects: &[(f32, f32, f32, f32)],
|
rects: &[(f32, f32, f32, f32)],
|
||||||
tolerance: f32,
|
tolerance: f32,
|
||||||
@@ -75,9 +97,20 @@ pub(crate) fn cluster_rects(
|
|||||||
let mut uf = UnionFind::new(n);
|
let mut uf = UnionFind::new(n);
|
||||||
|
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
|
// If rect i is already in an oversized component, no point comparing
|
||||||
|
// it against further rects — the component won't be used for table
|
||||||
|
// detection anyway.
|
||||||
|
if uf.component_size(i) >= MAX_CLUSTER_RECTS {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for j in (i + 1)..n {
|
for j in (i + 1)..n {
|
||||||
if rects_overlap(&rects[i], &rects[j], tolerance) {
|
if rects_overlap(&rects[i], &rects[j], tolerance) {
|
||||||
uf.union(i, j);
|
uf.union(i, j);
|
||||||
|
// Check if the merged component just exceeded the cap —
|
||||||
|
// if so, no need to test more pairs for rect i.
|
||||||
|
if uf.component_size(i) >= MAX_CLUSTER_RECTS {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,26 +286,31 @@ pub fn detect_tables_from_rects(
|
|||||||
// Only remove when the container is a similarly-sized cell (height
|
// Only remove when the container is a similarly-sized cell (height
|
||||||
// ratio < 4×), NOT when the container is a table-wide background
|
// ratio < 4×), NOT when the container is a table-wide background
|
||||||
// that dwarfs the sub-rect.
|
// that dwarfs the sub-rect.
|
||||||
let before = page_rects.len();
|
//
|
||||||
let snapshot = page_rects.clone();
|
// Skip this O(n²) dedup when there are too many rects — pages with
|
||||||
page_rects.retain(|&(ax, ay, aw, ah)| {
|
// thousands of vector-drawing rects won't benefit from cell dedup.
|
||||||
let tol = 2.0;
|
if page_rects.len() < MAX_CLUSTER_RECTS {
|
||||||
!snapshot.iter().any(|&(bx, by, bw, bh)| {
|
let before = page_rects.len();
|
||||||
// b must strictly contain a (b is larger in area)
|
let snapshot = page_rects.clone();
|
||||||
bw * bh > aw * ah * 1.2
|
page_rects.retain(|&(ax, ay, aw, ah)| {
|
||||||
&& bh < ah * 4.0 // container must be similarly sized, not a table background
|
let tol = 2.0;
|
||||||
&& bx <= ax + tol
|
!snapshot.iter().any(|&(bx, by, bw, bh)| {
|
||||||
&& (bx + bw) >= (ax + aw) - tol
|
// b must strictly contain a (b is larger in area)
|
||||||
&& by <= ay + tol
|
bw * bh > aw * ah * 1.2
|
||||||
&& (by + bh) >= (ay + ah) - tol
|
&& bh < ah * 4.0 // container must be similarly sized, not a table background
|
||||||
})
|
&& bx <= ax + tol
|
||||||
});
|
&& (bx + bw) >= (ax + aw) - tol
|
||||||
if page_rects.len() < before {
|
&& by <= ay + tol
|
||||||
debug!(
|
&& (by + bh) >= (ay + ah) - tol
|
||||||
"page {}: removed {} contained sub-rects",
|
})
|
||||||
page,
|
});
|
||||||
before - page_rects.len(),
|
if page_rects.len() < before {
|
||||||
);
|
debug!(
|
||||||
|
"page {}: removed {} contained sub-rects",
|
||||||
|
page,
|
||||||
|
before - page_rects.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user