mirror of
https://github.com/visioncortex/vtracer.git
synced 2025-12-06 17:15:41 -08:00
Compare commits
32 Commits
0.6.1
...
8889cbc7ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8889cbc7ea | ||
|
|
6b379a02ef | ||
|
|
2635d5b874 | ||
|
|
f6cf3e8705 | ||
|
|
36b16de17a | ||
|
|
7887c1ebf8 | ||
|
|
6fdfec8610 | ||
|
|
1aff9a300a | ||
|
|
ac0a89e08a | ||
|
|
b09f71a2b5 | ||
|
|
e4897dfe99 | ||
|
|
4544ca740d | ||
|
|
3d92586e33 | ||
|
|
725adf5364 | ||
|
|
05f82c7bb5 | ||
|
|
b7ac336b6d | ||
|
|
c03a8ffced | ||
|
|
3223ba56ec | ||
|
|
ddb47e1ad4 | ||
|
|
177797108d | ||
|
|
370083f818 | ||
|
|
c3012c6aef | ||
|
|
2774fc06c9 | ||
|
|
fa7d021055 | ||
|
|
cc43924601 | ||
|
|
9dbbd100df | ||
|
|
37a2570f49 | ||
|
|
b2cd1a9524 | ||
|
|
ac93bd4a51 | ||
|
|
e62f071b34 | ||
|
|
884092a5b9 | ||
|
|
5f5a6c6648 |
2
.github/workflows/python.yml
vendored
2
.github/workflows/python.yml
vendored
@@ -3,7 +3,7 @@
|
||||
#
|
||||
# maturin generate-ci github
|
||||
#
|
||||
name: python
|
||||
name: Python
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
30
.github/workflows/release.yml
vendored
Normal file
30
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: aarch64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
- target: x86_64-unknown-linux-musl
|
||||
os: ubuntu-latest
|
||||
- target: aarch64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: x86_64-apple-darwin
|
||||
os: macos-latest
|
||||
- target: x86_64-pc-windows-msvc
|
||||
os: windows-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/upload-rust-binary-action@v1
|
||||
with:
|
||||
bin: vtracer
|
||||
target: ${{ matrix.target }}
|
||||
# (required) GitHub token for uploading assets to GitHub Releases.
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
19
.github/workflows/rust.yml
vendored
19
.github/workflows/rust.yml
vendored
@@ -1,10 +1,23 @@
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
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:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 0.6.4 - 2024-03-29
|
||||
|
||||
* Update `visioncortex` version to `0.8.8`
|
||||
|
||||
## 0.6.3 - 2023-11-21
|
||||
|
||||
* New converter API https://github.com/visioncortex/vtracer/pull/59
|
||||
|
||||
## 0.6.1 - 2023-09-23
|
||||
|
||||
* Fixed "The two lines are parallel!"
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2022 Tsang Hao Fung
|
||||
Copyright (c) 2024 TSANG, Hao Fung
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
||||
48
README.md
48
README.md
@@ -10,9 +10,9 @@
|
||||
<h3>
|
||||
<a href="https://www.visioncortex.org/vtracer-docs">Article</a>
|
||||
<span> | </span>
|
||||
<a href="https://www.visioncortex.org/vtracer/">Demo</a>
|
||||
<a href="https://www.visioncortex.org/vtracer/">Web App</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/visioncortex/vtracer/releases/latest">Download</a>
|
||||
<a href="https://github.com/visioncortex/vtracer/releases">Download</a>
|
||||
</h3>
|
||||
|
||||
<sub>Built with 🦀 by <a href="https://www.visioncortex.org/">The Vision Cortex Research Group</a></sub>
|
||||
@@ -22,13 +22,13 @@
|
||||
|
||||
visioncortex VTracer is an open source software to convert raster images (like jpg & png) into vector graphics (svg). It can vectorize graphics and photographs and trace the curves to output compact vector files.
|
||||
|
||||
Comparing to [Potrace](http://potrace.sourceforge.net/) which only accept binarized inputs (Black & White pixmap), VTracer has an image processing pipeline which can handle colored high resolution scans.
|
||||
Comparing to [Potrace](http://potrace.sourceforge.net/) which only accept binarized inputs (Black & White pixmap), VTracer has an image processing pipeline which can handle colored high resolution scans. tl;dr: Potrace uses a `O(n^2)` fitting algorithm, whereas `vtracer` is entirely `O(n)`.
|
||||
|
||||
Comparing to Adobe Illustrator's [Image Trace](https://helpx.adobe.com/illustrator/using/image-trace.html), 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. 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
|
||||
|
||||
@@ -71,7 +71,7 @@ OPTIONS:
|
||||
-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).
|
||||
|
||||
@@ -81,13 +81,15 @@ You can also install the program from source from [crates.io/vtracer](https://cr
|
||||
cargo install vtracer
|
||||
```
|
||||
|
||||
> You are strongly advised to not download from any other third-party sources
|
||||
|
||||
### Usage
|
||||
|
||||
```sh
|
||||
./vtracer --input input.jpg --output output.svg
|
||||
```
|
||||
|
||||
## Rust Library
|
||||
### 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
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -105,7 +107,7 @@ pip install vtracer
|
||||
|
||||
## 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>
|
||||
<tbody>
|
||||
@@ -118,14 +120,36 @@ VTracer is used by the following projects (feel free to add yours!):
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## Citations
|
||||
|
||||
VTracer has since been cited in a few academic papers. Please kindly let us know if you have cited our work:
|
||||
|
||||
+ [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)
|
||||
+ [Image Vectorization: a Review](https://arxiv.org/pdf/2306.06441.pdf)
|
||||
+ [StarVector: Generating Scalable Vector Graphics Code from Images](https://arxiv.org/abs/2312.11556)
|
||||
|
||||
## How did VTracer come about?
|
||||
|
||||
> The following content is an excerpt from my [unpublished memoir](https://github.com/visioncortex/memoir).
|
||||
|
||||
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.
|
||||
|
||||
The plan was to fully develop the vectorizer: to handle color images and output splines. I recruited a very talented intern, [@shpun817](https://github.com/shpun817), to work on VTracer. I grafted the frontend of the video processing engine - the ["The Clustering Algorithm"](https://www.visioncortex.org/impression-docs#the-clustering-algorithm) as the pre-processor.
|
||||
|
||||
Three months later, we published the first version on Reddit. Out of my surprise, the response of such an underwhelming project was overwhelming.
|
||||
|
||||
## What's next?
|
||||
|
||||
There are several things in my mind:
|
||||
|
||||
1. Pencil tracing. Instead of tracing shapes as closed paths, may be we can attempt to skeletonize the shapes as open paths. The output would be clean, fixed width strokes.
|
||||
1. Path simplification. Implement a post-process filter to the output paths to further reduce the number of splines.
|
||||
|
||||
2. Perfect cut-out mode. Right now in cut-out mode, the shapes do not share perfect boundaries, but have seams.
|
||||
2. Perfect cut-out mode. Right now in cut-out mode, the shapes do not share boundaries perfectly, but have seams.
|
||||
|
||||
3. Image cleaning. Right now the tracer works best on losslessly compressed pngs. If an image suffered from jpeg noises, it could impact the tracing quality. We might be able to develop a pre-filtering pass that denoises the input.
|
||||
3. Pencil tracing. Instead of tracing shapes as closed paths, may be we can attempt to skeletonize the shapes as open paths. The output would be clean, fixed width strokes.
|
||||
|
||||
If you are interested in working on them or willing to sponsor its development, feel free to get in touch.
|
||||
4. Image cleaning. Right now the tracer works best on losslessly compressed pngs. If an image suffered from jpeg noises, it could impact the tracing quality. We might be able to develop a pre-filtering pass that denoises the input.
|
||||
|
||||
If you are interested in working on them or willing to sponsor its development, feel free to get in touch.
|
||||
|
||||
27
RELEASES.md
27
RELEASES.md
@@ -1,27 +0,0 @@
|
||||
# Version 0.6.0 (2023-09-08)
|
||||
|
||||
- Python bindings
|
||||
|
||||
# Version 0.5.0 (2022-10-09)
|
||||
|
||||
- Handle transparent png images
|
||||
|
||||
# Version 0.4.0 (2021-07-23)
|
||||
|
||||
- SVG path string numeric precision
|
||||
|
||||
# Version 0.3.0 (2021-01-24)
|
||||
|
||||
- Added cutout mode
|
||||
|
||||
# Version 0.2.0 (2020-11-15)
|
||||
|
||||
- Use relative & closed paths
|
||||
|
||||
# Version 0.1.1 (2020-11-01)
|
||||
|
||||
- SVG namespace
|
||||
|
||||
# Version 0.1.0 (2020-10-31)
|
||||
|
||||
- Initial release
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "vtracer"
|
||||
version = "0.6.1"
|
||||
version = "0.6.4"
|
||||
authors = ["Chris Tsang <chris.2y3@outlook.com>"]
|
||||
edition = "2021"
|
||||
description = "A cmd app to convert images into vector graphics."
|
||||
@@ -13,7 +13,7 @@ keywords = ["svg", "computer-graphics"]
|
||||
[dependencies]
|
||||
clap = "2.33.3"
|
||||
image = "0.23.10"
|
||||
visioncortex = { version = "0.8.1" }
|
||||
visioncortex = { version = "0.8.8" }
|
||||
fastrand = "1.8"
|
||||
pyo3 = { version = "0.19.0", optional = true }
|
||||
|
||||
@@ -22,4 +22,4 @@ python-binding = ["pyo3"]
|
||||
|
||||
[lib]
|
||||
name = "vtracer"
|
||||
crate-type = ["cdylib"]
|
||||
crate-type = ["rlib", "cdylib"]
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2022 Tsang Hao Fung
|
||||
Copyright (c) 2023 Tsang Hao Fung
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "vtracer"
|
||||
version = "0.6.10"
|
||||
version = "0.6.11"
|
||||
description = "Python bindings for the Rust Vtracer raster-to-vector library"
|
||||
authors = [ { name = "Chris Tsang", email = "chris.2y3@outlook.com" } ]
|
||||
readme = "vtracer/README.md"
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
use std::str::FromStr;
|
||||
use std::path::PathBuf;
|
||||
use clap::{Arg, App};
|
||||
use visioncortex::PathSimplifyMode;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Preset {
|
||||
Bw,
|
||||
Poster,
|
||||
Photo
|
||||
Photo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ColorMode {
|
||||
Color,
|
||||
Binary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Hierarchical {
|
||||
Stacked,
|
||||
Cutout,
|
||||
}
|
||||
|
||||
/// Converter config
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
pub input_path: PathBuf,
|
||||
pub output_path: PathBuf,
|
||||
pub color_mode: ColorMode,
|
||||
pub hierarchical: Hierarchical,
|
||||
pub filter_speckle: usize,
|
||||
@@ -36,9 +36,8 @@ pub struct Config {
|
||||
pub path_precision: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ConverterConfig {
|
||||
pub input_path: PathBuf,
|
||||
pub output_path: PathBuf,
|
||||
pub color_mode: ColorMode,
|
||||
pub hierarchical: Hierarchical,
|
||||
pub filter_speckle_area: usize,
|
||||
@@ -55,8 +54,6 @@ pub(crate) struct ConverterConfig {
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
input_path: PathBuf::default(),
|
||||
output_path: PathBuf::default(),
|
||||
color_mode: ColorMode::Color,
|
||||
hierarchical: Hierarchical::Stacked,
|
||||
mode: PathSimplifyMode::Spline,
|
||||
@@ -67,7 +64,7 @@ impl Default for Config {
|
||||
length_threshold: 4.0,
|
||||
splice_threshold: 45,
|
||||
max_iterations: 10,
|
||||
path_precision: Some(8),
|
||||
path_precision: Some(2),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,225 +106,10 @@ impl FromStr for Preset {
|
||||
}
|
||||
}
|
||||
|
||||
fn path_simplify_mode_from_str(s: &str) -> PathSimplifyMode {
|
||||
match s {
|
||||
"polygon" => PathSimplifyMode::Polygon,
|
||||
"spline" => PathSimplifyMode::Spline,
|
||||
"none" => PathSimplifyMode::None,
|
||||
_ => panic!("unknown PathSimplifyMode {}", s),
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_args() -> Self {
|
||||
let app = App::new("visioncortex VTracer ".to_owned() + env!("CARGO_PKG_VERSION"))
|
||||
.about("A cmd app to convert images into vector graphics.");
|
||||
|
||||
let app = app.arg(Arg::with_name("input")
|
||||
.long("input")
|
||||
.short("i")
|
||||
.takes_value(true)
|
||||
.help("Path to input raster image")
|
||||
.required(true));
|
||||
|
||||
let app = app.arg(Arg::with_name("output")
|
||||
.long("output")
|
||||
.short("o")
|
||||
.takes_value(true)
|
||||
.help("Path to output vector graphics")
|
||||
.required(true));
|
||||
|
||||
let app = app.arg(Arg::with_name("color_mode")
|
||||
.long("colormode")
|
||||
.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)
|
||||
.help("Use one of the preset configs `bw`, `poster`, `photo`"));
|
||||
|
||||
let app = app.arg(Arg::with_name("filter_speckle")
|
||||
.long("filter_speckle")
|
||||
.short("f")
|
||||
.takes_value(true)
|
||||
.help("Discard patches smaller than X px in size"));
|
||||
|
||||
let app = app.arg(Arg::with_name("color_precision")
|
||||
.long("color_precision")
|
||||
.short("p")
|
||||
.takes_value(true)
|
||||
.help("Number of significant bits to use in an RGB channel"));
|
||||
|
||||
let app = app.arg(Arg::with_name("gradient_step")
|
||||
.long("gradient_step")
|
||||
.short("g")
|
||||
.takes_value(true)
|
||||
.help("Color difference between gradient layers"));
|
||||
|
||||
let app = app.arg(Arg::with_name("corner_threshold")
|
||||
.long("corner_threshold")
|
||||
.short("c")
|
||||
.takes_value(true)
|
||||
.help("Minimum momentary angle (degree) to be considered a corner"));
|
||||
|
||||
let app = app.arg(Arg::with_name("segment_length")
|
||||
.long("segment_length")
|
||||
.short("l")
|
||||
.takes_value(true)
|
||||
.help("Perform iterative subdivide smooth until all segments are shorter than this length"));
|
||||
|
||||
let app = app.arg(Arg::with_name("splice_threshold")
|
||||
.long("splice_threshold")
|
||||
.short("s")
|
||||
.takes_value(true)
|
||||
.help("Minimum angle displacement (degree) to splice a spline"));
|
||||
|
||||
let app = app.arg(Arg::with_name("mode")
|
||||
.long("mode")
|
||||
.short("m")
|
||||
.takes_value(true)
|
||||
.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 decimal places to use in path string"));
|
||||
|
||||
// Extract matches
|
||||
let matches = app.get_matches();
|
||||
|
||||
let mut config = Config::default();
|
||||
let input_path = matches.value_of("input").expect("Input path is required, please specify it by --input or -i.");
|
||||
let output_path = matches.value_of("output").expect("Output path is required, please specify it by --output or -o.");
|
||||
|
||||
if let Some(value) = matches.value_of("preset") {
|
||||
config = Self::from_preset(Preset::from_str(value).unwrap(), input_path, output_path);
|
||||
}
|
||||
|
||||
config.input_path = PathBuf::from(input_path);
|
||||
config.output_path = PathBuf::from(output_path);
|
||||
|
||||
if let Some(value) = matches.value_of("color_mode") {
|
||||
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" {
|
||||
"none"
|
||||
} else if value == "polygon" {
|
||||
"polygon"
|
||||
} else if value == "spline" {
|
||||
"spline"
|
||||
} else {
|
||||
panic!("Parser Error: Curve fitting mode is invalid: {}", value);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("filter_speckle") {
|
||||
if value.trim().parse::<usize>().is_ok() { // is numeric
|
||||
let value = value.trim().parse::<usize>().unwrap();
|
||||
if value > 16 {
|
||||
panic!("Out of Range Error: Filter speckle is invalid at {}. It must be within [0,16].", value);
|
||||
}
|
||||
config.filter_speckle = value;
|
||||
} else {
|
||||
panic!("Parser Error: Filter speckle is not a positive integer: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("color_precision") {
|
||||
if value.trim().parse::<i32>().is_ok() { // is numeric
|
||||
let value = value.trim().parse::<i32>().unwrap();
|
||||
if value < 1 || value > 8 {
|
||||
panic!("Out of Range Error: Color precision is invalid at {}. It must be within [1,8].", value);
|
||||
}
|
||||
config.color_precision = value;
|
||||
} else {
|
||||
panic!("Parser Error: Color precision is not an integer: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("gradient_step") {
|
||||
if value.trim().parse::<i32>().is_ok() { // is numeric
|
||||
let value = value.trim().parse::<i32>().unwrap();
|
||||
if value < 0 || value > 255 {
|
||||
panic!("Out of Range Error: Gradient step is invalid at {}. It must be within [0,255].", value);
|
||||
}
|
||||
config.layer_difference = value;
|
||||
} else {
|
||||
panic!("Parser Error: Gradient step is not an integer: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("corner_threshold") {
|
||||
if value.trim().parse::<i32>().is_ok() { // is numeric
|
||||
let value = value.trim().parse::<i32>().unwrap();
|
||||
if value < 0 || value > 180 {
|
||||
panic!("Out of Range Error: Corner threshold is invalid at {}. It must be within [0,180].", value);
|
||||
}
|
||||
config.corner_threshold = value
|
||||
} else {
|
||||
panic!("Parser Error: Corner threshold is not numeric: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("segment_length") {
|
||||
if value.trim().parse::<f64>().is_ok() { // is numeric
|
||||
let value = value.trim().parse::<f64>().unwrap();
|
||||
if value < 3.5 || value > 10.0 {
|
||||
panic!("Out of Range Error: Segment length is invalid at {}. It must be within [3.5,10].", value);
|
||||
}
|
||||
config.length_threshold = value;
|
||||
} else {
|
||||
panic!("Parser Error: Segment length is not numeric: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("splice_threshold") {
|
||||
if value.trim().parse::<i32>().is_ok() { // is numeric
|
||||
let value = value.trim().parse::<i32>().unwrap();
|
||||
if value < 0 || value > 180 {
|
||||
panic!("Out of Range Error: Segment length is invalid at {}. It must be within [0,180].", value);
|
||||
}
|
||||
config.splice_threshold = value;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
pub fn from_preset(preset: Preset, input_path: &str, output_path: &str) -> Self {
|
||||
let input_path = PathBuf::from(input_path);
|
||||
let output_path = PathBuf::from(output_path);
|
||||
pub fn from_preset(preset: Preset) -> Self {
|
||||
match preset {
|
||||
Preset::Bw => Self {
|
||||
input_path,
|
||||
output_path,
|
||||
color_mode: ColorMode::Binary,
|
||||
hierarchical: Hierarchical::Stacked,
|
||||
filter_speckle: 4,
|
||||
@@ -338,11 +120,9 @@ impl Config {
|
||||
length_threshold: 4.0,
|
||||
max_iterations: 10,
|
||||
splice_threshold: 45,
|
||||
path_precision: Some(8),
|
||||
path_precision: Some(2),
|
||||
},
|
||||
Preset::Poster => Self {
|
||||
input_path,
|
||||
output_path,
|
||||
color_mode: ColorMode::Color,
|
||||
hierarchical: Hierarchical::Stacked,
|
||||
filter_speckle: 4,
|
||||
@@ -353,11 +133,9 @@ impl Config {
|
||||
length_threshold: 4.0,
|
||||
max_iterations: 10,
|
||||
splice_threshold: 45,
|
||||
path_precision: Some(8),
|
||||
path_precision: Some(2),
|
||||
},
|
||||
Preset::Photo => Self {
|
||||
input_path,
|
||||
output_path,
|
||||
color_mode: ColorMode::Color,
|
||||
hierarchical: Hierarchical::Stacked,
|
||||
filter_speckle: 10,
|
||||
@@ -368,15 +146,13 @@ impl Config {
|
||||
length_threshold: 4.0,
|
||||
max_iterations: 10,
|
||||
splice_threshold: 45,
|
||||
path_precision: Some(8),
|
||||
}
|
||||
path_precision: Some(2),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn into_converter_config(self) -> ConverterConfig {
|
||||
ConverterConfig {
|
||||
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,
|
||||
@@ -394,4 +170,4 @@ impl Config {
|
||||
|
||||
fn deg2rad(deg: i32) -> f64 {
|
||||
deg as f64 / 180.0 * std::f64::consts::PI
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,43 @@
|
||||
use std::path::PathBuf;
|
||||
use std::path::Path;
|
||||
use std::{fs::File, io::Write};
|
||||
|
||||
use fastrand::Rng;
|
||||
use visioncortex::{Color, ColorImage, ColorName};
|
||||
use visioncortex::color_clusters::{Runner, RunnerConfig, KeyingAction, HIERARCHICAL_MAX};
|
||||
use super::config::{Config, ColorMode, Hierarchical, ConverterConfig};
|
||||
use super::config::{ColorMode, Config, ConverterConfig, Hierarchical};
|
||||
use super::svg::SvgFile;
|
||||
use fastrand::Rng;
|
||||
use visioncortex::color_clusters::{KeyingAction, Runner, RunnerConfig, HIERARCHICAL_MAX};
|
||||
use visioncortex::{Color, ColorImage, ColorName};
|
||||
|
||||
const NUM_UNUSED_COLOR_ITERATIONS: usize = 6;
|
||||
/// The fraction of pixels in the top/bottom rows of the image that need to be transparent before
|
||||
/// the entire image will be keyed.
|
||||
const KEYING_THRESHOLD: f32 = 0.2;
|
||||
|
||||
/// Convert an image file into svg file
|
||||
pub fn convert_image_to_svg(config: Config) -> Result<(), String> {
|
||||
/// Convert an in-memory image into an in-memory SVG
|
||||
pub fn convert(img: ColorImage, config: Config) -> Result<SvgFile, String> {
|
||||
let config = config.into_converter_config();
|
||||
match config.color_mode {
|
||||
ColorMode::Color => color_image_to_svg(config),
|
||||
ColorMode::Binary => binary_image_to_svg(config),
|
||||
ColorMode::Color => color_image_to_svg(img, config),
|
||||
ColorMode::Binary => binary_image_to_svg(img, config),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an image file into svg file
|
||||
pub fn convert_image_to_svg(
|
||||
input_path: &Path,
|
||||
output_path: &Path,
|
||||
config: Config,
|
||||
) -> Result<(), String> {
|
||||
let img = read_image(input_path)?;
|
||||
let svg = convert(img, config)?;
|
||||
write_svg(svg, output_path)
|
||||
}
|
||||
|
||||
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
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,27 +46,24 @@ fn color_exists_in_image(img: &ColorImage, color: Color) -> bool {
|
||||
|
||||
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, 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(0, 255, 255),
|
||||
Color::new(255, 0, 255),
|
||||
]);
|
||||
let rng = Rng::new();
|
||||
let random_colors = (0..NUM_UNUSED_COLOR_ITERATIONS).map(|_| {
|
||||
Color::new(
|
||||
rng.u8(..),
|
||||
rng.u8(..),
|
||||
rng.u8(..),
|
||||
)
|
||||
});
|
||||
let random_colors =
|
||||
(0..NUM_UNUSED_COLOR_ITERATIONS).map(|_| Color::new(rng.u8(..), rng.u8(..), rng.u8(..)));
|
||||
for color in special_colors.chain(random_colors) {
|
||||
if !color_exists_in_image(img, color) {
|
||||
return Ok(color);
|
||||
}
|
||||
}
|
||||
Err(String::from("unable to find unused color in image to use as key"))
|
||||
Err(String::from(
|
||||
"unable to find unused color in image to use as key",
|
||||
))
|
||||
}
|
||||
|
||||
fn should_key_image(img: &ColorImage) -> bool {
|
||||
@@ -65,14 +73,20 @@ fn should_key_image(img: &ColorImage) -> bool {
|
||||
|
||||
// Check for transparency at several scanlines
|
||||
let threshold = ((img.width * 2) as f32 * KEYING_THRESHOLD) as usize;
|
||||
let mut num_transparent_boundary_pixels = 0;
|
||||
let y_positions = [0, img.height / 4, img.height / 2, 3 * img.height / 4, img.height - 1];
|
||||
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_boundary_pixels += 1;
|
||||
num_transparent_pixels += 1;
|
||||
}
|
||||
if num_transparent_boundary_pixels >= threshold {
|
||||
if num_transparent_pixels >= threshold {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -81,16 +95,9 @@ fn should_key_image(img: &ColorImage) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
|
||||
let (mut img, width, height);
|
||||
match read_image(config.input_path) {
|
||||
Ok(values) => {
|
||||
img = values.0;
|
||||
width = values.1;
|
||||
height = values.2;
|
||||
},
|
||||
Err(msg) => return Err(msg),
|
||||
}
|
||||
fn color_image_to_svg(mut img: ColorImage, config: ConverterConfig) -> Result<SvgFile, String> {
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
let key_color = if should_key_image(&img) {
|
||||
let key_color = find_unused_color_in_image(&img)?;
|
||||
@@ -107,23 +114,26 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
|
||||
Color::default()
|
||||
};
|
||||
|
||||
let runner = Runner::new(RunnerConfig {
|
||||
diagonal: config.layer_difference == 0,
|
||||
hierarchical: HIERARCHICAL_MAX,
|
||||
batch_size: 25600,
|
||||
good_min_area: config.filter_speckle_area,
|
||||
good_max_area: (width * height),
|
||||
is_same_color_a: config.color_precision_loss,
|
||||
is_same_color_b: 1,
|
||||
deepen_diff: config.layer_difference,
|
||||
hollow_neighbours: 1,
|
||||
key_color,
|
||||
keying_action: if matches!(config.hierarchical, Hierarchical::Cutout) {
|
||||
KeyingAction::Keep
|
||||
} else {
|
||||
KeyingAction::Discard
|
||||
let runner = Runner::new(
|
||||
RunnerConfig {
|
||||
diagonal: config.layer_difference == 0,
|
||||
hierarchical: HIERARCHICAL_MAX,
|
||||
batch_size: 25600,
|
||||
good_min_area: config.filter_speckle_area,
|
||||
good_max_area: (width * height),
|
||||
is_same_color_a: config.color_precision_loss,
|
||||
is_same_color_b: 1,
|
||||
deepen_diff: config.layer_difference,
|
||||
hollow_neighbours: 1,
|
||||
key_color,
|
||||
keying_action: if matches!(config.hierarchical, Hierarchical::Cutout) {
|
||||
KeyingAction::Keep
|
||||
} else {
|
||||
KeyingAction::Discard
|
||||
},
|
||||
},
|
||||
}, img);
|
||||
img,
|
||||
);
|
||||
|
||||
let mut clusters = runner.run();
|
||||
|
||||
@@ -132,21 +142,24 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
|
||||
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,
|
||||
key_color,
|
||||
keying_action: KeyingAction::Discard,
|
||||
}, 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,
|
||||
key_color,
|
||||
keying_action: KeyingAction::Discard,
|
||||
},
|
||||
image,
|
||||
);
|
||||
clusters = runner.run();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let view = clusters.view();
|
||||
@@ -161,26 +174,18 @@ fn color_image_to_svg(config: ConverterConfig) -> Result<(), String> {
|
||||
config.corner_threshold,
|
||||
config.length_threshold,
|
||||
config.max_iterations,
|
||||
config.splice_threshold
|
||||
config.splice_threshold,
|
||||
);
|
||||
svg.add_path(paths, cluster.residue_color());
|
||||
}
|
||||
|
||||
write_svg(svg, config.output_path)
|
||||
Ok(svg)
|
||||
}
|
||||
|
||||
fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
|
||||
|
||||
let (img, width, height);
|
||||
match read_image(config.input_path) {
|
||||
Ok(values) => {
|
||||
img = values.0;
|
||||
width = values.1;
|
||||
height = values.2;
|
||||
},
|
||||
Err(msg) => return Err(msg),
|
||||
}
|
||||
fn binary_image_to_svg(img: ColorImage, config: ConverterConfig) -> Result<SvgFile, String> {
|
||||
let img = img.to_binary_image(|x| x.r < 128);
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
|
||||
let clusters = img.to_clusters(false);
|
||||
|
||||
@@ -199,10 +204,10 @@ fn binary_image_to_svg(config: ConverterConfig) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
write_svg(svg, config.output_path)
|
||||
Ok(svg)
|
||||
}
|
||||
|
||||
fn read_image(input_path: PathBuf) -> Result<(ColorImage, usize, usize), String> {
|
||||
fn read_image(input_path: &Path) -> Result<ColorImage, String> {
|
||||
let img = image::open(input_path);
|
||||
let img = match img {
|
||||
Ok(file) => file.to_rgba8(),
|
||||
@@ -210,12 +215,16 @@ fn read_image(input_path: PathBuf) -> Result<(ColorImage, usize, usize), String>
|
||||
};
|
||||
|
||||
let (width, height) = (img.width() as usize, img.height() as usize);
|
||||
let img = ColorImage {pixels: img.as_raw().to_vec(), width, height};
|
||||
let img = ColorImage {
|
||||
pixels: img.as_raw().to_vec(),
|
||||
width,
|
||||
height,
|
||||
};
|
||||
|
||||
Ok((img, width, height))
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
fn write_svg(svg: SvgFile, output_path: PathBuf) -> Result<(), String> {
|
||||
fn write_svg(svg: SvgFile, output_path: &Path) -> Result<(), String> {
|
||||
let out_file = File::create(output_path);
|
||||
let mut out_file = match out_file {
|
||||
Ok(file) => file,
|
||||
@@ -225,4 +234,4 @@ fn write_svg(svg: SvgFile, output_path: PathBuf) -> Result<(), String> {
|
||||
write!(&mut out_file, "{}", svg).expect("failed to write file.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
|
||||
mod config;
|
||||
mod converter;
|
||||
mod svg;
|
||||
#[cfg(feature = "python-binding")]
|
||||
mod python;
|
||||
mod svg;
|
||||
|
||||
pub use config::*;
|
||||
pub use converter::*;
|
||||
pub use svg::*;
|
||||
#[cfg(feature = "python-binding")]
|
||||
pub use python::*;
|
||||
pub use python::*;
|
||||
pub use svg::*;
|
||||
pub use visioncortex::ColorImage;
|
||||
|
||||
@@ -2,15 +2,281 @@ mod config;
|
||||
mod converter;
|
||||
mod svg;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use config::{ColorMode, Config, Hierarchical, Preset};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use visioncortex::PathSimplifyMode;
|
||||
|
||||
fn path_simplify_mode_from_str(s: &str) -> PathSimplifyMode {
|
||||
match s {
|
||||
"polygon" => PathSimplifyMode::Polygon,
|
||||
"spline" => PathSimplifyMode::Spline,
|
||||
"none" => PathSimplifyMode::None,
|
||||
_ => panic!("unknown PathSimplifyMode {}", s),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config_from_args() -> (PathBuf, PathBuf, Config) {
|
||||
let app = App::new("visioncortex VTracer ".to_owned() + env!("CARGO_PKG_VERSION"))
|
||||
.about("A cmd app to convert images into vector graphics.");
|
||||
|
||||
let app = app.arg(
|
||||
Arg::with_name("input")
|
||||
.long("input")
|
||||
.short("i")
|
||||
.takes_value(true)
|
||||
.help("Path to input raster image")
|
||||
.required(true),
|
||||
);
|
||||
|
||||
let app = app.arg(
|
||||
Arg::with_name("output")
|
||||
.long("output")
|
||||
.short("o")
|
||||
.takes_value(true)
|
||||
.help("Path to output vector graphics")
|
||||
.required(true),
|
||||
);
|
||||
|
||||
let app = app.arg(
|
||||
Arg::with_name("color_mode")
|
||||
.long("colormode")
|
||||
.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)
|
||||
.help("Use one of the preset configs `bw`, `poster`, `photo`"),
|
||||
);
|
||||
|
||||
let app = app.arg(
|
||||
Arg::with_name("filter_speckle")
|
||||
.long("filter_speckle")
|
||||
.short("f")
|
||||
.takes_value(true)
|
||||
.help("Discard patches smaller than X px in size"),
|
||||
);
|
||||
|
||||
let app = app.arg(
|
||||
Arg::with_name("color_precision")
|
||||
.long("color_precision")
|
||||
.short("p")
|
||||
.takes_value(true)
|
||||
.help("Number of significant bits to use in an RGB channel"),
|
||||
);
|
||||
|
||||
let app = app.arg(
|
||||
Arg::with_name("gradient_step")
|
||||
.long("gradient_step")
|
||||
.short("g")
|
||||
.takes_value(true)
|
||||
.help("Color difference between gradient layers"),
|
||||
);
|
||||
|
||||
let app = app.arg(
|
||||
Arg::with_name("corner_threshold")
|
||||
.long("corner_threshold")
|
||||
.short("c")
|
||||
.takes_value(true)
|
||||
.help("Minimum momentary angle (degree) to be considered a corner"),
|
||||
);
|
||||
|
||||
let app = app.arg(Arg::with_name("segment_length")
|
||||
.long("segment_length")
|
||||
.short("l")
|
||||
.takes_value(true)
|
||||
.help("Perform iterative subdivide smooth until all segments are shorter than this length"));
|
||||
|
||||
let app = app.arg(
|
||||
Arg::with_name("splice_threshold")
|
||||
.long("splice_threshold")
|
||||
.short("s")
|
||||
.takes_value(true)
|
||||
.help("Minimum angle displacement (degree) to splice a spline"),
|
||||
);
|
||||
|
||||
let app = app.arg(
|
||||
Arg::with_name("mode")
|
||||
.long("mode")
|
||||
.short("m")
|
||||
.takes_value(true)
|
||||
.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 decimal places to use in path string"),
|
||||
);
|
||||
|
||||
// Extract matches
|
||||
let matches = app.get_matches();
|
||||
|
||||
let mut config = Config::default();
|
||||
let input_path = matches
|
||||
.value_of("input")
|
||||
.expect("Input path is required, please specify it by --input or -i.");
|
||||
let output_path = matches
|
||||
.value_of("output")
|
||||
.expect("Output path is required, please specify it by --output or -o.");
|
||||
|
||||
let input_path = PathBuf::from(input_path);
|
||||
let output_path = PathBuf::from(output_path);
|
||||
|
||||
if let Some(value) = matches.value_of("preset") {
|
||||
config = Config::from_preset(Preset::from_str(value).unwrap());
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("color_mode") {
|
||||
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" {
|
||||
"none"
|
||||
} else if value == "polygon" {
|
||||
"polygon"
|
||||
} else if value == "spline" {
|
||||
"spline"
|
||||
} else {
|
||||
panic!("Parser Error: Curve fitting mode is invalid: {}", value);
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("filter_speckle") {
|
||||
if value.trim().parse::<usize>().is_ok() {
|
||||
// is numeric
|
||||
let value = value.trim().parse::<usize>().unwrap();
|
||||
if value > 16 {
|
||||
panic!("Out of Range Error: Filter speckle is invalid at {}. It must be within [0,16].", value);
|
||||
}
|
||||
config.filter_speckle = value;
|
||||
} else {
|
||||
panic!(
|
||||
"Parser Error: Filter speckle is not a positive integer: {}.",
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("color_precision") {
|
||||
if value.trim().parse::<i32>().is_ok() {
|
||||
// is numeric
|
||||
let value = value.trim().parse::<i32>().unwrap();
|
||||
if value < 1 || value > 8 {
|
||||
panic!("Out of Range Error: Color precision is invalid at {}. It must be within [1,8].", value);
|
||||
}
|
||||
config.color_precision = value;
|
||||
} else {
|
||||
panic!(
|
||||
"Parser Error: Color precision is not an integer: {}.",
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("gradient_step") {
|
||||
if value.trim().parse::<i32>().is_ok() {
|
||||
// is numeric
|
||||
let value = value.trim().parse::<i32>().unwrap();
|
||||
if value < 0 || value > 255 {
|
||||
panic!("Out of Range Error: Gradient step is invalid at {}. It must be within [0,255].", value);
|
||||
}
|
||||
config.layer_difference = value;
|
||||
} else {
|
||||
panic!("Parser Error: Gradient step is not an integer: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("corner_threshold") {
|
||||
if value.trim().parse::<i32>().is_ok() {
|
||||
// is numeric
|
||||
let value = value.trim().parse::<i32>().unwrap();
|
||||
if value < 0 || value > 180 {
|
||||
panic!("Out of Range Error: Corner threshold is invalid at {}. It must be within [0,180].", value);
|
||||
}
|
||||
config.corner_threshold = value
|
||||
} else {
|
||||
panic!("Parser Error: Corner threshold is not numeric: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("segment_length") {
|
||||
if value.trim().parse::<f64>().is_ok() {
|
||||
// is numeric
|
||||
let value = value.trim().parse::<f64>().unwrap();
|
||||
if value < 3.5 || value > 10.0 {
|
||||
panic!("Out of Range Error: Segment length is invalid at {}. It must be within [3.5,10].", value);
|
||||
}
|
||||
config.length_threshold = value;
|
||||
} else {
|
||||
panic!("Parser Error: Segment length is not numeric: {}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(value) = matches.value_of("splice_threshold") {
|
||||
if value.trim().parse::<i32>().is_ok() {
|
||||
// is numeric
|
||||
let value = value.trim().parse::<i32>().unwrap();
|
||||
if value < 0 || value > 180 {
|
||||
panic!("Out of Range Error: Segment length is invalid at {}. It must be within [0,180].", value);
|
||||
}
|
||||
config.splice_threshold = value;
|
||||
} else {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
(input_path, output_path, config)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let config = config::Config::from_args();
|
||||
let result = converter::convert_image_to_svg(config);
|
||||
let (input_path, output_path, config) = config_from_args();
|
||||
let result = converter::convert_image_to_svg(&input_path, &output_path, config);
|
||||
match result {
|
||||
Ok(()) => {
|
||||
println!("Conversion successful.");
|
||||
},
|
||||
}
|
||||
Err(msg) => {
|
||||
panic!("Conversion failed with error message: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 visioncortex::PathSimplifyMode;
|
||||
|
||||
@@ -23,6 +25,148 @@ fn convert_image_to_svg_py(
|
||||
let input_path = PathBuf::from(image_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
|
||||
// accept the strings 'color' or 'binary'
|
||||
let color_mode = match colormode.unwrap_or("color") {
|
||||
@@ -52,9 +196,7 @@ fn convert_image_to_svg_py(
|
||||
let splice_threshold = splice_threshold.unwrap_or(45);
|
||||
let max_iterations = max_iterations.unwrap_or(10);
|
||||
|
||||
let config = Config {
|
||||
input_path,
|
||||
output_path,
|
||||
Config {
|
||||
color_mode,
|
||||
hierarchical,
|
||||
filter_speckle,
|
||||
@@ -67,15 +209,14 @@ fn convert_image_to_svg_py(
|
||||
splice_threshold,
|
||||
path_precision,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
convert_image_to_svg(config).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A Python module implemented in Rust.
|
||||
#[pymodule]
|
||||
fn vtracer(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::fmt;
|
||||
use visioncortex::{Color, CompoundPath, PointF64};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SvgFile {
|
||||
pub paths: Vec<SvgPath>,
|
||||
pub width: usize,
|
||||
@@ -8,6 +9,7 @@ pub struct SvgFile {
|
||||
pub path_precision: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SvgPath {
|
||||
pub path: CompoundPath,
|
||||
pub color: Color,
|
||||
@@ -24,25 +26,27 @@ impl SvgFile {
|
||||
}
|
||||
|
||||
pub fn add_path(&mut self, path: CompoundPath, color: Color) {
|
||||
self.paths.push(SvgPath {
|
||||
path,
|
||||
color,
|
||||
})
|
||||
self.paths.push(SvgPath { path, color })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SvgFile {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
|
||||
writeln!(f, r#"<!-- Generator: visioncortex VTracer {} -->"#, env!("CARGO_PKG_VERSION"))?;
|
||||
writeln!(f,
|
||||
writeln!(
|
||||
f,
|
||||
r#"<!-- Generator: visioncortex VTracer {} -->"#,
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)?;
|
||||
writeln!(
|
||||
f,
|
||||
r#"<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="{}" height="{}">"#,
|
||||
self.width, self.height
|
||||
)?;
|
||||
|
||||
for path in &self.paths {
|
||||
path.fmt_with_precision(f, self.path_precision)?;
|
||||
};
|
||||
}
|
||||
|
||||
writeln!(f, "</svg>")
|
||||
}
|
||||
@@ -56,11 +60,16 @@ impl fmt::Display for SvgPath {
|
||||
|
||||
impl SvgPath {
|
||||
fn fmt_with_precision(&self, f: &mut fmt::Formatter, precision: Option<u32>) -> fmt::Result {
|
||||
let (string, offset) = self.path.to_svg_string(true, PointF64::default(), precision);
|
||||
let (string, offset) = self
|
||||
.path
|
||||
.to_svg_string(true, PointF64::default(), precision);
|
||||
writeln!(
|
||||
f, "<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>",
|
||||
string, self.color.to_hex_string(),
|
||||
offset.x, offset.y
|
||||
f,
|
||||
"<path d=\"{}\" fill=\"{}\" transform=\"translate({},{})\"/>",
|
||||
string,
|
||||
self.color.to_hex_string(),
|
||||
offset.x,
|
||||
offset.y
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
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,
|
||||
out,
|
||||
colormode = 'color', # ["color"] or "binary"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -15,3 +15,35 @@ def convert_image_to_svg_py(image_path: str,
|
||||
path_precision: Optional[int] = None, # default: 8
|
||||
) -> 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:
|
||||
...
|
||||
@@ -22,7 +22,7 @@ console_log = { version = "0.2", features = ["color"] }
|
||||
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
visioncortex = "0.6.0"
|
||||
visioncortex = "0.8.1"
|
||||
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
|
||||
@@ -35,7 +35,11 @@ document.addEventListener('paste', function (e) {
|
||||
|
||||
// Download as SVG
|
||||
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);
|
||||
|
||||
this.href = url;
|
||||
@@ -444,7 +448,7 @@ class ConverterRunner {
|
||||
this.converter.init();
|
||||
this.stopped = false;
|
||||
if (clustering_mode == 'binary') {
|
||||
svg.style.background = '#000';
|
||||
svg.style.background = '#fff';
|
||||
canvas.style.display = 'none';
|
||||
} else {
|
||||
svg.style.background = '';
|
||||
|
||||
@@ -77,7 +77,7 @@ impl BinaryImageConverter {
|
||||
self.params.max_iterations,
|
||||
self.params.splice_threshold
|
||||
);
|
||||
let color = Color::color(&ColorName::White);
|
||||
let color = Color::color(&ColorName::Black);
|
||||
self.svg.prepend_path(
|
||||
&paths,
|
||||
&color,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use visioncortex::PathSimplifyMode;
|
||||
use visioncortex::color_clusters::{IncrementalBuilder, Clusters, Runner, RunnerConfig, HIERARCHICAL_MAX};
|
||||
use visioncortex::{Color, ColorImage, PathSimplifyMode};
|
||||
use visioncortex::color_clusters::{Clusters, Runner, RunnerConfig, HIERARCHICAL_MAX, IncrementalBuilder, KeyingAction};
|
||||
|
||||
use crate::canvas::*;
|
||||
use crate::svg::*;
|
||||
@@ -8,6 +8,8 @@ use crate::svg::*;
|
||||
use serde::Deserialize;
|
||||
use super::util;
|
||||
|
||||
const KEYING_THRESHOLD: f32 = 0.2;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ColorImageConverterParams {
|
||||
pub canvas_id: String,
|
||||
@@ -67,7 +69,26 @@ impl ColorImageConverter {
|
||||
pub fn init(&mut self) {
|
||||
let width = self.canvas.width() 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 {
|
||||
diagonal: self.params.layer_difference == 0,
|
||||
hierarchical: HIERARCHICAL_MAX,
|
||||
@@ -78,6 +99,12 @@ impl ColorImageConverter {
|
||||
is_same_color_b: 1,
|
||||
deepen_diff: self.params.layer_difference,
|
||||
hollow_neighbours: 1,
|
||||
key_color,
|
||||
keying_action: if self.params.hierarchical == "cutout" {
|
||||
KeyingAction::Keep
|
||||
} else {
|
||||
KeyingAction::Discard
|
||||
},
|
||||
}, image);
|
||||
self.stage = Stage::Clustering(runner.start());
|
||||
}
|
||||
@@ -108,6 +135,8 @@ impl ColorImageConverter {
|
||||
is_same_color_b: 1,
|
||||
deepen_diff: 0,
|
||||
hollow_neighbours: 0,
|
||||
key_color: Default::default(),
|
||||
keying_action: KeyingAction::Discard,
|
||||
}, image);
|
||||
self.stage = Stage::Reclustering(runner.start());
|
||||
},
|
||||
@@ -167,4 +196,56 @@ impl ColorImageConverter {
|
||||
}) 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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
mod binary_image;
|
||||
mod color_image;
|
||||
mod util;
|
||||
|
||||
pub use binary_image::*;
|
||||
pub use color_image::*;
|
||||
Reference in New Issue
Block a user