JavaCAD
ImageTracer.java
Go to the documentation of this file.
1 /*
2  ImageTracer.java
3  (Desktop version with javax.imageio. See ImageTracerAndroid.java for the Android version.)
4  Simple raster image tracer and vectorizer written in Java. This is a port of imagetracer.js.
5  by AndrĂ¡s Jankovics 2015, 2016
6  andras@jankovics.net
7 
8  */
9 
10 /*
11 
12 The Unlicense / PUBLIC DOMAIN
13 
14 This is free and unencumbered software released into the public domain.
15 
16 Anyone is free to copy, modify, publish, use, compile, sell, or
17 distribute this software, either in source code form or as a compiled
18 binary, for any purpose, commercial or non-commercial, and by any
19 means.
20 
21 In jurisdictions that recognize copyright laws, the author or authors
22 of this software dedicate any and all copyright interest in the
23 software to the public domain. We make this dedication for the benefit
24 of the public at large and to the detriment of our heirs and
25 successors. We intend this dedication to be an overt act of
26 relinquishment in perpetuity of all present and future rights to this
27 software under copyright law.
28 
29 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
30 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
31 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
32 IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
33 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
34 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
35 OTHER DEALINGS IN THE SOFTWARE.
36 
37 For more information, please refer to http://unlicense.org/
38 
39  */
40 package eu.mihosoft.vrl.v3d.svg;
41 
42 import java.awt.image.BufferedImage;
43 import java.io.BufferedWriter;
44 import java.io.File;
45 import java.io.FileWriter;
46 import java.util.ArrayList;
47 import java.util.HashMap;
48 import java.util.Map.Entry;
49 import java.util.TreeMap;
50 
51 import javax.imageio.ImageIO;
52 
53 public class ImageTracer{
54 
55  public static String versionnumber = "1.1.1";
56 
57  public static int arraycontains(String [] arr, String str){
58  for(int j=0; j<arr.length; j++ ){ if(arr[j].toLowerCase().equals(str)){ return j; } } return -1;
59  }
60 
61  public static float parsenext(String [] arr, int i){
62  if(i<(arr.length-1)){ try{ return Float.parseFloat(arr[i+1]); }catch(Exception e){} } return -1;
63  }
64 
65  // Container for the color-indexed image before and tracedata after vectorizing
66  public static class IndexedImage{
67  public int width, height;
68  public int [][] array; // array[x][y] of palette colors
69  public byte [][] palette;// array[palettelength][4] RGBA color palette
70  public ArrayList<ArrayList<ArrayList<Double[]>>> layers;// tracedata
71 
72  public IndexedImage(int [][] marray, byte [][] mpalette){
73  array = marray; palette = mpalette;
74  width = marray[0].length-2; height = marray.length-2;// Color quantization adds +2 to the original width and height
75  }
76  }
77 
78  // https://developer.mozilla.org/en-US/docs/Web/API/ImageData
79  public static class ImageData{
80  public int width, height;
81  public byte[] data; // raw byte data: R G B A R G B A ...
82  public ImageData(int mwidth, int mheight, byte[] mdata){
83  width = mwidth; height = mheight; data = mdata;
84  }
85  }
86 
87  // Saving a String as a file
88  public static void saveString(String filename, String str) throws Exception {
89  File file = new File(filename);
90  // if file doesnt exists, then create it
91  if(!file.exists()){ file.createNewFile(); }
92  FileWriter fw = new FileWriter(file.getAbsoluteFile());
93  BufferedWriter bw = new BufferedWriter(fw);
94  bw.write(str);
95  bw.close();
96  }
97 
98  // Loading a file to ImageData, ARGB byte order
99  public static ImageData loadImageData(String filename) throws Exception {
100  BufferedImage image = ImageIO.read(new File(filename));
101  return loadImageData(image);
102  }
103  public static ImageData loadImageData(BufferedImage image) throws Exception {
104  int width = image.getWidth(); int height = image.getHeight();
105  int[] rawdata = image.getRGB(0, 0, width, height, null, 0, width);
106  byte[] data = new byte[rawdata.length*4];
107  for(int i=0; i<rawdata.length; i++){
108  data[(i*4)+3] = bytetrans((byte)(rawdata[i] >>> 24));
109  data[i*4 ] = bytetrans((byte)(rawdata[i] >>> 16));
110  data[(i*4)+1] = bytetrans((byte)(rawdata[i] >>> 8));
111  data[(i*4)+2] = bytetrans((byte)(rawdata[i]));
112  }
113  return new ImageData(width,height,data);
114  }
115 
116  // The bitshift method in loadImageData creates signed bytes where -1 -> 255 unsigned ; -128 -> 128 unsigned ;
117  // 127 -> 127 unsigned ; 0 -> 0 unsigned ; These will be converted to -128 (representing 0 unsigned) ...
118  // 127 (representing 255 unsigned) and tosvgcolorstr will add +128 to create RGB values 0..255
119  public static byte bytetrans(byte b){
120  if(b<0){ return (byte)(b+128); }else{ return (byte)(b-128); }
121  }
122 
124  //
125  // User friendly functions
126  //
128 
129  // Loading an image from a file, tracing when loaded, then returning the SVG String
130  public static String imageToSVG (String filename, HashMap<String,Float> options, byte [][] palette) throws Exception{
131  options = checkoptions(options);
132  ImageData imgd = loadImageData(filename);
133  return imagedataToSVG(imgd,options,palette);
134  }// End of imageToSVG()
135  public static String imageToSVG(BufferedImage image, HashMap<String,Float> options, byte [][] palette) throws Exception{
136  options = checkoptions(options);
137  ImageData imgd = loadImageData(image);
138  return imagedataToSVG(imgd,options,palette);
139  }// End of imageToSVG()
140 
141  // Tracing ImageData, then returning the SVG String
142  public static String imagedataToSVG (ImageData imgd, HashMap<String,Float> options, byte [][] palette){
143  options = checkoptions(options);
144  IndexedImage ii = imagedataToTracedata(imgd,options,palette);
145  return getsvgstring(ii, options);
146  }// End of imagedataToSVG()
147 
148  // Loading an image from a file, tracing when loaded, then returning IndexedImage with tracedata in layers
149  public IndexedImage imageToTracedata(String filename, HashMap<String,Float> options, byte [][] palette) throws Exception{
150  options = checkoptions(options);
151  ImageData imgd = loadImageData(filename);
152  return imagedataToTracedata(imgd,options,palette);
153  }// End of imageToTracedata()
154  public IndexedImage imageToTracedata(BufferedImage image, HashMap<String,Float> options, byte [][] palette) throws Exception{
155  options = checkoptions(options);
156  ImageData imgd = loadImageData(image);
157  return imagedataToTracedata(imgd,options,palette);
158  }// End of imageToTracedata()
159 
160  // Tracing ImageData, then returning IndexedImage with tracedata in layers
161  public static IndexedImage imagedataToTracedata (ImageData imgd, HashMap<String,Float> options, byte [][] palette){
162  // 1. Color quantization
163  IndexedImage ii = colorquantization(imgd, palette, options);
164  // 2. Layer separation and edge detection
165  int[][][] rawlayers = layering(ii);
166  // 3. Batch pathscan
167  ArrayList<ArrayList<ArrayList<Integer[]>>> bps = batchpathscan(rawlayers,(int)(Math.floor(options.get("pathomit"))));
168  // 4. Batch interpollation
169  ArrayList<ArrayList<ArrayList<Double[]>>> bis = batchinternodes(bps);
170  // 5. Batch tracing
171  ii.layers = batchtracelayers(bis,options.get("ltres"),options.get("qtres"));
172  return ii;
173  }// End of imagedataToTracedata()
174 
175  // creating options object, setting defaults for missing values
176  public static HashMap<String,Float> checkoptions(HashMap<String,Float> options){
177  if(options==null){ options = new HashMap<String,Float>(); }
178  // Tracing
179  if(!options.containsKey("ltres")){ options.put("ltres",1f); }
180  if(!options.containsKey("qtres")){ options.put("qtres",1f); }
181  if(!options.containsKey("pathomit")){ options.put("pathomit",8f); }
182  // Color quantization
183  if(!options.containsKey("colorsampling")){ options.put("colorsampling",1f); }
184  if(!options.containsKey("numberofcolors")){ options.put("numberofcolors",16f); }
185  if(!options.containsKey("mincolorratio")){ options.put("mincolorratio",0.02f); }
186  if(!options.containsKey("colorquantcycles")){ options.put("colorquantcycles",3f); }
187  // SVG rendering
188  if(!options.containsKey("scale")){ options.put("scale",1f); }
189  if(!options.containsKey("simplifytolerance")){ options.put("simplifytolerance",0f); }
190  if(!options.containsKey("roundcoords")){ options.put("roundcoords",1f); }
191  if(!options.containsKey("lcpr")){ options.put("lcpr",0f); }
192  if(!options.containsKey("qcpr")){ options.put("qcpr",0f); }
193  if(!options.containsKey("desc")){ options.put("desc",1f); }
194  if(!options.containsKey("viewbox")){ options.put("viewbox",0f); }
195  // Blur
196  if(!options.containsKey("blurradius")){ options.put("blurradius",0f); }
197  if(!options.containsKey("blurdelta")){ options.put("blurdelta",20f); }
198 
199  return options;
200  }// End of checkoptions()
201 
202 
204  //
205  // Vectorizing functions
206  //
208 
209  // 1. Color quantization repeated "cycles" times, based on K-means clustering
210  // https://en.wikipedia.org/wiki/Color_quantization https://en.wikipedia.org/wiki/K-means_clustering
211  public static IndexedImage colorquantization (ImageData imgd, byte [][] palette, HashMap<String,Float> options){
212  int numberofcolors = (int)Math.floor(options.get("numberofcolors")); float minratio = options.get("mincolorratio"); int cycles = (int)Math.floor(options.get("colorquantcycles"));
213  // Creating indexed color array arr which has a boundary filled with -1 in every direction
214  int [][] arr = new int[imgd.height+2][imgd.width+2];
215  for(int j=0; j<(imgd.height+2); j++){ arr[j][0] = -1; arr[j][imgd.width+1 ] = -1; }
216  for(int i=0; i<(imgd.width+2) ; i++){ arr[0][i] = -1; arr[imgd.height+1][i] = -1; }
217 
218  int idx=0, cd,cdl,ci,c1,c2,c3,c4;
219 
220  // Use custom palette if pal is defined or sample or generate custom length palette
221  if(palette==null){
222  if(options.get("colorsampling")!=0){
223  palette = samplepalette(numberofcolors,imgd);
224  }else{
225  palette = generatepalette(numberofcolors);
226  }
227  }
228 
229  // Selective Gaussian blur preprocessing
230  if( options.get("blurradius") > 0 ){ imgd = blur( imgd, options.get("blurradius"), options.get("blurdelta") ); }
231 
232  long [][] paletteacc = new long[palette.length][5];
233 
234  // Repeat clustering step "cycles" times
235  for(int cnt=0;cnt<cycles;cnt++){
236 
237  // Average colors from the second iteration
238  if(cnt>0){
239  // averaging paletteacc for palette
240  float ratio;
241  for(int k=0;k<palette.length;k++){
242  // averaging
243  if(paletteacc[k][3]>0){
244  palette[k][0] = (byte) (-128+Math.floor(paletteacc[k][0]/paletteacc[k][4]));
245  palette[k][1] = (byte) (-128+Math.floor(paletteacc[k][1]/paletteacc[k][4]));
246  palette[k][2] = (byte) (-128+Math.floor(paletteacc[k][2]/paletteacc[k][4]));
247  palette[k][3] = (byte) (-128+Math.floor(paletteacc[k][3]/paletteacc[k][4]));
248  }
249  ratio = paletteacc[k][4]/(imgd.width*imgd.height);
250 
251  // Randomizing a color, if there are too few pixels and there will be a new cycle
252  if((ratio<minratio)&&(cnt<(cycles-1))){
253  palette[k][0] = (byte) (-128+Math.floor(Math.random()*255));
254  palette[k][1] = (byte) (-128+Math.floor(Math.random()*255));
255  palette[k][2] = (byte) (-128+Math.floor(Math.random()*255));
256  palette[k][3] = (byte) (-128+Math.floor(Math.random()*255));
257  }
258 
259  }// End of palette loop
260  }// End of Average colors from the second iteration
261 
262  // Reseting palette accumulator for averaging
263  for(int i=0;i<palette.length;i++){
264  paletteacc[i][0]=0;
265  paletteacc[i][1]=0;
266  paletteacc[i][2]=0;
267  paletteacc[i][3]=0;
268  paletteacc[i][4]=0;
269  }
270 
271  // loop through all pixels
272  for(int j=0;j<imgd.height;j++){
273  for(int i=0;i<imgd.width;i++){
274 
275  idx = ((j*imgd.width)+i)*4;
276 
277  // find closest color from palette by measuring (rectilinear) color distance between this pixel and all palette colors
278  cdl = 256+256+256+256; ci=0;
279  for(int k=0;k<palette.length;k++){
280 
281  // In my experience, https://en.wikipedia.org/wiki/Rectilinear_distance works better than https://en.wikipedia.org/wiki/Euclidean_distance
282  c1 = Math.abs(palette[k][0]-imgd.data[idx]);
283  c2 = Math.abs(palette[k][1]-imgd.data[idx+1]);
284  c3 = Math.abs(palette[k][2]-imgd.data[idx+2]);
285  c4 = Math.abs(palette[k][3]-imgd.data[idx+3]);
286  cd = c1+c2+c3+(c4*4); // weighted alpha seems to help images with transparency
287 
288  // Remember this color if this is the closest yet
289  if(cd<cdl){ cdl = cd; ci = k; }
290 
291  }// End of palette loop
292 
293  // add to palettacc
294  paletteacc[ci][0] += 128+imgd.data[idx];
295  paletteacc[ci][1] += 128+imgd.data[idx+1];
296  paletteacc[ci][2] += 128+imgd.data[idx+2];
297  paletteacc[ci][3] += 128+imgd.data[idx+3];
298  paletteacc[ci][4]++;
299 
300  arr[j+1][i+1] = ci;
301  }// End of i loop
302  }// End of j loop
303 
304  }// End of Repeat clustering step "cycles" times
305 
306  return new IndexedImage(arr, palette);
307  }// End of colorquantization
308 
309  // Generating a palette with numberofcolors, array[numberofcolors][4] where [i][0] = R ; [i][1] = G ; [i][2] = B ; [i][3] = A
310  public static byte[][] generatepalette(int numberofcolors){
311  byte [][] palette = new byte[numberofcolors][4];
312  if(numberofcolors<8){
313 
314  // Grayscale
315  byte graystep = (byte) Math.floor(255/(numberofcolors-1));
316  for(byte ccnt=0;ccnt<numberofcolors;ccnt++){
317  palette[ccnt][0] = (byte)(-128+(ccnt*graystep));
318  palette[ccnt][1] = (byte)(-128+(ccnt*graystep));
319  palette[ccnt][2] = (byte)(-128+(ccnt*graystep));
320  palette[ccnt][3] = (byte)255;
321  }
322 
323  }else{
324 
325  // RGB color cube
326  int colorqnum = (int) Math.floor(Math.pow(numberofcolors, 1.0/3.0)); // Number of points on each edge on the RGB color cube
327  int colorstep = (int) Math.floor(255/(colorqnum-1)); // distance between points
328  int ccnt = 0;
329  for(int rcnt=0;rcnt<colorqnum;rcnt++){
330  for(int gcnt=0;gcnt<colorqnum;gcnt++){
331  for(int bcnt=0;bcnt<colorqnum;bcnt++){
332  palette[ccnt][0] = (byte)(-128+(rcnt*colorstep));
333  palette[ccnt][1] = (byte)(-128+(gcnt*colorstep));
334  palette[ccnt][2] = (byte)(-128+(bcnt*colorstep));
335  palette[ccnt][3] = (byte)127;
336  ccnt++;
337  }// End of blue loop
338  }// End of green loop
339  }// End of red loop
340 
341  // Rest is random
342  for(int rcnt=ccnt;rcnt<numberofcolors;rcnt++){
343  palette[ccnt][0] = (byte)(-128+Math.floor(Math.random()*255));
344  palette[ccnt][1] = (byte)(-128+Math.floor(Math.random()*255));
345  palette[ccnt][2] = (byte)(-128+Math.floor(Math.random()*255));
346  palette[ccnt][3] = (byte)(-128+Math.floor(Math.random()*255));
347  }
348 
349  }// End of numberofcolors check
350 
351  return palette;
352  };// End of generatepalette()
353 
354  public static byte[][] samplepalette(int numberofcolors, ImageData imgd){
355  int idx=0; byte [][] palette = new byte[numberofcolors][4];
356  for(int i=0; i<numberofcolors; i++){
357  idx = (int) (Math.floor( (Math.random() * imgd.data.length) / 4 ) * 4);
358  palette[i][0] = imgd.data[idx ];
359  palette[i][1] = imgd.data[idx+1];
360  palette[i][2] = imgd.data[idx+2];
361  palette[i][3] = imgd.data[idx+3];
362  }
363  return palette;
364  }// End of samplepalette()
365 
366  // 2. Layer separation and edge detection
367  // Edge node types ( â–“:light or 1; â–‘:dark or 0 )
368  // 12 â–‘â–‘ â–“â–‘ â–‘â–“ â–“â–“ â–‘â–‘ â–“â–‘ â–‘â–“ â–“â–“ â–‘â–‘ â–“â–‘ â–‘â–“ â–“â–“ â–‘â–‘ â–“â–‘ â–‘â–“ â–“â–“
369  // 48 â–‘â–‘ â–‘â–‘ â–‘â–‘ â–‘â–‘ â–‘â–“ â–‘â–“ â–‘â–“ â–‘â–“ â–“â–‘ â–“â–‘ â–“â–‘ â–“â–‘ â–“â–“ â–“â–“ â–“â–“ â–“â–“
370  // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
371  //
372  public static int[][][] layering (IndexedImage ii){
373  // Creating layers for each indexed color in arr
374  int val=0, aw = ii.array[0].length, ah = ii.array.length, n1,n2,n3,n4,n5,n6,n7,n8;
375  int[][][] layers = new int[ii.palette.length][ah][aw];
376 
377  // Looping through all pixels and calculating edge node type
378  for(int j=1; j<(ah-1); j++){
379  for(int i=1; i<(aw-1); i++){
380 
381  // This pixel's indexed color
382  val = ii.array[j][i];
383 
384  // Are neighbor pixel colors the same?
385  if((j>0) && (i>0)) { n1 = ii.array[j-1][i-1]==val?1:0; }else{ n1 = 0; }
386  if (j>0) { n2 = ii.array[j-1][i ]==val?1:0; }else{ n2 = 0; }
387  if((j>0) && (i<(aw-1))){ n3 = ii.array[j-1][i+1]==val?1:0; }else{ n3 = 0; }
388  if (i>0) { n4 = ii.array[j ][i-1]==val?1:0; }else{ n4 = 0; }
389  if (i<(aw-1)) { n5 = ii.array[j ][i+1]==val?1:0; }else{ n5 = 0; }
390  if((j<(ah-1)) && (i>0)) { n6 = ii.array[j+1][i-1]==val?1:0; }else{ n6 = 0; }
391  if (j<(ah-1)) { n7 = ii.array[j+1][i ]==val?1:0; }else{ n7 = 0; }
392  if((j<(ah-1)) && (i<(aw-1))){ n8 = ii.array[j+1][i+1]==val?1:0; }else{ n8 = 0; }
393 
394  // this pixel"s type and looking back on previous pixels
395  layers[val][j+1][i+1] = 1 + (n5 * 2) + (n8 * 4) + (n7 * 8) ;
396  if(n4==0){ layers[val][j+1][i ] = 0 + 2 + (n7 * 4) + (n6 * 8) ; }
397  if(n2==0){ layers[val][j ][i+1] = 0 + (n3*2) + (n5 * 4) + 8 ; }
398  if(n1==0){ layers[val][j ][i ] = 0 + (n2*2) + 4 + (n4 * 8) ; }
399 
400  }// End of i loop
401  }// End of j loop
402 
403  return layers;
404  }// End of layering()
405 
406  // 3. Walking through an edge node array, discarding edge node types 0 and 15 and creating paths from the rest.
407  // Walk directions (dir): 0 > ; 1 ^ ; 2 < ; 3 v
408  // Edge node types ( â–“:light or 1; â–‘:dark or 0 )
409  // â–‘â–‘ â–“â–‘ â–‘â–“ â–“â–“ â–‘â–‘ â–“â–‘ â–‘â–“ â–“â–“ â–‘â–‘ â–“â–‘ â–‘â–“ â–“â–“ â–‘â–‘ â–“â–‘ â–‘â–“ â–“â–“
410  // â–‘â–‘ â–‘â–‘ â–‘â–‘ â–‘â–‘ â–‘â–“ â–‘â–“ â–‘â–“ â–‘â–“ â–“â–‘ â–“â–‘ â–“â–‘ â–“â–‘ â–“â–“ â–“â–“ â–“â–“ â–“â–“
411  // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
412  //
413  public static ArrayList<ArrayList<Integer[]>> pathscan (int [][] arr,float pathomit){
414  ArrayList<ArrayList<Integer[]>> paths = new ArrayList<ArrayList<Integer[]>>();
415  ArrayList<Integer[]> thispath;
416  int px=0,py=0,w=arr[0].length,h=arr.length,dir=0;
417  boolean pathfinished=true, holepath = false;
418 
419  for(int j=0;j<h;j++){
420  for(int i=0;i<w;i++){
421  if((arr[j][i]!=0)&&(arr[j][i]!=15)){
422  // Init
423  px = i; py = j;
424  paths.add(new ArrayList<Integer[]>());
425  thispath = paths.get(paths.size()-1);
426  pathfinished = false;
427  // fill paths will be drawn, but hole paths are also required to remove unnecessary edge nodes
428  if(arr[py][px]==1){dir = 0;}
429  if(arr[py][px]==2){dir = 3;}
430  if(arr[py][px]==3){dir = 0;}
431  if(arr[py][px]==4){dir = 1; holepath = false;}
432  if(arr[py][px]==5){dir = 0;}
433  if(arr[py][px]==6){dir = 3;}
434  if(arr[py][px]==7){dir = 0; holepath = true;}
435  if(arr[py][px]==8){dir = 0;}
436  if(arr[py][px]==9){dir = 3;}
437  if(arr[py][px]==10){dir = 3;}
438  if(arr[py][px]==11){dir = 1; holepath = true;}
439  if(arr[py][px]==12){dir = 0;}
440  if(arr[py][px]==13){dir = 3; holepath = true;}
441  if(arr[py][px]==14){dir = 0; holepath = true;}
442  // Path points loop
443  while(!pathfinished){
444 
445  // New path point
446  thispath.add(new Integer[3]);
447  thispath.get(thispath.size()-1)[0] = px-1;
448  thispath.get(thispath.size()-1)[1] = py-1;
449  thispath.get(thispath.size()-1)[2] = arr[py][px];
450 
451  // Node types
452  if(arr[py][px]==1){
453  arr[py][px] = 0;
454  if(dir==0){
455  py--;dir=1;
456  }else if(dir==3){
457  px--;dir=2;
458  }else{pathfinished=true;paths.remove(thispath);}
459  }
460 
461  else if(arr[py][px]==2){
462  arr[py][px] = 0;
463  if(dir==3){
464  px++;dir=0;
465  }else if(dir==2){
466  py--;dir=1;
467  }else{pathfinished=true;paths.remove(thispath);}
468  }
469 
470  else if(arr[py][px]==3){
471  arr[py][px] = 0;
472  if(dir==0){
473  px++;
474  }else if(dir==2){
475  px--;
476  }else{pathfinished=true;paths.remove(thispath);}
477  }
478 
479  else if(arr[py][px]==4){
480  arr[py][px] = 0;
481  if(dir==1){
482  px++;dir=0;
483  }else if(dir==2){
484  py++;dir=3;
485  }else{pathfinished=true;paths.remove(thispath);}
486  }
487 
488  else if(arr[py][px]==5){
489  if(dir==0){
490  arr[py][px] = 13;py++;dir=3;
491  }else if(dir==1){
492  arr[py][px] = 13;px--;dir=2;
493  }else if(dir==2){
494  arr[py][px] = 7;py--;dir=1;
495  }else if(dir==3){
496  arr[py][px] = 7;px++;dir=0;
497  }
498  }
499 
500  else if(arr[py][px]==6){
501  arr[py][px] = 0;
502  if(dir==1){
503  py--;
504  }else if(dir==3){
505  py++;
506  }else{pathfinished=true;paths.remove(thispath);}
507  }
508 
509  else if(arr[py][px]==7){
510  arr[py][px] = 0;
511  if(dir==0){
512  py++;dir=3;
513  }else if(dir==1){
514  px--;dir=2;
515  }else{pathfinished=true;paths.remove(thispath);}
516  }
517 
518  else if(arr[py][px]==8){
519  arr[py][px] = 0;
520  if(dir==0){
521  py++;dir=3;
522  }else if(dir==1){
523  px--;dir=2;
524  }else{pathfinished=true;paths.remove(thispath);}
525  }
526 
527  else if(arr[py][px]==9){
528  arr[py][px] = 0;
529  if(dir==1){
530  py--;
531  }else if(dir==3){
532  py++;
533  }else{pathfinished=true;paths.remove(thispath);}
534  }
535 
536  else if(arr[py][px]==10){
537  if(dir==0){
538  arr[py][px] = 11;py--;dir=1;
539  }else if(dir==1){
540  arr[py][px] = 14;px++;dir=0;
541  }else if(dir==2){
542  arr[py][px] = 14;py++;dir=3;
543  }else if(dir==3){
544  arr[py][px] = 11;px--;dir=2;
545  }
546  }
547 
548  else if(arr[py][px]==11){
549  arr[py][px] = 0;
550  if(dir==1){
551  px++;dir=0;
552  }else if(dir==2){
553  py++;dir=3;
554  }else{pathfinished=true;paths.remove(thispath);}
555  }
556 
557  else if(arr[py][px]==12){
558  arr[py][px] = 0;
559  if(dir==0){
560  px++;
561  }else if(dir==2){
562  px--;
563  }else{pathfinished=true;paths.remove(thispath);}
564  }
565 
566  else if(arr[py][px]==13){
567  arr[py][px] = 0;
568  if(dir==2){
569  py--;dir=1;
570  }else if(dir==3){
571  px++;dir=0;
572  }else{pathfinished=true;paths.remove(thispath);}
573  }
574 
575  else if(arr[py][px]==14){
576  arr[py][px] = 0;
577  if(dir==0){
578  py--;dir=1;
579  }else if(dir==3){
580  px--;dir=2;
581  }else{pathfinished=true;paths.remove(thispath);}
582  }
583 
584  // Close path
585  if(((px-1)==thispath.get(0)[0])&&((py-1)==thispath.get(0)[1])){
586  pathfinished = true;
587  // Discarding 'hole' type paths and paths shorter than pathomit
588  if( (holepath) || (thispath.size()<pathomit) ){
589  paths.remove(thispath);
590  }
591  }
592 
593  }// End of Path points loop
594 
595  }// End of Follow path
596 
597  }// End of i loop
598  }// End of j loop
599 
600  return paths;
601  }// End of pathscan()
602 
603 
604  // 3. Batch pathscan
605  public static ArrayList<ArrayList<ArrayList<Integer[]>>> batchpathscan (int [][][] layers, float pathomit){
606  ArrayList<ArrayList<ArrayList<Integer[]>>> bpaths = new ArrayList<ArrayList<ArrayList<Integer[]>>>();
607  for (int[][] layer : layers) {
608  bpaths.add(pathscan(layer,pathomit));
609  }
610  return bpaths;
611  }
612 
613  // 4. interpolating between path points for nodes with 8 directions ( East, SouthEast, S, SW, W, NW, N, NE )
614  public static ArrayList<ArrayList<Double[]>> internodes (ArrayList<ArrayList<Integer[]>> paths){
615  ArrayList<ArrayList<Double[]>> ins = new ArrayList<ArrayList<Double[]>>();
616  ArrayList<Double[]> thisinp;
617  Double[] thispoint, nextpoint = new Double[2];
618  Integer[] pp1, pp2, pp3;
619 
620  int palen=0,nextidx=0,nextidx2=0;
621  // paths loop
622  for(int pacnt=0; pacnt<paths.size(); pacnt++){
623  ins.add(new ArrayList<Double[]>());
624  thisinp = ins.get(ins.size()-1);
625  palen = paths.get(pacnt).size();
626  // pathpoints loop
627  for(int pcnt=0;pcnt<palen;pcnt++){
628 
629  // interpolate between two path points
630  nextidx = (pcnt+1)%palen; nextidx2 = (pcnt+2)%palen;
631  thisinp.add(new Double[3]);
632  thispoint = thisinp.get(thisinp.size()-1);
633  pp1 = paths.get(pacnt).get(pcnt);
634  pp2 = paths.get(pacnt).get(nextidx);
635  pp3 = paths.get(pacnt).get(nextidx2);
636  thispoint[0] = (pp1[0]+pp2[0]) / 2.0;
637  thispoint[1] = (pp1[1]+pp2[1]) / 2.0;
638  nextpoint[0] = (pp2[0]+pp3[0]) / 2.0;
639  nextpoint[1] = (pp2[1]+pp3[1]) / 2.0;
640 
641 
642  // line segment direction to the next point
643  if(thispoint[0] < nextpoint[0]){
644  if (thispoint[1] < nextpoint[1]){ thispoint[2] = 1.0; }// SouthEast
645  else if(thispoint[1] > nextpoint[1]){ thispoint[2] = 7.0; }// NE
646  else { thispoint[2] = 0.0; } // E
647  }else if(thispoint[0] > nextpoint[0]){
648  if (thispoint[1] < nextpoint[1]){ thispoint[2] = 3.0; }// SW
649  else if(thispoint[1] > nextpoint[1]){ thispoint[2] = 5.0; }// NW
650  else { thispoint[2] = 4.0; }// N
651  }else{
652  if (thispoint[1] < nextpoint[1]){ thispoint[2] = 2.0; }// S
653  else if(thispoint[1] > nextpoint[1]){ thispoint[2] = 6.0; }// N
654  else { thispoint[2] = 8.0; }// center, this should not happen
655  }
656 
657  }// End of pathpoints loop
658 
659  }// End of paths loop
660 
661  return ins;
662  }// End of internodes()
663 
664  // 4. Batch interpollation
665  static ArrayList<ArrayList<ArrayList<Double[]>>> batchinternodes (ArrayList<ArrayList<ArrayList<Integer[]>>> bpaths){
666  ArrayList<ArrayList<ArrayList<Double[]>>> binternodes = new ArrayList<ArrayList<ArrayList<Double[]>>>();
667  for(int k=0; k<bpaths.size(); k++) {
668  binternodes.add(internodes(bpaths.get(k)));
669  }
670  return binternodes;
671  }
672 
673  // 5. tracepath() : recursively trying to fit straight and quadratic spline segments on the 8 direction internode path
674 
675  // 5.1. Find sequences of points with only 2 segment types
676  // 5.2. Fit a straight line on the sequence
677  // 5.3. If the straight line fails (an error>ltreshold), find the point with the biggest error
678  // 5.4. Fit a quadratic spline through errorpoint (project this to get controlpoint), then measure errors on every point in the sequence
679  // 5.5. If the spline fails (an error>qtreshold), find the point with the biggest error, set splitpoint = (fitting point + errorpoint)/2
680  // 5.6. Split sequence and recursively apply 5.2. - 5.7. to startpoint-splitpoint and splitpoint-endpoint sequences
681  // 5.7. TODO? If splitpoint-endpoint is a spline, try to add new points from the next sequence
682 
683  // This returns an SVG Path segment as a double[7] where
684  // segment[0] ==1.0 linear ==2.0 quadratic interpolation
685  // segment[1] , segment[2] : x1 , y1
686  // segment[3] , segment[4] : x2 , y2 ; middle point of Q curve, endpoint of L line
687  // segment[5] , segment[6] : x3 , y3 for Q curve, should be 0.0 , 0.0 for L line
688  //
689  // path type is discarded, no check for path.size < 3 , which should not happen
690 
691  public static ArrayList<Double[]> tracepath (ArrayList<Double[]> path, float ltreshold, float qtreshold){
692  int pcnt=0, seqend=0; double segtype1, segtype2;
693  ArrayList<Double[]> smp = new ArrayList<Double[]>();
694  //Double [] thissegment;
695  int pathlength = path.size();
696 
697  while(pcnt<pathlength){
698  // 5.1. Find sequences of points with only 2 segment types
699  segtype1 = path.get(pcnt)[2]; segtype2 = -1; seqend=pcnt+1;
700  while(
701  ((path.get(seqend)[2]==segtype1) || (path.get(seqend)[2]==segtype2) || (segtype2==-1))
702  && (seqend<(pathlength-1))){
703  if((path.get(seqend)[2]!=segtype1) && (segtype2==-1)){ segtype2 = path.get(seqend)[2];}
704  seqend++;
705  }
706  if(seqend==(pathlength-1)){ seqend = 0; }
707 
708  // 5.2. - 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences
709  smp.addAll(fitseq(path,ltreshold,qtreshold,pcnt,seqend));
710  // 5.7. TODO? If splitpoint-endpoint is a spline, try to add new points from the next sequence
711 
712  // forward pcnt;
713  if(seqend>0){ pcnt = seqend; }else{ pcnt = pathlength; }
714 
715  }// End of pcnt loop
716 
717  return smp;
718 
719  }// End of tracepath()
720 
721  // 5.2. - 5.6. recursively fitting a straight or quadratic line segment on this sequence of path nodes,
722  // called from tracepath()
723  public static ArrayList<Double[]> fitseq (ArrayList<Double[]> path, float ltreshold, float qtreshold, int seqstart, int seqend){
724  ArrayList<Double[]> segment = new ArrayList<Double[]>();
725  Double [] thissegment;
726  int pathlength = path.size();
727 
728  // return if invalid seqend
729  if((seqend>pathlength)||(seqend<0)){return segment;}
730 
731  int errorpoint=seqstart;
732  boolean curvepass=true;
733  double px, py, dist2, errorval=0;
734  double tl = (seqend-seqstart); if(tl<0){ tl += pathlength; }
735  double vx = (path.get(seqend)[0]-path.get(seqstart)[0]) / tl,
736  vy = (path.get(seqend)[1]-path.get(seqstart)[1]) / tl;
737 
738  // 5.2. Fit a straight line on the sequence
739  int pcnt = (seqstart+1)%pathlength;
740  double pl;
741  while(pcnt != seqend){
742  pl = pcnt-seqstart; if(pl<0){ pl += pathlength; }
743  px = path.get(seqstart)[0] + (vx * pl); py = path.get(seqstart)[1] + (vy * pl);
744  dist2 = ((path.get(pcnt)[0]-px)*(path.get(pcnt)[0]-px)) + ((path.get(pcnt)[1]-py)*(path.get(pcnt)[1]-py));
745  if(dist2>ltreshold){curvepass=false;}
746  if(dist2>errorval){ errorpoint=pcnt; errorval=dist2; }
747  pcnt = (pcnt+1)%pathlength;
748  }
749 
750  // return straight line if fits
751  if(curvepass){
752  segment.add(new Double[7]);
753  thissegment = segment.get(segment.size()-1);
754  thissegment[0] = 1.0;
755  thissegment[1] = path.get(seqstart)[0];
756  thissegment[2] = path.get(seqstart)[1];
757  thissegment[3] = path.get(seqend)[0];
758  thissegment[4] = path.get(seqend)[1];
759  thissegment[5] = 0.0;
760  thissegment[6] = 0.0;
761  return segment;
762  }
763 
764  // 5.3. If the straight line fails (an error>ltreshold), find the point with the biggest error
765  int fitpoint = errorpoint; curvepass = true; errorval = 0;
766 
767  // 5.4. Fit a quadratic spline through this point, measure errors on every point in the sequence
768  // helpers and projecting to get control point
769  double t=(fitpoint-seqstart)/tl, t1=(1.0-t)*(1.0-t), t2=2.0*(1.0-t)*t, t3=t*t;
770  double cpx = (((t1*path.get(seqstart)[0]) + (t3*path.get(seqend)[0])) - path.get(fitpoint)[0])/-t2 ,
771  cpy = (((t1*path.get(seqstart)[1]) + (t3*path.get(seqend)[1])) - path.get(fitpoint)[1])/-t2 ;
772 
773  // Check every point
774  pcnt = seqstart+1;
775  while(pcnt != seqend){
776 
777  t=(pcnt-seqstart)/tl; t1=(1.0-t)*(1.0-t); t2=2.0*(1.0-t)*t; t3=t*t;
778  px = (t1 * path.get(seqstart)[0]) + (t2 * cpx) + (t3 * path.get(seqend)[0]);
779  py = (t1 * path.get(seqstart)[1]) + (t2 * cpy) + (t3 * path.get(seqend)[1]);
780 
781  dist2 = ((path.get(pcnt)[0]-px)*(path.get(pcnt)[0]-px)) + ((path.get(pcnt)[1]-py)*(path.get(pcnt)[1]-py));
782 
783  if(dist2>qtreshold){curvepass=false;}
784  if(dist2>errorval){ errorpoint=pcnt; errorval=dist2; }
785  pcnt = (pcnt+1)%pathlength;
786  }
787 
788  // return spline if fits
789  if(curvepass){
790  segment.add(new Double[7]);
791  thissegment = segment.get(segment.size()-1);
792  thissegment[0] = 2.0;
793  thissegment[1] = path.get(seqstart)[0];
794  thissegment[2] = path.get(seqstart)[1];
795  thissegment[3] = cpx;
796  thissegment[4] = cpy;
797  thissegment[5] = path.get(seqend)[0];
798  thissegment[6] = path.get(seqend)[1];
799  return segment;
800  }
801 
802  // 5.5. If the spline fails (an error>qtreshold), find the point with the biggest error,
803  // set splitpoint = (fitting point + errorpoint)/2
804  int splitpoint = (fitpoint + errorpoint)/2;
805 
806  // 5.6. Split sequence and recursively apply 5.2. - 5.6. to startpoint-splitpoint and splitpoint-endpoint sequences
807  segment = fitseq(path,ltreshold,qtreshold,seqstart,splitpoint);
808  segment.addAll(fitseq(path,ltreshold,qtreshold,splitpoint,seqend));
809  return segment;
810 
811  }// End of fitseq()
812 
813  // 5. Batch tracing paths
814  public static ArrayList<ArrayList<Double[]>> batchtracepaths(ArrayList<ArrayList<Double[]>> internodepaths, float ltres,float qtres){
815  ArrayList<ArrayList<Double[]>> btracedpaths = new ArrayList<ArrayList<Double[]>>();
816  for(int k=0;k<internodepaths.size();k++){
817  btracedpaths.add(tracepath(internodepaths.get(k),ltres,qtres) );
818  }
819  return btracedpaths;
820  }
821 
822  // 5. Batch tracing layers
823  public static ArrayList<ArrayList<ArrayList<Double[]>>> batchtracelayers(ArrayList<ArrayList<ArrayList<Double[]>>> binternodes, float ltres, float qtres){
824  ArrayList<ArrayList<ArrayList<Double[]>>> btbis = new ArrayList<ArrayList<ArrayList<Double[]>>>();
825  for(int k=0; k<binternodes.size(); k++){
826  btbis.add( batchtracepaths( binternodes.get(k),ltres,qtres) );
827  }
828  return btbis;
829  }
830 
832  //
833  // SVG Drawing functions
834  //
836 
837  public static float roundtodec(float val, float places){
838  return (float)(Math.round(val*Math.pow(10,places))/Math.pow(10,places));
839  }
840 
841  // Getting SVG path element string from a traced path
842  public static void svgpathstring(StringBuilder sb, String desc, ArrayList<Double[]> segments, String colorstr, HashMap<String,Float> options){
843  float scale = options.get("scale");
844  float lcpr = options.get("lcpr");
845  float qcpr = options.get("qcpr");
846  float roundcoords = (float) Math.floor(options.get("roundcoords"));
847  // Path
848  sb
849  .append("<path ")
850  .append(desc)
851  .append(colorstr)
852  .append("d=\"" )
853  .append("M ")
854  .append(segments.get(0)[1]*scale)
855  .append(" ")
856  .append(segments.get(0)[2]*scale)
857  .append(" ");
858 
859  if( roundcoords == -1 ){
860  for(int pcnt=0;pcnt<segments.size();pcnt++){
861  if(segments.get(pcnt)[0]==1.0){
862  sb
863  .append("L ")
864  .append(segments.get(pcnt)[3]*scale)
865  .append(" ")
866  .append(segments.get(pcnt)[4]*scale)
867  .append(" ");
868  }else{
869  sb
870  .append("Q ")
871  .append(segments.get(pcnt)[3]*scale)
872  .append(" ")
873  .append(segments.get(pcnt)[4]*scale)
874  .append(" ")
875  .append(segments.get(pcnt)[5]*scale)
876  .append(" ")
877  .append(segments.get(pcnt)[6]*scale)
878  .append(" ");
879  }
880  }
881  }else{
882  for(int pcnt=0;pcnt<segments.size();pcnt++){
883  if(segments.get(pcnt)[0]==1.0){
884  sb
885  .append("L ")
886  .append(roundtodec((float)(segments.get(pcnt)[3]*scale),roundcoords))
887  .append(" ")
888  .append(roundtodec((float)(segments.get(pcnt)[4]*scale),roundcoords))
889  .append(" ");
890  }else{
891  sb
892  .append("Q ")
893  .append(roundtodec((float)(segments.get(pcnt)[3]*scale),roundcoords))
894  .append(" ")
895  .append(roundtodec((float)(segments.get(pcnt)[4]*scale),roundcoords)).append(" ")
896  .append(roundtodec((float)(segments.get(pcnt)[5]*scale),roundcoords)).append(" ")
897  .append(roundtodec((float)(segments.get(pcnt)[6]*scale),roundcoords)).append(" ");
898  }
899  }
900  }// End of roundcoords check
901 
902  sb.append("Z\" />");
903 
904  // Rendering control points
905  for(int pcnt=0;pcnt<segments.size();pcnt++){
906  if((lcpr>0)&&(segments.get(pcnt)[0]==1.0)){
907  sb
908  .append( "<circle cx=\"")
909  .append(segments.get(pcnt)[3]*scale)
910  .append("\" cy=\"")
911  .append(segments.get(pcnt)[4]*scale)
912  .append("\" r=\"")
913  .append(lcpr)
914  .append("\" fill=\"white\" stroke-width=\"")
915  .append(lcpr*0.2)
916  .append("\" stroke=\"black\" />");
917  }
918  if((qcpr>0)&&(segments.get(pcnt)[0]==2.0)){
919  sb
920  .append( "<circle cx=\"")
921  .append(segments.get(pcnt)[3]*scale)
922  .append("\" cy=\"")
923  .append(segments.get(pcnt)[4]*scale)
924  .append("\" r=\"")
925  .append(qcpr)
926  .append("\" fill=\"cyan\" stroke-width=\"")
927  .append(qcpr*0.2)
928  .append("\" stroke=\"black\" />");
929  sb
930  .append( "<circle cx=\"")
931  .append(segments.get(pcnt)[5]*scale)
932  .append("\" cy=\"")
933  .append(segments.get(pcnt)[6]*scale)
934  .append("\" r=\"")
935  .append(qcpr)
936  .append("\" fill=\"white\" stroke-width=\"")
937  .append(qcpr*0.2)
938  .append("\" stroke=\"black\" />");
939  sb
940  .append( "<line x1=\"")
941  .append(segments.get(pcnt)[1]*scale)
942  .append("\" y1=\"")
943  .append(segments.get(pcnt)[2]*scale)
944  .append("\" x2=\"")
945  .append(segments.get(pcnt)[3]*scale)
946  .append("\" y2=\"")
947  .append(segments.get(pcnt)[4]*scale)
948  .append("\" stroke-width=\"")
949  .append(qcpr*0.2)
950  .append("\" stroke=\"cyan\" />");
951  sb
952  .append( "<line x1=\"")
953  .append(segments.get(pcnt)[3]*scale)
954  .append("\" y1=\"")
955  .append(segments.get(pcnt)[4]*scale)
956  .append("\" x2=\"")
957  .append(segments.get(pcnt)[5]*scale)
958  .append("\" y2=\"")
959  .append(segments.get(pcnt)[6]*scale)
960  .append("\" stroke-width=\"")
961  .append(qcpr*0.2)
962  .append("\" stroke=\"cyan\" />");
963  }// End of quadratic control points
964  }
965 
966  }// End of svgpathstring()
967 
968 
969  // Converting tracedata to an SVG string, paths are drawn according to a Z-index
970  // the optional lcpr and qcpr are linear and quadratic control point radiuses
971  public static String getsvgstring(IndexedImage ii, HashMap<String,Float> options){
972  options = checkoptions(options);
973  // SVG start
974  int w = (int) (ii.width * options.get("scale")), h = (int) (ii.height * options.get("scale"));
975  String viewboxorviewport = options.get("viewbox")!=0 ? "viewBox=\"0 0 "+w+" "+h+"\" " : "width=\""+w+"\" height=\""+h+"\" ";
976  StringBuilder svgstr = new StringBuilder("<svg "+viewboxorviewport+"version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" ");
977  if(options.get("desc")!=0){ svgstr.append("desc=\"Created with ImageTracer.java version "+ImageTracer.versionnumber+"\" "); }
978  svgstr.append(">");
979  svgstr.append("<g id=\"g37\">\n");
980  // creating Z-index
981  TreeMap <Double,Integer[]> zindex = new TreeMap <Double,Integer[]>();
982  double label;
983  // Layer loop
984  for(int k=0; k<ii.layers.size(); k++) {
985 
986  // Path loop
987  for(int pcnt=0; pcnt<ii.layers.get(k).size(); pcnt++){
988 
989  // Label (Z-index key) is the startpoint of the path, linearized
990  label = (ii.layers.get(k).get(pcnt).get(0)[2] * w) + ii.layers.get(k).get(pcnt).get(0)[1];
991  // Creating new list if required
992  if(!zindex.containsKey(label)){ zindex.put(label,new Integer[2]); }
993  // Adding layer and path number to list
994  zindex.get(label)[0] = new Integer(k);
995  zindex.get(label)[1] = new Integer(pcnt);
996  }// End of path loop
997 
998  }// End of layer loop
999 
1000  // Sorting Z-index is not required, TreeMap is sorted automatically
1001 
1002  // Drawing
1003  // Z-index loop
1004  String thisdesc = "";
1005  for(Entry<Double, Integer[]> entry : zindex.entrySet()) {
1006  if(options.get("desc")!=0){ thisdesc = "desc=\"l "+entry.getValue()[0]+" p "+entry.getValue()[1]+"\" "; }else{ thisdesc = ""; }
1007  svgpathstring(svgstr,
1008  thisdesc,
1009  ii.layers.get(entry.getValue()[0]).get(entry.getValue()[1]),
1010  tosvgcolorstr(ii.palette[entry.getValue()[0]]),
1011  options);
1012  }
1013  svgstr.append("</g>\n");
1014  // SVG End
1015  svgstr.append("</svg>");
1016 
1017  return svgstr.toString();
1018 
1019  }// End of getsvgstring()
1020 
1021  static String tosvgcolorstr(byte[] c){
1022  //hack this to export cuttable files
1023  //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)+"\" ";
1024 
1025  return "fill=\"none\" stroke=\"rgb("+(0)+","+(0)+","+(0)+")\" stroke-width=\"0.354\" opacity=\""+1+"\" ";
1026  }
1027 
1028  // Gaussian kernels for blur
1029  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},
1030  {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} };
1031 
1032  // Selective Gaussian blur for preprocessing
1033  static ImageData blur(ImageData imgd, float rad, float del){
1034  int i,j,k,d,idx;
1035  double racc,gacc,bacc,aacc,wacc;
1036  ImageData imgd2 = new ImageData(imgd.width,imgd.height,new byte[imgd.width*imgd.height*4]);
1037 
1038  // radius and delta limits, this kernel
1039  int radius = (int)Math.floor(rad); if(radius<1){ return imgd; } if(radius>5){ radius = 5; }
1040  int delta = (int)Math.abs(del); if(delta>1024){ delta = 1024; }
1041  double[] thisgk = gks[radius-1];
1042 
1043  // loop through all pixels, horizontal blur
1044  for( j=0; j < imgd.height; j++ ){
1045  for( i=0; i < imgd.width; i++ ){
1046 
1047  racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0;
1048  // gauss kernel loop
1049  for( k = -radius; k < (radius+1); k++){
1050  // add weighted color values
1051  if( ((i+k) > 0) && ((i+k) < imgd.width) ){
1052  idx = ((j*imgd.width)+i+k)*4;
1053  racc += imgd.data[idx ] * thisgk[k+radius];
1054  gacc += imgd.data[idx+1] * thisgk[k+radius];
1055  bacc += imgd.data[idx+2] * thisgk[k+radius];
1056  aacc += imgd.data[idx+3] * thisgk[k+radius];
1057  wacc += thisgk[k+radius];
1058  }
1059  }
1060  // The new pixel
1061  idx = ((j*imgd.width)+i)*4;
1062  imgd2.data[idx ] = (byte) Math.floor(racc / wacc);
1063  imgd2.data[idx+1] = (byte) Math.floor(gacc / wacc);
1064  imgd2.data[idx+2] = (byte) Math.floor(bacc / wacc);
1065  imgd2.data[idx+3] = (byte) Math.floor(aacc / wacc);
1066 
1067  }// End of width loop
1068  }// End of horizontal blur
1069 
1070  // copying the half blurred imgd2
1071  byte[] himgd = imgd2.data.clone();
1072 
1073  // loop through all pixels, vertical blur
1074  for( j=0; j < imgd.height; j++ ){
1075  for( i=0; i < imgd.width; i++ ){
1076 
1077  racc = 0; gacc = 0; bacc = 0; aacc = 0; wacc = 0;
1078  // gauss kernel loop
1079  for( k = -radius; k < (radius+1); k++){
1080  // add weighted color values
1081  if( ((j+k) > 0) && ((j+k) < imgd.height) ){
1082  idx = (((j+k)*imgd.width)+i)*4;
1083  racc += himgd[idx ] * thisgk[k+radius];
1084  gacc += himgd[idx+1] * thisgk[k+radius];
1085  bacc += himgd[idx+2] * thisgk[k+radius];
1086  aacc += himgd[idx+3] * thisgk[k+radius];
1087  wacc += thisgk[k+radius];
1088  }
1089  }
1090  // The new pixel
1091  idx = ((j*imgd.width)+i)*4;
1092  imgd2.data[idx ] = (byte) Math.floor(racc / wacc);
1093  imgd2.data[idx+1] = (byte) Math.floor(gacc / wacc);
1094  imgd2.data[idx+2] = (byte) Math.floor(bacc / wacc);
1095  imgd2.data[idx+3] = (byte) Math.floor(aacc / wacc);
1096 
1097  }// End of width loop
1098  }// End of vertical blur
1099 
1100  // Selective blur: loop through all pixels
1101  for( j=0; j < imgd.height; j++ ){
1102  for( i=0; i < imgd.width; i++ ){
1103 
1104  idx = ((j*imgd.width)+i)*4;
1105  // d is the difference between the blurred and the original pixel
1106  d = Math.abs(imgd2.data[idx ] - imgd.data[idx ]) + Math.abs(imgd2.data[idx+1] - imgd.data[idx+1]) +
1107  Math.abs(imgd2.data[idx+2] - imgd.data[idx+2]) + Math.abs(imgd2.data[idx+3] - imgd.data[idx+3]);
1108  // selective blur: if d>delta, put the original pixel back
1109  if(d>delta){
1110  imgd2.data[idx ] = imgd.data[idx ];
1111  imgd2.data[idx+1] = imgd.data[idx+1];
1112  imgd2.data[idx+2] = imgd.data[idx+2];
1113  imgd2.data[idx+3] = imgd.data[idx+3];
1114  }
1115  }
1116  }// End of Selective blur
1117 
1118  return imgd2;
1119 
1120  }// End of blur()
1121 
1122 }// End of ImageTracer class
static float roundtodec(float val, float places)
static ArrayList< Double[]> fitseq(ArrayList< Double[]> path, float ltreshold, float qtreshold, int seqstart, int seqend)
static String getsvgstring(IndexedImage ii, HashMap< String, Float > options)
static String imageToSVG(BufferedImage image, HashMap< String, Float > options, byte[][] palette)
static byte[][] generatepalette(int numberofcolors)
static ArrayList< ArrayList< Integer[]> > pathscan(int[][] arr, float pathomit)
static ImageData loadImageData(BufferedImage image)
IndexedImage imageToTracedata(BufferedImage image, HashMap< String, Float > options, byte[][] palette)
static float parsenext(String[] arr, int i)
static String imageToSVG(String filename, HashMap< String, Float > options, byte[][] palette)
static ArrayList< ArrayList< ArrayList< Double[]> > > batchtracelayers(ArrayList< ArrayList< ArrayList< Double[]>>> binternodes, float ltres, float qtres)
IndexedImage imageToTracedata(String filename, HashMap< String, Float > options, byte[][] palette)
static ArrayList< ArrayList< Double[]> > batchtracepaths(ArrayList< ArrayList< Double[]>> internodepaths, float ltres, float qtres)
static HashMap< String, Float > checkoptions(HashMap< String, Float > options)
static int[][][] layering(IndexedImage ii)
static byte[][] samplepalette(int numberofcolors, ImageData imgd)
static IndexedImage imagedataToTracedata(ImageData imgd, HashMap< String, Float > options, byte[][] palette)
static ImageData loadImageData(String filename)
static void svgpathstring(StringBuilder sb, String desc, ArrayList< Double[]> segments, String colorstr, HashMap< String, Float > options)
static int arraycontains(String[] arr, String str)
static String imagedataToSVG(ImageData imgd, HashMap< String, Float > options, byte[][] palette)
static void saveString(String filename, String str)
static ArrayList< ArrayList< ArrayList< Integer[]> > > batchpathscan(int[][][] layers, float pathomit)
static ArrayList< Double[]> tracepath(ArrayList< Double[]> path, float ltreshold, float qtreshold)
static IndexedImage colorquantization(ImageData imgd, byte[][] palette, HashMap< String, Float > options)
static ArrayList< ArrayList< Double[]> > internodes(ArrayList< ArrayList< Integer[]>> paths)