Repository: jankovicsandras/imagetracerjava
Branch: master
Commit: 135f00e5e638
Files: 12
Total size: 58.4 KB
Directory structure:
gitextract_s987juft/
├── .classpath
├── .gitattributes
├── .gitignore
├── .project
├── .settings/
│ ├── org.eclipse.core.resources.prefs
│ └── org.eclipse.jdt.core.prefs
├── ImageTracer.jar
├── LICENSE
├── README.md
├── deterministic.md
├── jankovicsandras/
│ └── imagetracer/
│ └── ImageTracer.java
└── process_overview.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .classpath
================================================
================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
================================================
FILE: .gitignore
================================================
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msm
*.msp
# Windows shortcuts
*.lnk
# =========================
# Operating System Files
# =========================
# OSX
# =========================
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
/bin
================================================
FILE: .project
================================================
jankovicsandras.imagetracer-javaorg.eclipse.jdt.core.javabuilderorg.eclipse.jdt.core.javanature
================================================
FILE: .settings/org.eclipse.core.resources.prefs
================================================
eclipse.preferences.version=1
encoding//jankovicsandras/imagetracer/ImageTracer.java=UTF-8
================================================
FILE: .settings/org.eclipse.jdt.core.prefs
================================================
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.8
================================================
FILE: LICENSE
================================================
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to
================================================
FILE: README.md
================================================
# imagetracerjava

