15 Commits

Author SHA1 Message Date
Chris Tsang
efa4351b2c Update README.md 2024-09-27 10:43:47 +01:00
Chris Tsang
a46292b5ed Update README.md 2024-09-27 10:43:02 +01:00
Chris Tsang
8889cbc7ea Tweaks
Some checks failed
Rust / build (push) Has been cancelled
2024-09-26 12:59:45 +01:00
Wil Carmon
6b379a02ef Update svg.rs (#92) 2024-09-26 12:58:43 +01:00
Wil Carmon
2635d5b874 Update config.rs (#91)
added #[derive(Clone, Debug)]
2024-09-26 12:58:30 +01:00
Chris Tsang
f6cf3e8705 Refactor 2024-05-30 10:04:23 +01:00
Chris Tsang
36b16de17a Key transparent image in webapp 2024-05-29 15:10:11 +01:00
Chris Tsang
7887c1ebf8 Update readme 2024-05-02 23:57:13 +01:00
Chris Tsang
6fdfec8610 python 0.6.11 2024-05-02 22:58:11 +01:00
York
1aff9a300a Add support for conversion from python bytes (#79)
* Allow conversion from python bytes

* Update function name

* Add convert_pixels_to_svg python function

* Update README.md

* Update README.md
2024-05-02 22:24:56 +01:00
Chris Tsang
ac0a89e08a README 2024-04-20 16:24:41 +01:00
Chris Tsang
b09f71a2b5 LICENSE 2024-04-20 16:21:36 +01:00
Chris Tsang
e4897dfe99 README 2024-04-20 16:16:57 +01:00
Chris Tsang
4544ca740d README 2024-04-20 16:14:21 +01:00
Chris Tsang
3d92586e33 Update CI script 2024-04-20 15:52:58 +01:00
16 changed files with 335 additions and 37 deletions

View File

@@ -1,10 +1,23 @@
name: Rust name: Rust
on: on:
push:
branches: [ master ]
pull_request: pull_request:
branches: [ master ] paths-ignore:
- '**.md'
- '.github/ISSUE_TEMPLATE/**'
push:
paths-ignore:
- '**.md'
- '.github/ISSUE_TEMPLATE/**'
branches:
- master
- 0.*.x
- pr/**/ci
- ci-*
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref || github.run_id }}
cancel-in-progress: true
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always

View File

@@ -1,4 +1,4 @@
Copyright (c) 2023 Tsang Hao Fung Copyright (c) 2024 TSANG, Hao Fung
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated

View File

@@ -10,9 +10,9 @@
<h3> <h3>
<a href="https://www.visioncortex.org/vtracer-docs">Article</a> <a href="https://www.visioncortex.org/vtracer-docs">Article</a>
<span> | </span> <span> | </span>
<a href="https://www.visioncortex.org/vtracer/">Demo</a> <a href="https://www.visioncortex.org/vtracer/">Web App</a>
<span> | </span> <span> | </span>
<a href="https://github.com/visioncortex/vtracer/releases/tag/0.6.0">Download</a> <a href="https://github.com/visioncortex/vtracer/releases">Download</a>
</h3> </h3>
<sub>Built with 🦀 by <a href="https://www.visioncortex.org/">The Vision Cortex Research Group</a></sub> <sub>Built with 🦀 by <a href="https://www.visioncortex.org/">The Vision Cortex Research Group</a></sub>
@@ -28,7 +28,7 @@ Comparing to Adobe Illustrator's [Image Trace](https://helpx.adobe.com/illustrat
VTracer is originally designed for processing high resolution scans of historic blueprints up to gigapixels. At the same time, VTracer can also handle low resolution pixel art, simulating `image-rendering: pixelated` for retro game artworks. VTracer is originally designed for processing high resolution scans of historic blueprints up to gigapixels. At the same time, VTracer can also handle low resolution pixel art, simulating `image-rendering: pixelated` for retro game artworks.
A technical description of the algorithm is on [visioncortex.org/vtracer-docs](https://www.visioncortex.org/vtracer-docs). Technical descriptions of the [tracing algorithm](https://www.visioncortex.org/vtracer-docs) and [clustering algorithm](https://www.visioncortex.org/impression-docs).
## Web App ## Web App
@@ -71,9 +71,9 @@ OPTIONS:
-s, --splice_threshold <splice_threshold> Minimum angle displacement (degree) to splice a spline -s, --splice_threshold <splice_threshold> Minimum angle displacement (degree) to splice a spline
``` ```
### Install ## Downloads
You can download pre-built binaries from [Releases](https://github.com/visioncortex/vtracer/releases/tag/0.6.0). You can download pre-built binaries from [Releases](https://github.com/visioncortex/vtracer/releases).
You can also install the program from source from [crates.io/vtracer](https://crates.io/crates/vtracer): You can also install the program from source from [crates.io/vtracer](https://crates.io/crates/vtracer):
@@ -81,13 +81,15 @@ You can also install the program from source from [crates.io/vtracer](https://cr
cargo install vtracer cargo install vtracer
``` ```
> You are strongly advised to not download from any other third-party sources
### Usage ### Usage
```sh ```sh
./vtracer --input input.jpg --output output.svg ./vtracer --input input.jpg --output output.svg
``` ```
## Rust Library ### Rust Library
You can install [`vtracer`](https://crates.io/crates/vtracer) as a Rust library. You can install [`vtracer`](https://crates.io/crates/vtracer) as a Rust library.
@@ -95,7 +97,7 @@ You can install [`vtracer`](https://crates.io/crates/vtracer) as a Rust library.
cargo add vtracer cargo add vtracer
``` ```
## Python Library ### Python Library
Since `0.6`, [`vtracer`](https://pypi.org/project/vtracer/) is also packaged as Python native extensions, thanks to the awesome [pyo3](https://github.com/PyO3/pyo3) project. Since `0.6`, [`vtracer`](https://pypi.org/project/vtracer/) is also packaged as Python native extensions, thanks to the awesome [pyo3](https://github.com/PyO3/pyo3) project.
@@ -105,7 +107,7 @@ pip install vtracer
## In the wild ## In the wild
VTracer is used by the following projects (feel free to add yours!): VTracer is used by the following products (open a PR to add yours):
<table> <table>
<tbody> <tbody>
@@ -118,12 +120,20 @@ VTracer is used by the following projects (feel free to add yours!):
</tbody> </tbody>
</table> </table>
## Anecdotes ## Citations
VTracer has since been cited by a few academic papers in computer graphics / vision research. Please kindly let us know if you have cited our work:
+ SKILL 2023 [Framework to Vectorize Digital Artworks for Physical Fabrication based on Geometric Stylization Techniques](https://www.researchgate.net/publication/374448489_Framework_to_Vectorize_Digital_Artworks_for_Physical_Fabrication_based_on_Geometric_Stylization_Techniques)
+ arXiv 2023 [Image Vectorization: a Review](https://arxiv.org/abs/2306.06441)
+ arXiv 2023 [StarVector: Generating Scalable Vector Graphics Code from Images](https://arxiv.org/abs/2312.11556)
+ arXiv 2024 [Text-Based Reasoning About Vector Graphics](https://arxiv.org/abs/2404.06479)
+ arXiv 2024 [Delving into LLMs' visual understanding ability using SVG to bridge image and text](https://openreview.net/pdf?id=pwlm6Po61I)
## How did VTracer come about?
> The following content is an excerpt from my [unpublished memoir](https://github.com/visioncortex/memoir). > The following content is an excerpt from my [unpublished memoir](https://github.com/visioncortex/memoir).
### How / when / where did VTracer come about?
At my teenage, two open source projects in the vector graphics space inspired me the most: Potrace and Anti-Grain Geometry (AGG). At my teenage, two open source projects in the vector graphics space inspired me the most: Potrace and Anti-Grain Geometry (AGG).
Many years later, in 2020, I was developing a video processing engine. And it became evident that it requires way more investment to be commercially viable. So before abandoning the project, I wanted to publish *something* as open-source for posterity. At that time, I already developed a prototype vector graphics tracer. It can convert high-resolution scans of hand-drawn blueprints into vectors. But it can only process black and white images, and can only output polygons, not splines. Many years later, in 2020, I was developing a video processing engine. And it became evident that it requires way more investment to be commercially viable. So before abandoning the project, I wanted to publish *something* as open-source for posterity. At that time, I already developed a prototype vector graphics tracer. It can convert high-resolution scans of hand-drawn blueprints into vectors. But it can only process black and white images, and can only output polygons, not splines.

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "vtracer" name = "vtracer"
version = "0.6.10" version = "0.6.11"
description = "Python bindings for the Rust Vtracer raster-to-vector library" description = "Python bindings for the Rust Vtracer raster-to-vector library"
authors = [ { name = "Chris Tsang", email = "chris.2y3@outlook.com" } ] authors = [ { name = "Chris Tsang", email = "chris.2y3@outlook.com" } ]
readme = "vtracer/README.md" readme = "vtracer/README.md"

View File

@@ -1,23 +1,27 @@
use std::str::FromStr; use std::str::FromStr;
use visioncortex::PathSimplifyMode; use visioncortex::PathSimplifyMode;
#[derive(Debug, Clone)]
pub enum Preset { pub enum Preset {
Bw, Bw,
Poster, Poster,
Photo, Photo,
} }
#[derive(Debug, Clone)]
pub enum ColorMode { pub enum ColorMode {
Color, Color,
Binary, Binary,
} }
#[derive(Debug, Clone)]
pub enum Hierarchical { pub enum Hierarchical {
Stacked, Stacked,
Cutout, Cutout,
} }
/// Converter config /// Converter config
#[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub color_mode: ColorMode, pub color_mode: ColorMode,
pub hierarchical: Hierarchical, pub hierarchical: Hierarchical,
@@ -32,6 +36,7 @@ pub struct Config {
pub path_precision: Option<u32>, pub path_precision: Option<u32>,
} }
#[derive(Debug, Clone)]
pub(crate) struct ConverterConfig { pub(crate) struct ConverterConfig {
pub color_mode: ColorMode, pub color_mode: ColorMode,
pub hierarchical: Hierarchical, pub hierarchical: Hierarchical,

View File

@@ -73,7 +73,7 @@ fn should_key_image(img: &ColorImage) -> bool {
// Check for transparency at several scanlines // Check for transparency at several scanlines
let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize; let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize;
let mut num_transparent_boundary_pixels = 0; let mut num_transparent_pixels = 0;
let y_positions = [ let y_positions = [
0, 0,
img.height / 4, img.height / 4,
@@ -84,9 +84,9 @@ fn should_key_image(img: &ColorImage) -> bool {
for y in y_positions { for y in y_positions {
for x in 0..img.width { for x in 0..img.width {
if img.get_pixel(x, y).a == 0 { if img.get_pixel(x, y).a == 0 {
num_transparent_boundary_pixels += 1; num_transparent_pixels += 1;
} }
if num_transparent_boundary_pixels >= threshold { if num_transparent_pixels >= threshold {
return true; return true;
} }
} }

View File

@@ -1,5 +1,7 @@
use crate::*; use crate::*;
use pyo3::prelude::*; use image::{io::Reader, ImageFormat};
use pyo3::{exceptions::PyException, prelude::*};
use std::io::{BufReader, Cursor};
use std::path::PathBuf; use std::path::PathBuf;
use visioncortex::PathSimplifyMode; use visioncortex::PathSimplifyMode;
@@ -23,6 +25,148 @@ fn convert_image_to_svg_py(
let input_path = PathBuf::from(image_path); let input_path = PathBuf::from(image_path);
let output_path = PathBuf::from(out_path); let output_path = PathBuf::from(out_path);
let config = construct_config(
colormode,
hierarchical,
mode,
filter_speckle,
color_precision,
layer_difference,
corner_threshold,
length_threshold,
max_iterations,
splice_threshold,
path_precision,
);
convert_image_to_svg(&input_path, &output_path, config).unwrap();
Ok(())
}
#[pyfunction]
fn convert_raw_image_to_svg(
img_bytes: Vec<u8>,
img_format: Option<&str>, // Format of the image (e.g. 'jpg', 'png'... A full list of supported formats can be found [here](https://docs.rs/image/latest/image/enum.ImageFormat.html)). If not provided, the image format will be guessed based on its contents.
colormode: Option<&str>, // "color" or "binary"
hierarchical: Option<&str>, // "stacked" or "cutout"
mode: Option<&str>, // "polygon", "spline", "none"
filter_speckle: Option<usize>, // default: 4
color_precision: Option<i32>, // default: 6
layer_difference: Option<i32>, // default: 16
corner_threshold: Option<i32>, // default: 60
length_threshold: Option<f64>, // in [3.5, 10] default: 4.0
max_iterations: Option<usize>, // default: 10
splice_threshold: Option<i32>, // default: 45
path_precision: Option<u32>, // default: 8
) -> PyResult<String> {
let config = construct_config(
colormode,
hierarchical,
mode,
filter_speckle,
color_precision,
layer_difference,
corner_threshold,
length_threshold,
max_iterations,
splice_threshold,
path_precision,
);
let mut img_reader = Reader::new(BufReader::new(Cursor::new(img_bytes)));
let img_format = img_format.and_then(|ext_name| ImageFormat::from_extension(ext_name));
let img = match img_format {
Some(img_format) => {
img_reader.set_format(img_format);
img_reader.decode()
}
None => img_reader
.with_guessed_format()
.map_err(|_| PyException::new_err("Unrecognized image format. "))?
.decode(),
};
let img = match img {
Ok(img) => img.to_rgba8(),
Err(_) => return Err(PyException::new_err("Failed to decode img_bytes. ")),
};
let (width, height) = (img.width() as usize, img.height() as usize);
let img = ColorImage {
pixels: img.as_raw().to_vec(),
width,
height,
};
let svg =
convert(img, config).map_err(|_| PyException::new_err("Failed to convert the image. "))?;
Ok(format!("{}", svg))
}
#[pyfunction]
fn convert_pixels_to_svg(
rgba_pixels: Vec<(u8, u8, u8, u8)>,
size: (usize, usize),
colormode: Option<&str>, // "color" or "binary"
hierarchical: Option<&str>, // "stacked" or "cutout"
mode: Option<&str>, // "polygon", "spline", "none"
filter_speckle: Option<usize>, // default: 4
color_precision: Option<i32>, // default: 6
layer_difference: Option<i32>, // default: 16
corner_threshold: Option<i32>, // default: 60
length_threshold: Option<f64>, // in [3.5, 10] default: 4.0
max_iterations: Option<usize>, // default: 10
splice_threshold: Option<i32>, // default: 45
path_precision: Option<u32>, // default: 8
) -> PyResult<String> {
let expected_pixel_count = size.0 * size.1;
if rgba_pixels.len() != expected_pixel_count {
return Err(PyException::new_err(format!(
"Length of rgba_pixels does not match given image size. Expected {} ({} * {}), got {}. ",
expected_pixel_count,
size.0,
size.1,
rgba_pixels.len()
)));
}
let config = construct_config(
colormode,
hierarchical,
mode,
filter_speckle,
color_precision,
layer_difference,
corner_threshold,
length_threshold,
max_iterations,
splice_threshold,
path_precision,
);
let mut flat_pixels: Vec<u8> = vec![];
for (r, g, b, a) in rgba_pixels {
flat_pixels.push(r);
flat_pixels.push(g);
flat_pixels.push(b);
flat_pixels.push(a);
}
let mut img = ColorImage::new();
img.pixels = flat_pixels;
(img.width, img.height) = size;
let svg =
convert(img, config).map_err(|_| PyException::new_err("Failed to convert the image. "))?;
Ok(format!("{}", svg))
}
fn construct_config(
colormode: Option<&str>,
hierarchical: Option<&str>,
mode: Option<&str>,
filter_speckle: Option<usize>,
color_precision: Option<i32>,
layer_difference: Option<i32>,
corner_threshold: Option<i32>,
length_threshold: Option<f64>,
max_iterations: Option<usize>,
splice_threshold: Option<i32>,
path_precision: Option<u32>,
) -> Config {
// TODO: enforce color mode with an enum so that we only // TODO: enforce color mode with an enum so that we only
// accept the strings 'color' or 'binary' // accept the strings 'color' or 'binary'
let color_mode = match colormode.unwrap_or("color") { let color_mode = match colormode.unwrap_or("color") {
@@ -52,7 +196,7 @@ fn convert_image_to_svg_py(
let splice_threshold = splice_threshold.unwrap_or(45); let splice_threshold = splice_threshold.unwrap_or(45);
let max_iterations = max_iterations.unwrap_or(10); let max_iterations = max_iterations.unwrap_or(10);
let config = Config { Config {
color_mode, color_mode,
hierarchical, hierarchical,
filter_speckle, filter_speckle,
@@ -65,15 +209,14 @@ fn convert_image_to_svg_py(
splice_threshold, splice_threshold,
path_precision, path_precision,
..Default::default() ..Default::default()
}; }
convert_image_to_svg(&input_path, &output_path, config).unwrap();
Ok(())
} }
/// A Python module implemented in Rust. /// A Python module implemented in Rust.
#[pymodule] #[pymodule]
fn vtracer(_py: Python, m: &PyModule) -> PyResult<()> { fn vtracer(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(convert_image_to_svg_py, m)?)?; m.add_function(wrap_pyfunction!(convert_image_to_svg_py, m)?)?;
m.add_function(wrap_pyfunction!(convert_raw_image_to_svg, m)?)?;
m.add_function(wrap_pyfunction!(convert_pixels_to_svg, m)?)?;
Ok(()) Ok(())
} }

View File

@@ -1,6 +1,7 @@
use std::fmt; use std::fmt;
use visioncortex::{Color, CompoundPath, PointF64}; use visioncortex::{Color, CompoundPath, PointF64};
#[derive(Debug, Clone)]
pub struct SvgFile { pub struct SvgFile {
pub paths: Vec<SvgPath>, pub paths: Vec<SvgPath>,
pub width: usize, pub width: usize,
@@ -8,6 +9,7 @@ pub struct SvgFile {
pub path_precision: Option<u32>, pub path_precision: Option<u32>,
} }
#[derive(Debug, Clone)]
pub struct SvgPath { pub struct SvgPath {
pub path: CompoundPath, pub path: CompoundPath,
pub color: Color, pub color: Color,

View File

@@ -52,7 +52,17 @@ vtracer.convert_image_to_svg_py(inp, out)
# Single-color example. Good for line art, and much faster than full color: # Single-color example. Good for line art, and much faster than full color:
vtracer.convert_image_to_svg_py(inp, out, colormode='binary') vtracer.convert_image_to_svg_py(inp, out, colormode='binary')
# All the bells & whistles # Convert from raw image bytes
input_img_bytes: bytes = get_bytes() # e.g. reading bytes from a file or a HTTP request body
svg_str: str = vtracer.convert_raw_image_to_svg(input_img_bytes, img_format='jpg')
# Convert from RGBA image pixels
from PIL import Image
img = Image.open(input_path).convert('RGBA')
pixels: list[tuple[int, int, int, int]] = list(img.getdata())
svg_str: str = vtracer.convert_pixels_to_svg(pixels, img.size)
# All the bells & whistles, also applicable to convert_raw_image_to_svg and convert_pixels_to_svg.
vtracer.convert_image_to_svg_py(inp, vtracer.convert_image_to_svg_py(inp,
out, out,
colormode = 'color', # ["color"] or "binary" colormode = 'color', # ["color"] or "binary"

View File

@@ -1 +1,2 @@
from .vtracer import convert_image_to_svg_py from .vtracer import (convert_image_to_svg_py, convert_pixels_to_svg,
convert_raw_image_to_svg)

View File

@@ -15,3 +15,35 @@ def convert_image_to_svg_py(image_path: str,
path_precision: Optional[int] = None, # default: 8 path_precision: Optional[int] = None, # default: 8
) -> None: ) -> None:
... ...
def convert_raw_image_to_svg(img_bytes: bytes,
img_format: Optional[str] = None, # Format of the image (e.g. 'jpg', 'png'... A full list of supported formats can be found [here](https://docs.rs/image/latest/image/enum.ImageFormat.html)). If not provided, the image format will be guessed based on its contents.
colormode: Optional[str] = None, # ["color"] or "binary"
hierarchical: Optional[str] = None, # ["stacked"] or "cutout"
mode: Optional[str] = None, # ["spline"], "polygon", "none"
filter_speckle: Optional[int] = None, # default: 4
color_precision: Optional[int] = None, # default: 6
layer_difference: Optional[int] = None, # default: 16
corner_threshold: Optional[int] = None, # default: 60
length_threshold: Optional[float] = None, # in [3.5, 10] default: 4.0
max_iterations: Optional[int] = None, # default: 10
splice_threshold: Optional[int] = None, # default: 45
path_precision: Optional[int] = None, # default: 8
) -> str:
...
def convert_pixels_to_svg(rgba_pixels: list[tuple[int, int, int, int]],
size: tuple[int, int],
colormode: Optional[str] = None, # ["color"] or "binary"
hierarchical: Optional[str] = None, # ["stacked"] or "cutout"
mode: Optional[str] = None, # ["spline"], "polygon", "none"
filter_speckle: Optional[int] = None, # default: 4
color_precision: Optional[int] = None, # default: 6
layer_difference: Optional[int] = None, # default: 16
corner_threshold: Optional[int] = None, # default: 60
length_threshold: Optional[float] = None, # in [3.5, 10] default: 4.0
max_iterations: Optional[int] = None, # default: 10
splice_threshold: Optional[int] = None, # default: 45
path_precision: Optional[int] = None, # default: 8
) -> str:
...

View File

@@ -22,7 +22,7 @@ console_log = { version = "0.2", features = ["color"] }
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] } wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
visioncortex = "0.6.0" visioncortex = "0.8.1"
# The `console_error_panic_hook` crate provides better debugging of panics by # The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires # logging them with `console.error`. This is great for development, but requires

View File

@@ -35,7 +35,11 @@ document.addEventListener('paste', function (e) {
// Download as SVG // Download as SVG
document.getElementById('export').addEventListener('click', function (e) { document.getElementById('export').addEventListener('click', function (e) {
const blob = new Blob([new XMLSerializer().serializeToString(svg)], {type: 'octet/stream'}), const blob = new Blob([
`<?xml version="1.0" encoding="UTF-8"?>\n`,
`<!-- Generator: visioncortex VTracer -->\n`,
new XMLSerializer().serializeToString(svg)
], {type: 'octet/stream'}),
url = window.URL.createObjectURL(blob); url = window.URL.createObjectURL(blob);
this.href = url; this.href = url;
@@ -444,7 +448,7 @@ class ConverterRunner {
this.converter.init(); this.converter.init();
this.stopped = false; this.stopped = false;
if (clustering_mode == 'binary') { if (clustering_mode == 'binary') {
svg.style.background = '#000'; svg.style.background = '#fff';
canvas.style.display = 'none'; canvas.style.display = 'none';
} else { } else {
svg.style.background = ''; svg.style.background = '';

View File

@@ -77,7 +77,7 @@ impl BinaryImageConverter {
self.params.max_iterations, self.params.max_iterations,
self.params.splice_threshold self.params.splice_threshold
); );
let color = Color::color(&ColorName::White); let color = Color::color(&ColorName::Black);
self.svg.prepend_path( self.svg.prepend_path(
&paths, &paths,
&color, &color,

View File

@@ -1,6 +1,6 @@
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use visioncortex::PathSimplifyMode; use visioncortex::{Color, ColorImage, PathSimplifyMode};
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig, HIERARCHICAL_MAX}; use visioncortex::color_clusters::{Clusters, Runner, RunnerConfig, HIERARCHICAL_MAX, IncrementalBuilder, KeyingAction};
use crate::canvas::*; use crate::canvas::*;
use crate::svg::*; use crate::svg::*;
@@ -8,6 +8,8 @@ use crate::svg::*;
use serde::Deserialize; use serde::Deserialize;
use super::util; use super::util;
const KEYING_THRESHOLD: f32 = 0.2;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ColorImageConverterParams { pub struct ColorImageConverterParams {
pub canvas_id: String, pub canvas_id: String,
@@ -67,7 +69,26 @@ impl ColorImageConverter {
pub fn init(&mut self) { pub fn init(&mut self) {
let width = self.canvas.width() as u32; let width = self.canvas.width() as u32;
let height = self.canvas.height() as u32; let height = self.canvas.height() as u32;
let image = self.canvas.get_image_data_as_color_image(0, 0, width, height); let mut image = self.canvas.get_image_data_as_color_image(0, 0, width, height);
let key_color = if Self::should_key_image(&image) {
if let Ok(key_color) = Self::find_unused_color_in_image(&image) {
for y in 0..height as usize {
for x in 0..width as usize {
if image.get_pixel(x, y).a == 0 {
image.set_pixel(x, y, &key_color);
}
}
}
key_color
} else {
Color::default()
}
} else {
// The default color is all zeroes, which is treated by visioncortex as a special value meaning no keying will be applied.
Color::default()
};
let runner = Runner::new(RunnerConfig { let runner = Runner::new(RunnerConfig {
diagonal: self.params.layer_difference == 0, diagonal: self.params.layer_difference == 0,
hierarchical: HIERARCHICAL_MAX, hierarchical: HIERARCHICAL_MAX,
@@ -78,6 +99,12 @@ impl ColorImageConverter {
is_same_color_b: 1, is_same_color_b: 1,
deepen_diff: self.params.layer_difference, deepen_diff: self.params.layer_difference,
hollow_neighbours: 1, hollow_neighbours: 1,
key_color,
keying_action: if self.params.hierarchical == "cutout" {
KeyingAction::Keep
} else {
KeyingAction::Discard
},
}, image); }, image);
self.stage = Stage::Clustering(runner.start()); self.stage = Stage::Clustering(runner.start());
} }
@@ -108,6 +135,8 @@ impl ColorImageConverter {
is_same_color_b: 1, is_same_color_b: 1,
deepen_diff: 0, deepen_diff: 0,
hollow_neighbours: 0, hollow_neighbours: 0,
key_color: Default::default(),
keying_action: KeyingAction::Discard,
}, image); }, image);
self.stage = Stage::Reclustering(runner.start()); self.stage = Stage::Reclustering(runner.start());
}, },
@@ -167,4 +196,56 @@ impl ColorImageConverter {
}) as i32 }) as i32
} }
fn color_exists_in_image(img: &ColorImage, color: Color) -> bool {
for y in 0..img.height {
for x in 0..img.width {
let pixel_color = img.get_pixel(x, y);
if pixel_color.r == color.r && pixel_color.g == color.g && pixel_color.b == color.b {
return true
}
}
}
false
}
fn find_unused_color_in_image(img: &ColorImage) -> Result<Color, String> {
let special_colors = IntoIterator::into_iter([
Color::new(255, 0, 0),
Color::new(0, 255, 0),
Color::new(0, 0, 255),
Color::new(255, 255, 0),
Color::new(0, 255, 255),
Color::new(255, 0, 255),
Color::new(128, 128, 128),
]);
for color in special_colors {
if !Self::color_exists_in_image(img, color) {
return Ok(color);
}
}
Err(String::from("unable to find unused color in image to use as key"))
}
fn should_key_image(img: &ColorImage) -> bool {
if img.width == 0 || img.height == 0 {
return false;
}
// Check for transparency at several scanlines
let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize;
let mut num_transparent_pixels = 0;
let y_positions = [0, img.height / 4, img.height / 2, 3 * img.height / 4, img.height - 1];
for y in y_positions {
for x in 0..img.width {
if img.get_pixel(x, y).a == 0 {
num_transparent_pixels += 1;
}
if num_transparent_pixels >= threshold {
return true;
}
}
}
false
}
} }

View File

@@ -1,6 +1,3 @@
mod binary_image; mod binary_image;
mod color_image; mod color_image;
mod util; mod util;
pub use binary_image::*;
pub use color_image::*;