9 Commits

Author SHA1 Message Date
Chris Tsang c2a5626afa 0.4.0 2021-07-23 23:35:54 +08:00
Bobby Ng d0593e716a SVG path string numeric precision 2021-07-23 23:35:45 +08:00
Chris Tsang 9ce7df176a Update .gitattributes 2021-07-23 18:29:12 +08:00
Chris Tsang 5928c0a7f5 Update screenshots 2021-03-01 21:13:24 +08:00
Chris Tsang b2c95a50cd Update README.md 2021-03-01 21:13:24 +08:00
Chris Tsang bcd3ffb40e Update COPYRIGHT 2021-02-07 14:22:26 +08:00
Chris Tsang 20da3efd3c Create .gitattributes 2021-01-25 00:57:44 +08:00
Chris Tsang 0c27d1f06a Update README.md 2021-01-25 00:53:01 +08:00
Chris Tsang 0c7c7fc808 Demo fixup 2021-01-24 22:12:01 +08:00
16 changed files with 108 additions and 46 deletions
+1
View File
@@ -0,0 +1 @@
docs/** linguist-generated
+30 -26
View File
@@ -26,7 +26,7 @@ Comparing to [Potrace]() which only accept binarized inputs (Black & White pixma
Comparing to Adobe Illustrator's Live Trace, VTracer's output is much more compact (less shapes) as we adopt a stacking strategy and avoid producing shapes with holes. Comparing to Adobe Illustrator's Live Trace, VTracer's output is much more compact (less shapes) as we adopt a stacking strategy and avoid producing shapes with holes.
VTracer is originally designed for processing high resolution scans of historic blueprints up to gigapixels. By setting both `filter_speckle` and `layer_difference` to `0`, VTracer can also handle low resolution pixel art, effectively achieving `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](//www.visioncortex.org/vtracer-docs). A technical description of the algorithm is on [visioncortex.org/vtracer-docs](//www.visioncortex.org/vtracer-docs).
@@ -39,31 +39,35 @@ VTracer and its [core library](//github.com/visioncortex/visioncortex) is implem
![screenshot](docs/images/screenshot-02.png) ![screenshot](docs/images/screenshot-02.png)
## Command Line ## Command Line
```
visioncortex VTracer ```sh
A cmd app to convert images into vector graphics. visioncortex VTracer 0.3.0
A cmd app to convert images into vector graphics.
USAGE:
vtracer [OPTIONS] --input <input> --output <output> USAGE:
vtracer [OPTIONS] --input <input> --output <output>
FLAGS:
-h, --help Prints help information FLAGS:
-V, --version Prints version information -h, --help Prints help information
-V, --version Prints version information
OPTIONS:
--colormode <color_mode> True color image `color` (default) or Binary image `bw` OPTIONS:
-p, --color_precision <color_precision> Number of significant bits to use in an RGB channel --colormode <color_mode> True color image `color` (default) or Binary image `bw`
-c, --corner_threshold <corner_threshold> Minimum momentary angle (degree) to be considered a corner -p, --color_precision <color_precision> Number of significant bits to use in an RGB channel
-f, --filter_speckle <filter_speckle> Discard patches smaller than X px in size -c, --corner_threshold <corner_threshold> Minimum momentary angle (degree) to be considered a corner
-g, --gradient_step <gradient_step> Color difference between gradient layers -f, --filter_speckle <filter_speckle> Discard patches smaller than X px in size
-i, --input <input> Path to input raster image -g, --gradient_step <gradient_step> Color difference between gradient layers
-m, --mode <mode> Curver fitting mode `pixel`, `polygon`, `spline` --hierarchical <hierarchical>
-o, --output <output> Path to output vector graphics Hierarchical clustering `stacked` (default) or non-stacked `cutout`. Only applies to color mode.
--preset <preset> Use one of the preset configs `bw`, `poster`, `photo`
-l, --segment_length <segment_length> -i, --input <input> Path to input raster image
Perform iterative subdivide smooth until all segments are shorter than this length -m, --mode <mode> Curver fitting mode `pixel`, `polygon`, `spline`
-o, --output <output> Path to output vector graphics
-s, --splice_threshold <splice_threshold> Minimum angle displacement (degree) to splice a spline --preset <preset> Use one of the preset configs `bw`, `poster`, `photo`
-l, --segment_length <segment_length>
Perform iterative subdivide smooth until all segments are shorter than this length
-s, --splice_threshold <splice_threshold> Minimum angle displacement (degree) to splice a spline
``` ```
### Usage ### Usage
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "vtracer" name = "vtracer"
version = "0.3.0" version = "0.4.0"
authors = ["Chris Tsang <tyt2y7@gmail.com>"] authors = ["Chris Tsang <tyt2y7@gmail.com>"]
edition = "2018" edition = "2018"
description = "A cmd app to convert images into vector graphics." description = "A cmd app to convert images into vector graphics."
@@ -13,4 +13,4 @@ keywords = ["svg", "computer-graphics"]
[dependencies] [dependencies]
clap = "2.33.3" clap = "2.33.3"
image = "0.23.10" image = "0.23.10"
visioncortex = "0.4.0" visioncortex = "0.6.0"
+28 -7
View File
@@ -33,6 +33,7 @@ pub struct Config {
pub length_threshold: f64, pub length_threshold: f64,
pub max_iterations: usize, pub max_iterations: usize,
pub splice_threshold: i32, pub splice_threshold: i32,
pub path_precision: Option<u32>,
} }
pub(crate) struct ConverterConfig { pub(crate) struct ConverterConfig {
@@ -48,6 +49,7 @@ pub(crate) struct ConverterConfig {
pub length_threshold: f64, pub length_threshold: f64,
pub max_iterations: usize, pub max_iterations: usize,
pub splice_threshold: f64, pub splice_threshold: f64,
pub path_precision: Option<u32>,
} }
impl Default for Config { impl Default for Config {
@@ -65,6 +67,7 @@ impl Default for Config {
length_threshold: 4.0, length_threshold: 4.0,
splice_threshold: 45, splice_threshold: 45,
max_iterations: 10, max_iterations: 10,
path_precision: Some(8),
} }
} }
} }
@@ -194,6 +197,11 @@ impl Config {
.takes_value(true) .takes_value(true)
.help("Curver fitting mode `pixel`, `polygon`, `spline`")); .help("Curver fitting mode `pixel`, `polygon`, `spline`"));
let app = app.arg(Arg::with_name("path_precision")
.long("path_precision")
.takes_value(true)
.help("Number of deciaml places to use in path string"));
// Extract matches // Extract matches
let matches = app.get_matches(); let matches = app.get_matches();
@@ -225,7 +233,7 @@ impl Config {
} else if value == "spline" { } else if value == "spline" {
"spline" "spline"
} else { } else {
panic!("Parser Error: Curve fitting mode is invalid with value {}", value); panic!("Parser Error: Curve fitting mode is invalid: {}", value);
}); });
} }
@@ -237,7 +245,7 @@ impl Config {
} }
config.filter_speckle = value; config.filter_speckle = value;
} else { } else {
panic!("Parser Error: Filter speckle is not a positive integer with value {}.", value); panic!("Parser Error: Filter speckle is not a positive integer: {}.", value);
} }
} }
@@ -249,7 +257,7 @@ impl Config {
} }
config.color_precision = value; config.color_precision = value;
} else { } else {
panic!("Parser Error: Color precision is not an integer with value {}.", value); panic!("Parser Error: Color precision is not an integer: {}.", value);
} }
} }
@@ -261,7 +269,7 @@ impl Config {
} }
config.layer_difference = value; config.layer_difference = value;
} else { } else {
panic!("Parser Error: Gradient step is not an integer with value {}.", value); panic!("Parser Error: Gradient step is not an integer: {}.", value);
} }
} }
@@ -273,7 +281,7 @@ impl Config {
} }
config.corner_threshold = value config.corner_threshold = value
} else { } else {
panic!("Parser Error: Corner threshold is not numeric with value {}.", value); panic!("Parser Error: Corner threshold is not numeric: {}.", value);
} }
} }
@@ -285,7 +293,7 @@ impl Config {
} }
config.length_threshold = value; config.length_threshold = value;
} else { } else {
panic!("Parser Error: Segment length is not numeric with value {}.", value); panic!("Parser Error: Segment length is not numeric: {}.", value);
} }
} }
@@ -297,7 +305,16 @@ impl Config {
} }
config.splice_threshold = value; config.splice_threshold = value;
} else { } else {
panic!("Parser Error: Segment length is not numeric with value {}.", value); panic!("Parser Error: Segment length is not numeric: {}.", value);
}
}
if let Some(value) = matches.value_of("path_precision") {
if value.trim().parse::<u32>().is_ok() { // is numeric
let value = value.trim().parse::<u32>().ok();
config.path_precision = value;
} else {
panic!("Parser Error: Path precision is not an unsigned integer: {}.", value);
} }
} }
@@ -321,6 +338,7 @@ impl Config {
length_threshold: 4.0, length_threshold: 4.0,
max_iterations: 10, max_iterations: 10,
splice_threshold: 45, splice_threshold: 45,
path_precision: Some(8),
}, },
Preset::Poster => Self { Preset::Poster => Self {
input_path, input_path,
@@ -335,6 +353,7 @@ impl Config {
length_threshold: 4.0, length_threshold: 4.0,
max_iterations: 10, max_iterations: 10,
splice_threshold: 45, splice_threshold: 45,
path_precision: Some(8),
}, },
Preset::Photo => Self { Preset::Photo => Self {
input_path, input_path,
@@ -349,6 +368,7 @@ impl Config {
length_threshold: 4.0, length_threshold: 4.0,
max_iterations: 10, max_iterations: 10,
splice_threshold: 45, splice_threshold: 45,
path_precision: Some(8),
} }
} }
} }
@@ -367,6 +387,7 @@ impl Config {
length_threshold: self.length_threshold, length_threshold: self.length_threshold,
max_iterations: self.max_iterations, max_iterations: self.max_iterations,
splice_threshold: deg2rad(self.splice_threshold), splice_threshold: deg2rad(self.splice_threshold),
path_precision: self.path_precision,
} }
} }
} }
+2 -2
View File
@@ -74,7 +74,7 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
config.max_iterations, config.max_iterations,
config.splice_threshold config.splice_threshold
); );
svg.add_path(paths, cluster.residue_color()); svg.add_path(paths, cluster.residue_color(), config.path_precision);
} }
write_svg(svg, config.output_path) write_svg(svg, config.output_path)
@@ -106,7 +106,7 @@ fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
config.max_iterations, config.max_iterations,
config.splice_threshold, config.splice_threshold,
); );
svg.add_path(paths, Color::color(&ColorName::Black)); svg.add_path(paths, Color::color(&ColorName::Black), config.path_precision);
} }
} }
+4 -2
View File
@@ -10,6 +10,7 @@ pub struct SvgFile {
pub struct SvgPath { pub struct SvgPath {
pub path: CompoundPath, pub path: CompoundPath,
pub color: Color, pub color: Color,
pub path_precision: Option<u32>,
} }
impl SvgFile { impl SvgFile {
@@ -21,10 +22,11 @@ impl SvgFile {
} }
} }
pub fn add_path(&mut self, path: CompoundPath, color: Color) { pub fn add_path(&mut self, path: CompoundPath, color: Color, path_precision: Option<u32>) {
self.paths.push(SvgPath { self.paths.push(SvgPath {
path, path,
color, color,
path_precision,
}) })
} }
} }
@@ -47,7 +49,7 @@ impl fmt::Display for SvgFile {
impl fmt::Display for SvgPath { impl fmt::Display for SvgPath {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let (string, offset) = self.path.to_svg_string(true, PointF64::default()); let (string, offset) = self.path.to_svg_string(true, PointF64::default(), self.path_precision);
writeln!( writeln!(
f, "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>", f, "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>",
string, self.color.to_hex_string(), string, self.color.to_hex_string(),
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
Copyright (c) 2020 Tsang Hao Fung Copyright (c) 2020 Tsang Hao Fung
All Rights Reserved The content in the /docs directory is for GitHub pages, and is not covered under open source licenses.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 154 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 243 KiB

+1 -1
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.4.0" visioncortex = "0.6.0"
# 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
+13
View File
@@ -185,6 +185,19 @@
<input id="splice" type="range" min="0" max="180" step="1" value="45"> <input id="splice" type="range" min="0" max="180" step="1" value="45">
</div> </div>
<div style="display: none">
<div class="spline-options">
<div title="Number of decimal places used in svg path string">
Path Precision <span>(More digits)</span>
</div>
</div>
<div id="pathprecisionvalue" class="spline-options">
8
</div>
<div class="spline-options">
<input id="pathprecision" class="uk-range" type="range" min="0" max="16" step="1" value="8">
</div>
</div>
</div> </div>
</div> </div>
<script src="./bootstrap.js"></script> <script src="./bootstrap.js"></script>
+19 -2
View File
@@ -52,6 +52,7 @@ var presetConfigs = [
clustering_hierarchical: 'stacked', clustering_hierarchical: 'stacked',
filter_speckle: 4, filter_speckle: 4,
color_precision: 6, color_precision: 6,
path_precision: 8,
layer_difference: 16, layer_difference: 16,
mode: 'spline', mode: 'spline',
corner_threshold: 60, corner_threshold: 60,
@@ -66,6 +67,7 @@ var presetConfigs = [
clustering_hierarchical: 'stacked', clustering_hierarchical: 'stacked',
filter_speckle: 4, filter_speckle: 4,
color_precision: 8, color_precision: 8,
path_precision: 8,
layer_difference: 25, layer_difference: 25,
mode: 'spline', mode: 'spline',
corner_threshold: 60, corner_threshold: 60,
@@ -77,9 +79,10 @@ var presetConfigs = [
{ {
src: 'assets/samples/Gum Tree Vector.jpg', src: 'assets/samples/Gum Tree Vector.jpg',
clustering_mode: 'color', clustering_mode: 'color',
clustering_hierarchical: 'cutout', clustering_hierarchical: 'stacked',
filter_speckle: 4, filter_speckle: 4,
color_precision: 8, color_precision: 8,
path_precision: 8,
layer_difference: 28, layer_difference: 28,
mode: 'spline', mode: 'spline',
corner_threshold: 60, corner_threshold: 60,
@@ -94,6 +97,7 @@ var presetConfigs = [
clustering_hierarchical: 'stacked', clustering_hierarchical: 'stacked',
filter_speckle: 8, filter_speckle: 8,
color_precision: 7, color_precision: 7,
path_precision: 8,
layer_difference: 64, layer_difference: 64,
mode: 'spline', mode: 'spline',
corner_threshold: 60, corner_threshold: 60,
@@ -108,6 +112,7 @@ var presetConfigs = [
clustering_hierarchical: 'stacked', clustering_hierarchical: 'stacked',
filter_speckle: 10, filter_speckle: 10,
color_precision: 8, color_precision: 8,
path_precision: 8,
layer_difference: 48, layer_difference: 48,
mode: 'spline', mode: 'spline',
corner_threshold: 180, corner_threshold: 180,
@@ -122,6 +127,7 @@ var presetConfigs = [
clustering_hierarchical: 'stacked', clustering_hierarchical: 'stacked',
filter_speckle: 0, filter_speckle: 0,
color_precision: 8, color_precision: 8,
path_precision: 8,
layer_difference: 0, layer_difference: 0,
mode: 'none', mode: 'none',
corner_threshold: 180, corner_threshold: 180,
@@ -178,6 +184,9 @@ function loadConfig(config) {
document.getElementById('layerdifferencevalue').innerHTML = globallayerdifference; document.getElementById('layerdifferencevalue').innerHTML = globallayerdifference;
document.getElementById('layerdifference').value = globallayerdifference; document.getElementById('layerdifference').value = globallayerdifference;
globalpathprecision = config.path_precision;
document.getElementById('pathprecisionvalue').innerHTML = globalpathprecision;
document.getElementById('pathprecision').value = globalpathprecision;
} }
// Choose template from gallery // Choose template from gallery
@@ -244,7 +253,8 @@ var globalcorner = parseInt(document.getElementById('corner').value),
globalsplice = parseInt(document.getElementById('splice').value), globalsplice = parseInt(document.getElementById('splice').value),
globalfilterspeckle = parseInt(document.getElementById('filterspeckle').value), globalfilterspeckle = parseInt(document.getElementById('filterspeckle').value),
globalcolorprecision = parseInt(document.getElementById('colorprecision').value), globalcolorprecision = parseInt(document.getElementById('colorprecision').value),
globallayerdifference = parseInt(document.getElementById('layerdifference').value); globallayerdifference = parseInt(document.getElementById('layerdifference').value),
globalpathprecision = parseInt(document.getElementById('pathprecision').value);
// Load past inputs from localStorage // Load past inputs from localStorage
/* /*
@@ -327,6 +337,12 @@ document.getElementById('splice').addEventListener('change', function (e) {
restart(); restart();
}); });
document.getElementById('pathprecision').addEventListener('change', function (e) {
globalpathprecision = parseInt(this.value);
document.getElementById('pathprecisionvalue').innerHTML = this.value;
restart();
});
// Save inputs before unloading // Save inputs before unloading
/* /*
window.addEventListener('beforeunload', function () { window.addEventListener('beforeunload', function () {
@@ -404,6 +420,7 @@ function restart() {
'filter_speckle': globalfilterspeckle*globalfilterspeckle, 'filter_speckle': globalfilterspeckle*globalfilterspeckle,
'color_precision': 8-globalcolorprecision, 'color_precision': 8-globalcolorprecision,
'layer_difference': globallayerdifference, 'layer_difference': globallayerdifference,
'path_precision': globalpathprecision,
}); });
if (runner) { if (runner) {
runner.stop(); runner.stop();
+2
View File
@@ -17,6 +17,7 @@ pub struct BinaryImageConverterParams {
pub max_iterations: usize, pub max_iterations: usize,
pub splice_threshold: f64, pub splice_threshold: f64,
pub filter_speckle: usize, pub filter_speckle: usize,
pub path_precision: u32,
} }
#[wasm_bindgen] #[wasm_bindgen]
@@ -80,6 +81,7 @@ impl BinaryImageConverter {
self.svg.prepend_path( self.svg.prepend_path(
&paths, &paths,
&color, &color,
Some(self.params.path_precision),
); );
} }
self.counter += 1; self.counter += 1;
+2
View File
@@ -21,6 +21,7 @@ pub struct ColorImageConverterParams {
pub filter_speckle: usize, pub filter_speckle: usize,
pub color_precision: i32, pub color_precision: i32,
pub layer_difference: i32, pub layer_difference: i32,
pub path_precision: u32,
} }
#[wasm_bindgen] #[wasm_bindgen]
@@ -137,6 +138,7 @@ impl ColorImageConverter {
self.svg.prepend_path( self.svg.prepend_path(
&paths, &paths,
&cluster.residue_color(), &cluster.residue_color(),
Some(self.params.path_precision),
); );
self.counter += 1; self.counter += 1;
false false
+2 -2
View File
@@ -13,11 +13,11 @@ impl Svg {
Self { element } Self { element }
} }
pub fn prepend_path(&mut self, paths: &CompoundPath, color: &Color) { pub fn prepend_path(&mut self, paths: &CompoundPath, color: &Color, precision: Option<u32>) {
let path = document() let path = document()
.create_element_ns(Some("http://www.w3.org/2000/svg"), "path") .create_element_ns(Some("http://www.w3.org/2000/svg"), "path")
.unwrap(); .unwrap();
let (string, offset) = paths.to_svg_string(true, PointF64::default()); let (string, offset) = paths.to_svg_string(true, PointF64::default(), precision);
path.set_attribute("d", &string).unwrap(); path.set_attribute("d", &string).unwrap();
path.set_attribute( path.set_attribute(
"transform", "transform",