Simple raster image tracer and vectorizer written in Java for desktop. See https://github.com/jankovicsandras/imagetracerandroid for the Android version.
by András Jankovics
This is a port of imagetracer.js: https://github.com/jankovicsandras/imagetracerjs
### Check this out for a refactored version with better color quantization algorithm: https://github.com/miguelemosreverte/imagetracerjava
### Check this out for a C++ port: https://github.com/dov/ImageTracerCpp
### 1.1.2
- minor bugfixes
- lookup based ```pathscan()```
### 1.1.1
- Bugfix: CSS3 RGBA output in SVG was technically incorrect (however supported by major browsers), so this is changed. [More info](https://stackoverflow.com/questions/6042550/svg-fill-color-transparency-alpha)
- transparency support: alpha is not discarded now, it is given more weight in color quantization
- new ```options.roundcoords``` : rounding coordinates to a given decimal place. This can reduce SVG length significantly (>20%) with minor loss of precision.
- new ```options.desc``` : setting this to false will turn off path descriptions, reducing SVG length.
- new ```options.viewbox``` : setting this to true will use viewBox instead of exact width and height
- new ```options.colorsampling``` : color quantization will sample the colors now by default, can be turned off.
- new ```options.blurradius``` : setting this to 1..5 will preprocess the image with a selective Gaussian blur with ```options.blurdelta``` treshold. This can filter noise and improve quality.
- ```IndexedImage``` has width and height
- ```getsvgstring()``` needs now only ```IndexedImage``` (tracedata) and ```options``` as parameters
- ```colorquantization()``` needs now only ```imgd```, ```palette``` and ```options``` as parameters
- background field is removed from the results of color quantization
### Running as a standalone program
Warning: if the outfilename parameter is not specified, then this will overwrite .svg .
Basic usage:
```bash
java -jar ImageTracer.jar smiley.png
```
With options:
```bash
java -jar ImageTracer.jar smiley.png outfilename output.svg ltres 1 qtres 1 pathomit 8 colorsampling 1 numberofcolors 16 mincolorratio 0.02 colorquantcycles 3 scale 1 simplifytolerance 0 roundcoords 1 lcpr 0 qcpr 0 desc 1 viewbox 0 blurradius 0 blurdelta 20
```
### Including in Java projects
Add ImageTracer.jar to your build path, import, then use the static methods:
```java
import jankovicsandras.imagetracer.ImageTracer;
...
ImageTracer.saveString(
"output.svg" ,
ImageTracer.imageToSVG("input.jpg",null,null)
);
```
With options and palette
```java
// Options
HashMap options = new HashMap();
// Tracing
options.put("ltres",1f);
options.put("qtres",1f);
options.put("pathomit",8f);
// Color quantization
options.put("colorsampling",1f); // 1f means true ; 0f means false: starting with generated palette
options.put("numberofcolors",16f);
options.put("mincolorratio",0.02f);
options.put("colorquantcycles",3f);
// SVG rendering
options.put("scale",1f);
options.put("roundcoords",1f); // 1f means rounded to 1 decimal places, like 7.3 ; 3f means rounded to 3 places, like 7.356 ; etc.
options.put("lcpr",0f);
options.put("qcpr",0f);
options.put("desc",1f); // 1f means true ; 0f means false: SVG descriptions deactivated
options.put("viewbox",0f); // 1f means true ; 0f means false: fixed width and height
// Selective Gauss Blur
options.put("blurradius",0f); // 0f means deactivated; 1f .. 5f : blur with this radius
options.put("blurdelta",20f); // smaller than this RGB difference will be blurred
// Palette
// This is an example of a grayscale palette
// please note that signed byte values [ -128 .. 127 ] will be converted to [ 0 .. 255 ] in the getsvgstring function
byte[][] palette = new byte[8][4];
for(int colorcnt=0; colorcnt < 8; colorcnt++){
palette[colorcnt][0] = (byte)( -128 + colorcnt * 32); // R
palette[colorcnt][1] = (byte)( -128 + colorcnt * 32); // G
palette[colorcnt][2] = (byte)( -128 + colorcnt * 32); // B
palette[colorcnt][3] = (byte)127; // A
}
ImageTracer.saveString(
"output.svg" ,
ImageTracer.imageToSVG("input.jpg",options,palette)
);
```
### Deterministic output
See [options for deterministic tracing](https://github.com/jankovicsandras/imagetracerjava/blob/master/deterministic.md)
### Main Functions
|Function name|Arguments|Returns|
|-------------|---------|-------|
|```imageToSVG```|```String filename, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```String /*SVG content*/```|
|```imageToSVG```|```BufferedImage image, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```String /*SVG content*/```|
|```imagedataToSVG```|```ImageData imgd, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```String /*SVG content*/```|
|```imageToTracedata```|```String filename, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```IndexedImage /*read the source for details*/```|
|```imageToTracedata```|```BufferedImage image, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```IndexedImage /*read the source for details*/```|
|```imagedataToTracedata```|```ImageData imgd, HashMap options /*can be null*/, byte [][] palette /*can be null*/```|```IndexedImage /*read the source for details*/```|
#### Helper Functions
|Function name|Arguments|Returns|
|-------------|---------|-------|
|```saveString```|```String filename, String str```|```void```|
|```loadImageData```|```String filename```|```ImageData /*read the source for details*/```|
|```loadImageData```|```BufferedImage image```|```ImageData /*read the source for details*/```|
```ImageData``` is similar to [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) here.
There are more functions for advanced users, read the source if you are interested. :)
### Options
|Option name|Default value|Meaning|
|-----------|-------------|-------|
|```ltres```|```1f```|Error treshold for straight lines.|
|```qtres```|```1f```|Error treshold for quadratic splines.|
|```pathomit```|```8f```|Edge node paths shorter than this will be discarded for noise reduction.|
|```colorsampling```|```1f```|Enable or disable color sampling. 1f is on, 0f is off.|
|```numberofcolors```|```16f```|Number of colors to use on palette if pal object is not defined.|
|```mincolorratio```|```0.02f```|Color quantization will randomize a color if fewer pixels than (total pixels*mincolorratio) has it.|
|```colorquantcycles```|```3f```|Color quantization will be repeated this many times.|
|```blurradius```|```0f```|Set this to 1f..5f for selective Gaussian blur preprocessing.|
|```blurdelta```|```20f```|RGBA delta treshold for selective Gaussian blur preprocessing.|
|```scale```|```1f```|Every coordinate will be multiplied with this, to scale the SVG.|
|```roundcoords```|```1f```|rounding coordinates to a given decimal place. 1f means rounded to 1 decimal place like 7.3 ; 3f means rounded to 3 places, like 7.356|
|```viewbox```|```0f```|Enable or disable SVG viewBox. 1f is on, 0f is off.|
|```desc```|```1f```|Enable or disable SVG descriptions. 1f is on, 0f is off.|
|```lcpr```|```0f```|Straight line control point radius, if this is greater than zero, small circles will be drawn in the SVG. Do not use this for big/complex images.|
|```qcpr```|```0f```|Quadratic spline control point radius, if this is greater than zero, small circles and lines will be drawn in the SVG. Do not use this for big/complex images.|
### Process overview
See [Process overview and Ideas for improvement](https://github.com/jankovicsandras/imagetracerjava/blob/master/process_overview.md)
### License
#### The Unlicense / PUBLIC DOMAIN
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to [http://unlicense.org](http://unlicense.org)
================================================
FILE: deterministic.md
================================================
## TLDR; options for deterministic tracing:
custom palette, `colorquantcycles`:1
custom palette, `mincolorratio`:0
`colorsampling`:0 (false), `mincolorratio`:0, `numberofcolors`<8
`colorsampling`:0 (false), `mincolorratio`:0, `numberofcolors`:n^3 eg. 8, 27...
`colorsampling`:0 (false), `colorquantcycles`:1, `numberofcolors`<8
`colorsampling`:0 (false), `colorquantcycles`:1, `numberofcolors`:n^3 eg. 8, 27...
---
## The long story: ☺
Only color quantization uses randomization, all the other processing steps are deterministic.
There are two "sources of random" which make the `colorquantization()` non-deterministic by default, but these can be turned off. `colorquantization()` is based on [K-means clustering](https://en.wikipedia.org/wiki/K-means_clustering) , the initial palette contains the initial means. It makes often sense to use randomization creating the initial palette (see below). Some clusters may have very few members, so they should be "recycled": the new cluster center (palette color) is generated randomly. These non-deterministic defaults can be changed:
### 1. There are 3 ways to create the initial palette before color clustering, listed by priority:
- use a custom palette (deterministic) IF it's defined ELSE
- sample the input image randomly (non-deterministic) IF `colorsampling` is 1 (true, the default) ELSE
- generate a palette
- grayscale (deterministic) IF `numberofcolors`<8 ELSE
- RGB cubic grid (deterministic) "from the cubic part of" `numberofcolors` AND
- random colors (non-deterministic) "from the rest of" `numberofcolors`
So to create a deterministic initial palette:
- use custom palette OR
- set `colorsampling`:0 (false) AND
- use less than 8 colors eg. `numberofcolors`:7 OR
- set `numberofcolors` to a cubic number eg. 8, 27, 64, 125...
### 2. Clusters which have very few members, can be "recycled" to improve clustering:
the new cluster center (palette color) is generated randomly. This depends on `mincolorratio` : if the ratio of pixels that belong to this color (cluster) is less than `mincolorratio` , then this color will be randomized. The default 0.02 means that if fewer than 2% of all pixels are similar to this color, then this is probably a "bad" color and will be "recycled".
IF the clustering is not repeated ( `colorquantcycles`:1 ) OR no color will be recycled ( `mincolorratio`:0 ) THEN this will be deterministic.
These design choices were made so that the color quantization would be:
- flexible : the user can use a custom palette or tweak many parameters
- heuristic: sometimes it's bad but sometimes it's good, instead of being deterministic and mediocre. It's recommended to run tracing multiple times and keep the best result.
- simple to implement.
================================================
FILE: jankovicsandras/imagetracer/ImageTracer.java
================================================
/*
ImageTracer.java
(Desktop version with javax.imageio. See ImageTracerAndroid.java for the Android version.)
Simple raster image tracer and vectorizer written in Java. This is a port of imagetracer.js.
by András Jankovics 2015, 2016
andras@jankovics.net
*/
/*
The Unlicense / PUBLIC DOMAIN
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to http://unlicense.org/
*/
package jankovicsandras.imagetracer;
import java.awt.image.BufferedImage;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.TreeMap;
import javax.imageio.ImageIO;
public class ImageTracer{
public static String versionnumber = "1.1.2";
public ImageTracer(){}
public static void main (String[] args){
try{
if(args.length<1){
System.out.println("ERROR: there's no input filename. Basic usage: \r\n\r\njava -jar ImageTracer.jar "+
"\r\n\r\nor\r\n\r\njava -jar ImageTracer.jar help");
} else if(arraycontains(args,"help")>-1){
System.out.println("Example usage:\r\n\r\njava -jar ImageTracer.jar outfilename test.svg "+
"ltres 1 qtres 1 pathomit 8 colorsampling 1 numberofcolors 16 mincolorratio 0.02 colorquantcycles 3 "+
"scale 1 simplifytolerance 0 roundcoords 1 lcpr 0 qcpr 0 desc 1 viewbox 0 blurradius 0 blurdelta 20 \r\n"+
"\r\nOnly is mandatory, if some of the other optional parameters are missing, they will be set to these defaults. "+
"\r\nWarning: if outfilename is not specified, then .svg will be overwritten."+
"\r\nSee https://github.com/jankovicsandras/imagetracerjava for details. \r\nThis is version "+versionnumber);
} else {
// Parameter parsing
String outfilename = args[0]+".svg";
HashMap options = new HashMap();
String[] parameternames = {"ltres","qtres","pathomit","colorsampling","numberofcolors","mincolorratio","colorquantcycles","scale","simplifytolerance","roundcoords","lcpr","qcpr","desc","viewbox","blurradius","blurdelta","outfilename"};
int j = -1; float f = -1;
for (String parametername : parameternames) {
j = arraycontains(args,parametername);
if(j>-1){
if(parametername=="outfilename"){
if( j < (args.length-1)){ outfilename = args[j+1]; }
}else{
f = parsenext(args,j); if(f>-1){ options.put(parametername, new Float(f)); }
}
}
}// End of parameternames loop
// Loading image, tracing, rendering SVG, saving SVG file
saveString(outfilename,imageToSVG(args[0],options,null));
}// End of parameter parsing and processing
}catch(Exception e){ e.printStackTrace(); }
}// End of main()
public static int arraycontains (String [] arr, String str){
for(int j=0; j>> layers;// tracedata
public IndexedImage(int [][] marray, byte [][] mpalette){
array = marray; palette = mpalette;
width = marray[0].length-2; height = marray.length-2;// Color quantization adds +2 to the original width and height
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/ImageData
public static class ImageData{
public int width, height;
public byte[] data; // raw byte data: R G B A R G B A ...
public ImageData(int mwidth, int mheight, byte[] mdata){
width = mwidth; height = mheight; data = mdata;
}
}
// Saving a String as a file
public static void saveString (String filename, String str) throws Exception {
File file = new File(filename);
// if file doesnt exists, then create it
if(!file.exists()){ file.createNewFile(); }
FileWriter fw = new FileWriter(file.getAbsoluteFile());
BufferedWriter bw = new BufferedWriter(fw);
bw.write(str);
bw.close();
}
// Loading a file to ImageData, ARGB byte order
public static ImageData loadImageData (String filename) throws Exception {
BufferedImage image = ImageIO.read(new File(filename));
return loadImageData(image);
}
public static ImageData loadImageData (BufferedImage image) throws Exception {
int width = image.getWidth(); int height = image.getHeight();
int[] rawdata = image.getRGB(0, 0, width, height, null, 0, width);
byte[] data = new byte[rawdata.length*4];
for(int i=0; i>> 24));
data[i*4 ] = bytetrans((byte)(rawdata[i] >>> 16));
data[(i*4)+1] = bytetrans((byte)(rawdata[i] >>> 8));
data[(i*4)+2] = bytetrans((byte)(rawdata[i]));
}
return new ImageData(width,height,data);
}
// The bitshift method in loadImageData creates signed bytes where -1 -> 255 unsigned ; -128 -> 128 unsigned ;
// 127 -> 127 unsigned ; 0 -> 0 unsigned ; These will be converted to -128 (representing 0 unsigned) ...
// 127 (representing 255 unsigned) and tosvgcolorstr will add +128 to create RGB values 0..255
public static byte bytetrans (byte b){
if(b<0){ return (byte)(b+128); }else{ return (byte)(b-128); }
}
////////////////////////////////////////////////////////////
//
// User friendly functions
//
////////////////////////////////////////////////////////////
// Loading an image from a file, tracing when loaded, then returning the SVG String
public static String imageToSVG (String filename, HashMap options, byte [][] palette) throws Exception{
options = checkoptions(options);
ImageData imgd = loadImageData(filename);
return imagedataToSVG(imgd,options,palette);
}// End of imageToSVG()
public static String imageToSVG (BufferedImage image, HashMap options, byte [][] palette) throws Exception{
options = checkoptions(options);
ImageData imgd = loadImageData(image);
return imagedataToSVG(imgd,options,palette);
}// End of imageToSVG()
// Tracing ImageData, then returning the SVG String
public static String imagedataToSVG (ImageData imgd, HashMap options, byte [][] palette){
options = checkoptions(options);
IndexedImage ii = imagedataToTracedata(imgd,options,palette);
return getsvgstring(ii, options);
}// End of imagedataToSVG()
// Loading an image from a file, tracing when loaded, then returning IndexedImage with tracedata in layers
public IndexedImage imageToTracedata (String filename, HashMap options, byte [][] palette) throws Exception{
options = checkoptions(options);
ImageData imgd = loadImageData(filename);
return imagedataToTracedata(imgd,options,palette);
}// End of imageToTracedata()
public IndexedImage imageToTracedata (BufferedImage image, HashMap options, byte [][] palette) throws Exception{
options = checkoptions(options);
ImageData imgd = loadImageData(image);
return imagedataToTracedata(imgd,options,palette);
}// End of imageToTracedata()
// Tracing ImageData, then returning IndexedImage with tracedata in layers
public static IndexedImage imagedataToTracedata (ImageData imgd, HashMap options, byte [][] palette){
// 1. Color quantization
IndexedImage ii = colorquantization(imgd, palette, options);
// 2. Layer separation and edge detection
int[][][] rawlayers = layering(ii);
// 3. Batch pathscan
ArrayList>> bps = batchpathscan(rawlayers,(int)(Math.floor(options.get("pathomit"))));
// 4. Batch interpollation
ArrayList>> bis = batchinternodes(bps);
// 5. Batch tracing
ii.layers = batchtracelayers(bis,options.get("ltres"),options.get("qtres"));
return ii;
}// End of imagedataToTracedata()
// creating options object, setting defaults for missing values
public static HashMap checkoptions (HashMap options){
if(options==null){ options = new HashMap(); }
// Tracing
if(!options.containsKey("ltres")){ options.put("ltres",1f); }
if(!options.containsKey("qtres")){ options.put("qtres",1f); }
if(!options.containsKey("pathomit")){ options.put("pathomit",8f); }
// Color quantization
if(!options.containsKey("colorsampling")){ options.put("colorsampling",1f); }
if(!options.containsKey("numberofcolors")){ options.put("numberofcolors",16f); }
if(!options.containsKey("mincolorratio")){ options.put("mincolorratio",0.02f); }
if(!options.containsKey("colorquantcycles")){ options.put("colorquantcycles",3f); }
// SVG rendering
if(!options.containsKey("scale")){ options.put("scale",1f); }
if(!options.containsKey("simplifytolerance")){ options.put("simplifytolerance",0f); }
if(!options.containsKey("roundcoords")){ options.put("roundcoords",1f); }
if(!options.containsKey("lcpr")){ options.put("lcpr",0f); }
if(!options.containsKey("qcpr")){ options.put("qcpr",0f); }
if(!options.containsKey("desc")){ options.put("desc",1f); }
if(!options.containsKey("viewbox")){ options.put("viewbox",0f); }
// Blur
if(!options.containsKey("blurradius")){ options.put("blurradius",0f); }
if(!options.containsKey("blurdelta")){ options.put("blurdelta",20f); }
return options;
}// End of checkoptions()
////////////////////////////////////////////////////////////
//
// Vectorizing functions
//
////////////////////////////////////////////////////////////
// 1. Color quantization repeated "cycles" times, based on K-means clustering
// https://en.wikipedia.org/wiki/Color_quantization https://en.wikipedia.org/wiki/K-means_clustering
public static IndexedImage colorquantization (ImageData imgd, byte [][] palette, HashMap options){
int numberofcolors = (int)Math.floor(options.get("numberofcolors")); float minratio = options.get("mincolorratio"); int cycles = (int)Math.floor(options.get("colorquantcycles"));
// Creating indexed color array arr which has a boundary filled with -1 in every direction
int [][] arr = new int[imgd.height+2][imgd.width+2];
for(int j=0; j<(imgd.height+2); j++){ arr[j][0] = -1; arr[j][imgd.width+1 ] = -1; }
for(int i=0; i<(imgd.width+2) ; i++){ arr[0][i] = -1; arr[imgd.height+1][i] = -1; }
int idx=0, cd,cdl,ci,c1,c2,c3,c4;
// Use custom palette if pal is defined or sample or generate custom length palette
if(palette==null){
if(options.get("colorsampling")!=0){
palette = samplepalette(numberofcolors,imgd);
}else{
palette = generatepalette(numberofcolors);
}
}
// Selective Gaussian blur preprocessing
if( options.get("blurradius") > 0 ){ imgd = blur( imgd, options.get("blurradius"), options.get("blurdelta") ); }
long [][] paletteacc = new long[palette.length][5];
// Repeat clustering step "cycles" times
for(int cnt=0;cnt0){
// averaging paletteacc for palette
float ratio;
for(int k=0;k0){
palette[k][0] = (byte) (-128 + (paletteacc[k][0] / paletteacc[k][4]));
palette[k][1] = (byte) (-128 + (paletteacc[k][1] / paletteacc[k][4]));
palette[k][2] = (byte) (-128 + (paletteacc[k][2] / paletteacc[k][4]));
palette[k][3] = (byte) (-128 + (paletteacc[k][3] / paletteacc[k][4]));
}
ratio = (float)( (double)(paletteacc[k][4]) / (double)(imgd.width*imgd.height) );
// Randomizing a color, if there are too few pixels and there will be a new cycle
if( (ratio ; 1 ^ ; 2 < ; 3 v
// Edge node types ( ▓:light or 1; ░:dark or 0 )
// ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓ ░░ ▓░ ░▓ ▓▓
// ░░ ░░ ░░ ░░ ░▓ ░▓ ░▓ ░▓ ▓░ ▓░ ▓░ ▓░ ▓▓ ▓▓ ▓▓ ▓▓
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
//
public static ArrayList> pathscan (int [][] arr,float pathomit){
ArrayList> paths = new ArrayList>();
ArrayList thispath;
int px=0,py=0,w=arr[0].length,h=arr.length,dir=0;
boolean pathfinished=true, holepath = false;
byte[] lookuprow;
for(int j=0;j());
thispath = paths.get(paths.size()-1);
pathfinished = false;
// fill paths will be drawn, but hole paths are also required to remove unnecessary edge nodes
dir = pathscan_dir_lookup[ arr[py][px] ]; holepath = pathscan_holepath_lookup[ arr[py][px] ];
// Path points loop
while(!pathfinished){
// New path point
thispath.add(new Integer[3]);
thispath.get(thispath.size()-1)[0] = px-1;
thispath.get(thispath.size()-1)[1] = py-1;
thispath.get(thispath.size()-1)[2] = arr[py][px];
// Next: look up the replacement, direction and coordinate changes = clear this cell, turn if required, walk forward
lookuprow = pathscan_combined_lookup[ arr[py][px] ][ dir ];
arr[py][px] = lookuprow[0]; dir = lookuprow[1]; px += lookuprow[2]; py += lookuprow[3];
// Close path
if(((px-1)==thispath.get(0)[0])&&((py-1)==thispath.get(0)[1])){
pathfinished = true;
// Discarding 'hole' type paths and paths shorter than pathomit
if( (holepath) || (thispath.size()>> batchpathscan (int [][][] layers, float pathomit){
ArrayList>> bpaths = new ArrayList>>();
for (int[][] layer : layers) {
bpaths.add(pathscan(layer,pathomit));
}
return bpaths;
}
// 4. interpolating between path points for nodes with 8 directions ( East, SouthEast, S, SW, W, NW, N, NE )
public static ArrayList> internodes (ArrayList> paths){
ArrayList> ins = new ArrayList>();
ArrayList thisinp;
Double[] thispoint, nextpoint = new Double[2];
Integer[] pp1, pp2, pp3;
int palen=0,nextidx=0,nextidx2=0;
// paths loop
for(int pacnt=0; pacnt());
thisinp = ins.get(ins.size()-1);
palen = paths.get(pacnt).size();
// pathpoints loop
for(int pcnt=0;pcnt nextpoint[1]){ thispoint[2] = 7.0; }// NE
else { thispoint[2] = 0.0; } // E
}else if(thispoint[0] > nextpoint[0]){
if (thispoint[1] < nextpoint[1]){ thispoint[2] = 3.0; }// SW
else if(thispoint[1] > nextpoint[1]){ thispoint[2] = 5.0; }// NW
else { thispoint[2] = 4.0; }// W
}else{
if (thispoint[1] < nextpoint[1]){ thispoint[2] = 2.0; }// S
else if(thispoint[1] > nextpoint[1]){ thispoint[2] = 6.0; }// N
else { thispoint[2] = 8.0; }// center, this should not happen
}
}// End of pathpoints loop
}// End of paths loop
return ins;
}// End of internodes()
// 4. Batch interpollation
static ArrayList>> batchinternodes (ArrayList>> bpaths){
ArrayList>> binternodes = new ArrayList>>();
for(int k=0; kltreshold), find the point with the biggest error
// 5.4. Fit a quadratic spline through errorpoint (project this to get controlpoint), then measure errors on every point in the sequence
// 5.5. If the spline fails (an error>qtreshold), find the point with the biggest error, set splitpoint = (fitting point + errorpoint)/2
// 5.6. Split sequence and recursively apply 5.2. - 5.7. to startpoint-splitpoint and splitpoint-endpoint sequences
// 5.7. TODO? If splitpoint-endpoint is a spline, try to add new points from the next sequence
// This returns an SVG Path segment as a double[7] where
// segment[0] ==1.0 linear ==2.0 quadratic interpolation
// segment[1] , segment[2] : x1 , y1
// segment[3] , segment[4] : x2 , y2 ; middle point of Q curve, endpoint of L line
// segment[5] , segment[6] : x3 , y3 for Q curve, should be 0.0 , 0.0 for L line
//
// path type is discarded, no check for path.size < 3 , which should not happen
public static ArrayList tracepath (ArrayList path, float ltreshold, float qtreshold){
int pcnt=0, seqend=0; double segtype1, segtype2;
ArrayList smp = new ArrayList();
//Double [] thissegment;
int pathlength = path.size();
while(pcnt0){ pcnt = seqend; }else{ pcnt = pathlength; }
}// End of pcnt loop
return smp;
}// End of tracepath()
// 5.2. - 5.6. recursively fitting a straight or quadratic line segment on this sequence of path nodes,
// called from tracepath()
public static ArrayList fitseq (ArrayList path, float ltreshold, float qtreshold, int seqstart, int seqend){
ArrayList segment = new ArrayList();
Double [] thissegment;
int pathlength = path.size();
// return if invalid seqend
if((seqend>pathlength)||(seqend<0)){return segment;}
int errorpoint=seqstart;
boolean curvepass=true;
double px, py, dist2, errorval=0;
double tl = (seqend-seqstart); if(tl<0){ tl += pathlength; }
double vx = (path.get(seqend)[0]-path.get(seqstart)[0]) / tl,
vy = (path.get(seqend)[1]-path.get(seqstart)[1]) / tl;
// 5.2. Fit a straight line on the sequence
int pcnt = (seqstart+1)%pathlength;
double pl;
while(pcnt != seqend){
pl = pcnt-seqstart; if(pl<0){ pl += pathlength; }
px = path.get(seqstart)[0] + (vx * pl); py = path.get(seqstart)[1] + (vy * pl);
dist2 = ((path.get(pcnt)[0]-px)*(path.get(pcnt)[0]-px)) + ((path.get(pcnt)[1]-py)*(path.get(pcnt)[1]-py));
if(dist2>ltreshold){curvepass=false;}
if(dist2>errorval){ errorpoint=pcnt; errorval=dist2; }
pcnt = (pcnt+1)%pathlength;
}
// return straight line if fits
if(curvepass){
segment.add(new Double[7]);
thissegment = segment.get(segment.size()-1);
thissegment[0] = 1.0;
thissegment[1] = path.get(seqstart)[0];
thissegment[2] = path.get(seqstart)[1];
thissegment[3] = path.get(seqend)[0];
thissegment[4] = path.get(seqend)[1];
thissegment[5] = 0.0;
thissegment[6] = 0.0;
return segment;
}
// 5.3. If the straight line fails (an error>ltreshold), find the point with the biggest error
int fitpoint = errorpoint; curvepass = true; errorval = 0;
// 5.4. Fit a quadratic spline through this point, measure errors on every point in the sequence
// helpers and projecting to get control point
double t=(fitpoint-seqstart)/tl, t1=(1.0-t)*(1.0-t), t2=2.0*(1.0-t)*t, t3=t*t;
double cpx = (((t1*path.get(seqstart)[0]) + (t3*path.get(seqend)[0])) - path.get(fitpoint)[0])/-t2 ,
cpy = (((t1*path.get(seqstart)[1]) + (t3*path.get(seqend)[1])) - path.get(fitpoint)[1])/-t2 ;
// Check every point
pcnt = seqstart+1;
while(pcnt != seqend){
t=(pcnt-seqstart)/tl; t1=(1.0-t)*(1.0-t); t2=2.0*(1.0-t)*t; t3=t*t;
px = (t1 * path.get(seqstart)[0]) + (t2 * cpx) + (t3 * path.get(seqend)[0]);
py = (t1 * path.get(seqstart)[1]) + (t2 * cpy) + (t3 * path.get(seqend)[1]);
dist2 = ((path.get(pcnt)[0]-px)*(path.get(pcnt)[0]-px)) + ((path.get(pcnt)[1]-py)*(path.get(pcnt)[1]-py));
if(dist2>qtreshold){curvepass=false;}
if(dist2>errorval){ errorpoint=pcnt; errorval=dist2; }
pcnt = (pcnt+1)%pathlength;
}
// return spline if fits
if(curvepass){
segment.add(new Double[7]);
thissegment = segment.get(segment.size()-1);
thissegment[0] = 2.0;
thissegment[1] = path.get(seqstart)[0];
thissegment[2] = path.get(seqstart)[1];
thissegment[3] = cpx;
thissegment[4] = cpy;
thissegment[5] = path.get(seqend)[0];
thissegment[6] = path.get(seqend)[1];
return segment;
}
// 5.5. If the spline fails (an error>qtreshold), find the point with the biggest error,
// set splitpoint = (fitting point + errorpoint)/2
int splitpoint = (fitpoint + errorpoint)/2;
// 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences
segment = fitseq(path,ltreshold,qtreshold,seqstart,splitpoint);
segment.addAll(fitseq(path,ltreshold,qtreshold,splitpoint,seqend));
return segment;
}// End of fitseq()
// 5. Batch tracing paths
public static ArrayList> batchtracepaths (ArrayList> internodepaths, float ltres,float qtres){
ArrayList> btracedpaths = new ArrayList>();
for(int k=0; k>> batchtracelayers (ArrayList>> binternodes, float ltres, float qtres){
ArrayList>> btbis = new ArrayList>>();
for(int k=0; k segments, String colorstr, HashMap options){
float scale = options.get("scale"), lcpr = options.get("lcpr"), qcpr = options.get("qcpr"), roundcoords = (float) Math.floor(options.get("roundcoords"));
// Path
sb.append("");
// Rendering control points
for(int pcnt=0;pcnt0)&&(segments.get(pcnt)[0]==1.0)){
sb.append( "");
}
if((qcpr>0)&&(segments.get(pcnt)[0]==2.0)){
sb.append( "");
sb.append( "");
sb.append( "");
sb.append( "");
}// End of quadratic control points
}
}// End of svgpathstring()
// Converting tracedata to an SVG string, paths are drawn according to a Z-index
// the optional lcpr and qcpr are linear and quadratic control point radiuses
public static String getsvgstring (IndexedImage ii, HashMap options){
options = checkoptions(options);
// SVG start
int w = (int) (ii.width * options.get("scale")), h = (int) (ii.height * options.get("scale"));
String viewboxorviewport = options.get("viewbox")!=0 ? "viewBox=\"0 0 "+w+" "+h+"\" " : "width=\""+w+"\" height=\""+h+"\" ";
StringBuilder svgstr = new StringBuilder("");
return svgstr.toString();
}// End of getsvgstring()
static String tosvgcolorstr (byte[] c){
return "fill=\"rgb("+(c[0]+128)+","+(c[1]+128)+","+(c[2]+128)+")\" stroke=\"rgb("+(c[0]+128)+","+(c[1]+128)+","+(c[2]+128)+")\" stroke-width=\"1\" opacity=\""+((c[3]+128)/255.0)+"\" ";
}
// Gaussian kernels for blur
static double[][] gks = { {0.27901,0.44198,0.27901}, {0.135336,0.228569,0.272192,0.228569,0.135336}, {0.086776,0.136394,0.178908,0.195843,0.178908,0.136394,0.086776},
{0.063327,0.093095,0.122589,0.144599,0.152781,0.144599,0.122589,0.093095,0.063327}, {0.049692,0.069304,0.089767,0.107988,0.120651,0.125194,0.120651,0.107988,0.089767,0.069304,0.049692} };
// Selective Gaussian blur for preprocessing
static ImageData blur (ImageData imgd, float rad, float del){
int i,j,k,d,idx;
double racc,gacc,bacc,aacc,wacc;
ImageData imgd2 = new ImageData(imgd.width,imgd.height,new byte[imgd.width*imgd.height*4]);
// radius and delta limits, this kernel
int radius = (int)Math.floor(rad); if(radius<1){ return imgd; } if(radius>5){ radius = 5; }
int delta = (int)Math.abs(del); if(delta>1024){ delta = 1024; }
double[] thisgk = gks[radius-1];
// loop through all pixels, horizontal blur
for( j=0; j < imgd.height; j++ ){
for( i=0; i < imgd.width; i++ ){
racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0;
// gauss kernel loop
for( k = -radius; k < (radius+1); k++){
// add weighted color values
if( ((i+k) > 0) && ((i+k) < imgd.width) ){
idx = ((j*imgd.width)+i+k)*4;
racc += imgd.data[idx ] * thisgk[k+radius];
gacc += imgd.data[idx+1] * thisgk[k+radius];
bacc += imgd.data[idx+2] * thisgk[k+radius];
aacc += imgd.data[idx+3] * thisgk[k+radius];
wacc += thisgk[k+radius];
}
}
// The new pixel
idx = ((j*imgd.width)+i)*4;
imgd2.data[idx ] = (byte) Math.floor(racc / wacc);
imgd2.data[idx+1] = (byte) Math.floor(gacc / wacc);
imgd2.data[idx+2] = (byte) Math.floor(bacc / wacc);
imgd2.data[idx+3] = (byte) Math.floor(aacc / wacc);
}// End of width loop
}// End of horizontal blur
// copying the half blurred imgd2
byte[] himgd = imgd2.data.clone();
// loop through all pixels, vertical blur
for( j=0; j < imgd.height; j++ ){
for( i=0; i < imgd.width; i++ ){
racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0;
// gauss kernel loop
for( k = -radius; k < (radius+1); k++){
// add weighted color values
if( ((j+k) > 0) && ((j+k) < imgd.height) ){
idx = (((j+k)*imgd.width)+i)*4;
racc += himgd[idx ] * thisgk[k+radius];
gacc += himgd[idx+1] * thisgk[k+radius];
bacc += himgd[idx+2] * thisgk[k+radius];
aacc += himgd[idx+3] * thisgk[k+radius];
wacc += thisgk[k+radius];
}
}
// The new pixel
idx = ((j*imgd.width)+i)*4;
imgd2.data[idx ] = (byte) Math.floor(racc / wacc);
imgd2.data[idx+1] = (byte) Math.floor(gacc / wacc);
imgd2.data[idx+2] = (byte) Math.floor(bacc / wacc);
imgd2.data[idx+3] = (byte) Math.floor(aacc / wacc);
}// End of width loop
}// End of vertical blur
// Selective blur: loop through all pixels
for( j=0; j < imgd.height; j++ ){
for( i=0; i < imgd.width; i++ ){
idx = ((j*imgd.width)+i)*4;
// d is the difference between the blurred and the original pixel
d = Math.abs(imgd2.data[idx ] - imgd.data[idx ]) + Math.abs(imgd2.data[idx+1] - imgd.data[idx+1]) +
Math.abs(imgd2.data[idx+2] - imgd.data[idx+2]) + Math.abs(imgd2.data[idx+3] - imgd.data[idx+3]);
// selective blur: if d>delta, put the original pixel back
if(d>delta){
imgd2.data[idx ] = imgd.data[idx ];
imgd2.data[idx+1] = imgd.data[idx+1];
imgd2.data[idx+2] = imgd.data[idx+2];
imgd2.data[idx+3] = imgd.data[idx+3];
}
}
}// End of Selective blur
return imgd2;
}// End of blur()
}// End of ImageTracer class
================================================
FILE: process_overview.md
================================================
### Process overview
#### 1. Color quantization
The **colorquantization** function creates an indexed image (https://en.wikipedia.org/wiki/Indexed_color)

#### 2. Layer separation and edge detection
The **layering** function creates arrays for every color, and calculates edge node types. These are at the center of every 4 pixels, shown here as dots.



#### 3. Pathscan
The **pathscan** function finds chains of edge nodes, example: the cyan dots and lines.

#### 4. Interpolation
The **internodes** function interpolates the coordinates of the edge node paths. Every line segment in the new path has one of the 8 directions (East, North East, N, NW, W, SW, S, SE).


#### 5. Tracing
The **tracepath** function splits the interpolated paths into sequences with two directions.

The **fitseq** function tries to fit a straight line on the start- and endpoint of the sequence (black line). If the distance error between the calculated points (black line) and actual sequence points (blue dots) is greater than the treshold, the point with the greatest error is selected (red line).

The **fitseq** function tries to fit a quadratic spline through the error point.



If the **fitseq** function can not fit a straight line or a quadratic spline to the sequence with the given error tresholds, then it will split the sequence in two and recursively call **fitseq** on each part.
#### 6. SVG rendering
The coordinates are rendered to [SVG Paths](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths) in the **getsvgstring** function.
### Ideas for improvement
- Error handling: there's very little error handling now, Out of memory can happen easily with big images or many layers.
- Color quantization: other algorithms?
- Color quantization: colors with few pixels are randomized, but probably the most distant colors should be found instead.
- Tracing: 5.1. finding more suitable sequences.
- Tracing: 5.5. Set splitpoint = (fitting point + errorpoint)/2 ; this is just a guess, there might be a better splitpoint.
- Tracing: 5.7. If splitpoint-endpoint is a spline, try to add new points from the next sequence; this is not implemented.
- Tracing: cubic splines or other curves?
- Default values: they are chosen because they seemed OK, not based on calculations.
- Output: [PDF](https://en.wikipedia.org/wiki/Portable_Document_Format), [DXF](https://en.wikipedia.org/wiki/AutoCAD_DXF), [G-code](https://en.wikipedia.org/wiki/G-code) or other output?