mirror of
https://github.com/visioncortex/vtracer.git
synced 2026-08-30 17:05:57 -07:00
Union same-paint layers in one pass, not pairwise
MergeAdjacent folded RegionMask::union over each run of same-paint layers, and every union allocates a mask over the combined bounding box and copies both inputs into it. The accumulator reaches full-canvas size after the first few merges, so each remaining layer reallocated and rewrote the whole canvas — O(n * width * height) for a run of n layers. A single palette color is the worst case, since every layer then shares a paint and the entire stack folds into one accumulator: 474 layers at 1400x775 spent ~0.8s of the 1.17s conversion there. --max-colors escaped it only because runs of identical consecutive paints stay short. RegionMask::union_all sizes the destination from the combined bounding box in one cheap pass, then blits each source exactly once; union delegates to it with two elements. MergeAdjacent groups a run and unions it as a whole. Gum tree with a one-color palette goes 1.17s -> 0.31s (now under the no-palette baseline, since one merged layer leaves less geometry to fit), with byte-identical output. Only the --palette and --max-colors paths construct MergeAdjacent, so the default pipeline is untouched.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use crate::ir::{Layer, Segmentation};
|
||||
use crate::ir::{Layer, RegionMask, Segmentation};
|
||||
|
||||
use super::ColorFitter;
|
||||
|
||||
@@ -8,21 +8,39 @@ use super::ColorFitter;
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct MergeAdjacent;
|
||||
|
||||
/// Collapse one run of same-paint layers and push the result.
|
||||
///
|
||||
/// The whole run is unioned in a single pass — folding pairwise would reallocate
|
||||
/// and rewrite a canvas-sized accumulator once per layer. See
|
||||
/// [`RegionMask::union_all`].
|
||||
fn flush(run: &mut Vec<Layer>, out: &mut Vec<Layer>) {
|
||||
match run.len() {
|
||||
0 => {}
|
||||
1 => out.push(run.pop().expect("run is non-empty")),
|
||||
_ => {
|
||||
let paint = run[0].paint;
|
||||
let masks: Vec<&RegionMask> = run.iter().map(|l| &l.mask).collect();
|
||||
let mask = RegionMask::union_all(&masks);
|
||||
out.push(Layer { paint, mask });
|
||||
run.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorFitter for MergeAdjacent {
|
||||
fn fit(&self, seg: &mut Segmentation) {
|
||||
if seg.layers.len() < 2 {
|
||||
return;
|
||||
}
|
||||
let mut merged: Vec<Layer> = Vec::with_capacity(seg.layers.len());
|
||||
let mut run: Vec<Layer> = Vec::new();
|
||||
for layer in seg.layers.drain(..) {
|
||||
if let Some(last) = merged.last_mut() {
|
||||
if last.paint == layer.paint {
|
||||
last.mask = last.mask.union(&layer.mask);
|
||||
continue;
|
||||
}
|
||||
if run.first().is_some_and(|first| first.paint != layer.paint) {
|
||||
flush(&mut run, &mut merged);
|
||||
}
|
||||
merged.push(layer);
|
||||
run.push(layer);
|
||||
}
|
||||
flush(&mut run, &mut merged);
|
||||
seg.layers = merged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,24 +43,47 @@ impl RegionMask {
|
||||
/// Combine two masks into one covering the union of their bounding boxes.
|
||||
/// Foreground is the OR of both; this is used by the layer-merge step.
|
||||
pub fn union(&self, other: &RegionMask) -> RegionMask {
|
||||
let left = self.offset.x.min(other.offset.x);
|
||||
let top = self.offset.y.min(other.offset.y);
|
||||
let right = (self.offset.x + self.image.width as i32)
|
||||
.max(other.offset.x + other.image.width as i32);
|
||||
let bottom = (self.offset.y + self.image.height as i32)
|
||||
.max(other.offset.y + other.image.height as i32);
|
||||
Self::union_all(&[self, other])
|
||||
}
|
||||
|
||||
/// Union any number of masks in one pass: size the destination from the
|
||||
/// combined bounding box, then blit each source into it exactly once.
|
||||
///
|
||||
/// Folding [`union`](Self::union) instead costs one full-size allocation and
|
||||
/// rewrite of the accumulator *per input*. That is quadratic in the canvas
|
||||
/// area, and it bites precisely when a palette snap leaves a long run of
|
||||
/// same-paint layers for [`MergeAdjacent`](crate::colorfit::MergeAdjacent):
|
||||
/// the accumulator grows to the full canvas after the first few merges, so
|
||||
/// every remaining layer copies the entire canvas again.
|
||||
///
|
||||
/// An empty input yields an empty mask at the origin.
|
||||
pub fn union_all(masks: &[&RegionMask]) -> RegionMask {
|
||||
let Some((first, rest)) = masks.split_first() else {
|
||||
return RegionMask::new(BinaryImage::new_w_h(0, 0), PointI32 { x: 0, y: 0 });
|
||||
};
|
||||
|
||||
let mut left = first.offset.x;
|
||||
let mut top = first.offset.y;
|
||||
let mut right = first.offset.x + first.image.width as i32;
|
||||
let mut bottom = first.offset.y + first.image.height as i32;
|
||||
for m in rest {
|
||||
left = left.min(m.offset.x);
|
||||
top = top.min(m.offset.y);
|
||||
right = right.max(m.offset.x + m.image.width as i32);
|
||||
bottom = bottom.max(m.offset.y + m.image.height as i32);
|
||||
}
|
||||
|
||||
let width = (right - left) as usize;
|
||||
let height = (bottom - top) as usize;
|
||||
let mut image = BinaryImage::new_w_h(width, height);
|
||||
|
||||
for src in [self, other] {
|
||||
for src in masks {
|
||||
let dx = (src.offset.x - left) as usize;
|
||||
let dy = (src.offset.y - top) as usize;
|
||||
for y in 0..src.image.height {
|
||||
for x in 0..src.image.width {
|
||||
if src.image.get_pixel(x, y) {
|
||||
let gx = (src.offset.x + x as i32 - left) as usize;
|
||||
let gy = (src.offset.y + y as i32 - top) as usize;
|
||||
image.set_pixel(gx, gy, true);
|
||||
image.set_pixel(x + dx, y + dy, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user