3 Commits

Author SHA1 Message Date
Chris Tsang 97ac767844 0.3.0 2021-01-24 21:31:55 +08:00
Chris Tsang 0b37051d40 Cutout mode 2021-01-24 21:31:08 +08:00
Chris Tsang 2695d7b59b Release (with cutout mode) 2021-01-24 21:17:18 +08:00
12 changed files with 198 additions and 67 deletions
+6
View File
@@ -1,3 +1,9 @@
Version 0.3.0 (2021-01-24)
==========================
- Added cutout mode
Version 0.2.0 (2020-11-15)
==========================
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "vtracer"
version = "0.2.1"
version = "0.3.0"
authors = ["Chris Tsang <tyt2y7@gmail.com>"]
edition = "2018"
description = "A cmd app to convert images into vector graphics."
+36 -3
View File
@@ -9,18 +9,22 @@ pub enum Preset {
Photo
}
#[derive(Debug)]
pub enum ColorMode {
Color,
Binary,
}
pub enum Hierarchical {
Stacked,
Cutout,
}
/// Converter config
#[derive(Debug)]
pub struct Config {
pub input_path: PathBuf,
pub output_path: PathBuf,
pub color_mode: ColorMode,
pub hierarchical: Hierarchical,
pub filter_speckle: usize,
pub color_precision: i32,
pub layer_difference: i32,
@@ -31,11 +35,11 @@ pub struct Config {
pub splice_threshold: i32,
}
#[derive(Debug)]
pub(crate) struct ConverterConfig {
pub input_path: PathBuf,
pub output_path: PathBuf,
pub color_mode: ColorMode,
pub hierarchical: Hierarchical,
pub filter_speckle_area: usize,
pub color_precision_loss: i32,
pub layer_difference: i32,
@@ -52,6 +56,7 @@ impl Default for Config {
input_path: PathBuf::default(),
output_path: PathBuf::default(),
color_mode: ColorMode::Color,
hierarchical: Hierarchical::Stacked,
mode: PathSimplifyMode::Spline,
filter_speckle: 4,
color_precision: 6,
@@ -76,6 +81,18 @@ impl FromStr for ColorMode {
}
}
impl FromStr for Hierarchical {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"stacked" => Ok(Self::Stacked),
"cutout" => Ok(Self::Cutout),
_ => Err(format!("unknown Hierarchical {}", s)),
}
}
}
impl FromStr for Preset {
type Err = String;
@@ -122,6 +139,14 @@ impl Config {
.takes_value(true)
.help("True color image `color` (default) or Binary image `bw`"));
let app = app.arg(Arg::with_name("hierarchical")
.long("hierarchical")
.takes_value(true)
.help(
"Hierarchical clustering `stacked` (default) or non-stacked `cutout`. \
Only applies to color mode. "
));
let app = app.arg(Arg::with_name("preset")
.long("preset")
.takes_value(true)
@@ -187,6 +212,10 @@ impl Config {
config.color_mode = ColorMode::from_str(if value.trim() == "bw" || value.trim() == "BW" {"binary"} else {"color"}).unwrap()
}
if let Some(value) = matches.value_of("hierarchical") {
config.hierarchical = Hierarchical::from_str(value).unwrap()
}
if let Some(value) = matches.value_of("mode") {
let value = value.trim();
config.mode = path_simplify_mode_from_str(if value == "pixel" {
@@ -283,6 +312,7 @@ impl Config {
input_path,
output_path,
color_mode: ColorMode::Binary,
hierarchical: Hierarchical::Stacked,
filter_speckle: 4,
color_precision: 6,
layer_difference: 16,
@@ -296,6 +326,7 @@ impl Config {
input_path,
output_path,
color_mode: ColorMode::Color,
hierarchical: Hierarchical::Stacked,
filter_speckle: 4,
color_precision: 8,
layer_difference: 16,
@@ -309,6 +340,7 @@ impl Config {
input_path,
output_path,
color_mode: ColorMode::Color,
hierarchical: Hierarchical::Stacked,
filter_speckle: 10,
color_precision: 8,
layer_difference: 48,
@@ -326,6 +358,7 @@ impl Config {
input_path: self.input_path,
output_path: self.output_path,
color_mode: self.color_mode,
hierarchical: self.hierarchical,
filter_speckle_area: self.filter_speckle * self.filter_speckle,
color_precision_loss: 8 - self.color_precision,
layer_difference: self.layer_difference,
+22 -2
View File
@@ -3,7 +3,7 @@ use std::{fs::File, io::Write};
use visioncortex::{Color, ColorImage, ColorName};
use visioncortex::color_clusters::{Runner, RunnerConfig, HIERARCHICAL_MAX};
use super::config::{Config, ColorMode, ConverterConfig};
use super::config::{Config, ColorMode, Hierarchical, ConverterConfig};
use super::svg::SvgFile;
/// Convert an image file into svg file
@@ -38,7 +38,27 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
hollow_neighbours: 1,
}, img);
let clusters = runner.run();
let mut clusters = runner.run();
match config.hierarchical {
Hierarchical::Stacked => {}
Hierarchical::Cutout => {
let view = clusters.view();
let image = view.to_color_image();
let runner = Runner::new(RunnerConfig {
diagonal: false,
hierarchical: 64,
batch_size: 25600,
good_min_area: 0,
good_max_area: (image.width * image.height) as usize,
is_same_color_a: 0,
is_same_color_b: 1,
deepen_diff: 0,
hollow_neighbours: 0,
}, image);
clusters = runner.run();
},
}
let view = clusters.view();
+2 -2
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -258,7 +258,7 @@
/******/ promises.push(installedWasmModuleData);
/******/ else {
/******/ var importObject = wasmImportObjects[wasmModuleId]();
/******/ var req = fetch(__webpack_require__.p + "" + {"../pkg/vtracer_webapp_bg.wasm":"a62b3494f02004811a57"}[wasmModuleId] + ".module.wasm");
/******/ var req = fetch(__webpack_require__.p + "" + {"../pkg/vtracer_webapp_bg.wasm":"bad37dda49ff45dee696"}[wasmModuleId] + ".module.wasm");
/******/ var promise;
/******/ if(importObject instanceof Promise && typeof WebAssembly.compileStreaming === 'function') {
/******/ promise = Promise.all([WebAssembly.compileStreaming(req), importObject]).then(function(items) {
+7
View File
@@ -192,6 +192,13 @@
</div>
</div>
<div class="clustering-color-options uk-width-1-1 uk-flex uk-flex-right">
<div class="uk-button-group">
<button id="clustering-cutout" class="uk-button uk-button-default uk-button-small" uk-tooltip="Shapes disjoint with others">Cutout</button>
<button id="clustering-stacked" class="selected uk-button uk-button-default uk-button-small" uk-tooltip="Stack shapes on top of another">Stacked</button>
</div>
</div>
<div class="uk-width-1-1 uk-flex uk-flex-right">
<div uk-tooltip="pos: left; title: Discard patches small than X px in size">
Filter Speckle <span class="uk-text-meta">(Cleaner)</span>
+7
View File
@@ -90,6 +90,13 @@
</div>
</div>
<div>
<div>
<button id="clustering-cutout" title="Shapes disjoint with others">Cutout</button>
<button id="clustering-stacked" title="Stack shapes on top of another">Stacked</button>
</div>
</div>
<div>
<div title="Discard patches small than X px in size">
Filter Speckle <span>(Cleaner)</span>
+82 -58
View File
@@ -7,7 +7,7 @@ const svg = document.getElementById('svg');
const img = new Image();
const progress = document.getElementById('progressbar');
const progressregion = document.getElementById('progressregion');
let mode = 'spline', clustering_mode = 'color';
let mode = 'spline', clustering_mode = 'color', clustering_hierarchical = 'stacked';
// Hide canas and svg on load
canvas.style.display = 'none';
@@ -15,22 +15,22 @@ svg.style.display = 'none';
// Paste from clipboard
document.addEventListener('paste', function (e) {
if (e.clipboardData) {
var items = e.clipboardData.items;
if (!items) return;
if (e.clipboardData) {
var items = e.clipboardData.items;
if (!items) return;
//access data directly
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
//image
var blob = items[i].getAsFile();
var URLObj = window.URL || window.webkitURL;
var source = URLObj.createObjectURL(blob);
setSourceAndRestart(source);
}
}
e.preventDefault();
}
//access data directly
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
//image
var blob = items[i].getAsFile();
var URLObj = window.URL || window.webkitURL;
var source = URLObj.createObjectURL(blob);
setSourceAndRestart(source);
}
}
e.preventDefault();
}
});
// Download as SVG
@@ -49,6 +49,7 @@ var presetConfigs = [
{
src: 'assets/samples/K1_drawing.jpg',
clustering_mode: 'binary',
clustering_hierarchical: 'stacked',
filter_speckle: 4,
color_precision: 6,
layer_difference: 16,
@@ -62,6 +63,7 @@ var presetConfigs = [
{
src: 'assets/samples/Cityscape Sunset_DFM3-01.jpg',
clustering_mode: 'color',
clustering_hierarchical: 'stacked',
filter_speckle: 4,
color_precision: 8,
layer_difference: 25,
@@ -75,6 +77,7 @@ var presetConfigs = [
{
src: 'assets/samples/Gum Tree Vector.jpg',
clustering_mode: 'color',
clustering_hierarchical: 'cutout',
filter_speckle: 4,
color_precision: 8,
layer_difference: 28,
@@ -88,6 +91,7 @@ var presetConfigs = [
{
src: 'assets/samples/vectorstock_31191940.png',
clustering_mode: 'color',
clustering_hierarchical: 'stacked',
filter_speckle: 8,
color_precision: 7,
layer_difference: 64,
@@ -101,6 +105,7 @@ var presetConfigs = [
{
src: 'assets/samples/angel-luciano-LATYeZyw88c-unsplash-s.jpg',
clustering_mode: 'color',
clustering_hierarchical: 'stacked',
filter_speckle: 10,
color_precision: 8,
layer_difference: 48,
@@ -114,6 +119,7 @@ var presetConfigs = [
{
src: 'assets/samples/tank-unit-preview.png',
clustering_mode: 'color',
clustering_hierarchical: 'stacked',
filter_speckle: 0,
color_precision: 8,
layer_difference: 0,
@@ -127,63 +133,66 @@ var presetConfigs = [
];
// Insert gallery items dynamically
for (let i = 0; i < presetConfigs.length; i++) {
document.getElementById('galleryslider').innerHTML +=
`<li>
<div class="galleryitem uk-panel uk-flex uk-flex-center">
<a href="#">
<img src="${presetConfigs[i].src}" title="${presetConfigs[i].source}">
</a>
</div>
</li>`;
document.getElementById('credits-modal-content').innerHTML +=
`<p>${presetConfigs[i].credit}</p>`;
if (document.getElementById('galleryslider')) {
for (let i = 0; i < presetConfigs.length; i++) {
document.getElementById('galleryslider').innerHTML +=
`<li>
<div class="galleryitem uk-panel uk-flex uk-flex-center">
<a href="#">
<img src="${presetConfigs[i].src}" title="${presetConfigs[i].source}">
</a>
</div>
</li>`;
document.getElementById('credits-modal-content').innerHTML +=
`<p>${presetConfigs[i].credit}</p>`;
}
}
// Function to load a given config WITHOUT restarting
function loadConfig(config) {
mode = config.mode;
clustering_mode = config.clustering_mode;
mode = config.mode;
clustering_mode = config.clustering_mode;
clustering_hierarchical = config.clustering_hierarchical;
globalcorner = config.corner_threshold;
document.getElementById('cornervalue').innerHTML = globalcorner;
document.getElementById('corner').value = globalcorner;
globallength = config.length_threshold;
document.getElementById('lengthvalue').innerHTML = globallength;
document.getElementById('length').value = globallength;
globalsplice = config.splice_threshold;
document.getElementById('splicevalue').innerHTML = globalsplice;
document.getElementById('splice').value = globalsplice;
globalcorner = config.corner_threshold;
document.getElementById('cornervalue').innerHTML = globalcorner;
document.getElementById('corner').value = globalcorner;
globallength = config.length_threshold;
document.getElementById('lengthvalue').innerHTML = globallength;
document.getElementById('length').value = globallength;
globalsplice = config.splice_threshold;
document.getElementById('splicevalue').innerHTML = globalsplice;
document.getElementById('splice').value = globalsplice;
globalfilterspeckle = config.filter_speckle;
document.getElementById('filterspecklevalue').innerHTML = globalfilterspeckle;
document.getElementById('filterspeckle').value = globalfilterspeckle;
globalfilterspeckle = config.filter_speckle;
document.getElementById('filterspecklevalue').innerHTML = globalfilterspeckle;
document.getElementById('filterspeckle').value = globalfilterspeckle;
globalcolorprecision = config.color_precision;
document.getElementById('colorprecisionvalue').innerHTML = globalcolorprecision;
document.getElementById('colorprecision').value = globalcolorprecision;
globalcolorprecision = config.color_precision;
document.getElementById('colorprecisionvalue').innerHTML = globalcolorprecision;
document.getElementById('colorprecision').value = globalcolorprecision;
globallayerdifference = config.layer_difference;
document.getElementById('layerdifferencevalue').innerHTML = globallayerdifference;
document.getElementById('layerdifference').value = globallayerdifference;
globallayerdifference = config.layer_difference;
document.getElementById('layerdifferencevalue').innerHTML = globallayerdifference;
document.getElementById('layerdifference').value = globallayerdifference;
}
// Choose template from gallery
let chooseGalleryButtons = document.querySelectorAll('.galleryitem a');
chooseGalleryButtons.forEach(item => {
item.addEventListener('click', function (e) {
// Load preset template config
let i = Array.prototype.indexOf.call(chooseGalleryButtons, item);
if (presetConfigs.length > i) {
loadConfig(presetConfigs[i]);
}
item.addEventListener('click', function (e) {
// Load preset template config
let i = Array.prototype.indexOf.call(chooseGalleryButtons, item);
if (presetConfigs.length > i) {
loadConfig(presetConfigs[i]);
}
// Set source as specified
setSourceAndRestart(this.firstElementChild.src);
});
// Set source as specified
setSourceAndRestart(this.firstElementChild.src);
});
});
// Upload button
@@ -272,6 +281,16 @@ document.getElementById('clustering-color').addEventListener('click', function (
restart();
}, false);
document.getElementById('clustering-cutout').addEventListener('click', function (e) {
clustering_hierarchical = 'cutout';
restart();
}, false);
document.getElementById('clustering-stacked').addEventListener('click', function (e) {
clustering_hierarchical = 'stacked';
restart();
}, false);
document.getElementById('filterspeckle').addEventListener('change', function (e) {
globalfilterspeckle = parseInt(this.value);
document.getElementById('filterspecklevalue').innerHTML = this.value;
@@ -352,6 +371,10 @@ function restart() {
el.style.display = clustering_mode == 'color' ? '' : 'none';
});
document.getElementById('clustering-cutout').classList.remove('selected');
document.getElementById('clustering-stacked').classList.remove('selected');
document.getElementById('clustering-' + clustering_hierarchical).classList.add('selected');
document.getElementById('none').classList.remove('selected');
document.getElementById('polygon').classList.remove('selected');
document.getElementById('spline').classList.remove('selected');
@@ -373,6 +396,7 @@ function restart() {
'svg_id': svg.id,
'mode': mode,
'clustering_mode': clustering_mode,
'hierarchical': clustering_hierarchical,
'corner_threshold': deg2rad(globalcorner),
'length_threshold': globallength,
'max_iterations': 10,
+34
View File
@@ -13,6 +13,7 @@ pub struct ColorImageConverterParams {
pub canvas_id: String,
pub svg_id: String,
pub mode: String,
pub hierarchical: String,
pub corner_threshold: f64,
pub length_threshold: f64,
pub max_iterations: usize,
@@ -35,6 +36,7 @@ pub struct ColorImageConverter {
pub enum Stage {
New,
Clustering(IncrementalBuilder),
Reclustering(IncrementalBuilder),
Vectorize(Clusters),
}
@@ -86,6 +88,35 @@ impl ColorImageConverter {
},
Stage::Clustering(builder) => {
self.canvas.log("Clustering tick");
if builder.tick() {
match self.params.hierarchical.as_str() {
"stacked" => {
self.stage = Stage::Vectorize(builder.result());
},
"cutout" => {
let clusters = builder.result();
let view = clusters.view();
let image = view.to_color_image();
let runner = Runner::new(RunnerConfig {
diagonal: false,
hierarchical: 64,
batch_size: 25600,
good_min_area: 0,
good_max_area: (image.width * image.height) as usize,
is_same_color_a: 0,
is_same_color_b: 1,
deepen_diff: 0,
hollow_neighbours: 0,
}, image);
self.stage = Stage::Reclustering(runner.start());
},
_ => panic!("unknown hierarchical `{}`", self.params.hierarchical)
}
}
false
},
Stage::Reclustering(builder) => {
self.canvas.log("Reclustering tick");
if builder.tick() {
self.stage = Stage::Vectorize(builder.result())
}
@@ -125,6 +156,9 @@ impl ColorImageConverter {
Stage::Clustering(builder) => {
builder.progress() / 2
},
Stage::Reclustering(_builder) => {
50
},
Stage::Vectorize(clusters) => {
50 + 50 * self.counter as u32 / clusters.view().clusters_output.len() as u32
}