1mod contour;
7mod quantize;
8mod region;
9mod segment;
10mod svg;
11
12use std::path::Path;
13
14use image::RgbImage;
15use resvg::usvg::{Options as UsvgOptions, Tree};
16use tiny_skia::{Pixmap, Transform};
17
18pub use contour::Pt;
19pub use uzor_vision::GpuMode;
20
21#[derive(Clone, Debug)]
22pub struct VectorizeOptions {
23 pub colors: u32,
24 pub merge: f32,
25 pub min_area: u32,
26 pub epsilon: f32,
27 pub absorb_dist: f32,
28 pub kmeans_iters: u32,
29 pub majority: bool,
31 pub tau: f32,
33 pub gpu: GpuMode,
34 pub slic: u32,
36}
37
38impl Default for VectorizeOptions {
39 fn default() -> Self {
40 Self {
41 colors: 48,
42 merge: 14.0,
43 min_area: 32,
44 epsilon: 0.4,
45 absorb_dist: 48.0,
46 kmeans_iters: 0,
47 majority: false,
48 tau: 80.0,
49 gpu: GpuMode::Hybrid,
50 slic: 512,
51 }
52 }
53}
54
55#[derive(Debug)]
56pub struct SvgDocument {
57 pub svg: String,
58 pub width: u32,
59 pub height: u32,
60 pub colors_kept: u32,
61 pub contours: u32,
62 pub quantized: Vec<u8>,
64 pub denoise_device: String,
65}
66
67#[derive(Debug)]
68pub enum VectorizeError {
69 Image(String),
70 Empty,
71 Raster(String),
72}
73
74impl std::fmt::Display for VectorizeError {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 VectorizeError::Image(s) => write!(f, "image: {s}"),
78 VectorizeError::Empty => write!(f, "empty image"),
79 VectorizeError::Raster(s) => write!(f, "raster: {s}"),
80 }
81 }
82}
83
84impl std::error::Error for VectorizeError {}
85
86fn loop_len(lp: &[contour::Pt]) -> f32 {
87 let mut s = 0.0f32;
88 for i in 1..lp.len() {
89 let dx = (lp[i].0 - lp[i - 1].0) as f32;
90 let dy = (lp[i].1 - lp[i - 1].1) as f32;
91 s += (dx * dx + dy * dy).sqrt();
92 }
93 s
94}
95
96pub fn vectorize_path(
97 path: impl AsRef<Path>,
98 opt: &VectorizeOptions,
99) -> Result<SvgDocument, VectorizeError> {
100 let img = image::open(path.as_ref())
101 .map_err(|e| VectorizeError::Image(e.to_string()))?
102 .to_rgb8();
103 vectorize_rgb(&img, opt)
104}
105
106pub fn vectorize_rgb(img: &RgbImage, opt: &VectorizeOptions) -> Result<SvgDocument, VectorizeError> {
107 let w = img.width() as usize;
108 let h = img.height() as usize;
109 if w == 0 || h == 0 {
110 return Err(VectorizeError::Empty);
111 }
112 let mut rgb = Vec::with_capacity(w * h * 3);
113 for p in img.pixels() {
114 rgb.extend_from_slice(&p.0);
115 }
116 let orig = rgb.clone();
117
118 let vcfg = uzor_vision::Config {
119 gpu: opt.gpu,
120 slic_k: opt.slic,
121 merge: opt.merge,
122 };
123 let frame = uzor_vision::process(&rgb, w, h, &vcfg);
124 let rgb = frame.rgb;
125 let denoise_device = frame.device;
126 let (mut idx, mut pal) = if opt.slic > 0 && !frame.idx.is_empty() {
127 (frame.idx, frame.pal)
128 } else if opt.tau > 0.0 {
129 let sp = segment::felzenszwalb(&rgb, w, h, opt.tau, opt.min_area);
130 let mut idx = sp.0;
131 let mut pal = sp.1;
132 region::merge_adjacent_similar(&mut idx, &mut pal, w, h, opt.merge);
133 (idx, pal)
134 } else {
135 let k = opt.colors.max(2) as usize;
136 quantize::median_cut(&rgb, k)
137 };
138 if opt.kmeans_iters > 0 {
139 quantize::kmeans_refine(&rgb, &mut idx, &mut pal, opt.kmeans_iters);
140 }
141 if opt.slic == 0 && opt.tau <= 0.0 {
142 quantize::merge_similar(&mut idx, &mut pal, opt.merge);
143 }
144 if opt.majority {
145 region::majority_snap(&mut idx, w, h);
146 }
147 region::absorb_speckles(
148 &mut idx,
149 &pal,
150 w,
151 h,
152 opt.min_area,
153 opt.absorb_dist,
154 );
155 region::despeckle_labels(&mut idx, w, h);
156 region::fill_small_holes(&mut idx, w, h, 16);
157 quantize::snap_palette_median(&orig, &idx, &mut pal, w, h);
158
159 let mut quantized = vec![0u8; w * h * 3];
160 for i in 0..w * h {
161 let c = pal[idx[i] as usize];
162 quantized[i * 3] = c[0];
163 quantized[i * 3 + 1] = c[1];
164 quantized[i * 3 + 2] = c[2];
165 }
166
167 let mut area_of = vec![0u32; pal.len()];
168 for &lab in idx.iter() {
169 let i = lab as usize;
170 if i < area_of.len() {
171 area_of[i] += 1;
172 }
173 }
174 let img_area = (w * h) as u32;
175 let mut fills: Vec<(u32, [u8; 3], Vec<Vec<contour::Pt>>)> = Vec::new();
176 let mut strokes: Vec<(u32, [u8; 3], f32, Vec<Vec<contour::Pt>>)> = Vec::new();
177 let mut contour_count = 0u32;
178 for (lab, loops) in contour::planar_loops(&idx, w, h) {
179 let li = lab as usize;
180 if li >= pal.len() {
181 continue;
182 }
183 let area = area_of[li];
184 if area < opt.min_area || loops.is_empty() {
185 continue;
186 }
187 let mut peri = 0.0f32;
188 for lp in &loops {
189 peri += loop_len(lp);
190 }
191 contour_count += loops.len() as u32;
192 let color = pal[li];
193 let width = 2.0 * area as f32 / peri.max(1.0);
194 let luma = (color[0] as u32 + color[1] as u32 + color[2] as u32) / 3;
195 let thin_dark = luma < 40 && width < 2.2 && area < img_area / 20;
196 if thin_dark {
197 strokes.push((area, color, width.clamp(0.8, 5.0), loops));
198 } else {
199 fills.push((area, color, loops));
200 }
201 }
202 fills.sort_by(|a, b| b.0.cmp(&a.0));
203 let mut fill_by_color: Vec<(u32, [u8; 3], Vec<Vec<contour::Pt>>)> = Vec::new();
204 for (area, color, loops) in fills {
205 if let Some(slot) = fill_by_color.iter_mut().find(|s| s.1 == color) {
206 slot.0 += area;
207 slot.2.extend(loops);
208 } else {
209 fill_by_color.push((area, color, loops));
210 }
211 }
212 fill_by_color.sort_by(|a, b| b.0.cmp(&a.0));
213 let mut paths: Vec<String> = Vec::new();
214 for (_, color, loops) in fill_by_color {
215 let hex = svg::hex_color(color);
216 paths.push(svg::fill_path(&hex, &svg::path_d(&loops)));
217 }
218 let mut stroke_by_color: Vec<(u32, [u8; 3], f32, u32, Vec<Vec<contour::Pt>>)> = Vec::new();
219 for (area, color, width, loops) in strokes {
220 if let Some(slot) = stroke_by_color.iter_mut().find(|s| s.1 == color) {
221 slot.0 += area;
222 slot.2 += width * area as f32;
223 slot.3 += area;
224 slot.4.extend(loops);
225 } else {
226 stroke_by_color.push((area, color, width * area as f32, area, loops));
227 }
228 }
229 for (_, color, wsum, warea, loops) in stroke_by_color {
230 let hex = svg::hex_color(color);
231 let sw = (wsum / warea.max(1) as f32).clamp(0.8, 5.0);
232 paths.push(svg::stroke_path(&hex, sw, &svg::path_d(&loops)));
233 }
234 let colors_kept = paths.len() as u32;
235 let svg = svg::document(w as u32, h as u32, &paths);
236 Ok(SvgDocument {
237 svg,
238 width: w as u32,
239 height: h as u32,
240 colors_kept,
241 contours: contour_count,
242 quantized,
243 denoise_device,
244 })
245}
246
247pub fn rasterize_svg(svg: &str, width: u32, height: u32) -> Result<RgbImage, VectorizeError> {
248 let tree = Tree::from_data(svg.as_bytes(), &UsvgOptions::default())
249 .map_err(|e| VectorizeError::Raster(e.to_string()))?;
250 let mut pixmap = Pixmap::new(width, height)
251 .ok_or_else(|| VectorizeError::Raster("pixmap".into()))?;
252 let sz = tree.size();
253 let transform = Transform::from_scale(width as f32 / sz.width(), height as f32 / sz.height());
254 resvg::render(&tree, transform, &mut pixmap.as_mut());
255 let data = pixmap.data();
256 let mut img = RgbImage::new(width, height);
257 for y in 0..height {
258 for x in 0..width {
259 let i = ((y * width + x) * 4) as usize;
260 img.put_pixel(x, y, image::Rgb([data[i], data[i + 1], data[i + 2]]));
262 }
263 }
264 Ok(img)
265}
266
267#[derive(Debug, Clone)]
268pub struct ParityStats {
269 pub mae: f32,
270 pub close_pct: f32,
271 pub visible_pct: f32,
272}
273
274pub fn parity_rgb(a: &RgbImage, b: &RgbImage) -> ParityStats {
275 let w = a.width().min(b.width());
276 let h = a.height().min(b.height());
277 let n = (w * h) as f32;
278 let mut sum = 0.0f32;
279 let mut close = 0u32;
280 let mut vis = 0u32;
281 for y in 0..h {
282 for x in 0..w {
283 let pa = a.get_pixel(x, y).0;
284 let pb = b.get_pixel(x, y).0;
285 let mut maxc = 0i32;
286 for c in 0..3 {
287 let d = (pa[c] as i32 - pb[c] as i32).abs();
288 sum += d as f32;
289 if d > maxc {
290 maxc = d;
291 }
292 }
293 if maxc <= 8 {
294 close += 1;
295 }
296 if maxc > 18 {
297 vis += 1;
298 }
299 }
300 }
301 ParityStats {
302 mae: sum / (n * 3.0),
303 close_pct: close as f32 / n * 100.0,
304 visible_pct: vis as f32 / n * 100.0,
305 }
306}
307
308pub fn parity_sheet(src: &RgbImage, svg: &RgbImage, quant: &RgbImage, diff: &RgbImage) -> RgbImage {
310 let w = src.width();
311 let h = src.height();
312 let mut sheet = RgbImage::new(w * 2, h * 2);
313 blit(&mut sheet, src, 0, 0);
314 blit(&mut sheet, svg, w, 0);
315 blit(&mut sheet, quant, 0, h);
316 blit(&mut sheet, diff, w, h);
317 sheet
318}
319
320fn blit(dst: &mut RgbImage, src: &RgbImage, ox: u32, oy: u32) {
321 let iw = src.width();
322 let ih = src.height();
323 for y in 0..ih {
324 for x in 0..iw {
325 dst.put_pixel(ox + x, oy + y, *src.get_pixel(x, y));
326 }
327 }
328}
329
330pub fn quantized_image(doc: &SvgDocument) -> RgbImage {
331 let mut img = RgbImage::new(doc.width, doc.height);
332 for y in 0..doc.height {
333 for x in 0..doc.width {
334 let i = ((y * doc.width + x) * 3) as usize;
335 img.put_pixel(
336 x,
337 y,
338 image::Rgb([
339 doc.quantized[i],
340 doc.quantized[i + 1],
341 doc.quantized[i + 2],
342 ]),
343 );
344 }
345 }
346 img
347}
348
349pub fn diff_heatmap(src: &RgbImage, svg: &RgbImage) -> RgbImage {
350 let w = src.width().min(svg.width());
351 let h = src.height().min(svg.height());
352 let mut out = RgbImage::new(w, h);
353 for y in 0..h {
354 for x in 0..w {
355 let a = src.get_pixel(x, y).0;
356 let b = svg.get_pixel(x, y).0;
357 let mut maxc = 0i32;
358 for c in 0..3 {
359 maxc = maxc.max((a[c] as i32 - b[c] as i32).abs());
360 }
361 let luma = (((a[0] as u16 + a[1] as u16 + a[2] as u16) / 3) * 35 / 100) as u8;
362 let heat = if maxc <= 8 {
363 0u8
364 } else {
365 ((maxc - 8) * 4).clamp(0, 255) as u8
366 };
367 out.put_pixel(
368 x,
369 y,
370 image::Rgb([luma.saturating_add(heat), luma, luma]),
371 );
372 }
373 }
374 out
375}
376
377#[derive(Clone, Debug)]
378pub struct Hotspot {
379 pub x: u32,
380 pub y: u32,
381 pub w: u32,
382 pub h: u32,
383 pub area: u32,
384 pub mean_err: f32,
385}
386
387pub fn vis_hotspots(src: &RgbImage, svg: &RgbImage, n: usize) -> Vec<Hotspot> {
390 let w = src.width().min(svg.width()) as usize;
391 let h = src.height().min(svg.height()) as usize;
392 let mut vis = vec![false; w * h];
393 let mut err = vec![0u16; w * h];
394 for y in 0..h {
395 for x in 0..w {
396 let a = src.get_pixel(x as u32, y as u32).0;
397 let b = svg.get_pixel(x as u32, y as u32).0;
398 let mut maxc = 0i32;
399 for c in 0..3 {
400 maxc = maxc.max((a[c] as i32 - b[c] as i32).abs());
401 }
402 if maxc > 18 {
403 vis[y * w + x] = true;
404 err[y * w + x] = maxc as u16;
405 }
406 }
407 }
408 let mut parent: Vec<u32> = (0..(w * h) as u32).collect();
409 let find = |p: &mut [u32], mut a: u32| -> u32 {
410 while p[a as usize] != a {
411 let n = p[a as usize];
412 p[a as usize] = p[n as usize];
413 a = n;
414 }
415 a
416 };
417 for y in 0..h {
418 for x in 0..w {
419 let i = y * w + x;
420 if !vis[i] {
421 continue;
422 }
423 if x + 1 < w && vis[i + 1] {
424 let ra = find(&mut parent, i as u32);
425 let rb = find(&mut parent, (i + 1) as u32);
426 if ra != rb {
427 parent[rb as usize] = ra;
428 }
429 }
430 if y + 1 < h && vis[i + w] {
431 let ra = find(&mut parent, i as u32);
432 let rb = find(&mut parent, (i + w) as u32);
433 if ra != rb {
434 parent[rb as usize] = ra;
435 }
436 }
437 }
438 }
439 use std::collections::HashMap;
440 let mut acc: HashMap<u32, (u32, u32, u32, u32, u32, u32)> = HashMap::new();
441 for y in 0..h {
443 for x in 0..w {
444 let i = y * w + x;
445 if !vis[i] {
446 continue;
447 }
448 let r = find(&mut parent, i as u32);
449 let e = acc.entry(r).or_insert((x as u32, y as u32, x as u32, y as u32, 0, 0));
450 e.0 = e.0.min(x as u32);
451 e.1 = e.1.min(y as u32);
452 e.2 = e.2.max(x as u32);
453 e.3 = e.3.max(y as u32);
454 e.4 += 1;
455 e.5 += err[i] as u32;
456 }
457 }
458 let mut spots: Vec<Hotspot> = acc
459 .into_values()
460 .filter(|e| e.4 >= 40)
461 .map(|e| Hotspot {
462 x: e.0,
463 y: e.1,
464 w: e.2 - e.0 + 1,
465 h: e.3 - e.1 + 1,
466 area: e.4,
467 mean_err: e.5 as f32 / e.4 as f32,
468 })
469 .collect();
470 spots.sort_by(|a, b| b.area.cmp(&a.area));
471 spots.truncate(n.max(1));
472 let pad = 24u32;
473 let ww = src.width();
474 let hh = src.height();
475 for s in spots.iter_mut() {
476 let x0 = s.x.saturating_sub(pad);
477 let y0 = s.y.saturating_sub(pad);
478 let x1 = (s.x + s.w + pad).min(ww);
479 let y1 = (s.y + s.h + pad).min(hh);
480 s.x = x0;
481 s.y = y0;
482 s.w = x1 - x0;
483 s.h = y1 - y0;
484 }
485 spots
486}
487
488pub fn hotspot_strip(src: &RgbImage, svg: &RgbImage, heat: &RgbImage, hs: &Hotspot) -> RgbImage {
489 let mut strip = RgbImage::new(hs.w * 3, hs.h);
490 for y in 0..hs.h {
491 for x in 0..hs.w {
492 let sx = hs.x + x;
493 let sy = hs.y + y;
494 strip.put_pixel(x, y, *src.get_pixel(sx, sy));
495 strip.put_pixel(hs.w + x, y, *svg.get_pixel(sx, sy));
496 strip.put_pixel(hs.w * 2 + x, y, *heat.get_pixel(sx, sy));
497 }
498 }
499 strip
500}