diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index bdb0cab..0000000 --- a/.gitattributes +++ /dev/null @@ -1,17 +0,0 @@ -# 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 diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 27b36bd..0000000 --- a/.gitignore +++ /dev/null @@ -1,44 +0,0 @@ -# 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 \ No newline at end of file diff --git a/.project b/.project index 4b5cb1b..d4de5ee 100644 --- a/.project +++ b/.project @@ -1,6 +1,6 @@ - jankovicsandras.imagetracer-java + imagetracer @@ -10,8 +10,14 @@ + + org.eclipse.m2e.core.maven2Builder + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs deleted file mode 100644 index 785033e..0000000 --- a/.settings/org.eclipse.core.resources.prefs +++ /dev/null @@ -1,2 +0,0 @@ -eclipse.preferences.version=1 -encoding//jankovicsandras/imagetracer/ImageTracer.java=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index 3a21537..0000000 --- a/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,11 +0,0 @@ -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 diff --git a/ImageTracer.jar b/ImageTracer.jar deleted file mode 100644 index cb81f1a..0000000 Binary files a/ImageTracer.jar and /dev/null differ diff --git a/README.md b/README.md index d7537f4..a2a3944 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,16 @@ Simple raster image tracer and vectorizer written in Java for desktop. See https by András Jankovics This is a port of imagetracer.js: https://github.com/jankovicsandras/imagetracerjs +### 1.1.4 + +- No changes in algorithm +- Ported the project setup to maven + +### 1.1.3 + +- Imported Quantize.java by Adam Doppelt to replace the old quantization algorithm, which, because it was based on K-means, it averaged the colors, resulting in a non-intuitive final result, where the palette was less saturated than the original. +- Refactored the code so that it is distributed along smaller files. + ### 1.1.2 @@ -24,15 +34,16 @@ This is a port of imagetracer.js: https://github.com/jankovicsandras/imagetracer - ```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 +- background field is removed from the results of color quantization -### Running as a standalone program +### Running as a standalone program Warning: if the outfilename parameter is not specified, then this will overwrite .svg . -Basic usage: +Basic usage: ```bash -java -jar ImageTracer.jar smiley.png +mvn package +java -jar target/ImageTracer-1.1.4.jar smiley.png ``` With options: @@ -41,7 +52,7 @@ java -jar ImageTracer.jar smiley.png outfilename output.svg ltres 1 qtres 1 path ``` ### Including in Java projects -Add ImageTracer.jar to your build path, import, then use the static methods: +Add ImageTracer-1.1.4.jar to your build path, import, then use the static methods: ```java import jankovicsandras.imagetracer.ImageTracer; @@ -112,7 +123,7 @@ See [options for deterministic tracing](https://github.com/jankovicsandras/image |```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| |-------------|---------|-------| @@ -123,7 +134,7 @@ See [options for deterministic tracing](https://github.com/jankovicsandras/image ```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| |-----------|-------------|-------| diff --git a/deterministic.md b/deterministic.md deleted file mode 100644 index 8562319..0000000 --- a/deterministic.md +++ /dev/null @@ -1,46 +0,0 @@ -## 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. - diff --git a/docimages/quantization/correctly_quantized.png b/docimages/quantization/correctly_quantized.png new file mode 100644 index 0000000..d8f0b7d Binary files /dev/null and b/docimages/quantization/correctly_quantized.png differ diff --git a/docimages/quantization/original.png b/docimages/quantization/original.png new file mode 100644 index 0000000..95c0190 Binary files /dev/null and b/docimages/quantization/original.png differ diff --git a/docimages/quantization/original_quantization_with_samples.png b/docimages/quantization/original_quantization_with_samples.png new file mode 100644 index 0000000..9b42b72 Binary files /dev/null and b/docimages/quantization/original_quantization_with_samples.png differ diff --git a/docimages/quantization/original_quantization_without_samples.png b/docimages/quantization/original_quantization_without_samples.png new file mode 100644 index 0000000..e8c7d80 Binary files /dev/null and b/docimages/quantization/original_quantization_without_samples.png differ diff --git a/jankovicsandras/imagetracer/ImageTracer.java b/jankovicsandras/imagetracer/ImageTracer.java deleted file mode 100644 index 7c65a9b..0000000 --- a/jankovicsandras/imagetracer/ImageTracer.java +++ /dev/null @@ -1,992 +0,0 @@ -/* - 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(""); - - // creating Z-index - TreeMap zindex = new TreeMap (); - double label; - // Layer loop - for(int k=0; k entry : zindex.entrySet()) { - if(options.get("desc")!=0){ thisdesc = "desc=\"l "+entry.getValue()[0]+" p "+entry.getValue()[1]+"\" "; }else{ thisdesc = ""; } - svgpathstring(svgstr, - thisdesc, - ii.layers.get(entry.getValue()[0]).get(entry.getValue()[1]), - tosvgcolorstr(ii.palette[entry.getValue()[0]]), - options); - } - - // SVG End - svgstr.append(""); - - 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 \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5424c39 --- /dev/null +++ b/pom.xml @@ -0,0 +1,78 @@ + + 4.0.0 + jankovicsandras.imagetracer + ImageTracer + 1.1.4 + imagetracer + Simple image tracer + + UTF-8 + 8 + 1.8 + 1.8 + 4.13-beta-3 + 3.1.1 + 1.22.0 + + + + + + de.erichseifert.vectorgraphics2d + VectorGraphics2D + 0.13 + + + + + junit + junit + ${junit.version} + test + + + + + + + com.diffplug.spotless + spotless-maven-plugin + ${plugin.diffplug.spotless.version} + + + + + + + + scala,java,javax,org,com + + + + + + compile + + apply + + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + jankovicsandras.imagetracer.ImageTracer + + + + + + + diff --git a/process_overview.md b/process_overview.md deleted file mode 100644 index f22dcbd..0000000 --- a/process_overview.md +++ /dev/null @@ -1,54 +0,0 @@ -### Process overview -#### 1. Color quantization -The **colorquantization** function creates an indexed image (https://en.wikipedia.org/wiki/Indexed_color) - -![alt Original image (20x scale)](docimages/s2.png) - -#### 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. - -![alt layer 0: black](docimages/s3.png) -![alt layer 1: yellow](docimages/s4.png) -![alt edge node examples](docimages/s7.png) - -#### 3. Pathscan -The **pathscan** function finds chains of edge nodes, example: the cyan dots and lines. - -![alt an edge node path](docimages/s8.png) - -#### 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). - -![alt interpolating](docimages/s9.png) -![alt interpolation result](docimages/s10.png) - -#### 5. Tracing -The **tracepath** function splits the interpolated paths into sequences with two directions. - -![alt a sequence](docimages/s11.png) - -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). - -![alt fitting a straight line](docimages/s12.png) - -The **fitseq** function tries to fit a quadratic spline through the error point. - -![alt fitting a quadratic spline](docimages/s13.png) -![alt fitting line segments](docimages/s14.png) -![alt result with control points](docimages/s15.png) - -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? diff --git a/src/main/java/jankovicsandras/imagetracer/ImageTracer.java b/src/main/java/jankovicsandras/imagetracer/ImageTracer.java new file mode 100644 index 0000000..7c2560a --- /dev/null +++ b/src/main/java/jankovicsandras/imagetracer/ImageTracer.java @@ -0,0 +1,255 @@ + +package jankovicsandras.imagetracer; + +import java.awt.Color; +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 javax.imageio.ImageIO; + + +public class ImageTracer{ + + public static String versionnumber = "1.1.3"; + private static int[] rawdata; + + 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"); + + + //System.out.println("Starting anyway with default value for testing purposes."); + //saveString("output.svg",imageToSVG("input.jpg",new HashMap())); + + + + } 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 1 numberofcolors 128 colorquantcycles 15 "+ + "scale 1 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]; + HashMap options = new HashMap(); + String[] parameternames = {"ltres","qtres","pathomit","numberofcolors","colorquantcycles","format","scale","roundcoords","lcpr","qcpr","desc","viewbox","outfilename", "blurammount"}; + 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 + + options = checkoptions(options); + + // Loading image, tracing, rendering, saving output file + if (options.get("format") == 0) { + saveString(outfilename + ".svg", imageToSVG(args[0], options)); + } else if (options.get("format") == 1) { + saveString(outfilename + ".pdf", imageToPDF(args[0], options)); + } else { + System.out.println("ERROR: Incorrect output format. Options: 0 - SVG, 1 - PDF"); + } + + }// 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, HashMap options) 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(); + 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); } + } + + public static byte[][] getPalette(BufferedImage image, HashMap options){ + int numberofcolors = options.get("numberofcolors").intValue(); + int[][] pixels = new int[image.getWidth()][image.getHeight()]; + + for(int i = 0; i < image.getWidth(); i++) + for(int j = 0; j < image.getHeight(); j++){ + pixels[i][j] = image.getRGB(i, j); + } + int[] palette = Quantize.quantizeImage(pixels, numberofcolors); + byte[][] bytepalette = new byte[numberofcolors][4]; + + for(int i = 0; i < palette.length; i++) { + Color c = new Color(palette[i]); + bytepalette[i][0] = (byte) c.getRed(); + bytepalette[i][1] = (byte) c.getGreen(); + bytepalette[i][2] = (byte) c.getBlue(); + bytepalette[i][3] = 0; + } + return bytepalette; + } + + //////////////////////////////////////////////////////////// + // + // 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) throws Exception{ + System.out.println(options.toString()); + ImageData imgd = loadImageData(filename, options); + return imagedataToSVG(imgd,options, getPalette(ImageIO.read(new File(filename)), options)); + }// End of imageToSVG() + + // Tracing ImageData, then returning the SVG String + public static String imagedataToSVG (ImageData imgd, HashMap options, byte [][] palette){ + IndexedImage ii = imagedataToTracedata(imgd,options,palette); + return SVGUtils.getsvgstring(ii, options); + }// End of imagedataToSVG() + + // Loading an image from a file, tracing when loaded, then returning PDF String + public static String imageToPDF (String filename, HashMap options) throws Exception { + ImageData imgd = loadImageData(filename, options); + IndexedImage ii = imagedataToTracedata(imgd,options,getPalette(ImageIO.read(new File(filename)), options)); + return PDFUtils.getPDFString(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{ + ImageData imgd = loadImageData(filename, options); + return imagedataToTracedata(imgd,options,palette); + }// End of imageToTracedata() + public IndexedImage imageToTracedata (BufferedImage image, HashMap options, byte [][] palette) throws Exception{ + 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 = VectorizingUtils.colorquantization(imgd, palette, options); + // 2. Layer separation and edge detection + int[][][] rawlayers = VectorizingUtils.layering(ii); + // 3. Batch pathscan + ArrayList>> bps = VectorizingUtils.batchpathscan(rawlayers,(int)(Math.floor(options.get("pathomit")))); + // 4. Batch interpollation + ArrayList>> bis = VectorizingUtils.batchinternodes(bps); + // 5. Batch tracing + ii.layers = VectorizingUtils.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",10f); } + if(!options.containsKey("qtres")){ options.put("qtres",10f); } + if(!options.containsKey("pathomit")){ options.put("pathomit",1f); } + // Color quantization + if(!options.containsKey("numberofcolors")){ options.put("numberofcolors",128f); } + if(!options.containsKey("colorquantcycles")){ options.put("colorquantcycles",15f); } + // Output rendering + if(!options.containsKey("format")){ options.put("format",0f); } + if(!options.containsKey("scale")){ options.put("scale",1f); } + 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",5f); } + if(!options.containsKey("blurdelta")){ options.put("blurdelta",50f); } + + return options; + }// End of checkoptions() + + + + + +}// End of ImageTracer class diff --git a/src/main/java/jankovicsandras/imagetracer/PDFUtils.java b/src/main/java/jankovicsandras/imagetracer/PDFUtils.java new file mode 100644 index 0000000..0355285 --- /dev/null +++ b/src/main/java/jankovicsandras/imagetracer/PDFUtils.java @@ -0,0 +1,133 @@ +package jankovicsandras.imagetracer; + +import java.awt.*; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; +import java.awt.geom.Rectangle2D; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +import de.erichseifert.vectorgraphics2d.Document; +import de.erichseifert.vectorgraphics2d.VectorGraphics2D; +import de.erichseifert.vectorgraphics2d.intermediate.CommandSequence; +import de.erichseifert.vectorgraphics2d.pdf.PDFProcessor; +import de.erichseifert.vectorgraphics2d.util.PageSize; +import jankovicsandras.imagetracer.ImageTracer.IndexedImage; + +public class PDFUtils { + + public static String getPDFString (IndexedImage ii, HashMap options){ + // Document setup + int w = (int) (ii.width * options.get("scale")), h = (int) (ii.height * options.get("scale")); + VectorGraphics2D vg2d = new VectorGraphics2D(); + + // creating Z-index + TreeMap zindex = new TreeMap (); + double label; + // Layer loop + for(int k=0; k entry : zindex.entrySet()) { + byte[] c = ii.palette[entry.getValue()[0]]; + if(options.get("desc")!=0){ thisdesc = "desc=\"l "+entry.getValue()[0]+" p "+entry.getValue()[1]+"\" "; }else{ thisdesc = ""; } + drawPdfPath(vg2d, + thisdesc, + ii.layers.get(entry.getValue()[0]).get(entry.getValue()[1]), + new Color(c[0]+128, c[1]+128, c[2]+128), + options); + } + + // Write result + PDFProcessor pdfProcessor = new PDFProcessor(false); + CommandSequence commands = vg2d.getCommands(); + Document doc = pdfProcessor.getDocument(commands, new PageSize(0.0, 0.0, w, h)); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + doc.writeTo(bos); + } catch (IOException e) { + e.printStackTrace(); + } + return bos.toString(); + } + + public static void drawPdfPath (VectorGraphics2D graphics, String desc, ArrayList segments, Color color, HashMap options) { + float scale = options.get("scale"), lcpr = options.get("lcpr"), qcpr = options.get("qcpr"), roundcoords = (float) Math.floor(options.get("roundcoords")); + graphics.setColor(color); + + final Path2D path = new Path2D.Double(); + path.moveTo(segments.get(0)[1]*scale, segments.get(0)[2]*scale); + + if( roundcoords == -1 ){ + for(int pcnt=0;pcnt0)&&(segments.get(pcnt)[0]==1.0)){ + graphics.setColor(Color.BLACK); + graphics.fill(new Ellipse2D.Double(segments.get(pcnt)[3]*scale, segments.get(pcnt)[4]*scale, lcpr, lcpr)); + } + if((qcpr>0)&&(segments.get(pcnt)[0]==2.0)){ + graphics.setColor(Color.CYAN); + graphics.setStroke(new BasicStroke((float) (qcpr*0.2), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0F, (float[])null, 0.0F)); + graphics.fill(new Ellipse2D.Double(segments.get(pcnt)[3]*scale, segments.get(pcnt)[4]*scale, qcpr, qcpr)); + graphics.fill(new Ellipse2D.Double(segments.get(pcnt)[5]*scale, segments.get(pcnt)[6]*scale, qcpr, qcpr)); + graphics.draw(new Line2D.Double(segments.get(pcnt)[1]*scale, segments.get(pcnt)[2]*scale, segments.get(pcnt)[3]*scale, segments.get(pcnt)[4]*scale)); + graphics.draw(new Line2D.Double(segments.get(pcnt)[3]*scale, segments.get(pcnt)[4]*scale, segments.get(pcnt)[5]*scale, segments.get(pcnt)[6]*scale)); + }// End of quadratic control points + } + } + + public static float roundtodec (float val, float places){ + return (float)(Math.round(val*Math.pow(10,places))/Math.pow(10,places)); + } +} diff --git a/src/main/java/jankovicsandras/imagetracer/Quantize.java b/src/main/java/jankovicsandras/imagetracer/Quantize.java new file mode 100644 index 0000000..4c5d784 --- /dev/null +++ b/src/main/java/jankovicsandras/imagetracer/Quantize.java @@ -0,0 +1,711 @@ +package jankovicsandras.imagetracer; + + + +/* +* @(#)Quantize.java 0.90 9/19/00 Adam Doppelt +*/ + + +/** +* An efficient color quantization algorithm, adapted from the C++ +* implementation quantize.c in ImageMagick. The pixels for +* an image are placed into an oct tree. The oct tree is reduced in +* size, and the pixels from the original image are reassigned to the +* nodes in the reduced tree.

+* +* Here is the copyright notice from ImageMagick: +* +*

+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%  Permission is hereby granted, free of charge, to any person obtaining a    %
+%  copy of this software and associated documentation files ("ImageMagick"),  %
+%  to deal in ImageMagick without restriction, including without limitation   %
+%  the rights to use, copy, modify, merge, publish, distribute, sublicense,   %
+%  and/or sell copies of ImageMagick, and to permit persons to whom the       %
+%  ImageMagick is furnished to do so, subject to the following conditions:    %
+%                                                                             %
+%  The above copyright notice and this permission notice shall be included in %
+%  all copies or substantial portions of ImageMagick.                         %
+%                                                                             %
+%  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   %
+%  E. I. du Pont de Nemours and Company 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 ImageMagick or the use or other %
+%  dealings in ImageMagick.                                                   %
+%                                                                             %
+%  Except as contained in this notice, the name of the E. I. du Pont de       %
+%  Nemours and Company shall not be used in advertising or otherwise to       %
+%  promote the sale, use or other dealings in ImageMagick without prior       %
+%  written authorization from the E. I. du Pont de Nemours and Company.       %
+%                                                                             %
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+* +* +* @version 0.90 19 Sep 2000 +* @author Adam Doppelt +*/ +public class Quantize { + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% QQQ U U AAA N N TTTTT IIIII ZZZZZ EEEEE % +% Q Q U U A A NN N T I ZZ E % +% Q Q U U AAAAA N N N T I ZZZ EEEEE % +% Q QQ U U A A N NN T I ZZ E % +% QQQQ UUU A A N N T IIIII ZZZZZ EEEEE % +% % +% % +% Reduce the Number of Unique Colors in an Image % +% % +% % +% Software Design % +% John Cristy % +% July 1992 % +% % +% % +% Copyright 1998 E. I. du Pont de Nemours and Company % +% % +% Permission is hereby granted, free of charge, to any person obtaining a % +% copy of this software and associated documentation files ("ImageMagick"), % +% to deal in ImageMagick without restriction, including without limitation % +% the rights to use, copy, modify, merge, publish, distribute, sublicense, % +% and/or sell copies of ImageMagick, and to permit persons to whom the % +% ImageMagick is furnished to do so, subject to the following conditions: % +% % +% The above copyright notice and this permission notice shall be included in % +% all copies or substantial portions of ImageMagick. % +% % +% 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 % +% E. I. du Pont de Nemours and Company 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 ImageMagick or the use or other % +% dealings in ImageMagick. % +% % +% Except as contained in this notice, the name of the E. I. du Pont de % +% Nemours and Company shall not be used in advertising or otherwise to % +% promote the sale, use or other dealings in ImageMagick without prior % +% written authorization from the E. I. du Pont de Nemours and Company. % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% Realism in computer graphics typically requires using 24 bits/pixel to +% generate an image. Yet many graphic display devices do not contain +% the amount of memory necessary to match the spatial and color +% resolution of the human eye. The QUANTIZE program takes a 24 bit +% image and reduces the number of colors so it can be displayed on +% raster device with less bits per pixel. In most instances, the +% quantized image closely resembles the original reference image. +% +% A reduction of colors in an image is also desirable for image +% transmission and real-time animation. +% +% Function Quantize takes a standard RGB or monochrome images and quantizes +% them down to some fixed number of colors. +% +% For purposes of color allocation, an image is a set of n pixels, where +% each pixel is a point in RGB space. RGB space is a 3-dimensional +% vector space, and each pixel, pi, is defined by an ordered triple of +% red, green, and blue coordinates, (ri, gi, bi). +% +% Each primary color component (red, green, or blue) represents an +% intensity which varies linearly from 0 to a maximum value, cmax, which +% corresponds to full saturation of that color. Color allocation is +% defined over a domain consisting of the cube in RGB space with +% opposite vertices at (0,0,0) and (cmax,cmax,cmax). QUANTIZE requires +% cmax = 255. +% +% The algorithm maps this domain onto a tree in which each node +% represents a cube within that domain. In the following discussion +% these cubes are defined by the coordinate of two opposite vertices: +% The vertex nearest the origin in RGB space and the vertex farthest +% from the origin. +% +% The tree's root node represents the the entire domain, (0,0,0) through +% (cmax,cmax,cmax). Each lower level in the tree is generated by +% subdividing one node's cube into eight smaller cubes of equal size. +% This corresponds to bisecting the parent cube with planes passing +% through the midpoints of each edge. +% +% The basic algorithm operates in three phases: Classification, +% Reduction, and Assignment. Classification builds a color +% description tree for the image. Reduction collapses the tree until +% the number it represents, at most, the number of colors desired in the +% output image. Assignment defines the output image's color map and +% sets each pixel's color by reclassification in the reduced tree. +% Our goal is to minimize the numerical discrepancies between the original +% colors and quantized colors (quantization error). +% +% Classification begins by initializing a color description tree of +% sufficient depth to represent each possible input color in a leaf. +% However, it is impractical to generate a fully-formed color +% description tree in the classification phase for realistic values of +% cmax. If colors components in the input image are quantized to k-bit +% precision, so that cmax= 2k-1, the tree would need k levels below the +% root node to allow representing each possible input color in a leaf. +% This becomes prohibitive because the tree's total number of nodes is +% 1 + sum(i=1,k,8k). +% +% A complete tree would require 19,173,961 nodes for k = 8, cmax = 255. +% Therefore, to avoid building a fully populated tree, QUANTIZE: (1) +% Initializes data structures for nodes only as they are needed; (2) +% Chooses a maximum depth for the tree as a function of the desired +% number of colors in the output image (currently log2(colormap size)). +% +% For each pixel in the input image, classification scans downward from +% the root of the color description tree. At each level of the tree it +% identifies the single node which represents a cube in RGB space +% containing the pixel's color. It updates the following data for each +% such node: +% +% n1: Number of pixels whose color is contained in the RGB cube +% which this node represents; +% +% n2: Number of pixels whose color is not represented in a node at +% lower depth in the tree; initially, n2 = 0 for all nodes except +% leaves of the tree. +% +% Sr, Sg, Sb: Sums of the red, green, and blue component values for +% all pixels not classified at a lower depth. The combination of +% these sums and n2 will ultimately characterize the mean color of a +% set of pixels represented by this node. +% +% E: The distance squared in RGB space between each pixel contained +% within a node and the nodes' center. This represents the quantization +% error for a node. +% +% Reduction repeatedly prunes the tree until the number of nodes with +% n2 > 0 is less than or equal to the maximum number of colors allowed +% in the output image. On any given iteration over the tree, it selects +% those nodes whose E count is minimal for pruning and merges their +% color statistics upward. It uses a pruning threshold, Ep, to govern +% node selection as follows: +% +% Ep = 0 +% while number of nodes with (n2 > 0) > required maximum number of colors +% prune all nodes such that E <= Ep +% Set Ep to minimum E in remaining nodes +% +% This has the effect of minimizing any quantization error when merging +% two nodes together. +% +% When a node to be pruned has offspring, the pruning procedure invokes +% itself recursively in order to prune the tree from the leaves upward. +% n2, Sr, Sg, and Sb in a node being pruned are always added to the +% corresponding data in that node's parent. This retains the pruned +% node's color characteristics for later averaging. +% +% For each node, n2 pixels exist for which that node represents the +% smallest volume in RGB space containing those pixel's colors. When n2 +% > 0 the node will uniquely define a color in the output image. At the +% beginning of reduction, n2 = 0 for all nodes except a the leaves of +% the tree which represent colors present in the input image. +% +% The other pixel count, n1, indicates the total number of colors +% within the cubic volume which the node represents. This includes n1 - +% n2 pixels whose colors should be defined by nodes at a lower level in +% the tree. +% +% Assignment generates the output image from the pruned tree. The +% output image consists of two parts: (1) A color map, which is an +% array of color descriptions (RGB triples) for each color present in +% the output image; (2) A pixel array, which represents each pixel as +% an index into the color map array. +% +% First, the assignment phase makes one pass over the pruned color +% description tree to establish the image's color map. For each node +% with n2 > 0, it divides Sr, Sg, and Sb by n2 . This produces the +% mean color of all pixels that classify no lower than this node. Each +% of these colors becomes an entry in the color map. +% +% Finally, the assignment phase reclassifies each pixel in the pruned +% tree to identify the deepest node containing the pixel's color. The +% pixel's value in the pixel array becomes the index of this node's mean +% color in the color map. +% +% With the permission of USC Information Sciences Institute, 4676 Admiralty +% Way, Marina del Rey, California 90292, this code was adapted from module +% ALCOLS written by Paul Raveling. +% +% The names of ISI and USC are not used in advertising or publicity +% pertaining to distribution of the software without prior specific +% written permission from ISI. +% +*/ + +final static boolean QUICK = true; + +final static int MAX_RGB = 255; +final static int MAX_NODES = 266817; +final static int MAX_TREE_DEPTH = 8; + +// these are precomputed in advance +static int SQUARES[]; +static int SHIFT[]; + +static { +SQUARES = new int[MAX_RGB + MAX_RGB + 1]; +for (int i= -MAX_RGB; i <= MAX_RGB; i++) { + SQUARES[i + MAX_RGB] = i * i; +} + +SHIFT = new int[MAX_TREE_DEPTH + 1]; +for (int i = 0; i < MAX_TREE_DEPTH + 1; ++i) { + SHIFT[i] = 1 << (15 - i); +} +} + + +/** +* Reduce the image to the given number of colors. The pixels are +* reduced in place. +* @return The new color palette. +*/ +public static int[] quantizeImage(int pixels[][], int max_colors) { +Cube cube = new Cube(pixels, max_colors); +cube.classification(); +cube.reduction(); +cube.assignment(); +return cube.colormap; +} + +static class Cube { +int pixels[][]; +int max_colors; +int colormap[]; + +Node root; +int depth; + +// counter for the number of colors in the cube. this gets +// recalculated often. +int colors; + +// counter for the number of nodes in the tree +int nodes; + +Cube(int pixels[][], int max_colors) { + this.pixels = pixels; + this.max_colors = max_colors; + + int i = max_colors; + // tree_depth = log max_colors + // 4 + for (depth = 1; i != 0; depth++) { + i /= 4; + } + if (depth > 1) { + --depth; + } + if (depth > MAX_TREE_DEPTH) { + depth = MAX_TREE_DEPTH; + } else if (depth < 2) { + depth = 2; + } + + root = new Node(this); +} + +/* + * Procedure Classification begins by initializing a color + * description tree of sufficient depth to represent each + * possible input color in a leaf. However, it is impractical + * to generate a fully-formed color description tree in the + * classification phase for realistic values of cmax. If + * colors components in the input image are quantized to k-bit + * precision, so that cmax= 2k-1, the tree would need k levels + * below the root node to allow representing each possible + * input color in a leaf. This becomes prohibitive because the + * tree's total number of nodes is 1 + sum(i=1,k,8k). + * + * A complete tree would require 19,173,961 nodes for k = 8, + * cmax = 255. Therefore, to avoid building a fully populated + * tree, QUANTIZE: (1) Initializes data structures for nodes + * only as they are needed; (2) Chooses a maximum depth for + * the tree as a function of the desired number of colors in + * the output image (currently log2(colormap size)). + * + * For each pixel in the input image, classification scans + * downward from the root of the color description tree. At + * each level of the tree it identifies the single node which + * represents a cube in RGB space containing It updates the + * following data for each such node: + * + * number_pixels : Number of pixels whose color is contained + * in the RGB cube which this node represents; + * + * unique : Number of pixels whose color is not represented + * in a node at lower depth in the tree; initially, n2 = 0 + * for all nodes except leaves of the tree. + * + * total_red/green/blue : Sums of the red, green, and blue + * component values for all pixels not classified at a lower + * depth. The combination of these sums and n2 will + * ultimately characterize the mean color of a set of pixels + * represented by this node. + */ +void classification() { + int pixels[][] = this.pixels; + + int width = pixels.length; + int height = pixels[0].length; + + // convert to indexed color + for (int x = width; x-- > 0; ) { + for (int y = height; y-- > 0; ) { + int pixel = pixels[x][y]; + int red = (pixel >> 16) & 0xFF; + int green = (pixel >> 8) & 0xFF; + int blue = (pixel >> 0) & 0xFF; + + // a hard limit on the number of nodes in the tree + if (nodes > MAX_NODES) { + //System.out.println("pruning"); + root.pruneLevel(); + --depth; + } + + // walk the tree to depth, increasing the + // number_pixels count for each node + Node node = root; + for (int level = 1; level <= depth; ++level) { + int id = (((red > node.mid_red ? 1 : 0) << 0) | + ((green > node.mid_green ? 1 : 0) << 1) | + ((blue > node.mid_blue ? 1 : 0) << 2)); + if (node.child[id] == null) { + new Node(node, id, level); + } + node = node.child[id]; + node.number_pixels += SHIFT[level]; + } + + ++node.unique; + node.total_red += red; + node.total_green += green; + node.total_blue += blue; + } + } +} + +/* + * reduction repeatedly prunes the tree until the number of + * nodes with unique > 0 is less than or equal to the maximum + * number of colors allowed in the output image. + * + * When a node to be pruned has offspring, the pruning + * procedure invokes itself recursively in order to prune the + * tree from the leaves upward. The statistics of the node + * being pruned are always added to the corresponding data in + * that node's parent. This retains the pruned node's color + * characteristics for later averaging. + */ +void reduction() { + int threshold = 1; + while (colors > max_colors) { + colors = 0; + threshold = root.reduce(threshold, Integer.MAX_VALUE); + } +} + +/** + * The result of a closest color search. + */ +static class Search { + int distance; + int color_number; +} + +/* + * Procedure assignment generates the output image from the + * pruned tree. The output image consists of two parts: (1) A + * color map, which is an array of color descriptions (RGB + * triples) for each color present in the output image; (2) A + * pixel array, which represents each pixel as an index into + * the color map array. + * + * First, the assignment phase makes one pass over the pruned + * color description tree to establish the image's color map. + * For each node with n2 > 0, it divides Sr, Sg, and Sb by n2. + * This produces the mean color of all pixels that classify no + * lower than this node. Each of these colors becomes an entry + * in the color map. + * + * Finally, the assignment phase reclassifies each pixel in + * the pruned tree to identify the deepest node containing the + * pixel's color. The pixel's value in the pixel array becomes + * the index of this node's mean color in the color map. + */ +void assignment() { + colormap = new int[colors]; + + colors = 0; + root.colormap(); + + int pixels[][] = this.pixels; + + int width = pixels.length; + int height = pixels[0].length; + + Search search = new Search(); + + // convert to indexed color + for (int x = width; x-- > 0; ) { + for (int y = height; y-- > 0; ) { + int pixel = pixels[x][y]; + int red = (pixel >> 16) & 0xFF; + int green = (pixel >> 8) & 0xFF; + int blue = (pixel >> 0) & 0xFF; + + // walk the tree to find the cube containing that color + Node node = root; + for ( ; ; ) { + int id = (((red > node.mid_red ? 1 : 0) << 0) | + ((green > node.mid_green ? 1 : 0) << 1) | + ((blue > node.mid_blue ? 1 : 0) << 2) ); + if (node.child[id] == null) { + break; + } + node = node.child[id]; + } + + if (QUICK) { + // if QUICK is set, just use that + // node. Strictly speaking, this isn't + // necessarily best match. + pixels[x][y] = node.color_number; + } else { + // Find the closest color. + search.distance = Integer.MAX_VALUE; + node.parent.closestColor(red, green, blue, search); + pixels[x][y] = search.color_number; + } + } + } +} + +/** + * A single Node in the tree. + */ +static class Node { + Cube cube; + + // parent node + Node parent; + + // child nodes + Node child[]; + int nchild; + + // our index within our parent + int id; + // our level within the tree + int level; + // our color midpoint + int mid_red; + int mid_green; + int mid_blue; + + // the pixel count for this node and all children + int number_pixels; + + // the pixel count for this node + int unique; + // the sum of all pixels contained in this node + int total_red; + int total_green; + int total_blue; + + // used to build the colormap + int color_number; + + Node(Cube cube) { + this.cube = cube; + this.parent = this; + this.child = new Node[8]; + this.id = 0; + this.level = 0; + + this.number_pixels = Integer.MAX_VALUE; + + this.mid_red = (MAX_RGB + 1) >> 1; + this.mid_green = (MAX_RGB + 1) >> 1; + this.mid_blue = (MAX_RGB + 1) >> 1; + } + + Node(Node parent, int id, int level) { + this.cube = parent.cube; + this.parent = parent; + this.child = new Node[8]; + this.id = id; + this.level = level; + + // add to the cube + ++cube.nodes; + if (level == cube.depth) { + ++cube.colors; + } + + // add to the parent + ++parent.nchild; + parent.child[id] = this; + + // figure out our midpoint + int bi = (1 << (MAX_TREE_DEPTH - level)) >> 1; + mid_red = parent.mid_red + ((id & 1) > 0 ? bi : -bi); + mid_green = parent.mid_green + ((id & 2) > 0 ? bi : -bi); + mid_blue = parent.mid_blue + ((id & 4) > 0 ? bi : -bi); + } + + /** + * Remove this child node, and make sure our parent + * absorbs our pixel statistics. + */ + void pruneChild() { + --parent.nchild; + parent.unique += unique; + parent.total_red += total_red; + parent.total_green += total_green; + parent.total_blue += total_blue; + parent.child[id] = null; + --cube.nodes; + cube = null; + parent = null; + } + + /** + * Prune the lowest layer of the tree. + */ + void pruneLevel() { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + child[id].pruneLevel(); + } + } + } + if (level == cube.depth) { + pruneChild(); + } + } + + /** + * Remove any nodes that have fewer than threshold + * pixels. Also, as long as we're walking the tree: + * + * - figure out the color with the fewest pixels + * - recalculate the total number of colors in the tree + */ + int reduce(int threshold, int next_threshold) { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + next_threshold = child[id].reduce(threshold, next_threshold); + } + } + } + if (number_pixels <= threshold) { + pruneChild(); + } else { + if (unique != 0) { + cube.colors++; + } + if (number_pixels < next_threshold) { + next_threshold = number_pixels; + } + } + return next_threshold; + } + + /* + * colormap traverses the color cube tree and notes each + * colormap entry. A colormap entry is any node in the + * color cube tree where the number of unique colors is + * not zero. + */ + void colormap() { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + child[id].colormap(); + } + } + } + if (unique != 0) { + int r = ((total_red + (unique >> 1)) / unique); + int g = ((total_green + (unique >> 1)) / unique); + int b = ((total_blue + (unique >> 1)) / unique); + cube.colormap[cube.colors] = ((( 0xFF) << 24) | + ((r & 0xFF) << 16) | + ((g & 0xFF) << 8) | + ((b & 0xFF) << 0)); + color_number = cube.colors++; + } + } + + /* ClosestColor traverses the color cube tree at a + * particular node and determines which colormap entry + * best represents the input color. + */ + void closestColor(int red, int green, int blue, Search search) { + if (nchild != 0) { + for (int id = 0; id < 8; id++) { + if (child[id] != null) { + child[id].closestColor(red, green, blue, search); + } + } + } + + if (unique != 0) { + int color = cube.colormap[color_number]; + int distance = distance(color, red, green, blue); + if (distance < search.distance) { + search.distance = distance; + search.color_number = color_number; + } + } + } + + /** + * Figure out the distance between this node and som color. + */ + final static int distance(int color, int r, int g, int b) { + return (SQUARES[((color >> 16) & 0xFF) - r + MAX_RGB] + + SQUARES[((color >> 8) & 0xFF) - g + MAX_RGB] + + SQUARES[((color >> 0) & 0xFF) - b + MAX_RGB]); + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + if (parent == this) { + buf.append("root"); + } else { + buf.append("node"); + } + buf.append(' '); + buf.append(level); + buf.append(" ["); + buf.append(mid_red); + buf.append(','); + buf.append(mid_green); + buf.append(','); + buf.append(mid_blue); + buf.append(']'); + return new String(buf); + } +} +} +} + + + + + diff --git a/src/main/java/jankovicsandras/imagetracer/SVGUtils.java b/src/main/java/jankovicsandras/imagetracer/SVGUtils.java new file mode 100644 index 0000000..5ef6c30 --- /dev/null +++ b/src/main/java/jankovicsandras/imagetracer/SVGUtils.java @@ -0,0 +1,133 @@ +package jankovicsandras.imagetracer; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.TreeMap; +import java.util.Map.Entry; + +import jankovicsandras.imagetracer.ImageTracer.IndexedImage; + +public class SVGUtils { + + + + + //////////////////////////////////////////////////////////// + // + // SVG Drawing functions + // + //////////////////////////////////////////////////////////// + + public static float roundtodec (float val, float places){ + return (float)(Math.round(val*Math.pow(10,places))/Math.pow(10,places)); + } + + + // Getting SVG path element string from a traced path + public static void svgpathstring (StringBuilder sb, String desc, ArrayList 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){ + // 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(""); + + // creating Z-index + TreeMap zindex = new TreeMap (); + double label; + // Layer loop + for(int k=0; k entry : zindex.entrySet()) { + if(options.get("desc")!=0){ thisdesc = "desc=\"l "+entry.getValue()[0]+" p "+entry.getValue()[1]+"\" "; }else{ thisdesc = ""; } + svgpathstring(svgstr, + thisdesc, + ii.layers.get(entry.getValue()[0]).get(entry.getValue()[1]), + tosvgcolorstr(ii.palette[entry.getValue()[0]]), + options); + } + + // SVG End + svgstr.append(""); + + 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)+"\" "; + } + + +} diff --git a/src/main/java/jankovicsandras/imagetracer/SelectiveBlur.java b/src/main/java/jankovicsandras/imagetracer/SelectiveBlur.java new file mode 100644 index 0000000..4a003c5 --- /dev/null +++ b/src/main/java/jankovicsandras/imagetracer/SelectiveBlur.java @@ -0,0 +1,102 @@ +package jankovicsandras.imagetracer; + +import jankovicsandras.imagetracer.ImageTracer.ImageData; + +public class SelectiveBlur { + + + // 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() +} diff --git a/src/main/java/jankovicsandras/imagetracer/VectorizingUtils.java b/src/main/java/jankovicsandras/imagetracer/VectorizingUtils.java new file mode 100644 index 0000000..d4a02a9 --- /dev/null +++ b/src/main/java/jankovicsandras/imagetracer/VectorizingUtils.java @@ -0,0 +1,477 @@ +package jankovicsandras.imagetracer; + +import java.util.ArrayList; +import java.util.HashMap; + +import jankovicsandras.imagetracer.ImageTracer.ImageData; +import jankovicsandras.imagetracer.ImageTracer.IndexedImage; + +public class VectorizingUtils { + + + + + //////////////////////////////////////////////////////////// + // + // 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){ + + // Selective Gaussian blur preprocessing + if( options.get("blurradius") > 0 ){ imgd = SelectiveBlur.blur( imgd, options.get("blurradius"), options.get("blurdelta") ); } + + 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; + + + byte [][] original_palette_backup = palette; + 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